mcplane 0.1.0 → 0.3.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/README.md CHANGED
@@ -7,6 +7,8 @@ npx mcplane init --url https://mcp.example.com/mcp
7
7
  npx mcplane preflight
8
8
  ```
9
9
 
10
+ No terminal handy? Paste your URL at [reviewtimes.fyi/check](https://reviewtimes.fyi/check).
11
+
10
12
  ```
11
13
  ✗ DNS does not advertise Encrypted ClientHello [ChatGPT]
12
14
  example.com has ech= in its HTTPS record
@@ -44,7 +46,7 @@ Cursor, VS Code and others: add a stdio server with command `npx` and args `["-y
44
46
 
45
47
  Then run the **onboard** prompt (`/mcp__mcplane__onboard` in Claude Code). The agent creates `mcplane.json` and reads your code. It writes the listing copy and the review test cases (ChatGPT wants exactly 5 positive and 3 negative), runs preflight and fixes what it can. The **ship** prompt takes it from there: check, pack, publish, and a short list of what's left for you.
46
48
 
47
- Tools: `preflight`, `drift`, `pack`, `publish`, `record_submission`, `record_decision`, `submission_status`, `install_links`, `pull_listings`, `init`, `fleet`. Every tool takes an optional `project` folder, so one mcplane serves all your apps. Annotations are honest: `publish` and `record_*` are marked open-world, and `publish` does nothing without `confirm: true`.
49
+ Tools: `preflight`, `drift`, `pack`, `publish`, `record_submission`, `record_decision`, `submission_status`, `install_links`, `listings`, `init`, `fleet`. Every tool takes an optional `project` folder, so one mcplane serves all your apps. Annotations are honest: `publish` and `record_*` are marked open-world, and `publish` does nothing without `confirm: true`.
48
50
 
49
51
  ## Many apps
50
52
 
@@ -53,10 +55,11 @@ npx mcplane fleet --root ~/Projects
53
55
  ```
54
56
 
55
57
  ```
56
- ✓ Review Times preflight clean · live on chatgpt
57
- ✗ Invoice Bot 2 blocking · waiting on claude-connectors
58
- ✗ Weather Pro 1 store update needed
59
- chatgpt: Tools changed since the live version (v1.2.0) on 2026-09-02: added forecast_hourly
58
+ ✓ Review Times preflight clean · listed on mcp-registry, glama, chatgpt
59
+ ✗ Invoice Bot 2 blocking · waiting on claude-connectors · listed on mcp-registry
60
+ ✗ Weather Pro 1 store update needed · listed on mcp-registry, claude-connectors · 1 listing fix
61
+ chatgpt: Hints changed: send_alert
62
+ claude-connectors: listing doesn't show: forecast_hourly (ask the review team to resync)
60
63
 
61
64
  3 projects, 2 need attention.
62
65
  ```
@@ -67,13 +70,14 @@ npx mcplane fleet --root ~/Projects
67
70
 
68
71
  | Store | How mcplane gets you there | Keeping it current |
69
72
  |---|---|---|
70
- | Official MCP Registry | **Publishes**: writes `server.json`, runs `mcp-publisher`, signs in with GitHub OIDC in Actions. GitHub's MCP Registry (VS Code), PulseMCP and MCP.Directory read from it | Versions can't change: bump, or a prerelease like `1.2.0-1` for listing-only edits. mcplane checks what's already published |
73
+ | 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 |
71
74
  | 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 |
72
75
  | ChatGPT | **Prepares** `chatgpt-app-submission.json` for the portal's import: listing, tools, hint justifications, 5+3 tests | Tool changes roll out after OpenAI's automated checks. Listing text and hint justifications need a new version, and drift says when |
73
76
  | Claude connectors | **Prepares** every field of the directory form | Tool changes are live on deploy; listing edits (tool names included) are reviewed |
74
77
  | Claude plugins | **Prepares** and checks the plugin repo | The portal tracks your branch; drift tells you a new version is waiting for review |
75
78
  | Cursor | **Prepares** and checks the plugin repo | Pinned to the commit first added; drift flags when you've moved on |
76
79
  | Muse | **Prepares** the form | Flags changes |
80
+ | awesome-remote-mcp-servers | The three-line entry with your Glama connector badge and auth marker, ready for the PR | |
77
81
  | Cline, Glama, LobeHub, Smithery, Docker, awesome-mcp-servers | The prefilled issue, `glama.json`, CLI command or entry | |
