mcplane 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +108 -18
- package/dist/agent-plugins.d.ts +6 -0
- package/dist/agent-plugins.js +76 -0
- package/dist/cdp.d.ts +49 -0
- package/dist/cdp.js +253 -0
- package/dist/checks/ard.d.ts +67 -0
- package/dist/checks/ard.js +215 -0
- package/dist/checks/auth.js +2 -1
- package/dist/checks/chain.d.ts +45 -0
- package/dist/checks/chain.js +171 -0
- package/dist/checks/listing.js +23 -14
- package/dist/checks/oauth.d.ts +45 -0
- package/dist/checks/oauth.js +64 -0
- package/dist/checks/readiness.d.ts +58 -0
- package/dist/checks/readiness.js +230 -0
- package/dist/checks/tools.js +38 -7
- package/dist/checks/vercel.d.ts +27 -0
- package/dist/checks/vercel.js +153 -0
- package/dist/cli.js +54 -6
- package/dist/core.d.ts +1 -0
- package/dist/core.js +20 -4
- package/dist/demo.d.ts +60 -0
- package/dist/demo.js +573 -0
- package/dist/drift.js +10 -3
- package/dist/evals.d.ts +34 -0
- package/dist/evals.js +75 -0
- package/dist/extras.js +1 -1
- package/dist/images.d.ts +21 -0
- package/dist/images.js +99 -0
- package/dist/listings.d.ts +14 -0
- package/dist/listings.js +40 -1
- package/dist/mcp-client.d.ts +1 -5
- package/dist/mcp-client.js +8 -1
- package/dist/mcp-server.js +21 -11
- package/dist/packs.d.ts +48 -18
- package/dist/packs.js +380 -90
- package/dist/publish.js +13 -4
- package/dist/report.js +30 -13
- package/dist/skills.d.ts +21 -0
- package/dist/skills.js +180 -0
- package/dist/submissions.js +2 -1
- package/dist/types.d.ts +37 -4
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/video.d.ts +58 -0
- package/dist/video.js +190 -0
- package/dist/zip.d.ts +16 -2
- package/dist/zip.js +65 -5
- package/package.json +4 -1
- package/schema.json +86 -12
- package/schemas/agent-plugins-1.0.0/LICENSE +202 -0
- package/schemas/agent-plugins-1.0.0/mcp.schema.json +120 -0
- package/schemas/agent-plugins-1.0.0/plugin.schema.json +65 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0
|
|
4
|
+
|
|
5
|
+
New command
|
|
6
|
+
- `mcplane demo` (experimental): records the developer-mode demo video ChatGPT's review asks for, from your positive tests, by driving the ChatGPT desktop app on a Mac. It asks before sending any prompt, hides the sidebar, and writes the video to `.mcplane/packs/chatgpt/demo.mp4`. It finds its way by ChatGPT's button labels, so a ChatGPT release can break it.
|
|
7
|
+
|
|
8
|
+
New stores and checks
|
|
9
|
+
- Vercel Connect: preflight against its Required, Recommended and Optional tiers, from your discovery documents alone; `pack vercel-connect`; its directory in `listings`; drift rules. Manifests without `stores` now get these checks too, so an OAuth server with a Required gap exits 1 where it didn't before.
|
|
10
|
+
- Readiness for MCP 2026-07-28, in its own preflight section and never blocking: `server/discover`, `initialize` negotiated down, the `Mcp-Method` and `Mcp-Name` headers and CORS for them, issuer, CIMD, RFC 9207 `iss`, MCP Events.
|
|
11
|
+
- ARD: preflight reads `/.well-known/ard.json` (and the older `ai-catalog.json`) and validates it against ARD v0.91 and Lighthouse 13.5; `pack ard` writes a manifest both accept.
|
|
12
|
+
- `pack claude-eval`: your review tests as a `claude plugin eval` suite.
|
|
13
|
+
- Sign-in chain: plain GETs from the authorization endpoint to the sign-in page and the identity-provider hosts it loads, flagging bot challenges (`cf-mitigated: challenge`) and 403s. Nothing signs in and no client is registered. `server.signIn` or `--sign-in` names pages to check when the authorization server wants a registered client first.
|
|
14
|
+
- A new check level, `info`: a note that never blocks and isn't counted as something to review. Code that maps levels one by one needs to handle it.
|
|
15
|
+
|
|
16
|
+
OpenAI's annotation rules
|
|
17
|
+
- No more drafted hint justifications or 200-character warnings: OpenAI no longer asks for them. Any in `mcplane.json` become optional appeal notes.
|
|
18
|
+
- A new warning when a write that overwrites, revokes or deletes says `destructiveHint: false`.
|
|
19
|
+
- A ChatGPT hint change is a note in `drift`, so `drift --ci` no longer fails on it.
|
|
20
|
+
|
|
21
|
+
ChatGPT plugin ZIP
|
|
22
|
+
- Bundles Agent Skills from `chatgpt.skills`, checking names against folders, description length, front matter and scripts a skill runs but doesn't ship.
|
|
23
|
+
- Checks `plugin.json` and `mcp.json` against the Agent Plugins 1.0.0 JSON Schemas, and the root description against 1,024 characters (`plugin_description_too_long`, also in preflight).
|
|
24
|
+
- Icons can be local files (`icon`, `chatgpt.logo`, `composerIcon`, `logoDark`), and sizes are read from PNG, JPEG, WebP and SVG bytes, in preflight too.
|
|
25
|
+
- Builds behind sign-in without `--token`, and says what it couldn't check; `--tools <file>` takes a saved `tools/list` result.
|
|
26
|
+
- Entries are dated 1980-01-01 and keep Unix modes, so scripts stay executable and the same files give the same ZIP.
|
|
27
|
+
- The 10 KB icon warning is gone: the ZIP takes icons up to 5 MiB, and only developer mode's own upload is capped at 10 KB. An icon over 5 MiB now blocks.
|
package/README.md
CHANGED
|
@@ -17,9 +17,10 @@ No terminal handy? Paste your URL at [reviewtimes.fyi/check](https://reviewtimes
|
|
|
17
17
|
✗ Every tool has a human-readable title [Claude connectors, Claude plugins]
|
|
18
18
|
search_docs, fetch_page
|
|
19
19
|
Add "title" (or annotations.title) to each tool. Anthropic rejects connectors without them.
|
|
20
|
-
!
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
! Reviewers can reach your sign-in page
|
|
21
|
+
https://accounts.example.com/sign-in: Cloudflare challenge (cf-mitigated: challenge)
|
|
22
|
+
A bot challenge or a 403 on the sign-in chain leaves the OAuth window blank for reviewers, who sign
|
|
23
|
+
in from data-centre networks. Exempt the authorization endpoint, consent page and sign-in hosts …
|
|
23
24
|
✓ 15 checks passed (--verbose to list them)
|
|
24
25
|
|
|
25
26
|
2 blocking, 1 to review.
|
|
@@ -27,11 +28,11 @@ No terminal handy? Paste your URL at [reviewtimes.fyi/check](https://reviewtimes
|
|
|
27
28
|
|
|
28
29
|
## Why
|
|
29
30
|
|
|
30
|
-
An MCP server now has a dozen places to be listed: ChatGPT, Claude, Cursor, Grok, Muse, the official MCP Registry, Docker, Smithery and the directories that read from them. Each has its own form, its own rules and its own idea of what an update is. Most rejections are for the same few mechanical things, and each one costs you days in the queue. With one app that's annoying. With seventeen you stop shipping.
|
|
31
|
+
An MCP server now has a dozen places to be listed: ChatGPT, Claude, Cursor, Grok, Muse, Vercel Connect, the official MCP Registry, Docker, Smithery and the directories that read from them. Each has its own form, its own rules and its own idea of what an update is. Most rejections are for the same few mechanical things, and each one costs you days in the queue. With one app that's annoying. With seventeen you stop shipping.
|
|
31
32
|
|
|
32
33
|
iOS had the same problem in 2014. fastlane fixed it by putting your metadata in the repo, checking it before Apple could reject it, and turning every release into one command. mcplane does that for agent-era stores.
|
|
33
34
|
|
|
34
|
-
An agent with a browser could fill in a form. It can't tell you that your DNS will make ChatGPT's reviewer time out, that
|
|
35
|
+
An agent with a browser could fill in a form. It can't tell you that your DNS will make ChatGPT's reviewer time out, that ChatGPT counts overwriting a page as destructive, or which of your seventeen listings went stale on Tuesday. That's the part mcplane does.
|
|
35
36
|
|
|
36
37
|
## Use it from your agent
|
|
37
38
|
|
|
@@ -58,7 +59,7 @@ npx mcplane fleet --root ~/Projects
|
|
|
58
59
|
✓ Review Times preflight clean · listed on mcp-registry, glama, chatgpt
|
|
59
60
|
✗ Invoice Bot 2 blocking · waiting on claude-connectors · listed on mcp-registry
|
|
60
61
|
✗ Weather Pro 1 store update needed · listed on mcp-registry, claude-connectors · 1 listing fix
|
|
61
|
-
chatgpt:
|
|
62
|
+
chatgpt: Listing text changed (description)
|
|
62
63
|
claude-connectors: listing doesn't show: forecast_hourly (ask the review team to resync)
|
|
63
64
|
|
|
64
65
|
3 projects, 2 need attention.
|
|
@@ -72,11 +73,12 @@ npx mcplane fleet --root ~/Projects
|
|
|
72
73
|
|---|---|---|
|
|
73
74
|
| Official MCP Registry | **Publishes**: writes `server.json`, runs `mcp-publisher`, signs in with GitHub OIDC in Actions. Glama and PulseMCP import from it; GitHub's MCP gallery (VS Code) syncs it after a one-time manual onboarding | Versions can't change: bump, or a prerelease like `1.2.0-1` for listing-only edits. mcplane checks what's already published |
|
|
74
75
|
| Grok plugins | **Publishes**: opens the pull request to `xai-org/plugin-marketplace`, validated with xAI's own scripts | Opens a pin-bump PR when xAI's daily bump lags |
|
|
75
|
-
| ChatGPT | **Builds the plugin ZIP** the portal uploads since DevDay: `plugin.json` (Agent Plugins format) with listing, icons, 5+3 tests, demo video, release notes and translations, plus `mcp.json
|
|
76
|
+
| ChatGPT | **Builds the plugin ZIP** the portal uploads since DevDay: `plugin.json` (Agent Plugins format) with listing, icons, 5+3 tests, demo video, release notes and translations, plus `mcp.json` and your Agent Skills. Checked against the Agent Plugins 1.0.0 schema and OpenAI's submission rules | Tool changes, hints included, roll out after OpenAI's scans. Listing changes need a new ZIP and review, and drift says when |
|
|
76
77
|
| Claude connectors | **Prepares** every field of the directory form | Tool changes are live on deploy; listing edits (tool names included) are reviewed |
|
|
77
78
|
| Claude plugins | **Prepares** and checks the plugin repo | The portal tracks your branch; drift tells you a new version is waiting for review |
|
|
78
79
|
| Cursor | **Prepares** and checks the plugin repo | Pinned to the commit first added; drift flags when you've moved on |
|
|
79
80
|
| Muse | **Prepares** the form | Flags changes |
|
|
81
|
+
| Vercel Connect | **Checks** your OAuth discovery documents against Vercel's Required, Recommended and Optional tiers, and **prepares** Submit a Service: every field, the OAuth method as Connect will discover it, and the token test each OAuth method needs | Listing edits go back through review (Edit and Resubmit); tools aren't listed, so they can change freely |
|
|
80
82
|
| awesome-remote-mcp-servers | The three-line entry with your Glama connector badge and auth marker, ready for the PR | |
|
|
81
83
|
| Cline, Glama, LobeHub, Smithery, Docker, awesome-mcp-servers | The prefilled issue, `glama.json`, CLI command or entry | |
|
|
82
84
|
| mcp.so, MCP Market, mcpservers.org, cursor.directory | `mcplane pack directories`: one sheet with every value to paste | |
|
|
@@ -89,14 +91,66 @@ Checks come from real rejections, most of them ours:
|
|
|
89
91
|
|
|
90
92
|
- **Server**: `initialize`, `tools/list` and `ping` work. An OAuth server that initializes without a token also lists its tools without one (the half-way state leaves a new ChatGPT connector stuck on "no actions available"). Unknown methods return `-32601` (OpenAI's tool scan probes `server/discover` and gives up on a crash). The URL works with a trailing slash. CORS preflight answers. Automated clients aren't blocked by a bot filter. A TLS 1.2 client connects. DNS doesn't advertise Encrypted ClientHello, which review proxies reset without a trace in your logs.
|
|
91
93
|
- **OAuth**: unauthenticated calls get `401` with `resource_metadata`. Protected-resource and authorization-server metadata resolve, and PKCE S256 is supported. With `--register`, Dynamic Client Registration is tested, including the `cursor://` and other native redirect schemes that locked out every desktop client for us.
|
|
92
|
-
- **
|
|
93
|
-
- **
|
|
94
|
+
- **Sign-in chain**: plain GETs, the way a reviewer's browser goes: the authorization endpoint, the sign-in page it sends people to, and the identity-provider hosts that page loads. A bot challenge (`cf-mitigated: challenge`) or a 403 anywhere on it is flagged: every MCP check can pass while reviewers never reach your sign-in page. Nothing signs in and no client is registered, so an authorization server that wants a registered client stops the walk at its error page: name your sign-in pages in `server.signIn` (or `--sign-in <url>`) and they're checked too. A pass from your own network proves less than one from a cloud VM or CI.
|
|
95
|
+
- **Tools**: every tool has a title and explicit `readOnlyHint`, `destructiveHint` and `openWorldHint`, and the hints are consistent. Tools declare `outputSchema`. Descriptions don't instruct the model. Writes aren't authorised by a token passed through the chat (Anthropic rejected exactly that). Hints follow OpenAI's current definitions: `openWorldHint` for public or open-ended destinations (read-only web search included; connectivity alone doesn't decide it), `destructiveHint` for messages that can't be unsent and for overwriting, revoking access or deleting. No catch-all request tools, no tool names over 64 characters, no sensitive inputs, no upgrade or pricing copy.
|
|
96
|
+
- **Listing**: the icon is square and at least 512px behind a direct link (PNG, JPEG, WebP or SVG, measured from the bytes), and `/favicon.ico` resolves. The privacy policy covers collection, retention and user controls. Support is a web page. Name, subtitle, one-liner, description and starter prompts fit each store's limits, including the 1,024 characters the ChatGPT package allows for its root description. Every test case uses tools the live server actually has. The ChatGPT domain challenge is served.
|
|
94
97
|
- **Plugin repo**: public, on GitHub, with `.claude-plugin/plugin.json`, and no root `SKILL.md` shadowing the plugin's skills. Grok plugins come from an organisation.
|
|
98
|
+
- **Vercel Connect**: what its [For Service Providers](https://vercel.com/docs/connect/providers) page asks for, read from your discovery documents alone (nothing is registered). Required items block: metadata served as JSON (at the path-suffixed form when the issuer has a path), `token_endpoint`, `grant_types_supported`, and OAuth and OpenID Connect documents that agree. Recommended items warn: DCR or CIMD so Connect can create the client, a `token_endpoint_auth_methods_supported` it can use, PKCE S256, refresh tokens. Optional ones are notes: protected-resource metadata, `revocation_endpoint`, `scopes_supported`. What needs a real token (`expires_in`, refresh rotation, the `https://connect.vercel.com/callback` redirect) is on the list of things to check yourself.
|
|
99
|
+
- **ARD**: whether your domain publishes an [Agentic Resource Discovery](https://agenticresourcediscovery.org/spec/) manifest at `/.well-known/ard.json`, whether it's valid, whether it lists this server, and whether Lighthouse 13.5 (which still reads the older `/.well-known/ai-catalog.json`) accepts it.
|
|
95
100
|
|
|
96
|
-
`--json` for machines; exit code 1 when something blocks.
|
|
101
|
+
`--json` for machines; exit code 1 when something blocks. A check at level `info` is a note: it never blocks and isn't counted as something to review.
|
|
97
102
|
|
|
98
103
|
Preflight also lists what no script can see and that has still cost real submissions. For example: a reviewer account a stranger can actually use (not your Google login, email already confirmed, no 2FA), and a safe target for write tools so reviewers don't post to a real timeline.
|
|
99
104
|
|
|
105
|
+
## Readiness for MCP 2026-07-28
|
|
106
|
+
|
|
107
|
+
The [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28/changelog) drops the `initialize` handshake for per-request metadata and `server/discover`, requires `Mcp-Method` and `Mcp-Name` headers, and deprecates Dynamic Client Registration for Client ID Metadata Documents. Preflight reports how your server meets it in its own section. These checks never block: a warning means a 2026-07-28 client would fail against you today, a note means something deprecated or optional.
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
Readiness for MCP 2026-07-28 (never blocking)
|
|
111
|
+
i Speaks MCP 2026-07-28
|
|
112
|
+
legacy only: server/discover got HTTP 200, JSON-RPC -32601 Method not found: server/discover
|
|
113
|
+
! An initialize that offers 2026-07-28 is negotiated down
|
|
114
|
+
initialize answered with "2026-07-28"
|
|
115
|
+
2026-07-28 has no initialize handshake, so answering one with it tells the client you speak something you don't. …
|
|
116
|
+
i CORS allows the Mcp-Method and Mcp-Name headers
|
|
117
|
+
Access-Control-Allow-Headers: content-type, mcp-protocol-version, mcp-session-id, authorization
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- **Era**: a 2026-07-28 `server/discover` gets a `DiscoverResult` (modern), a JSON-RPC error or a 4xx (legacy, and clients that speak both fall back to `initialize`), or a crash (a warning).
|
|
121
|
+
- **Negotiation**: an `initialize` offering `2026-07-28` is answered with a version you implement, not echoed back.
|
|
122
|
+
- **Headers**: requests carrying `Mcp-Method` and `Mcp-Name` work, and your CORS preflight allows them.
|
|
123
|
+
- **Authorization**: the metadata's `issuer` matches the URL it was fetched from (2026-07-28 clients must refuse it otherwise), `client_id_metadata_document_supported`, and RFC 9207 `authorization_response_iss_parameter_supported`.
|
|
124
|
+
- **MCP Events**: whether `events/list` answers. Events is a [draft](https://github.com/modelcontextprotocol/experimental-ext-triggers-events) from the Triggers and Events Working Group, not part of 2026-07-28, so this is only ever a note.
|
|
125
|
+
|
|
126
|
+
Servers behind sign-in need `--token` for the probes; the authorization checks run from the discovery documents either way.
|
|
127
|
+
|
|
128
|
+
## ARD manifest
|
|
129
|
+
|
|
130
|
+
`mcplane pack ard` writes `.mcplane/packs/ard/ard.json`: one entry for your server (`application/mcp-server-card+json`, pointing at your server card when you serve one), with representative queries from your prompts and tests and capabilities from your tool names. It validates against [ARD v0.91](https://agenticresourcediscovery.org/spec/) and against the older schema [Lighthouse 13.5](https://github.com/GoogleChrome/lighthouse/blob/v13.5.0/core/audits/agentic/ard-schema.js) audits. Serve it at `/.well-known/ard.json`, and either at `/.well-known/ai-catalog.json` too or from an `Agentmap:` line in robots.txt, which Lighthouse reads first. ARD is a proposal, so treat it as cheap insurance rather than a requirement.
|
|
131
|
+
|
|
132
|
+
## Plugin evals
|
|
133
|
+
|
|
134
|
+
If a Claude Code plugin ships your server, `mcplane pack claude-eval` turns your review tests into a suite for [`claude plugin eval`](https://code.claude.com/docs/en/plugin-evals), which scores the plugin against a no-plugin baseline. Each positive test becomes a case graded on the reply, with the tools it names as indicators; each negative test a case that must not call your server. Tools answer from mocks built from your live `tools/list`, so a run needs no network or sign-in and writes nothing. The plugin and server names come from the plugin repo (or `claudePlugin` in mcplane.json).
|
|
135
|
+
|
|
136
|
+
Copy `.mcplane/packs/claude-eval/evals/` into the plugin repo, then gate pull requests on it:
|
|
137
|
+
|
|
138
|
+
```yaml
|
|
139
|
+
# .github/workflows/plugin-eval.yml, in the plugin repo
|
|
140
|
+
on: [pull_request]
|
|
141
|
+
jobs:
|
|
142
|
+
eval:
|
|
143
|
+
runs-on: ubuntu-latest
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@v4
|
|
146
|
+
- run: npm install -g @anthropic-ai/claude-code
|
|
147
|
+
- run: claude plugin eval . --trust-plugin --json results.json --threshold 0.8 --model claude-sonnet-5 --judge-model claude-haiku-4-5 --no-publish --max-cost-usd 5
|
|
148
|
+
env:
|
|
149
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It exits 1 when a case scores under the threshold or a case file doesn't load. Every run and every llm grader is a model call on your account.
|
|
153
|
+
|
|
100
154
|
## Where you're listed
|
|
101
155
|
|
|
102
156
|
```sh
|
|
@@ -119,27 +173,56 @@ A form that returned 200 is a claim; the public record is the result. `listings`
|
|
|
119
173
|
- Cursor and Grok.
|
|
120
174
|
- Glama's registry import.
|
|
121
175
|
- Both awesome lists and Docker's catalog.
|
|
176
|
+
- Vercel Connect's directory, from the agent-readable copy of [vercel.com/connect/browse](https://vercel.com/connect/browse) (it has no feed; if that copy stops parsing, mcplane says so and sends you to the page).
|
|
122
177
|
|
|
123
178
|
Coverage decays quietly, so run it on a schedule (`--ci` exits 1 on a mismatch). `fleet` runs it for every project.
|
|
124
179
|
|
|
125
180
|
## ChatGPT's plugin ZIP
|
|
126
181
|
|
|
127
|
-
Since DevDay (September 2026) ChatGPT takes a plugin ZIP. `mcplane pack chatgpt` builds it from `mcplane.json`: `plugin.json` in the Agent Plugins format, with listing, icons, the 5 positive and 3 negative test cases, demo video, release notes and translations, plus `mcp.json
|
|
182
|
+
Since DevDay (September 2026) ChatGPT takes a plugin ZIP. `mcplane pack chatgpt` builds it from `mcplane.json`: `plugin.json` in the Agent Plugins format, with listing, icons, the 5 positive and 3 negative test cases, demo video, release notes and translations, plus `mcp.json` and any Agent Skills you bundle. It checks the package first: `plugin.json` and `mcp.json` against the [Agent Plugins 1.0.0](https://agent-plugins.org) JSON Schemas, then OpenAI's submission rules: name and description lengths (1,024 characters for the root description), category, the four https URLs, at most 3 starter prompts, square icons of 48 to 4096 px in any of PNG, JPEG, WebP or SVG, brand-colour contrast.
|
|
128
183
|
|
|
129
184
|
```json
|
|
130
185
|
"chatgpt": {
|
|
131
186
|
"demoVideo": "https://example.com/demo.mp4",
|
|
132
187
|
"releaseNotes": "Adds hourly forecasts.",
|
|
133
188
|
"capabilities": ["Search forecasts", "Compare cities"],
|
|
134
|
-
"brandColor": "#2357C6"
|
|
189
|
+
"brandColor": "#2357C6",
|
|
190
|
+
"logo": "assets/logo.png",
|
|
191
|
+
"skills": "skills"
|
|
135
192
|
}
|
|
136
193
|
```
|
|
137
194
|
|
|
138
|
-
|
|
195
|
+
- **Icons** can be URLs or local files (relative to `mcplane.json`), so the package builds offline. `logo` defaults to `icon`, which stays a URL for the other stores.
|
|
196
|
+
- **Skills**: `skills` is a folder of skill folders, or a list of them, each with a `SKILL.md`. They go in the ZIP under `skills/` with their scripts, executable bits kept. mcplane checks what clients and OpenAI refuse: a `name` that doesn't match its folder, a description over 1,024 characters, front matter YAML can't read, a script the `SKILL.md` runs but the folder doesn't hold. Hidden files stay out. Skills are scanned at upload; `"skills": []` ships the server alone.
|
|
197
|
+
- **Behind sign-in**: the ZIP doesn't need your tool list, so `pack chatgpt` builds without `--token` and says what it couldn't check. `--tools <file>` takes a saved `tools/list` result instead.
|
|
198
|
+
- The ZIP is reproducible: entries are dated 1980-01-01 and carry Unix modes, so the same files give the same bytes.
|
|
199
|
+
|
|
200
|
+
What stays with you: upload the ZIP, enter reviewer credentials and make the attestations. Bump `version` for every upload. Tool changes don't need a new ZIP; OpenAI's scans pick them up, and `drift` tracks extension metadata (sidebar, panels, file viewers) as part of each tool.
|
|
201
|
+
|
|
202
|
+
**Annotation rules.** OpenAI's [plugin guidelines](https://developers.openai.com/plugins/plugin-guidelines) now ask only for explicit `readOnlyHint`, `destructiveHint` and `openWorldHint` booleans on every tool; its automated review checks them, and you appeal if it flags one you believe is right. Justifications are no longer required, so the pack no longer drafts them: any you wrote in `mcplane.json` become optional appeal notes. Writes count as destructive unless they only add (overwriting, cancelling, revoking access and irreversible sends are all destructive, and an undo button doesn't change that), and preflight warns when a write that overwrites, revokes or deletes says `destructiveHint: false`. `openWorldHint` is about where a tool reaches, not whether it calls an API. A hint change is a note in `drift`, not an action: OpenAI re-checks annotations itself, so `drift --ci` doesn't fail on one.
|
|
203
|
+
|
|
204
|
+
## Demo video
|
|
205
|
+
|
|
206
|
+
ChatGPT's review asks for a video recorded in developer mode that shows the main use cases and tools, and it goes stale whenever your tools change. `mcplane demo` records it from your positive tests, the way fastlane's `snapshot` takes screenshots:
|
|
207
|
+
|
|
208
|
+
```sh
|
|
209
|
+
mcplane demo --only 1,2,3
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
On a Mac with the ChatGPT desktop app, it opens ChatGPT with a local debugging port and hides the sidebar. For each test it opens a new chat, @mentions your plugin (the developer-mode one, when two share the name), types the prompt, waits for the answer and opens the summary of tool calls. ffmpeg joins the scenes behind a title card, with each test's scenario as its caption, into `.mcplane/packs/chatgpt/demo.mp4`. Upload it where a reviewer can open it without signing in and set `chatgpt.demoVideo` to its URL.
|
|
213
|
+
|
|
214
|
+
It drives ChatGPT with your account, so:
|
|
215
|
+
- it lists the prompts and asks before it starts (`--yes` skips that), and marks tests that call tools which write;
|
|
216
|
+
- it records only while a test runs, with the sidebar (your chats and name) hidden;
|
|
217
|
+
- tool approvals are yours to click, unless you pass `--approve`, which clicks "Allow once" (never "Always allow");
|
|
218
|
+
- it quits ChatGPT afterwards so the debugging port doesn't stay open (`--keep-open` leaves it running);
|
|
219
|
+
- the chats stay in your history, and answers can draw on your ChatGPT memory, so watch the video before you share it.
|
|
220
|
+
|
|
221
|
+
`--plugin` names the plugin as ChatGPT shows it, `--work` records in Work mode and `--out` writes somewhere else. It needs ffmpeg (`brew install ffmpeg`). It finds its way around ChatGPT by the labels on buttons, so a ChatGPT release can break it; they're all in one table at the top of `src/demo.ts`.
|
|
139
222
|
|
|
140
223
|
## Keeping listings current
|
|
141
224
|
|
|
142
|
-
Every store calls your live server, so fixes reach users on their own. What doesn't: listing text, tool names in Claude's listing,
|
|
225
|
+
Every store calls your live server, so fixes reach users on their own. What doesn't: listing text, tool names in Claude's listing, registry versions and plugins pinned to a commit. Each store has different rules for these. When you record a submission, mcplane snapshots what the store is reviewing. `mcplane drift` compares your live server and `mcplane.json` against that snapshot and tells you what each store needs.
|
|
143
226
|
|
|
144
227
|
```sh
|
|
145
228
|
mcplane submitted chatgpt --version 1.2.0 --app-id asdk_app_…
|
|
@@ -219,8 +302,8 @@ For a reverse-DNS name, set an `MCP_PRIVATE_KEY` secret and mcplane signs in wit
|
|
|
219
302
|
|
|
220
303
|
```
|
|
221
304
|
init --url <mcp url> Create mcplane.json from your live server
|
|
222
|
-
preflight [--store <id>] Check against every store's rejection causes (--url, --json, --verbose, --register, --token)
|
|
223
|
-
pack <store> Write a submission pack to .mcplane/packs (or "pack directories")
|
|
305
|
+
preflight [--store <id>] Check against every store's rejection causes (--url, --json, --verbose, --register, --token, --sign-in)
|
|
306
|
+
pack <store> Write a submission pack to .mcplane/packs (or "pack directories", "pack ard", "pack claude-eval"; --tools)
|
|
224
307
|
publish <store> [--yes] Publish where the store allows it; dry run without --yes
|
|
225
308
|
submitted <store> Record a submission (--date, --kind, --version, --app-id, --private)
|
|
226
309
|
decided <store> <outcome> approved | rejected | withdrawn
|
|
@@ -229,13 +312,14 @@ drift [--ci] What each store needs since it last saw your serve
|
|
|
229
312
|
baseline <store> Mark an existing live listing as the drift baseline
|
|
230
313
|
listings [--ci] Where you're listed, and whether what went live matches
|
|
231
314
|
try Install links for every client, for you and your testers
|
|
315
|
+
demo Record ChatGPT's review video from your positive tests (macOS; --only, --approve, --out)
|
|
232
316
|
fleet [--root <dir>] Every project at a glance (--quick, --json, --ci)
|
|
233
317
|
fleet run <command...> Run a command in every project
|
|
234
318
|
lanes / <lane> List lanes / run one
|
|
235
319
|
mcp Run as an MCP server over stdio
|
|
236
320
|
```
|
|
237
321
|
|
|
238
|
-
Servers behind sign-in: pass `--token` or set `MCPLANE_TOKEN` so tool checks can run.
|
|
322
|
+
Servers behind sign-in: pass `--token` or set `MCPLANE_TOKEN` so tool checks can run. Packs also take `--tools <file>`, a saved `tools/list` result.
|
|
239
323
|
|
|
240
324
|
## Use the checks in your own code
|
|
241
325
|
|
|
@@ -264,19 +348,25 @@ Each store can have its own listing text, the way fastlane keeps metadata per la
|
|
|
264
348
|
|
|
265
349
|
Packs, checks, `drift` and `listings` all use the right text for each store.
|
|
266
350
|
|
|
351
|
+
Store fields with no shared equivalent have their own blocks: `chatgpt` for the plugin ZIP (including its `logo` and `skills`), `grok` for the marketplace entry, and `vercelConnect` for a REST API target (`apiBase`), default OAuth `scopes` and an `apiKey` connection method. For OAuth servers, `server.signIn` lists the sign-in pages your consent screen uses, for the sign-in chain check.
|
|
352
|
+
|
|
267
353
|
## fastlane, mapped
|
|
268
354
|
|
|
269
355
|
| fastlane | mcplane |
|
|
270
356
|
|---|---|
|
|
271
357
|
| `precheck` | `preflight` |
|
|
272
|
-
| `deliver` + metadata in the repo | `mcplane.json`, `pack`, `publish
|
|
358
|
+
| `deliver` + metadata in the repo | `mcplane.json`, `pack`, `publish`; for ChatGPT, one reproducible ZIP with your skills |
|
|
273
359
|
| `download_metadata` | `listings`: reads what went live back and diffs it |
|
|
274
360
|
| `pilot` (TestFlight testers) | `try`: install links for every client |
|
|
361
|
+
| `snapshot` (screenshots on every device) | `demo`: the review video, recorded in ChatGPT from your tests |
|
|
275
362
|
| Fastfile lanes | `lanes` |
|
|
276
363
|
| `fastlane init` | `init`, and the **onboard** prompt that writes the listing for you |
|
|
277
364
|
| CI-first | `--ci` exit codes, JSON output, GitHub Action |
|
|
365
|
+
| `scan` (tests in CI) | `pack claude-eval`: your review tests as a `claude plugin eval` suite, scored against a no-plugin baseline |
|
|
278
366
|
| New: agent-driven | the whole thing is an MCP server |
|
|
279
367
|
| New: drift | knows what each store needs when your server changes |
|
|
368
|
+
| New: readiness | how your server meets the next MCP revision (2026-07-28), before clients require it |
|
|
369
|
+
| New: discovery | `pack ard`: the ARD manifest agents and Lighthouse look for |
|
|
280
370
|
|
|
281
371
|
## Contributing
|
|
282
372
|
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
2
|
+
export declare const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
3
|
+
/** OpenAI's limit for plugin.json's root description (plugin_description_too_long). */
|
|
4
|
+
export declare const DESCRIPTION_MAX = 1024;
|
|
5
|
+
/** Every way plugin.json and mcp.json break the Agent Plugins 1.0.0 schemas; empty when both pass. */
|
|
6
|
+
export declare function schemaProblems(plugin: unknown, mcp: unknown): Promise<string[]>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Plugins 1.0.0, the open package format ChatGPT's portal takes: plugin.json and mcp.json
|
|
3
|
+
* checked against the official JSON Schemas.
|
|
4
|
+
*
|
|
5
|
+
* schemas/agent-plugins-1.0.0/ holds unmodified copies of https://agent-plugins.org/schemas/1.0.0/
|
|
6
|
+
* (github.com/agentplugins/agent-plugins-spec), Apache License 2.0; the licence is in the same folder.
|
|
7
|
+
*/
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { Ajv2020 } from 'ajv/dist/2020.js';
|
|
10
|
+
export const PLUGIN_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json';
|
|
11
|
+
export const MCP_SCHEMA = 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json';
|
|
12
|
+
/** OpenAI's limit for plugin.json's root description (plugin_description_too_long). */
|
|
13
|
+
export const DESCRIPTION_MAX = 1024;
|
|
14
|
+
/** The MCP server forms, by the "type" that selects each one. */
|
|
15
|
+
const SERVER_FORMS = { stdio: 'stdioServer', 'streamable-http': 'streamableHttpServer', sse: 'sseServer' };
|
|
16
|
+
let compiled;
|
|
17
|
+
function validators() {
|
|
18
|
+
compiled ??= (async () => {
|
|
19
|
+
const load = async (file) => JSON.parse(await readFile(new URL(`../schemas/agent-plugins-1.0.0/${file}`, import.meta.url), 'utf8'));
|
|
20
|
+
const [plugin, mcp] = await Promise.all([load('plugin.schema.json'), load('mcp.schema.json')]);
|
|
21
|
+
const ajv = new Ajv2020({ allErrors: true, strict: true, logger: false });
|
|
22
|
+
const v = { plugin: ajv.compile(plugin), mcp: ajv.compile(mcp) };
|
|
23
|
+
const forms = Object.fromEntries(Object.entries(SERVER_FORMS).map(([type, def]) => [type, ajv.getSchema(`${MCP_SCHEMA}#/$defs/${def}`)]));
|
|
24
|
+
return { ...v, forms };
|
|
25
|
+
})();
|
|
26
|
+
return compiled;
|
|
27
|
+
}
|
|
28
|
+
/** One readable line per schema error, e.g. `plugin.json /author: "phone" isn't allowed by the Agent Plugins schema`. */
|
|
29
|
+
function describe(file, e) {
|
|
30
|
+
const at = `${file}${e.instancePath ? ` ${e.instancePath}` : ''}`;
|
|
31
|
+
const p = e.params;
|
|
32
|
+
switch (e.keyword) {
|
|
33
|
+
case 'required':
|
|
34
|
+
return `${at}: "${p.missingProperty}" is required by the Agent Plugins schema`;
|
|
35
|
+
case 'additionalProperties':
|
|
36
|
+
return `${at}: "${p.additionalProperty}" isn't allowed by the Agent Plugins schema`;
|
|
37
|
+
case 'const':
|
|
38
|
+
return `${at} must be ${JSON.stringify(p.allowedValue)}`;
|
|
39
|
+
case 'pattern':
|
|
40
|
+
return e.instancePath === '/name' ? `${at} must be lowercase letters, digits, dots and hyphens, with no "--" or ".." (Agent Plugins schema)` : `${at} must match ${p.pattern}`;
|
|
41
|
+
default:
|
|
42
|
+
return `${at} ${e.message ?? 'is invalid'} (Agent Plugins schema)`;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ajv reports every branch of a oneOf. An MCP server entry is checked again against the one form its
|
|
47
|
+
* "type" names, so a missing url reads as one problem rather than three.
|
|
48
|
+
*/
|
|
49
|
+
function readable(file, errors, doc, forms) {
|
|
50
|
+
const oneOfs = errors.filter((e) => e.keyword === 'oneOf').map((e) => e.instancePath);
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const path of oneOfs) {
|
|
53
|
+
const entry = path.split('/').slice(1).reduce((v, k) => v?.[k.replace(/~1/g, '/').replace(/~0/g, '~')], doc);
|
|
54
|
+
const form = typeof entry?.type === 'string' ? forms[entry.type] : undefined;
|
|
55
|
+
if (form && !form(entry))
|
|
56
|
+
out.push(...(form.errors ?? []).map((e) => describe(file, { ...e, instancePath: path + e.instancePath })));
|
|
57
|
+
else
|
|
58
|
+
out.push(`${file} ${path}: "type" must be stdio, streamable-http or sse (Agent Plugins schema)`);
|
|
59
|
+
}
|
|
60
|
+
for (const e of errors) {
|
|
61
|
+
if (e.keyword === 'oneOf' || oneOfs.some((p) => e.instancePath.startsWith(p)))
|
|
62
|
+
continue;
|
|
63
|
+
out.push(describe(file, e));
|
|
64
|
+
}
|
|
65
|
+
return [...new Set(out)];
|
|
66
|
+
}
|
|
67
|
+
/** Every way plugin.json and mcp.json break the Agent Plugins 1.0.0 schemas; empty when both pass. */
|
|
68
|
+
export async function schemaProblems(plugin, mcp) {
|
|
69
|
+
const v = await validators();
|
|
70
|
+
const out = [];
|
|
71
|
+
if (!v.plugin(plugin))
|
|
72
|
+
out.push(...readable('plugin.json', v.plugin.errors ?? [], plugin, v.forms));
|
|
73
|
+
if (!v.mcp(mcp))
|
|
74
|
+
out.push(...readable('mcp.json', v.mcp.errors ?? [], mcp, v.forms));
|
|
75
|
+
return out;
|
|
76
|
+
}
|
package/dist/cdp.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface Target {
|
|
2
|
+
id: string;
|
|
3
|
+
type: string;
|
|
4
|
+
title: string;
|
|
5
|
+
url: string;
|
|
6
|
+
webSocketDebuggerUrl?: string;
|
|
7
|
+
}
|
|
8
|
+
/** The debugging port's target list. Plain http with no keep-alive, so nothing holds the process open afterwards. */
|
|
9
|
+
export declare function listTargets(port: number, timeoutMs?: number): Promise<Target[]>;
|
|
10
|
+
/**
|
|
11
|
+
* The app's main window: the page target at exactly `url` (ChatGPT desktop: app://-/index.html).
|
|
12
|
+
* Its other windows (app://-/index.html?initialRoute=…), MCP App iframes, webviews and
|
|
13
|
+
* workers are separate targets and never match.
|
|
14
|
+
*/
|
|
15
|
+
export declare function pickMainWindow(targets: Target[], url: string): Target | undefined;
|
|
16
|
+
/** One client-to-server frame: final, masked as the RFC requires of clients. */
|
|
17
|
+
export declare function encodeFrame(payload: Buffer, opcode?: number, mask?: Buffer): Buffer;
|
|
18
|
+
export interface WsFrame {
|
|
19
|
+
fin: boolean;
|
|
20
|
+
opcode: number;
|
|
21
|
+
payload: Buffer;
|
|
22
|
+
}
|
|
23
|
+
/** Takes every complete frame off the front of `buf`; `rest` is the partial frame still arriving. */
|
|
24
|
+
export declare function decodeFrames(buf: Buffer): {
|
|
25
|
+
frames: WsFrame[];
|
|
26
|
+
rest: Buffer;
|
|
27
|
+
};
|
|
28
|
+
export declare class Cdp {
|
|
29
|
+
private socket;
|
|
30
|
+
private next;
|
|
31
|
+
private pending;
|
|
32
|
+
private listeners;
|
|
33
|
+
private closed;
|
|
34
|
+
private buf;
|
|
35
|
+
private parts;
|
|
36
|
+
private constructor();
|
|
37
|
+
/** Opens a DevTools WebSocket URL (ws://127.0.0.1:<port>/devtools/page/<id>). */
|
|
38
|
+
static connect(url: string, timeoutMs?: number): Promise<Cdp>;
|
|
39
|
+
send<T = any>(method: string, params?: Record<string, unknown>, timeoutMs?: number): Promise<T>;
|
|
40
|
+
/** Runs an expression in the page and returns its JSON value. */
|
|
41
|
+
evaluate<T>(expression: string): Promise<T>;
|
|
42
|
+
/** Subscribes to an event; returns the unsubscribe function. */
|
|
43
|
+
on(event: string, fn: (params: any) => void): () => void;
|
|
44
|
+
/** Sends a close frame and drops the socket without waiting for an answer. */
|
|
45
|
+
close(): void;
|
|
46
|
+
private ended;
|
|
47
|
+
private data;
|
|
48
|
+
private receive;
|
|
49
|
+
}
|