Compare commits

...

13 Commits

Author SHA1 Message Date
tobin
351dfafced mcp-server-dev: hosting, payload-cap, lifecycle, and directory guidance 2026-04-23 05:42:41 +00:00
Bryan Thompson
cf62a6c02d Merge pull request #1439 from anthropics/add-datadog
Add datadog plugin
2026-04-21 16:04:18 -05:00
Bryan Thompson
3bd94cc810 Bump SHA pins for 39 plugins (>7d stale) (#1502)
Rebased on latest main to resolve conflict with cockroachdb unpin (#1514)
and liquid-lsp addition (#1520). Excludes netsuite-suitecloud (4d).
2026-04-21 20:56:31 +01:00
Bryan Thompson
a8be018317 Merge pull request #1514 from anthropics/update-cockroachdb
Update cockroachdb plugin — add author + category, bump SHA
2026-04-21 14:38:05 -05:00
Bryan Thompson
33e62b9bd6 Remove SHA pin from cockroachdb entry
Let installs follow the repo's default branch instead of a fixed SHA.
Removes the plugin from the weekly SHA-bump rotation and lets developer
updates reach users directly on `claude plugin install`.
2026-04-21 14:32:10 -05:00
Bryan Thompson
9f103c621d Add liquid-lsp plugin (#1520) 2026-04-21 19:07:52 +01:00
Bryan Thompson
caa8c1a539 Update cockroachdb description + author per developer request
- Description: expand to reflect current capabilities (14 tools, 2 MCP
  backends, 3 agents, 32 skills, safety hooks)
- Author: "CockroachDB" → "Cockroach Labs" (company name)
2026-04-21 12:33:39 -05:00
Bryan Thompson
33fd73c8b9 Update cockroachdb plugin — add author + category, bump SHA 2026-04-21 07:03:06 -05:00
Bryan Thompson
777db5c30b Add liquid-skills plugin (#1507) 2026-04-20 22:01:55 +01:00
Karandeep Johar
aeecad8f43 fix(amplitude): use git-subdir source to point at plugins/amplitude (#1505)
The amplitude entry used source type "url" which clones the root of
https://github.com/amplitude/mcp-marketplace — a multi-plugin repo
where the actual plugin lives at plugins/amplitude/. Claude Code found
no skills there, so /reload-plugins loaded 0 skills for amplitude.

Switching to "git-subdir" with path "plugins/amplitude" (the same
pattern used by awslabs, bigdata-com, zapier, etc.) makes Claude Code
resolve the correct subdirectory and load all 27 amplitude skills.

Removing the pinned sha so the plugin tracks main, consistent with
how posthog and other unpinned entries behave.
2026-04-20 20:34:29 +01:00
Bryan Thompson
bb7730114d Consolidate Oracle NetSuite skills into a single plugin (#1464) 2026-04-17 22:03:25 +01:00
Bryan Thompson
3df5394ee9 Merge pull request #1463 from anthropics/add-netsuite-plugins
Add Oracle NetSuite agent skills (3 plugins)
2026-04-17 15:39:51 -05:00
Bryan Thompson
db52e65c44 Add datadog plugin 2026-04-17 07:28:58 -05:00
7 changed files with 361 additions and 105 deletions

View File

@@ -24,7 +24,7 @@
"source": {
"source": "url",
"url": "https://github.com/amekala/adspirer-mcp-plugin.git",
"sha": "aa70dbdbbbb843e94a794c10c2b13f5dd66b5e40"
"sha": "c40623f1aa7b568e960d3f2e2558a6fcf10e6c18"
},
"homepage": "https://www.adspirer.com"
},
@@ -47,7 +47,7 @@
"url": "https://github.com/techwolf-ai/ai-first-toolkit.git",
"path": "plugins/ai-firstify",
"ref": "main",
"sha": "7f18e11d694b9ae62ea3009fbbc175f08ae913df"
"sha": "852272ec21cebab98202df967dffee127209b6bc"
},
"homepage": "https://ai-first.techwolf.ai"
},
@@ -57,7 +57,7 @@
"source": {
"source": "url",
"url": "https://github.com/endorlabs/ai-plugins.git",
"sha": "a0f1d5632b6f9e6c26eaa9806f5d8d454ca5b06f"
"sha": "975f0ce422b1f2677681ffd085aef34ea1826b70"
},
"homepage": "https://www.endorlabs.com"
},
@@ -67,7 +67,7 @@
"source": {
"source": "url",
"url": "https://github.com/AikidoSec/aikido-claude-plugin.git",
"sha": "d7fa8b8e192680d9a26c1a5dcaead7cf5cdb7139"
"sha": "5d9c13d367218e9b43a11d4502f623ab98859225"
},
"homepage": "https://github.com/AikidoSec/aikido-claude-plugin"
},
@@ -86,9 +86,10 @@
{
"name": "amplitude",
"source": {
"source": "url",
"source": "git-subdir",
"url": "https://github.com/amplitude/mcp-marketplace.git",
"sha": "be54ccb66b10593721dd3a31e47b2db20ea02d2f"
"path": "plugins/amplitude",
"ref": "main"
},
"description": "Use Amplitude as an expert analyst — instrument Amplitude, discover product opportunities, analyze charts, create dashboards, manage experiments, and understand users and accounts.",
"category": "monitoring",
@@ -108,7 +109,7 @@
"source": {
"source": "url",
"url": "https://github.com/astronomer/agents.git",
"sha": "7ef022b02f5296b5ecc52ba0db3ba9345ec03c9e"
"sha": "5935c4330dea4dfb8e93568956b10a543ecdb3d1"
},
"homepage": "https://github.com/astronomer/agents"
},
@@ -185,7 +186,7 @@
"source": {
"source": "url",
"url": "https://github.com/AzureCosmosDB/cosmosdb-claude-code-plugin.git",
"sha": "56e6da0cae93cdee8bcfa5e624ecdd9a0a483181"
"sha": "23c168856e4435793bd27a72d4714f022a3a1e90"
},
"description": "Expert assistant for Azure Cosmos DB — data modeling, query optimization, performance tuning, and best practices.",
"category": "database",
@@ -234,7 +235,7 @@
"source": {
"source": "url",
"url": "https://github.com/box/box-for-ai.git",
"sha": "6f4ec3549f3e869b115628403555b1c9220b2b34"
"sha": "0fb23244e3c35cd562206c80eff1e22c456046ea"
},
"homepage": "https://github.com/box/box-for-ai"
},
@@ -244,7 +245,7 @@
"source": {
"source": "url",
"url": "https://github.com/brightdata/skills.git",
"sha": "e671da495f7ec0ed6be5e9fa71e260f886a1dc36"
"sha": "44b24797d82cfd535c5b97831d5c6ba86c9d60df"
},
"homepage": "https://docs.brightdata.com"
},
@@ -266,7 +267,7 @@
"source": {
"source": "url",
"url": "https://github.com/ChromeDevTools/chrome-devtools-mcp.git",
"sha": "c2d8009ff75f76bce1ec4cf79c2467b50d81725e"
"sha": "a1612be8e01401cf1711c64bc2ef5da5763ba956"
},
"homepage": "https://github.com/ChromeDevTools/chrome-devtools-mcp"
},
@@ -338,7 +339,7 @@
"source": {
"source": "url",
"url": "https://github.com/cloudflare/skills.git",
"sha": "5ec03da67e230df52b698255c8e5979dc9b124b6"
"sha": "0397d7d88fa6ac7517a88389622eb0799e86ded2"
},
"description": "Skills for the Cloudflare developer platform: Workers, Durable Objects, Agents SDK, MCP servers, Wrangler CLI, and web performance.",
"category": "deployment",
@@ -350,17 +351,20 @@
"source": {
"source": "url",
"url": "https://github.com/cloudinary-devs/cloudinary-plugin.git",
"sha": "137c5d7acd9c3f10e80cd2a400486971e1664f31"
"sha": "7b443d7dbd607bfe4850d8cfcab6ba4cbf1a57c3"
},
"homepage": "https://cloudinary.com/documentation"
},
{
"name": "cockroachdb",
"description": "CockroachDB plugin for Claude Code — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters directly from your AI coding agent.",
"description": "Connect Claude Code directly to your CockroachDB clusters for hands-on database work — explore schemas, write optimized SQL, debug queries, and manage distributed database clusters. This plugin provides 14 tools across two active MCP backends (self-hosted MCP Toolbox and managed CockroachDB Cloud MCP Server), three specialized agents (DBA, Developer, Operator), 32 skills across 6 operational domains, and built-in safety hooks.",
"author": {
"name": "Cockroach Labs"
},
"category": "database",
"source": {
"source": "url",
"url": "https://github.com/cockroachdb/claude-plugin.git",
"sha": "a54566e03c852567589ef85bb449d1e4de229667"
"url": "https://github.com/cockroachdb/claude-plugin.git"
},
"homepage": "https://github.com/cockroachdb/claude-plugin"
},
@@ -444,7 +448,7 @@
"source": {
"source": "url",
"url": "https://github.com/astronomer/agents.git",
"sha": "7ef022b02f5296b5ecc52ba0db3ba9345ec03c9e"
"sha": "5935c4330dea4dfb8e93568956b10a543ecdb3d1"
},
"homepage": "https://github.com/astronomer/agents"
},
@@ -454,7 +458,7 @@
"source": {
"source": "url",
"url": "https://github.com/astronomer/agents.git",
"sha": "85d6053b1e21724f9cefb1e3f5219bd54fc77224"
"sha": "5935c4330dea4dfb8e93568956b10a543ecdb3d1"
},
"homepage": "https://github.com/astronomer/agents"
},
@@ -470,6 +474,19 @@
},
"homepage": "https://github.com/awslabs/agent-plugins"
},
{
"name": "datadog",
"description": "Use Datadog directly in Claude Code through a preconfigured Datadog MCP server. Query logs, metrics, traces, dashboards, and more through natural conversation. This plugin is in preview.",
"author": {
"name": "Datadog"
},
"category": "monitoring",
"source": {
"source": "url",
"url": "https://github.com/datadog-labs/claude-code-plugin.git"
},
"homepage": "https://www.datadoghq.com/"
},
{
"name": "dataverse",
"description": "Agent skills for building on, analyzing, and managing Microsoft Dataverse — with Dataverse MCP, PAC CLI, and Python SDK.",
@@ -545,7 +562,7 @@
"source": {
"source": "url",
"url": "https://github.com/fastly/fastly-agent-toolkit.git",
"sha": "d9ba949011e725be55cae11acc741aa1f1f393d3"
"sha": "329331c887512850f13e481b45c4298c0387a4d2"
},
"homepage": "https://github.com/fastly/fastly-agent-toolkit/blob/main/README.md"
},
@@ -566,7 +583,7 @@
"source": {
"source": "url",
"url": "https://github.com/voxel51/fiftyone-skills.git",
"sha": "593e0553fc9fd94db52386ada2c9e2074a6ecf89"
"sha": "02bd4ea170ca01a751c2d2dd6bf2df8f62e65626"
},
"homepage": "https://docs.voxel51.com/"
},
@@ -623,7 +640,7 @@
"source": {
"source": "url",
"url": "https://github.com/followrabbit-ai/awesome-rabbit.git",
"sha": "f59ec3d1f6337a6ed825ef06836a221ed3d2ffb0"
"sha": "6926154501300d348a7b50d47479648fe87985b6"
},
"homepage": "https://subscriptions.agentic.followrabbit.ai/"
},
@@ -658,7 +675,7 @@
"source": {
"source": "url",
"url": "https://github.com/PAIR-Systems-Inc/goodmem-claude-code-plugin.git",
"sha": "215568baf203887b5d7f8245e0503dd4a81336c2"
"sha": "4e23ab2b3bc7cb4167c99e10d9640ad7089744d7"
},
"homepage": "https://github.com/PAIR-Systems-Inc/goodmem-claude-code-plugin"
},
@@ -697,7 +714,7 @@
"url": "https://github.com/helius-labs/core-ai.git",
"path": "helius-plugin",
"ref": "main",
"sha": "05ea4d1128d46618266bbcc23a5e7019c57be0d6"
"sha": "d9d252497bcf1e4bd5073a76715cd50a8353f9c3"
},
"homepage": "https://www.helius.dev/docs"
},
@@ -735,7 +752,7 @@
"source": {
"source": "url",
"url": "https://github.com/intercom/claude-plugin-external.git",
"sha": "eeef353eead2e3dc5f33f64dbaae54e1309e0d45"
"sha": "52653572c47700443eb61154c4e4334a355e755e"
},
"homepage": "https://github.com/intercom/claude-plugin-external"
},
@@ -823,6 +840,38 @@
"source": "./external_plugins/linear",
"homepage": "https://github.com/anthropics/claude-plugins-public/tree/main/external_plugins/linear"
},
{
"name": "liquid-lsp",
"description": "LSP integration for Shopify Liquid templates via the Shopify CLI theme language server.",
"author": {
"name": "Shopify"
},
"category": "development",
"source": {
"source": "git-subdir",
"url": "https://github.com/Shopify/liquid-skills.git",
"path": "plugins/liquid-lsp",
"ref": "main",
"sha": "a00ca039d82114a7af1b4cbc3025b16c624a42fa"
},
"homepage": "https://github.com/Shopify/liquid-skills/tree/main/plugins/liquid-lsp"
},
{
"name": "liquid-skills",
"description": "Liquid language fundamentals, CSS/JS/HTML coding standards, and WCAG accessibility patterns for Shopify themes",
"author": {
"name": "Shopify"
},
"category": "development",
"source": {
"source": "git-subdir",
"url": "https://github.com/Shopify/liquid-skills.git",
"path": "plugins/liquid-skills",
"ref": "main",
"sha": "bf7a7aa9f9809b0dcd80cb5f7fd2795a7208a7a3"
},
"homepage": "https://github.com/Shopify/liquid-skills/tree/main/plugins/liquid-skills"
},
{
"name": "lua-lsp",
"description": "Lua language server for code intelligence",
@@ -882,7 +931,7 @@
"source": {
"source": "url",
"url": "https://github.com/mintlify/mintlify-claude-plugin.git",
"sha": "ce435be18a700dc849d6a63a80da4816d1e2128c"
"sha": "acd6d2e0128c4f235d55cfb8d8c91ecbdd5df8cc"
},
"homepage": "https://www.mintlify.com/"
},
@@ -908,7 +957,7 @@
"source": {
"source": "url",
"url": "https://github.com/mongodb/agent-skills.git",
"sha": "c47079f65e88a113c52d1ce0618684cef300246c"
"sha": "24529d9540b962d57f30e75d25071bebea5809ad"
},
"homepage": "https://www.mongodb.com/docs/mcp-server/overview/"
},
@@ -921,7 +970,7 @@
"url": "https://github.com/neondatabase/agent-skills.git",
"path": "plugins/neon-postgres",
"ref": "main",
"sha": "54d7a9db2ddd476f84d5d1fd7bac323907858a8b"
"sha": "1438d7db4560a649d62eba99e9d5008b77ac5758"
},
"homepage": "https://github.com/neondatabase/agent-skills/tree/main/plugins/neon-postgres"
},
@@ -936,28 +985,8 @@
"homepage": "https://github.com/netlify/context-and-tools"
},
{
"name": "netsuite-aiconnector-service-skill",
"description": "NetSuite Intelligence skill — teaches AI the correct tool-selection order, output formatting, domain knowledge, multi-subsidiary and currency handling, and SuiteQL safety checklist for any session using the NetSuite AI Service Connector.",
"author": {
"name": "Oracle NetSuite"
},
"category": "productivity",
"source": {
"source": "git-subdir",
"url": "https://github.com/oracle/netsuite-suitecloud-sdk.git",
"path": "packages/agent-skills",
"ref": "master",
"sha": "43bacf43763e1eedd0892b4652be3d45df94f0e7"
},
"homepage": "https://github.com/oracle/netsuite-suitecloud-sdk",
"strict": false,
"skills": [
"./netsuite-ai-connector-instructions"
]
},
{
"name": "netsuite-sdf-roles-and-permissions",
"description": "Use when generating or reviewing NetSuite SDF permission configurations — customrole XML, script deployment permissions, permkey values, permlevel choices, run-as role design, and least-privilege access. Confirms exact ADMI_/LIST_/REGT_/REPO_/TRAN_ permission IDs and validates permissions against bundled NetSuite reference data.",
"name": "netsuite-suitecloud",
"description": "NetSuite agent skills from Oracle — authoring guidance for SuiteCloud Development Framework (SDF) objects and UIF single-page-app components, plus runtime guidance for the NetSuite AI Service Connector.",
"author": {
"name": "Oracle NetSuite"
},
@@ -969,31 +998,13 @@
"ref": "master",
"sha": "43bacf43763e1eedd0892b4652be3d45df94f0e7"
},
"homepage": "https://github.com/oracle/netsuite-suitecloud-sdk",
"strict": false,
"skills": [
"./netsuite-sdf-roles-and-permissions"
]
},
{
"name": "netsuite-uif-spa-reference",
"description": "Use when building, modifying, or debugging NetSuite UIF SPA components. Provides API/type lookup for @uif-js/core and @uif-js/component — constructors, methods, props, enums, hooks, and component options.",
"author": {
"name": "Oracle NetSuite"
},
"category": "development",
"source": {
"source": "git-subdir",
"url": "https://github.com/oracle/netsuite-suitecloud-sdk.git",
"path": "packages/agent-skills",
"ref": "master",
"sha": "43bacf43763e1eedd0892b4652be3d45df94f0e7"
},
"homepage": "https://github.com/oracle/netsuite-suitecloud-sdk",
"strict": false,
"skills": [
"./netsuite-ai-connector-instructions",
"./netsuite-sdf-roles-and-permissions",
"./netsuite-uif-spa-reference"
]
],
"homepage": "https://github.com/oracle/netsuite-suitecloud-sdk"
},
{
"name": "nightvision",
@@ -1040,7 +1051,7 @@
"source": {
"source": "url",
"url": "https://github.com/Optimal-AI/optibot-skill.git",
"sha": "981db1f630c3116d7df0a71e5967af55b08e813c"
"sha": "ce2be448ee713606aa653fc93ef2f98a200fe327"
},
"homepage": "https://getoptimal.ai"
},
@@ -1144,7 +1155,7 @@
"source": {
"source": "url",
"url": "https://github.com/gitroomhq/postiz-agent.git",
"sha": "c5d1bf5f7e95a71e230fc19ae2150ddd9c549854"
"sha": "37d627244c53a4b3a7ca94c52cc2db13aaaf468e"
},
"homepage": "https://postiz.com/agent"
},
@@ -1155,7 +1166,7 @@
"source": {
"source": "url",
"url": "https://github.com/Postman-Devrel/postman-claude-code-plugin.git",
"sha": "40b11ac3466c500cf4625ac016d5c01cd00046f4"
"sha": "416e40da03a237df7bf03f4362cf6fc7b989b567"
},
"homepage": "https://learning.postman.com/docs/developer/postman-mcp-server/"
},
@@ -1245,7 +1256,7 @@
"url": "https://github.com/railwayapp/railway-skills.git",
"path": "plugins/railway",
"ref": "main",
"sha": "d52f3741a6a33a3191d6138eb3d6c3355cb970d1"
"sha": "eaa89d8f594412b0b837b6531241e7d166e12202"
},
"homepage": "https://docs.railway.com/ai/claude-code-plugin"
},
@@ -1277,7 +1288,7 @@
"source": {
"source": "url",
"url": "https://github.com/Digital-Process-Tools/claude-remember.git",
"sha": "779ab61d8d412230eeec1840b8ca104bebea4358"
"sha": "914445ac5f06a164800ea90ba4db41a0486321ae"
},
"homepage": "https://github.com/Digital-Process-Tools/claude-remember"
},
@@ -1358,7 +1369,7 @@
"source": {
"source": "url",
"url": "https://github.com/sanity-io/agent-toolkit.git",
"sha": "4b1fb10bd707a22cf0cdfad5374ffc885f2ffa8d"
"sha": "bc09fa9854507c538a856648aafbd4e1a775a95c"
},
"homepage": "https://www.sanity.io"
},
@@ -1502,7 +1513,7 @@
"source": {
"source": "url",
"url": "https://github.com/sourcegraph-community/sourcegraph-claudecode-plugin.git",
"sha": "cfe3d44476957b16d1575261bef6b2dc7cb1e0b7"
"sha": "332ee0ca9a409ccd791abee43c7abf2606469017"
},
"homepage": "https://sourcegraph.com"
},
@@ -1513,7 +1524,7 @@
"source": {
"source": "url",
"url": "https://github.com/spotify/ads-claude-plugin.git",
"sha": "a4bce9912db071d47dfb410086a48004e0539efa"
"sha": "63585cc919da51dd24fab594d829869595301922"
},
"homepage": "https://github.com/spotify/ads-claude-plugin"
},
@@ -1560,7 +1571,7 @@
"source": {
"source": "url",
"url": "https://github.com/sumup/sumup-skills.git",
"sha": "802476c39a0422d3277e37288b03968ad731bc30"
"sha": "0fd0a911ecaffd7187fe35e914d8ead6de584ffd"
},
"homepage": "https://www.sumup.com/"
},
@@ -1660,7 +1671,7 @@
"url": "https://github.com/UI5/plugins-claude.git",
"path": "plugins/ui5",
"ref": "main",
"sha": "5070dfc1cef711d6efad40beb43750027039d71f"
"sha": "cec940abd4b7b6866de8e7e4522f3dba0449379d"
},
"homepage": "https://github.com/UI5/plugins-claude"
},
@@ -1673,7 +1684,7 @@
"url": "https://github.com/UI5/plugins-claude.git",
"path": "plugins/ui5-typescript-conversion",
"ref": "main",
"sha": "5070dfc1cef711d6efad40beb43750027039d71f"
"sha": "cec940abd4b7b6866de8e7e4522f3dba0449379d"
},
"homepage": "https://github.com/UI5/plugins-claude"
},
@@ -1693,7 +1704,7 @@
"source": {
"source": "url",
"url": "https://github.com/TSedmanDC/Voila-API-Skill.git",
"sha": "b9cfcb860cb5ae4ece57d67422a6cdd92ef96739"
"sha": "422c7beb772a0de4592a204584e0e990fc5dc139"
},
"homepage": "https://github.com/TSedmanDC/Voila-API-Skill"
},
@@ -1704,7 +1715,7 @@
"source": {
"source": "url",
"url": "https://github.com/wix/skills.git",
"sha": "15dda227e34959b1340e33bb9aede7e23a273f42"
"sha": "bf25b5a45b2413b3581f3dcbcd63f3737791a051"
},
"homepage": "https://dev.wix.com/docs/wix-cli/guides/development/about-wix-skills"
},
@@ -1714,7 +1725,7 @@
"source": {
"source": "url",
"url": "https://github.com/Automattic/claude-code-wordpress.com.git",
"sha": "e4d23c3bffdcdb7f70134ab6a1a110258ff75cfd"
"sha": "052ca970df2c577d7c651e784935186ff93e6779"
},
"homepage": "https://developer.wordpress.com/wordpress-com-claude-code-plugin/"
},
@@ -1727,7 +1738,7 @@
"url": "https://github.com/zapier/zapier-mcp.git",
"path": "plugins/zapier",
"ref": "main",
"sha": "b93007e9a726c6ee93c57a949e732744ef5acbfd"
"sha": "76c4669321847c8f72a6e0462c17f29fd437519a"
},
"homepage": "https://github.com/zapier/zapier-mcp/tree/main/plugins/zapier"
},