78
82
  | mcp.so, MCP Market, mcpservers.org, cursor.directory | `mcplane pack directories`: one sheet with every value to paste | |
79
83
 
@@ -83,14 +87,41 @@ npx mcplane fleet --root ~/Projects
83
87
 
84
88
  Checks come from real rejections, most of them ours:
85
89
 
86
- - **Server**: `initialize`, `tools/list` and `ping` work. 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.
90
+ - **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.
87
91
  - **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.
88
92
  - **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 anything that reaches the public internet (read-only web search included), `destructiveHint` for messages that can't be unsent. No catch-all request tools, no tool names over 64 characters, no sensitive inputs, no upgrade or pricing copy.
89
- - **Listing**: the icon is a square PNG of at least 512px behind a direct link, 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. The ChatGPT domain challenge is served.
93
+ - **Listing**: the icon is a square PNG of at least 512px behind a direct link, 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. Every test case uses tools the live server actually has. The ChatGPT domain challenge is served.
90
94
  - **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.
91
95
 
92
96
  `--json` for machines; exit code 1 when something blocks.
93
97
 
98
+ 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
+
100
+ ## Where you're listed
101
+
102
+ ```sh
103
+ mcplane listings
104
+ ```
105
+
106
+ ```
107
+ ✗ Official MCP Registry listed
108
+ latest is v1.0.0, mcplane.json says v1.0.1
109
+ ✓ Glama listed (Imported from the registry. Claim it with glama.json so the score badge is yours.)
110
+ ✗ Claude connectors listed (tier: community)
111
+ listing doesn't show: forecast_hourly (ask the review team to resync)
112
+ · Cursor Marketplace missing
113
+ ? ChatGPT unknown (OpenAI's directory can't be read automatically. Open your plugin page and compare.)
114
+ ```
115
+
116
+ A form that returned 200 is a claim; the public record is the result. `listings` reads every store with an open record and compares it with your live server and `mcplane.json`:
117
+ - MCP Registry: versions, status, endpoint.
118
+ - Claude: the connectors directory feed, including the tool inventory synced at submission, which you can't edit yourself; and the community plugin mirror.
119
+ - Cursor and Grok.
120
+ - Glama's registry import.
121
+ - Both awesome lists and Docker's catalog.
122
+
123
+ Coverage decays quietly, so run it on a schedule (`--ci` exits 1 on a mismatch). `fleet` runs it for every project.
124
+
94
125
  ## Keeping listings current
95
126
 
96
127
  Every store calls your live server, so fixes reach users on their own. What doesn't: listing text, tool names in Claude's listing, ChatGPT's hint justifications, 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.
@@ -115,7 +146,7 @@ Like fastlane's lanes: named workflows in `mcplane.json`.
115
146
  }
116
147
  ```
117
148
 
118
- `mcplane release` runs the steps in order and stops at the first failure. A `?` step may fail without stopping the lane. `handoff:` steps are listed at the end as your to-do. `check` and `release` are built in.
149
+ `mcplane release` runs the steps in order and stops at the first failure. A `?` step may fail without stopping the lane. `handoff:` steps are listed at the end as your to-do. `check`, `watch` and `release` are built in.
119
150
 
120
151
  ## CI
121
152
 
@@ -132,6 +163,21 @@ jobs:
132
163
  command: check # preflight + drift --ci
133
164
  ```
134
165
 
166
+ And weekly, to catch listings that went stale:
167
+
168
+ ```yaml
169
+ on:
170
+ schedule: [{ cron: '0 9 * * 1' }]
171
+ jobs:
172
+ listings:
173
+ runs-on: ubuntu-latest
174
+ steps:
175
+ - uses: actions/checkout@v4
176
+ - uses: stevysmith/mcplane@v0
177
+ with:
178
+ command: watch # listings --ci + drift --ci
179
+ ```
180
+
135
181
  Publishing to the MCP Registry from CI needs no secrets for `io.github.*` names:
136
182
 
137
183
  ```yaml
@@ -166,7 +212,7 @@ decided <store> <outcome> approved | rejected | withdrawn
166
212
  status Your submissions, days waited, each store's typical wait
167
213
  drift [--ci] What each store needs since it last saw your server
168
214
  baseline <store> Mark an existing live listing as the drift baseline
169
- pull Compare live public listings with mcplane.json
215
+ listings [--ci] Where you're listed, and whether what went live matches
170
216
  try Install links for every client, for you and your testers
171
217
  fleet [--root <dir>] Every project at a glance (--quick, --json, --ci)
172
218
  fleet run <command...> Run a command in every project
@@ -176,6 +222,17 @@ mcp Run as an MCP server over stdio
176
222
 
177
223
  Servers behind sign-in: pass `--token` or set `MCPLANE_TOKEN` so tool checks can run.
178
224
 
225
+ ## Use the checks in your own code
226
+
227
+ `mcplane/core` runs anywhere with `fetch` (Workers, Deno, Bun, Node):
228
+
229
+ ```ts
230
+ import { draftManifest, preflight } from 'mcplane/core';
231
+ const report = await preflight(await draftManifest('https://mcp.example.com/mcp'));
232
+ ```
233
+
234
+ It's what powers [reviewtimes.fyi/check](https://reviewtimes.fyi/check). The TLS 1.2 handshake needs a raw socket, so only the CLI runs that one.
235
+
179
236
  ## mcplane.json
180
237
 
181
238
  One file describes your server and its listings. [`schema.json`](schema.json) gives editors completion and validation; `mcplane init` adds the `$schema` line. [`examples/mcplane.json`](examples/mcplane.json) is a complete one, from [Review Times](https://reviewtimes.fyi)' own ChatGPT and Claude submissions.
@@ -186,7 +243,7 @@ One file describes your server and its listings. [`schema.json`](schema.json) gi
186
243
  |---|---|
187
244
  | `precheck` | `preflight` |
188
245
  | `deliver` + metadata in the repo | `mcplane.json`, `pack`, `publish` |
189
- | `download_metadata` | `pull` |
246
+ | `download_metadata` | `listings`: reads what went live back and diffs it |
190
247
  | `pilot` (TestFlight testers) | `try`: install links for every client |
191
248
  | Fastfile lanes | `lanes` |
192
249
  | `fastlane init` | `init`, and the **onboard** prompt that writes the listing for you |
@@ -0,0 +1,9 @@
1
+ /**
2
+ * OAuth checks, from Stacktree's ChatGPT rounds. Read-only by default:
3
+ * registering a client writes to your authorization server, so that only
4
+ * happens with --register.
5
+ */
6
+ import type { Check } from '../types.js';
7
+ export declare function authChecks(url: string, opts?: {
8
+ register?: boolean;
9
+ }): Promise<Check[]>;
@@ -0,0 +1,4 @@
1
+ import type { Check, Manifest, StoreId, Tool } from '../types.js';
2
+ export declare function listingChecks(m: Manifest, tools: Tool[], stores: StoreId[]): Promise<Check[]>;
3
+ /** Rules for plugin repositories (Claude plugins, Cursor, Grok). */
4
+ export declare function repoChecks(m: Manifest, stores: StoreId[]): Promise<Check[]>;
@@ -106,6 +106,15 @@ export async function listingChecks(m, tools, stores) {
106
106
  if (has(stores, 'claude-connectors', 'claude-plugins'))
107
107
  len(m.oneLiner, 200, 'listing.one-liner', 'One-liner', ['claude-connectors', 'claude-plugins']);
108
108
  len(m.description, 2000, 'listing.description', 'Description', []);
109
+ // Test cases must name tools the server actually has; descriptions drift away from live servers.
110
+ const named = [...new Set((m.tests?.positive ?? []).flatMap((t) => t.tools))];
111
+ if (tools.length && named.length) {
112
+ const live = new Set(tools.map((t) => t.name));
113
+ const unknown = named.filter((n) => !live.has(n));
114
+ checks.push(unknown.length
115
+ ? { id: 'listing.test-tools', level: 'fail', title: 'Test cases use tools the server has', detail: unknown.join(', '), fix: 'Reviewers run your test cases against the live server. Update the tests, and check the description doesn’t promise tools you no longer expose.' }
116
+ : { id: 'listing.test-tools', level: 'pass', title: 'Test cases use tools the server has' });
117
+ }
109
118
  // ChatGPT domain verification
110
119
  if (has(stores, 'chatgpt')) {
111
120
  const ch = await get(`${origin}/.well-known/openai-apps-challenge`);
@@ -0,0 +1,8 @@
1
+ import type { Check, Tool } from '../types.js';
2
+ /** Connects the way a store's scanner does and returns the tool list for the other checks. */
3
+ /** A check that needs a raw socket, which only some runtimes have. Node passes the TLS 1.2 check in. */
4
+ export type SocketCheck = (url: string) => Promise<Check>;
5
+ export declare function serverChecks(url: string, auth: 'none' | 'oauth', token?: string, extra?: SocketCheck[]): Promise<{
6
+ checks: Check[];
7
+ tools: Tool[];
8
+ }>;
@@ -1,8 +1,6 @@
1
- import { connect } from 'node:tls';
2
1
  import { McpClient } from '../mcp-client.js';
3
2
  const BROWSER_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36';
4
- /** Connects the way a store's scanner does and returns the tool list for the other checks. */
5
- export async function serverChecks(url, auth, token) {
3
+ export async function serverChecks(url, auth, token, extra = []) {
6
4
  const checks = [];
7
5
  const bearer = token ? { authorization: `Bearer ${token}` } : {};
8
6
  const client = new McpClient(url, bearer);
@@ -37,9 +35,28 @@ export async function serverChecks(url, auth, token) {
37
35
  const info = init.body?.result?.serverInfo;
38
36
  checks.push({ id: 'server.initialize', level: 'pass', title: 'initialize succeeds', detail: info ? `${info.name ?? ''} ${info.version ?? ''}`.trim() : undefined });
39
37
  }
40
- if (auth === 'none' || (token && init.status < 300)) {
38
+ // An OAuth server that answers initialize without a token should list its tools the same way. One that
39
+ // initializes but then refuses tools/list leaves ChatGPT's new connector stuck on "no actions available":
40
+ // ChatGPT reads the list before signing in; Claude and Cursor sign in first, so they never show it.
41
+ if (auth === 'oauth' && !token && init.status < 300) {
42
+ const peek = await client.request('tools/list').catch(() => null);
43
+ const peekTools = (peek?.body?.result?.tools ?? []);
44
+ checks.push(peekTools.length
45
+ ? { id: 'server.public-tools', level: 'pass', title: 'tools/list answers without a token, like initialize' }
46
+ : {
47
+ id: 'server.public-tools',
48
+ level: 'warn',
49
+ title: 'tools/list answers without a token, like initialize',
50
+ detail: `initialize works unsigned, tools/list gave HTTP ${peek?.status ?? 'error'} with ${peekTools.length} tools`,
51
+ fix: 'Either return 401 for everything until sign-in, or list tools without a token and gate execution. The half-way state froze one team’s ChatGPT connector at "no actions available" until they deleted and recreated it.',
52
+ stores: ['chatgpt'],
53
+ });
54
+ if (peekTools.length)
55
+ tools = peekTools;
56
+ }
57
+ if (auth === 'none' || (token && init.status < 300) || tools.length) {
41
58
  const list = await client.request('tools/list').catch(() => null);
42
- tools = list?.body?.result?.tools ?? [];
59
+ tools = list?.body?.result?.tools?.length ? list.body.result.tools : tools;
43
60
  checks.push(tools.length
44
61
  ? { id: 'server.tools', level: 'pass', title: 'tools/list returns tools', detail: tools.map((t) => t.name).join(', ') }
45
62
  : { id: 'server.tools', level: 'fail', title: 'tools/list returns tools', detail: `HTTP ${list?.status ?? 'error'}`, fix: 'Stores scan tools/list; with no tools there is nothing to list.' });
@@ -109,26 +126,11 @@ export async function serverChecks(url, auth, token) {
109
126
  fix: 'A firewall or bot rule is returning 403 to non-browser clients. Store scanners and reviewers are automated; allow them on the MCP path.',
110
127
  });
111
128
  }
112
- checks.push(await tls12Check(url));
129
+ for (const c of extra)
130
+ checks.push(await c(url));
113
131
  checks.push(await echCheck(url));
114
132
  return { checks, tools };
115
133
  }
116
- /** Some review networks only speak TLS 1.2. */
117
- function tls12Check(url) {
118
- const host = new URL(url).hostname;
119
- return new Promise((resolve) => {
120
- const sock = connect({ host, port: 443, servername: host, maxVersion: 'TLSv1.2', timeout: 8000 }, () => {
121
- sock.end();
122
- resolve({ id: 'server.tls12', level: 'pass', title: 'A TLS 1.2 client can connect' });
123
- });
124
- const bad = (why) => resolve({ id: 'server.tls12', level: 'warn', title: 'A TLS 1.2 client can connect', detail: why, fix: 'Allow TLS 1.2; some corporate and review proxies cannot do TLS 1.3.' });
125
- sock.on('error', (e) => bad(e.message));
126
- sock.on('timeout', () => {
127
- sock.destroy();
128
- bad('timed out');
129
- });
130
- });
131
- }
132
134
  /** Encrypted ClientHello in DNS makes TLS-inspecting proxies reset the connection. */
133
135
  async function echCheck(url) {
134
136
  const host = new URL(url).hostname;
@@ -0,0 +1,3 @@
1
+ import type { Check } from '../types.js';
2
+ /** Some review networks only speak TLS 1.2. */
3
+ export declare function tls12Check(url: string): Promise<Check>;
@@ -0,0 +1,17 @@
1
+ import { connect } from 'node:tls';
2
+ /** Some review networks only speak TLS 1.2. */
3
+ export function tls12Check(url) {
4
+ const host = new URL(url).hostname;
5
+ return new Promise((resolve) => {
6
+ const sock = connect({ host, port: 443, servername: host, maxVersion: 'TLSv1.2', timeout: 8000 }, () => {
7
+ sock.end();
8
+ resolve({ id: 'server.tls12', level: 'pass', title: 'A TLS 1.2 client can connect' });
9
+ });
10
+ const bad = (why) => resolve({ id: 'server.tls12', level: 'warn', title: 'A TLS 1.2 client can connect', detail: why, fix: 'Allow TLS 1.2; some corporate and review proxies cannot do TLS 1.3.' });
11
+ sock.on('error', (e) => bad(e.message));
12
+ sock.on('timeout', () => {
13
+ sock.destroy();
14
+ bad('timed out');
15
+ });
16
+ });
17
+ }
@@ -0,0 +1,2 @@
1
+ import type { Check, Tool } from '../types.js';
2
+ export declare function toolChecks(tools: Tool[]): Check[];
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { writeFile, access } from 'node:fs/promises';
3
3
  import { parseArgs } from 'node:util';
4
- import { MANIFEST_FILE, draftManifest, loadManifest } from './manifest.js';
4
+ import { MANIFEST_FILE, draftManifest, loadManifest, storesOf } from './manifest.js';
5
5
  import { VERSION } from './version.js';
6
6
  import { preflight } from './preflight.js';
7
7
  import { printPreflight } from './report.js';
@@ -10,7 +10,8 @@ import { recordDecision, recordSubmitted, status } from './submissions.js';
10
10
  import { chatgptPack, claudePack, directoriesPack, simplePack, writePack } from './packs.js';
11
11
  import { drift, printDrift, takeSnapshot } from './drift.js';
12
12
  import { publish } from './publish.js';
13
- import { lanes, pull, runLane, tryLinks } from './extras.js';
13
+ import { lanes, runLane, tryLinks } from './extras.js';
14
+ import { listings, printListings } from './listings.js';
14
15
  import { fleet, fleetRun, printFleet } from './fleet.js';
15
16
  const HELP = `mcplane: fastlane for MCP servers