View File

@@ -14,10 +14,15 @@ The UI layer is **additive**. Under the hood it's still tools, resources, and th
## Claude host specifics
- `_meta.ui.prefersBorder: false` on a `ui://` resource removes the outer card border (mobile).
| `_meta.ui.*` key | Where | Effect |
|---|---|---|
| `resourceUri` | tool | Which `ui://` resource the host renders for this tool's results. |
| `visibility: ["app"]` | tool | Hide a widget-only helper tool (e.g. geometry/image fetcher called via `callServerTool`) from Claude's tool list. |
| `prefersBorder: false` | resource | Drop the host's outer card border (mobile). |
| `csp.{connectDomains, resourceDomains, baseUriDomains}` | resource | Declare external origins; default is block-all. `frameDomains` is currently restricted in Claude. |
- `hostContext.safeAreaInsets: {top, right, bottom, left}` (px) — honor these for notches and the composer overlay.
- `_meta.ui.csp.{connectDomains, resourceDomains, baseUriDomains}`declare external origins per resource; default is block-all. `frameDomains` is currently restricted in Claude.
- Directory submission for MCP Apps requires 35 PNG screenshots, ≥1000px wide, cropped to the app response only (no prompt in the image). See https://claude.com/docs/connectors/building/submission#asset-specifications.
- Directory submission requires OAuth or **authless** (`none`)static bearer is private-deploy only and blocks listing — plus tool `annotations` and 35 PNG screenshots; see `references/directory-checklist.md`.
---
@@ -104,6 +109,7 @@ const server = new McpServer({ name: "contacts", version: "1.0.0" });
// 1. The tool — returns DATA, declares which UI to show
registerAppTool(server, "pick_contact", {
description: "Open an interactive contact picker",
annotations: { title: "Pick Contact", readOnlyHint: true },
inputSchema: { filter: z.string().optional() },
_meta: { ui: { resourceUri: "ui://widgets/contact-picker.html" } },
}, async ({ filter }) => {
@@ -172,7 +178,10 @@ The `/*__EXT_APPS_BUNDLE__*/` placeholder gets replaced by the server at startup
| `app.updateModelContext({...})` | Widget → host | Update context silently (no visible message) |
| `app.callServerTool({name, arguments})` | Widget → server | Call another tool on your server |
| `app.openLink({url})` | Widget → host | Open a URL in a new tab (sandbox blocks `window.open`) |
| `app.getHostContext()` / `app.onhostcontextchanged` | Host → widget | Theme (`light`/`dark`), locale, etc. |
| `app.getHostContext()` / `app.onhostcontextchanged` | Host → widget | Theme, host CSS vars, `containerDimensions`, `displayMode`, `deviceCapabilities` |
| `app.requestDisplayMode({mode})` | Widget → host | Ask for `inline` / `pip` / `fullscreen` |
| `app.downloadFile({name, mimeType, content})` | Widget → host | Host-mediated download (base64 content) |
| `new App(info, caps, {autoResize: true})` | — | Iframe height tracks rendered content |
`sendMessage` is the typical "user picked something, tell Claude" path. `updateModelContext` is for state that Claude should know about but shouldn't clutter the chat. `openLink` is **required** for any outbound navigation — `window.open` and `<a target="_blank">` are blocked by the sandbox attribute.
@@ -225,6 +234,7 @@ const pickerHtml = readFileSync("./widgets/picker.html", "utf8")
registerAppTool(server, "pick_contact", {
description: "Open an interactive contact picker. User selects one contact.",
annotations: { title: "Pick Contact", readOnlyHint: true },
inputSchema: { filter: z.string().optional().describe("Name/email prefix filter") },
_meta: { ui: { resourceUri: "ui://widgets/picker.html" } },
}, async ({ filter }) => {
@@ -348,6 +358,24 @@ Desktop caches UI resources aggressively. After editing widget HTML, **fully qui
The `sleep` keeps stdin open long enough to collect all responses. Parse the jsonl output with `jq` or a Python one-liner.
**Widget dev loop** — avoid the ⌘Q-relaunch cycle entirely by serving the inlined widget HTML at a plain GET route with a fake `ExtApps` shim that fires `ontoolresult` from a query param:
```ts
app.get("/widget-preview", (_req, res) => {
const shim = `globalThis.ExtApps={applyHostStyleVariables:()=>{},App:class{
constructor(){this.h={}} ontoolresult;onhostcontextchanged;
async connect(){const p=new URLSearchParams(location.search).get("payload");
if(p)this.ontoolresult?.({content:[{type:"text",text:p}]});}
getHostContext(){return{theme:"light"}}
sendMessage(m){console.log("sendMessage",m)} updateModelContext(){}
callServerTool(){return Promise.resolve({content:[]})} openLink(){} downloadFile(){}
}};`;
res.type("html").send(widgetHtml.replace("/*__EXT_APPS_BUNDLE__*/", shim));
});
```
Open `http://localhost:3000/widget-preview?payload={"rows":[...]}` in a normal browser tab and iterate with ordinary devtools.
**Host fallback** — use a host without the apps surface (or MCP Inspector) and confirm the tool's text content degrades gracefully.
**CSP debugging** — open the iframe's own devtools console. CSP violations are the #1 reason widgets silently fail (blank rectangle, no error in the main console). See `references/iframe-sandbox.md`.
@@ -356,6 +384,9 @@ The `sleep` keeps stdin open long enough to collect all responses. Parse the jso
## Reference files
- `references/iframe-sandbox.md` — CSP/sandbox constraints, the bundle-inlining pattern, image handling
- `references/iframe-sandbox.md` — CSP/sandbox constraints, the bundle-inlining pattern, image handling, host theming
- `references/widget-templates.md` — reusable HTML scaffolds for picker / confirm / progress / display
- `references/apps-sdk-messages.md` — the `App` class API: widget ↔ host ↔ server messaging
- `references/apps-sdk-messages.md` — the `App` class API: widget ↔ host ↔ server messaging, lifecycle & supersession
- `references/payload-budgeting.md` — host tool-result size caps, prune-then-truncate, heavy assets via `callServerTool`
- `references/abuse-protection.md` — Anthropic egress CIDRs, tiered rate limiting, `trust proxy`, response caching
- `references/directory-checklist.md` — pre-flight for connector-directory submission

View File

@@ -0,0 +1,60 @@
# Abuse protection for authless hosted servers
An authless StreamableHTTP server is reachable by anything on the internet.
There are three resources to protect: your compute, any upstream API quota
your tools consume, and egress bandwidth for large `callServerTool` payloads.
## You don't get a per-user identity
In authless mode there is no token and stateless transport gives no session
ID. Traffic from claude.ai is proxied through Anthropic's egress — every web
user arrives from the same small set of IPs:
```
160.79.104.0/21
2607:6bc0::/48
```
(See https://platform.claude.com/docs/en/api/ip-addresses.)
Claude Desktop, Claude Code, and other hosts connect **directly from the
user's machine**, so those *do* have distinct per-user IPs. Per-IP limiting
therefore works for direct-connect clients; for claude.ai you can only limit
the aggregate Anthropic pool. If true per-user limits matter, that's the
trigger to add OAuth.
## Tiered token-bucket (per-replica backstop)
```ts
const ANTHROPIC_CIDRS = ["160.79.104.0/21", "2607:6bc0::/48"];
const TIERS = {
anthropic: { capacity: 600, refillPerSec: 100 }, // shared pool
other: { capacity: 30, refillPerSec: 2 }, // per-IP
};
```
Match `req.ip` against the CIDRs, pick a bucket (`"anthropic"` or
`"ip:<addr>"`), 429 + `Retry-After` on exhaust. This is a per-replica
backstop — cross-replica enforcement belongs at the edge (Cloudflare, Cloud
Armor), which keeps the containers stateless.
## `trust proxy` must match your topology
`req.ip` only honours `X-Forwarded-For` if `app.set('trust proxy', N)` is
set. `true` trusts every hop, which lets a direct client send
`X-Forwarded-For: 160.79.108.42` and claim the Anthropic tier. Set it to the
exact number of trusted hops (e.g. `1` behind a single LB, `2` behind
Cloudflare → origin LB) and **never `true` in production**.
## Hard-allowlisting Anthropic IPs is a product decision
Blocking everything outside `160.79.104.0/21` locks out Desktop, Claude Code,
and every other MCP host. Use the CIDRs to **tier** rate limits, not to gate
access, unless claude.ai-only is an explicit goal.
## Cache upstream responses
For tools that wrap a third-party API, an in-process LRU keyed on the
normalized query (TTL hours, no secrets in the key) is the primary cost
control — repeat queries become free and absorb thundering-herd. Rate limits
are the safety net, not the first line.

View File

@@ -2,6 +2,18 @@
The `@modelcontextprotocol/ext-apps` package provides the `App` class (browser side) and `registerAppTool`/`registerAppResource` helpers (server side). Messaging is bidirectional and persistent.
## Construction
```js
const app = new App(
{ name: "MyWidget", version: "1.0.0" },
{}, // capabilities
{ autoResize: true }, // options
);
```
`autoResize: true` wires a `ResizeObserver` that emits `ui/notifications/size-changed` so the host iframe height tracks your rendered content. Without it the frame is fixed-height and tall renders get clipped — set it for any widget whose height depends on data.
---
## Widget → Host
@@ -63,6 +75,26 @@ card.querySelector("a").addEventListener("click", (e) => {
Host-mediated download (sandbox blocks direct `<a download>`). `content` is a base64 string.
```js
const csv = rows.map((r) => Object.values(r).join(",")).join("\n");
app.downloadFile({
name: "export.csv",
mimeType: "text/csv",
content: btoa(unescape(encodeURIComponent(csv))),
});
```
### `app.requestDisplayMode({ mode })`
Ask the host to switch the widget between `"inline"`, `"pip"`, or `"fullscreen"`. Check `getHostContext().availableDisplayModes` first; hide the control if the mode isn't offered. The host responds by firing `onhostcontextchanged` with new `displayMode` and `containerDimensions` — re-render at the new size.
```js
if (app.getHostContext()?.availableDisplayModes?.includes("fullscreen")) {
expandBtn.hidden = false;
expandBtn.onclick = () => app.requestDisplayMode({ mode: "fullscreen" });
}
```
---
## Host → Widget
@@ -84,9 +116,22 @@ app.ontoolresult = ({ content }) => {
Fires with the arguments Claude passed to the tool. Useful if the widget needs to know what was asked for (e.g., highlight the search term).
### `app.ontoolinputpartial = ({ arguments }) => {...}` / `app.ontoolcancelled = () => {...}`
`ontoolinputpartial` fires while Claude is still streaming arguments — use it to show a skeleton ("Preparing: <title>…") before the result lands. `ontoolcancelled` fires if the call is aborted; clear the skeleton.
### `app.getHostContext()` / `app.onhostcontextchanged = (ctx) => {...}`
Read and subscribe to host context`theme` (`"light"` / `"dark"`), locale, etc. Call `getHostContext()` **after** `connect()`. Subscribe for live updates (user toggles dark mode mid-conversation).
Read and subscribe to host context. Call `getHostContext()` **after** `connect()`. Subscribe for live updates (user toggles dark mode, expands to fullscreen).
| `ctx.` field | Use |
|---|---|
| `theme` | `"light"` / `"dark"` — toggle a `.dark` class |
| `styles.variables` | Host CSS tokens — pass to `applyHostStyleVariables()` so colors/fonts match host chrome |
| `displayMode` / `availableDisplayModes` | Current mode and which `requestDisplayMode` targets are valid |
| `containerDimensions.{maxHeight,width}` | Size your render to this instead of hard-coded px |
| `deviceCapabilities.touch` | Switch hover-only affordances to tap (`pointerdown`) |
| `safeAreaInsets` | Padding for notches / composer overlay |
```js
const applyTheme = (t) =>
@@ -129,14 +174,36 @@ No `{ notify }` destructure — `extra` is `RequestHandlerExtra`; progress goes
## Lifecycle
1. Claude calls a tool with `_meta.ui.resourceUri` declared
2. Host fetches the resource (your HTML) and renders it in an iframe
2. Host fetches the resource (your HTML) and mounts a **fresh iframe** for this call
3. Widget script runs, sets handlers, calls `await app.connect()`
4. Host pipes the tool's return value → `ontoolresult` fires
5. Widget renders, user interacts
6. Widget calls `sendMessage` / `updateModelContext` / `callServerTool` as needed
7. Widget persists until conversation context moves on — subsequent calls to the same tool reuse the iframe and fire `ontoolresult` again
7. Iframe persists in the transcript; **the next call to the same tool mounts another iframe** alongside it
There's no explicit "submit and close" — the widget is a long-lived surface.
There's no explicit "submit and close" — each instance is long-lived, but instances are not reused across calls.
### Supersession
Because earlier instances stay mounted, a click on a stale widget can `sendMessage` after a newer one has rendered. Detect this with a `BroadcastChannel` and make older instances inert:
```js
let superseded = false;
const seq = Date.now() + Math.random();
const bc = new BroadcastChannel("my-widget");
bc.onmessage = (e) => {
if (e.data?.seq > seq) {
superseded = true;
document.body.classList.add("superseded"); // opacity:.45; pointer-events:none
}
};
bc.postMessage({ seq });
// Guard outbound calls:
function safeSend(msg) {
if (!superseded) app.sendMessage(msg);
}
```
---

View File

@@ -0,0 +1,18 @@
# Connector-directory submission checklist
Pre-flight before submitting a remote MCP app to the Claude connector
directory. Each item is a hard review criterion.
| Area | Requirement |
|---|---|
| **Auth** | OAuth (DCR or CIMD) or **`none`** (authless). Static bearer tokens are private-deploy only and block listing. Authless is valid for public-data servers — the server holds any upstream API keys. |
| **Tool annotations** | Every tool sets `annotations.title` plus the relevant hints: `readOnlyHint: true` for fetch/search tools, `destructiveHint` / `idempotentHint` for writes, `openWorldHint: true` if the tool reaches an external system. |
| **Tool names** | ≤ 64 characters, snake/kebab case. |
| **Widget layout** | Inline height ≤ 500px, no nested scroll containers, 44pt minimum touch targets, WCAG-AA contrast in both themes. |
| **Theming** | `html, body { background: transparent }`, `<meta name="color-scheme" content="light dark">`, adopt host CSS tokens via `applyHostStyleVariables`. |
| **External links** | Use `app.openLink`. Declare each origin (e.g. `https://api.example.com`) in the connector's *Allowed link URIs* so the link skips the confirm modal. |
| **Helper tools** | Widget-only tools (geometry/image fetchers) carry `_meta.ui.visibility: ["app"]` so they don't appear in Claude's tool list. |
| **Screenshots** | 35 PNGs, ≥ 1000px wide, cropped to the app response only — no prompt text in frame. |
See `abuse-protection.md` for rate-limit and IP-tiering guidance once the
authless endpoint is public.

View File

@@ -122,23 +122,38 @@ that survives un-inlined.
---
## Dark mode
## Theme & host styles
```js
const applyTheme = (theme) =>
document.documentElement.classList.toggle("dark", theme === "dark");
The host renders the iframe inside its own card chrome — paint a **transparent** background and adopt host CSS tokens so the widget blends in across light/dark and across hosts.
app.onhostcontextchanged = (ctx) => applyTheme(ctx.theme);
await app.connect();
applyTheme(app.getHostContext()?.theme);
```html
<meta name="color-scheme" content="light dark" />
```
```css
:root { --ink:#0f1111; --bg:#fff; color-scheme:light; }
:root.dark { --ink:#e6e6e6; --bg:#1f2428; color-scheme:dark; }
:root {
--ink: var(--color-text-primary, #0f1111);
--sub: var(--color-text-secondary, #5a6270);
--line: var(--color-border-default, #e3e6ea);
}
html, body { background: transparent; color: var(--ink); }
:root.dark .thumb { mix-blend-mode: normal; } /* multiply → images vanish in dark */
```
```js
const { App, applyHostStyleVariables } = globalThis.ExtApps;
function applyHostContext(ctx) {
document.documentElement.classList.toggle("dark", ctx?.theme === "dark");
if (ctx?.styles?.variables) applyHostStyleVariables(ctx.styles.variables);
}
app.onhostcontextchanged = applyHostContext;
await app.connect();
applyHostContext(app.getHostContext());
```
`applyHostStyleVariables` writes the host's `--color-*` / `--font-*` / `--border-radius-*` tokens onto `:root`; the hex values above are fallbacks for hosts that don't supply them.
---
## Debugging

View File

@@ -0,0 +1,54 @@
# Payload budgeting
Hosts cap tool-result text. claude.ai and Claude Desktop truncate at roughly
**150,000 characters**; Claude Code at ~25k tokens. When a tool result exceeds
the cap, the host substitutes a file-pointer string in place of your JSON. The
widget then receives non-JSON in `ontoolresult`, `JSON.parse` throws, and the
user sees something like *"Bad payload: SyntaxError: Unexpected token 'E'"*
with no hint that size was the cause.
## Symptom → cause
| Symptom | Likely cause |
|---|---|
| Widget shows a JSON parse error on `content[0].text` | Result over the host cap; host swapped in a file-pointer string |
| Works for one query, breaks for "all of X" | Row count × column count crossed the cap |
| Works in MCP Inspector, breaks in Desktop | Inspector has no cap; Desktop does |
## Strategy
Cap your own payload at ~130KB and degrade in order:
1. **Ship full rows** when `JSON.stringify(rows).length` is under the cap.
2. **Prune columns** to those the rendering spec actually references. Walk the
spec for both `field: "..."` keys *and* `datum.X` / `datum['X']` inside
expression strings — if the spec aliases a column via a `calculate`
transform, the alias appears as `field:` but the source column only appears
as `datum.X`, and dropping it leaves the widget with NaN.
3. **Truncate rows** as a last resort and include `{ truncated: N }` in the
payload so the widget can label it.
```ts
const MAX = 130_000;
let out = rows;
if (JSON.stringify(out).length > MAX) {
const keep = referencedFields(spec); // field: + datum.X refs
out = rows.map((r) => pick(r, keep));
if (JSON.stringify(out).length > MAX) {
const per = JSON.stringify(out[0] ?? {}).length || 1;
out = out.slice(0, Math.floor(MAX / per));
}
}
```
## Heavy assets go via `callServerTool`, not the result
Geometry, image bytes, or any blob the widget needs but Claude doesn't should
be served by a separate tool the widget calls after mount:
```js
const topo = await app.callServerTool({ name: "get-topojson", arguments: { level } });
```
Mark that helper tool with `_meta.ui.visibility: ["app"]` so it doesn't appear
in Claude's tool list.