16
17
 
@@ -34,8 +35,8 @@ Usage
34
35
  mcplane baseline <store> [--version x.y.z] Record a listing that's already live as the drift baseline
35
36
  mcplane publish <store> [--yes] Publish where the store allows it (mcp-registry, grok); dry run without --yes
36
37
  mcplane try Install links for every client, for you and your testers
37
- mcplane pull Compare your live public listings with mcplane.json
38
- mcplane lanes List lanes; run one with "mcplane <lane>" (built in: check, release)
38
+ mcplane listings [--store <id>]... Where you're listed, and whether what went live matches your server (--ci)
39
+ mcplane lanes List lanes; run one with "mcplane <lane>" (built in: check, watch, release)
39
40
  mcplane fleet [--root <dir>] Every project under a folder: blockers, store updates needed, reviews waiting
40
41
  --quick Skip the live checks; just submissions
41
42
  mcplane fleet run <command...> Run any mcplane command in every project (e.g. fleet run publish mcp-registry --yes)
@@ -215,7 +216,7 @@ async function main() {
215
216
  console.log(JSON.stringify(rows, null, 2));
216
217
  else
217
218
  printFleet(rows);
218
- if (values.ci && rows.some((r) => r.error || r.blocking || r.driftActions?.length))
219
+ if (values.ci && rows.some((r) => r.error || r.blocking || r.driftActions?.length || r.listingIssues?.length))
219
220
  process.exitCode = 1;
220
221
  return;
221
222
  }
@@ -225,24 +226,16 @@ async function main() {
225
226
  console.log(`${t.client.padEnd(22)} ${t.how}`);
226
227
  return;
227
228
  }
228
- if (cmd === 'pull') {
229
+ if (cmd === 'listings' || cmd === 'pull') {
229
230
  const m = await loadManifest();
230
- const diffs = await pull(m);
231
- if (values.json) {
232
- console.log(JSON.stringify(diffs, null, 2));
233
- return;
234
- }
235
- for (const d of diffs) {
236
- if (!d.found)
237
- console.log(`${d.store}: not listed yet.`);
238
- else if (!d.fields.length)
239
- console.log(`${d.store}: matches mcplane.json.`);
240
- else {
241
- console.log(`${d.store}: live listing differs${d.url ? ` (${d.url})` : ''}`);
242
- for (const f of d.fields)
243
- console.log(` ${f.field}\n live: ${f.live.slice(0, 160)}\n yours: ${f.yours.slice(0, 160)}`);
244
- }
245
- }
231
+ const stores = (values.store?.length ? values.store : storesOf(m));
232
+ const rows = await listings(m, stores, { token: values.token ?? process.env.MCPLANE_TOKEN });
233
+ if (values.json)
234
+ console.log(JSON.stringify(rows, null, 2));
235
+ else
236
+ printListings(rows);
237
+ if (values.ci && rows.some((r) => r.issues.length))
238
+ process.exitCode = 1;
246
239
  return;
247
240
  }
248
241
  const m = await loadManifest().catch(() => null);
package/dist/core.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { SocketCheck } from './checks/server.js';
2
+ import type { Check, Manifest, StoreId } from './types.js';
3
+ export interface PreflightResult {
4
+ checks: Check[];
5
+ reminders: string[];
6
+ stores: StoreId[];
7
+ }
8
+ /** Runtime-neutral: fetch only. Node adds socket checks (TLS 1.2) through extraChecks; see preflight-node. */
9
+ export declare function preflight(m: Manifest, opts?: {
10
+ stores?: StoreId[];
11
+ register?: boolean;
12
+ token?: string;
13
+ extraChecks?: SocketCheck[];
14
+ }): Promise<PreflightResult>;
15
+ export { draftManifest, storesOf, SCHEMA_URL } from './draft.js';
16
+ export { toolChecks } from './checks/tools.js';
17
+ export { STORE_NAMES } from './types.js';
18
+ export type { Check, Level, Manifest, StoreId, Tool } from './types.js';
package/dist/core.js ADDED
@@ -0,0 +1,52 @@
1
+ /**
2
+ * mcplane's checks for any runtime with fetch (Workers, Deno, browsers' servers).
3
+ * Import from "mcplane/core".
4
+ */
5
+ import { authChecks } from './checks/auth.js';
6
+ import { listingChecks, repoChecks } from './checks/listing.js';
7
+ import { serverChecks } from './checks/server.js';
8
+ import { toolChecks } from './checks/tools.js';
9
+ import { storesOf } from './draft.js';
10
+ /** Things no script can see, which have still cost real submissions. */
11
+ function reminders(m, stores) {
12
+ const r = [];
13
+ if (m.server.auth === 'oauth') {
14
+ r.push('Give reviewers a demo account a stranger can use: email and password (not your Google or SSO login), email already confirmed, no 2FA or SMS, seeded with data for every test case. Re-check the seed right before submitting.');
15
+ r.push('Sign in through your OAuth flow once as a brand-new user. A blank post-sign-in redirect stranded a ChatGPT reviewer between registration and consent.');
16
+ }
17
+ if (stores.some((s) => s === 'chatgpt' || s === 'claude-connectors')) {
18
+ r.push('If a tool writes somewhere public (posts, sends, publishes), give reviewers a safe target: a sandbox account or a "playground" option that validates and then discards. Reviewers test writes; they shouldn’t post to a real timeline.');
19
+ }
20
+ if (stores.includes('chatgpt')) {
21
+ r.push('ChatGPT needs a demo video recorded in developer mode that shows the tools your test cases use. Re-record it whenever tools change.');
22
+ r.push('After "Submit for Review" the ChatGPT portal shows nothing for about 20 seconds. Confirm the status on the plugins list, not the button.');
23
+ r.push('Tool-annotation justifications are cut at 200 characters without warning.');
24
+ r.push('ChatGPT allows one version in review at a time. To change a submission, Cancel Review and resubmit the same draft.');
25
+ }
26
+ if (stores.includes('claude-connectors') || stores.includes('claude-plugins')) {
27
+ if (stores.includes('claude-connectors'))
28
+ r.push('Claude connectors are submitted from an organisation’s admin settings (claude.ai/directory/manage), so you need admin rights in a paid org. The tool list is synced from your server when you submit; it is not an editable field.');
29
+ if (stores.includes('claude-plugins'))
30
+ r.push('Run "claude plugin validate --strict" in the plugin repo; the portal validates every commit on the branch it tracks.');
31
+ r.push('Claude: "Passed review" is not the same as listed. Check claude.ai/directory for your listing after approval; with the new portal you choose when to publish.');
32
+ }
33
+ return r;
34
+ }
35
+ /** Runtime-neutral: fetch only. Node adds socket checks (TLS 1.2) through extraChecks; see preflight-node. */
36
+ export async function preflight(m, opts = {}) {
37
+ const stores = opts.stores?.length ? opts.stores : storesOf(m);
38
+ const auth = m.server.auth ?? 'none';
39
+ const server = await serverChecks(m.server.url, auth, opts.token, opts.extraChecks);
40
+ const [authC, listing, repo] = await Promise.all([
41
+ auth === 'oauth' ? authChecks(m.server.url, { register: opts.register }) : Promise.resolve([]),
42
+ listingChecks(m, server.tools, stores),
43
+ repoChecks(m, stores),
44
+ ]);
45
+ const all = [...server.checks, ...authC, ...toolChecks(server.tools), ...listing, ...repo];
46
+ // Keep checks that apply to every store or to one we're submitting to.
47
+ const checks = all.filter((c) => !c.stores?.length || c.stores.some((s) => stores.includes(s)));
48
+ return { checks, reminders: reminders(m, stores), stores };
49
+ }
50
+ export { draftManifest, storesOf, SCHEMA_URL } from './draft.js';
51
+ export { toolChecks } from './checks/tools.js';
52
+ export { STORE_NAMES } from './types.js';
@@ -0,0 +1,5 @@
1
+ import { type Manifest, type StoreId } from './types.js';
2
+ export declare const SCHEMA_URL = "https://unpkg.com/mcplane/schema.json";
3
+ export declare function storesOf(m: Manifest): StoreId[];
4
+ /** A starting manifest from what the live server and its domain reveal. */
5
+ export declare function draftManifest(url: string): Promise<Manifest>;
package/dist/draft.js ADDED
@@ -0,0 +1,55 @@
1
+ /** Runtime-neutral pieces of the manifest: fetch only, no filesystem. */
2
+ import { McpClient } from './mcp-client.js';
3
+ import { STORE_NAMES } from './types.js';
4
+ export const SCHEMA_URL = 'https://unpkg.com/mcplane/schema.json';
5
+ export function storesOf(m) {
6
+ return m.stores?.length ? m.stores : Object.keys(STORE_NAMES);
7
+ }
8
+ /** A starting manifest from what the live server and its domain reveal. */
9
+ export async function draftManifest(url) {
10
+ const client = new McpClient(url);
11
+ const init = await client.initialize().catch(() => null);
12
+ const auth = init?.status === 401 ? 'oauth' : 'none';
13
+ const info = init?.body?.result?.serverInfo ?? {};
14
+ const host = new URL(url).hostname;
15
+ const labels = host.split('.');
16
+ // api.example.com and mcp.example.com usually keep their pages on example.com.
17
+ const origins = [`https://${host}`, ...(labels.length > 2 ? [`https://${labels.slice(1).join('.')}`] : [])];
18
+ const origin = origins[origins.length - 1];
19
+ const found = async (p) => {
20
+ for (const o of origins) {
21
+ const r = await fetch(o + p, { method: 'GET', redirect: 'follow' }).catch(() => null);
22
+ if (r?.ok && !(r.headers.get('content-type') ?? '').includes('json'))
23
+ return o + p;
24
+ }
25
+ return undefined;
26
+ };
27
+ const first = async (...paths) => {
28
+ for (const p of paths) {
29
+ const hit = await found(p);
30
+ if (hit)
31
+ return hit;
32
+ }
33
+ return undefined;
34
+ };
35
+ const [privacy, support, terms, docs, icon] = await Promise.all([
36
+ first('/privacy', '/privacy-policy', '/legal/privacy'),
37
+ first('/support', '/contact', '/help'),
38
+ first('/terms', '/terms-of-service', '/legal/terms'),
39
+ first('/llms.txt', '/docs'),
40
+ first('/icon-512.png', '/icon.png', '/apple-touch-icon.png'),
41
+ ]);
42
+ return {
43
+ $schema: SCHEMA_URL,
44
+ name: String(info.name ?? new URL(url).hostname.split('.')[0]),
45
+ title: String(info.title ?? info.name ?? ''),
46
+ subtitle: '',
47
+ oneLiner: '',
48
+ description: '',
49
+ server: { url, auth },
50
+ author: { name: '', url: origin },
51
+ links: { website: origin, support, privacy, terms, docs },
52
+ icon,
53
+ stores: ['mcp-registry', 'chatgpt', 'claude-connectors', 'cursor', 'muse'],
54
+ };
55
+ }
@@ -0,0 +1,57 @@
1
+ import { type Manifest, type StoreId, type Tool } from './types.js';
2
+ interface ToolShape {
3
+ name: string;
4
+ title?: string;
5
+ description?: string;
6
+ hints: {
7
+ readOnly?: boolean;
8
+ destructive?: boolean;
9
+ openWorld?: boolean;
10
+ };
11
+ input: string;
12
+ output: string;
13
+ }
14
+ export interface Snapshot {
15
+ store: StoreId;
16
+ takenOn: string;
17
+ version?: string;
18
+ state: 'submitted' | 'live';
19
+ tools: ToolShape[];
20
+ listing: Record<string, string | undefined>;
21
+ repoSha?: string;
22
+ }
23
+ export interface DriftItem {
24
+ store: StoreId;
25
+ level: 'action' | 'info';
26
+ what: string;
27
+ todo: string;
28
+ }
29
+ export declare function liveTools(m: Manifest, token?: string): Promise<Tool[]>;
30
+ /** HEAD of the plugin repository on GitHub, when there is one. */
31
+ export declare function repoHead(repository?: string): Promise<string | undefined>;
32
+ export declare function takeSnapshot(m: Manifest, store: StoreId, opts: {
33
+ version?: string;
34
+ state: Snapshot['state'];
35
+ token?: string;
36
+ }, dir?: string): Promise<Snapshot>;
37
+ export declare function markLive(store: StoreId, dir?: string): Promise<void>;
38
+ /** What changed between a snapshot and now, as plain descriptions. */
39
+ export declare function diffTools(before: ToolShape[], now: ToolShape[]): {
40
+ added: string[];
41
+ removed: string[];
42
+ changed: {
43
+ tool: string;
44
+ fields: string[];
45
+ }[];
46
+ };
47
+ export declare function drift(m: Manifest, stores: StoreId[], opts?: {
48
+ token?: string;
49
+ }, dir?: string): Promise<{
50
+ items: DriftItem[];
51
+ missing: StoreId[];
52
+ }>;
53
+ export declare function printDrift(r: {
54
+ items: DriftItem[];
55
+ missing: StoreId[];
56
+ }): void;
57
+ export {};
@@ -0,0 +1,13 @@
1
+ import type { Manifest } from './types.js';
2
+ export declare const DEFAULT_LANES: Record<string, string[]>;
3
+ export declare function lanes(m: Manifest): Record<string, string[]>;
4
+ /**
5
+ * Runs a lane step by step. A step that fails stops the lane unless it starts
6
+ * with "?" (optional). "handoff:" steps are what only a person can do: policy
7
+ * statements, the final submit. They're collected and shown at the end.
8
+ */
9
+ export declare function runLane(name: string, steps: string[], passthrough: string[]): number;
10
+ export declare function tryLinks(m: Manifest): {
11
+ client: string;
12
+ how: string;
13
+ }[];