mcplane 0.1.0 → 0.2.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
@@ -44,7 +44,7 @@ Cursor, VS Code and others: add a stdio server with command `npx` and args `["-y
44
44
 
45
45
  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
46
 
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`.
47
+ 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
48
 
49
49
  ## Many apps
50
50
 
@@ -53,10 +53,11 @@ npx mcplane fleet --root ~/Projects
53
53
  ```
54
54
 
55
55
  ```
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
56
+ ✓ Review Times preflight clean · listed on mcp-registry, glama, chatgpt
57
+ ✗ Invoice Bot 2 blocking · waiting on claude-connectors · listed on mcp-registry
58
+ ✗ Weather Pro 1 store update needed · listed on mcp-registry, claude-connectors · 1 listing fix
59
+ chatgpt: Hints changed: send_alert
60
+ claude-connectors: listing doesn't show: forecast_hourly (ask the review team to resync)
60
61
 
61
62
  3 projects, 2 need attention.
62
63
  ```
@@ -67,13 +68,14 @@ npx mcplane fleet --root ~/Projects
67
68
 
68
69
  | Store | How mcplane gets you there | Keeping it current |
69
70
  |---|---|---|
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 |
71
+ | 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
72
  | 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
73
  | 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
74
  | Claude connectors | **Prepares** every field of the directory form | Tool changes are live on deploy; listing edits (tool names included) are reviewed |
74
75
  | Claude plugins | **Prepares** and checks the plugin repo | The portal tracks your branch; drift tells you a new version is waiting for review |
75
76
  | Cursor | **Prepares** and checks the plugin repo | Pinned to the commit first added; drift flags when you've moved on |
76
77
  | Muse | **Prepares** the form | Flags changes |
78
+ | awesome-remote-mcp-servers | The three-line entry with your Glama connector badge and auth marker, ready for the PR | |
77
79
  | Cline, Glama, LobeHub, Smithery, Docker, awesome-mcp-servers | The prefilled issue, `glama.json`, CLI command or entry | |
78
80
  | mcp.so, MCP Market, mcpservers.org, cursor.directory | `mcplane pack directories`: one sheet with every value to paste | |
79
81
 
@@ -83,14 +85,41 @@ npx mcplane fleet --root ~/Projects
83
85
 
84
86
  Checks come from real rejections, most of them ours:
85
87
 
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.
88
+ - **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
89
  - **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
90
  - **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.
91
+ - **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
92
  - **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
93
 
92
94
  `--json` for machines; exit code 1 when something blocks.
93
95
 
96
+ 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.
97
+
98
+ ## Where you're listed
99
+
100
+ ```sh
101
+ mcplane listings
102
+ ```
103
+
104
+ ```
105
+ ✗ Official MCP Registry listed
106
+ latest is v1.0.0, mcplane.json says v1.0.1
107
+ ✓ Glama listed (Imported from the registry. Claim it with glama.json so the score badge is yours.)
108
+ ✗ Claude connectors listed (tier: community)
109
+ listing doesn't show: forecast_hourly (ask the review team to resync)
110
+ · Cursor Marketplace missing
111
+ ? ChatGPT unknown (OpenAI's directory can't be read automatically. Open your plugin page and compare.)
112
+ ```
113
+
114
+ 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`:
115
+ - MCP Registry: versions, status, endpoint.
116
+ - Claude: the connectors directory feed, including the tool inventory synced at submission, which you can't edit yourself; and the community plugin mirror.
117
+ - Cursor and Grok.
118
+ - Glama's registry import.
119
+ - Both awesome lists and Docker's catalog.
120
+
121
+ Coverage decays quietly, so run it on a schedule (`--ci` exits 1 on a mismatch). `fleet` runs it for every project.
122
+
94
123
  ## Keeping listings current
95
124
 
96
125
  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 +144,7 @@ Like fastlane's lanes: named workflows in `mcplane.json`.
115
144
  }
116
145
  ```
117
146
 
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.
147
+ `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
148
 
120
149
  ## CI
121
150
 
@@ -132,6 +161,21 @@ jobs:
132
161
  command: check # preflight + drift --ci
133
162
  ```
134
163
 
164
+ And weekly, to catch listings that went stale:
165
+
166
+ ```yaml
167
+ on:
168
+ schedule: [{ cron: '0 9 * * 1' }]
169
+ jobs:
170
+ listings:
171
+ runs-on: ubuntu-latest
172
+ steps:
173
+ - uses: actions/checkout@v4
174
+ - uses: stevysmith/mcplane@v0
175
+ with:
176
+ command: watch # listings --ci + drift --ci
177
+ ```
178
+
135
179
  Publishing to the MCP Registry from CI needs no secrets for `io.github.*` names:
136
180
 
137
181
  ```yaml
@@ -166,7 +210,7 @@ decided <store> <outcome> approved | rejected | withdrawn
166
210
  status Your submissions, days waited, each store's typical wait
167
211
  drift [--ci] What each store needs since it last saw your server
168
212
  baseline <store> Mark an existing live listing as the drift baseline
169
- pull Compare live public listings with mcplane.json
213
+ listings [--ci] Where you're listed, and whether what went live matches
170
214
  try Install links for every client, for you and your testers
171
215
  fleet [--root <dir>] Every project at a glance (--quick, --json, --ci)
172
216
  fleet run <command...> Run a command in every project
@@ -186,7 +230,7 @@ One file describes your server and its listings. [`schema.json`](schema.json) gi
186
230
  |---|---|
187
231
  | `precheck` | `preflight` |
188
232
  | `deliver` + metadata in the repo | `mcplane.json`, `pack`, `publish` |
189
- | `download_metadata` | `pull` |
233
+ | `download_metadata` | `listings`: reads what went live back and diffs it |
190
234
  | `pilot` (TestFlight testers) | `try`: install links for every client |
191
235
  | Fastfile lanes | `lanes` |
192
236
  | `fastlane init` | `init`, and the **onboard** prompt that writes the listing for you |
@@ -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`);
@@ -37,9 +37,28 @@ export async function serverChecks(url, auth, token) {
37
37
  const info = init.body?.result?.serverInfo;
38
38
  checks.push({ id: 'server.initialize', level: 'pass', title: 'initialize succeeds', detail: info ? `${info.name ?? ''} ${info.version ?? ''}`.trim() : undefined });
39
39
  }
40
- if (auth === 'none' || (token && init.status < 300)) {
40
+ // An OAuth server that answers initialize without a token should list its tools the same way. One that
41
+ // initializes but then refuses tools/list leaves ChatGPT's new connector stuck on "no actions available":
42
+ // ChatGPT reads the list before signing in; Claude and Cursor sign in first, so they never show it.
43
+ if (auth === 'oauth' && !token && init.status < 300) {
44
+ const peek = await client.request('tools/list').catch(() => null);
45
+ const peekTools = (peek?.body?.result?.tools ?? []);
46
+ checks.push(peekTools.length
47
+ ? { id: 'server.public-tools', level: 'pass', title: 'tools/list answers without a token, like initialize' }
48
+ : {
49
+ id: 'server.public-tools',
50
+ level: 'warn',
51
+ title: 'tools/list answers without a token, like initialize',
52
+ detail: `initialize works unsigned, tools/list gave HTTP ${peek?.status ?? 'error'} with ${peekTools.length} tools`,
53
+ 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.',
54
+ stores: ['chatgpt'],
55
+ });
56
+ if (peekTools.length)
57
+ tools = peekTools;
58
+ }
59
+ if (auth === 'none' || (token && init.status < 300) || tools.length) {
41
60
  const list = await client.request('tools/list').catch(() => null);
42
- tools = list?.body?.result?.tools ?? [];
61
+ tools = list?.body?.result?.tools?.length ? list.body.result.tools : tools;
43
62
  checks.push(tools.length
44
63
  ? { id: 'server.tools', level: 'pass', title: 'tools/list returns tools', detail: tools.map((t) => t.name).join(', ') }
45
64
  : { 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.' });
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/extras.js CHANGED
@@ -2,14 +2,14 @@
2
2
  * The fastlane ideas, for agent-era stores:
3
3
  * lanes named workflows in mcplane.json ("mcplane release")
4
4
  * try install links for every client, like pilot for TestFlight testers
5
- * pull read your live public listings back and compare with mcplane.json,
6
- * like deliver's download_metadata
7
5
  */
8
6
  import { spawnSync } from 'node:child_process';
9
7
  /* ---------------- lanes ---------------- */
10
8
  export const DEFAULT_LANES = {
11
9
  // Fast checks for CI on every push or deploy.
12
10
  check: ['preflight', 'drift --ci'],
11
+ // On a schedule: listings that went stale or never appeared.
12
+ watch: ['listings --ci', 'drift --ci'],
13
13
  // Everything for a release: checks, packs, the stores that can be automated, then your hand-offs.
14
14
  release: [
15
15
  'preflight',
@@ -68,42 +68,3 @@ export function tryLinks(m) {
68
68
  { client: 'ChatGPT', how: `Turn on developer mode (Settings → Apps → Advanced), then Plugins → Add → Create MCP App → ${url}${m.server.auth === 'oauth' ? ' (OAuth)' : ' (No authentication)'}` },
69
69
  ];
70
70
  }
71
- const norm = (s) => (s ?? '').replace(/\s+/g, ' ').trim();
72
- const sameUrl = (a, b) => !!a && !!b && a.replace(/\/+$/, '').replace(/\.git$/, '').toLowerCase() === b.replace(/\/+$/, '').replace(/\.git$/, '').toLowerCase();
73
- /** Reads the public listings that have an open feed and compares them with mcplane.json. */
74
- export async function pull(m) {
75
- const out = [];
76
- // Claude connectors: Anthropic's public directory feed.
77
- const res = await fetch('https://api.anthropic.com/api/directory/servers?verified_tier=anthropic,partner,community&visibility=commercial&limit=5000', { headers: { 'user-agent': 'mcplane' } }).catch(() => null);
78
- const data = res?.ok ? (await res.json().catch(() => null)) : null;
79
- const hit = data?.servers?.find((s) => sameUrl(s.remote?.url, m.server.url));
80
- out.push({
81
- store: 'Claude connectors',
82
- found: !!hit,
83
- url: hit?.directory_url,
84
- fields: hit
85
- ? [
86
- ['name', hit.display_name ?? hit.name, m.title],
87
- ['one-liner', hit.one_liner, m.oneLiner],
88
- ['description', hit.description, m.description],
89
- ]
90
- .filter(([, live, yours]) => norm(live) && norm(yours) && norm(live) !== norm(yours))
91
- .map(([field, live, yours]) => ({ field, live: norm(live), yours: norm(yours) }))
92
- : [],
93
- });
94
- // Cursor: the marketplace page embeds every approved plugin.
95
- if (m.repository) {
96
- const page = await fetch('https://cursor.com/marketplace', { headers: { 'user-agent': 'mcplane' } }).then((r) => r.text()).catch(() => '');
97
- const html = page.replace(/\\"/g, '"');
98
- const repo = m.repository.replace(/\.git$/, '').replace(/\/+$/, '');
99
- const i = html.toLowerCase().indexOf(`"repositoryurl":"${repo.toLowerCase()}`);
100
- const around = i >= 0 ? html.slice(Math.max(0, i - 3000), i) : '';
101
- const desc = around.match(/"description":"((?:[^"\\]|\\.)*)"[^{]*$/)?.[1];
102
- out.push({
103
- store: 'Cursor',
104
- found: i >= 0,
105
- fields: desc && norm(desc) !== norm(m.oneLiner ?? m.description) ? [{ field: 'description', live: norm(desc), yours: norm(m.oneLiner ?? m.description) }] : [],
106
- });
107
- }
108
- return out;
109
- }
package/dist/fleet.js CHANGED
@@ -7,10 +7,11 @@ import { spawnSync } from 'node:child_process';
7
7
  import { readdir } from 'node:fs/promises';
8
8
  import { join, relative, resolve } from 'node:path';
9
9
  import { drift } from './drift.js';
10
+ import { listings } from './listings.js';
10
11
  import { MANIFEST_FILE, loadManifest, storesOf } from './manifest.js';
11
12
  import { preflight } from './preflight.js';
12
13
  import { load } from './submissions.js';
13
- const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.mcplane', '.next', '.wrangler', 'vendor', 'target']);
14
+ const SKIP = new Set(['node_modules', '.git', 'dist', 'build', '.mcplane', '.next', '.wrangler', 'vendor', 'target', 'examples', 'fixtures', 'test', 'tests']);
14
15
  /** Folders under root that hold an mcplane.json, depth-first, a few levels down. */
15
16
  export async function findProjects(root = process.cwd(), depth = 3) {
16
17
  const found = [];
@@ -47,7 +48,7 @@ async function pool(items, n, fn) {
47
48
  }));
48
49
  return out;
49
50
  }
50
- /** Preflight, drift and submissions for every project under root. Checks run four projects at a time. */
51
+ /** Preflight, drift, listings and submissions for every project under root. Checks run four projects at a time. */
51
52
  export async function fleet(root = process.cwd(), opts = {}) {
52
53
  const projects = await findProjects(root);
53
54
  return pool(projects, 4, async (p) => {
@@ -69,6 +70,9 @@ export async function fleet(root = process.cwd(), opts = {}) {
69
70
  const d = await drift(p.manifest, storesOf(p.manifest), { token: opts.token }, p.dir);
70
71
  row.driftActions = d.items.filter((i) => i.level === 'action').map((i) => ({ store: i.store, what: i.what }));
71
72
  row.noSnapshot = d.missing;
73
+ const l = await listings(p.manifest, storesOf(p.manifest), { token: opts.token });
74
+ row.listedOn = l.filter((x) => x.state === 'listed').map((x) => x.store);
75
+ row.listingIssues = l.flatMap((x) => x.issues.map((issue) => ({ store: x.store, issue })));
72
76
  }
73
77
  catch (e) {
74
78
  row.error = e.message;
@@ -87,14 +91,17 @@ export function printFleet(rows) {
87
91
  r.toReview ? `${r.toReview} to review` : '',
88
92
  r.driftActions?.length ? `${r.driftActions.length} store update${r.driftActions.length === 1 ? '' : 's'} needed` : '',
89
93
  r.waiting.length ? `waiting on ${r.waiting.map((w) => w.store).join(', ')}` : '',
90
- r.live.length ? `live on ${r.live.join(', ')}` : '',
94
+ r.listedOn ? `listed on ${r.listedOn.length ? r.listedOn.join(', ') : 'nothing yet'}` : r.live.length ? `live on ${r.live.join(', ')}` : '',
95
+ r.listingIssues?.length ? `${r.listingIssues.length} listing fix${r.listingIssues.length === 1 ? '' : 'es'}` : '',
91
96
  ].filter(Boolean);
92
- const mark = r.error || r.blocking || r.driftActions?.length ? '✗' : '✓';
97
+ const mark = r.error || r.blocking || r.driftActions?.length || r.listingIssues?.length ? '✗' : '✓';
93
98
  console.log(`${mark} ${(r.name ?? r.project).padEnd(24)} ${bits.join(' · ')}`);
94
99
  for (const a of r.driftActions ?? [])
95
100
  console.log(` ${a.store}: ${a.what}`);
101
+ for (const a of r.listingIssues ?? [])
102
+ console.log(` ${a.store}: ${a.issue}`);
96
103
  }
97
- const bad = rows.filter((r) => r.error || r.blocking || r.driftActions?.length).length;
104
+ const bad = rows.filter((r) => r.error || r.blocking || r.driftActions?.length || r.listingIssues?.length).length;
98
105
  console.log(`\n${rows.length} project${rows.length === 1 ? '' : 's'}, ${bad} need${bad === 1 ? 's' : ''} attention. Details: cd into one and run "mcplane preflight" or "mcplane drift".`);
99
106
  }
100
107
  /** Runs one mcplane command in every project, one after another. */
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Where you're listed, and whether what went live matches. Every store with an
3
+ * open record is read directly; a save that returned 200 is a claim, the
4
+ * public record is the result. Coverage decays quietly, so this is meant to
5
+ * run on a schedule.
6
+ */
7
+ import { liveTools, repoHead } from './drift.js';
8
+ import { registryName } from './publish.js';
9
+ import { STORE_NAMES } from './types.js';
10
+ const UA = { 'user-agent': 'mcplane' };
11
+ /** Public feeds are sometimes slow (the registry has taken 40s); one retry with a longer wait. */
12
+ const getJson = async (url) => {
13
+ for (const wait of [15_000, 45_000]) {
14
+ const r = await fetch(url, { headers: UA, signal: AbortSignal.timeout(wait) }).catch(() => null);
15
+ if (r?.ok)
16
+ return (await r.json().catch(() => null));
17
+ }
18
+ return null;
19
+ };
20
+ const getText = async (url) => {
21
+ const r = await fetch(url, { headers: UA, signal: AbortSignal.timeout(20_000) }).catch(() => null);
22
+ return r?.ok ? r.text() : null;
23
+ };
24
+ const status = async (url) => (await fetch(url, { headers: UA, redirect: 'follow', signal: AbortSignal.timeout(20_000) }).catch(() => null))?.status ?? 0;
25
+ const norm = (s) => (s ?? '').replace(/\s+/g, ' ').trim();
26
+ const bare = (u) => (u ?? '').replace(/\/+$/, '').replace(/\.git$/, '').toLowerCase();
27
+ async function registry(m) {
28
+ const name = registryName(m);
29
+ const data = await getJson(`https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(name)}`);
30
+ if (!data)
31
+ return { store: 'mcp-registry', state: 'unknown', issues: [], note: 'Registry API did not answer.' };
32
+ const mine = (data.servers ?? []).filter((x) => x.server.name === name);
33
+ if (!mine.length)
34
+ return { store: 'mcp-registry', state: 'missing', issues: [], note: `No ${name} in the registry. "mcplane publish mcp-registry".` };
35
+ const latest = mine.find((x) => x._meta?.['io.modelcontextprotocol.registry/official']?.isLatest) ?? mine[mine.length - 1];
36
+ const issues = [];
37
+ const st = latest._meta?.['io.modelcontextprotocol.registry/official']?.status;
38
+ if (st && st !== 'active')
39
+ issues.push(`latest version is ${st}`);
40
+ if (m.version && latest.server.version !== m.version)
41
+ issues.push(`latest is v${latest.server.version}, mcplane.json says v${m.version}`);
42
+ if (!latest.server.remotes?.some((r) => bare(r.url) === bare(m.server.url)))
43
+ issues.push(`endpoint differs: ${latest.server.remotes?.map((r) => r.url).join(', ') || 'none'}`);
44
+ return { store: 'mcp-registry', state: 'listed', url: `https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(name)}`, issues };
45
+ }
46
+ async function claudeConnectors(m, token) {
47
+ const data = await getJson('https://api.anthropic.com/api/directory/servers?verified_tier=anthropic,partner,community&visibility=commercial&limit=5000');
48
+ if (!data)
49
+ return { store: 'claude-connectors', state: 'unknown', issues: [], note: 'Anthropic’s directory feed did not answer.' };
50
+ const hit = data.servers?.find((s) => bare(s.remote?.url ?? s.url) === bare(m.server.url) || norm(s.name).toLowerCase() === norm(m.title).toLowerCase());
51
+ if (!hit)
52
+ return { store: 'claude-connectors', state: 'missing', issues: [] };
53
+ const issues = [];
54
+ if (m.oneLiner && norm(hit.one_liner) !== norm(m.oneLiner))
55
+ issues.push('one-liner differs from mcplane.json');
56
+ if (m.description && norm(hit.description) !== norm(m.description))
57
+ issues.push('description differs from mcplane.json');
58
+ // The tool list is synced from your server at submission and isn't an editable field: compare it with what you serve now.
59
+ const published = hit.tool_names ?? [];
60
+ const live = (await liveTools(m, token).catch(() => [])).map((t) => t.name);
61
+ if (live.length && published.length) {
62
+ const missing = live.filter((n) => !published.includes(n));
63
+ const gone = published.filter((n) => !live.includes(n));
64
+ if (missing.length)
65
+ issues.push(`listing doesn't show: ${missing.join(', ')} (ask the review team to resync)`);
66
+ if (gone.length)
67
+ issues.push(`listing still shows removed tools: ${gone.join(', ')}`);
68
+ }
69
+ const tier = hit.verified_tier ?? hit.tier;
70
+ return { store: 'claude-connectors', state: 'listed', url: hit.directory_url ?? `https://claude.ai/directory/${hit.id}`, issues, note: tier ? `tier: ${tier}` : undefined };
71
+ }
72
+ async function claudePlugins(m, head) {
73
+ if (!m.repository)
74
+ return { store: 'claude-plugins', state: 'unknown', issues: [], note: 'No repository in mcplane.json.' };
75
+ const data = await getJson('https://raw.githubusercontent.com/anthropics/claude-plugins-community/main/.claude-plugin/marketplace.json');
76
+ if (!data)
77
+ return { store: 'claude-plugins', state: 'unknown', issues: [] };
78
+ const hit = data.plugins?.find((p) => bare(p.source?.url) === bare(m.repository));
79
+ if (!hit)
80
+ return { store: 'claude-plugins', state: 'missing', issues: [], note: 'Not in the community marketplace mirror (synced nightly from Anthropic’s review).' };
81
+ const issues = head && hit.source?.sha && hit.source.sha !== head ? [`serves ${hit.source.sha.slice(0, 7)}, your repo is at ${head.slice(0, 7)}`] : [];
82
+ return { store: 'claude-plugins', state: 'listed', issues, note: `as "${hit.name}"` };
83
+ }
84
+ async function cursor(m) {
85
+ if (!m.repository)
86
+ return { store: 'cursor', state: 'unknown', issues: [], note: 'No repository in mcplane.json.' };
87
+ const page = await getText('https://cursor.com/marketplace');
88
+ if (!page)
89
+ return { store: 'cursor', state: 'unknown', issues: [] };
90
+ const html = page.replace(/\\"/g, '"').toLowerCase();
91
+ const found = html.includes(`"repositoryurl":"${bare(m.repository)}`);
92
+ return { store: 'cursor', state: found ? 'listed' : 'missing', issues: [] };
93
+ }
94
+ async function grok(m, head) {
95
+ const data = await getJson('https://raw.githubusercontent.com/xai-org/plugin-marketplace/main/.grok-plugin/marketplace.json');
96
+ if (!data)
97
+ return { store: 'grok', state: 'unknown', issues: [] };
98
+ const hit = data.plugins?.find((p) => p.name === m.name || (m.repository && bare(p.source?.url) === bare(m.repository)));
99
+ if (!hit)
100
+ return { store: 'grok', state: 'missing', issues: [] };
101
+ const issues = head && hit.source?.sha && hit.source.sha !== head ? [`pinned to ${hit.source.sha.slice(0, 7)}, your repo is at ${head.slice(0, 7)}`] : [];
102
+ return { store: 'grok', state: 'listed', issues };
103
+ }
104
+ async function glama(m) {
105
+ const url = `https://glama.ai/mcp/connectors/${registryName(m)}`;
106
+ const code = await status(url);
107
+ if (code === 200)
108
+ return { store: 'glama', state: 'listed', url, issues: [], note: 'Imported from the registry. Claim it with glama.json so the score badge is yours.' };
109
+ if (code === 404)
110
+ return { store: 'glama', state: 'missing', issues: [], note: 'Glama imports connectors from the official registry; publish there first.' };
111
+ return { store: 'glama', state: 'unknown', issues: [] };
112
+ }
113
+ async function awesome(m, store, repo) {
114
+ const readme = await getText(`https://raw.githubusercontent.com/${repo}/main/README.md`);
115
+ if (!readme)
116
+ return { store, state: 'unknown', issues: [] };
117
+ const text = readme.toLowerCase();
118
+ const keys = [m.server.url, m.repository, m.links?.website].filter(Boolean).map((u) => bare(u));
119
+ return { store, state: keys.some((k) => text.includes(k)) ? 'listed' : 'missing', issues: [] };
120
+ }
121
+ async function docker(m) {
122
+ const code = await status(`https://github.com/docker/mcp-registry/tree/main/servers/${m.name}`);
123
+ return { store: 'docker', state: code === 200 ? 'listed' : code === 404 ? 'missing' : 'unknown', issues: [] };
124
+ }
125
+ export function printListings(rows) {
126
+ const mark = { listed: '✓', missing: '·', unknown: '?' };
127
+ for (const r of rows) {
128
+ const flag = r.state === 'listed' && r.issues.length ? '✗' : mark[r.state];
129
+ console.log(`${flag} ${STORE_NAMES[r.store].padEnd(28)} ${r.state}${r.note ? ` (${r.note})` : ''}`);
130
+ if (r.url && r.state === 'listed')
131
+ console.log(` ${r.url}`);
132
+ for (const i of r.issues)
133
+ console.log(` ${i}`);
134
+ }
135
+ const listed = rows.filter((r) => r.state === 'listed');
136
+ console.log(`\nListed on ${listed.length} of ${rows.length}; ${listed.filter((r) => r.issues.length).length} need a fix. ? means no public record to read.`);
137
+ }
138
+ const MANUAL = {
139
+ chatgpt: 'OpenAI’s directory can’t be read automatically. Open your plugin page and compare.',
140
+ muse: 'Meta publishes no directory feed yet.',
141
+ smithery: 'Check smithery.ai; its public "verified" flag can lag behind passed proofs.',
142
+ cline: 'Cline’s marketplace has no public feed; watch your submission issue.',
143
+ lobehub: 'Check market.lobehub.com.',
144
+ };
145
+ export async function listings(m, stores, opts = {}) {
146
+ const head = await repoHead(m.repository);
147
+ const jobs = stores.map((s) => {
148
+ if (s === 'mcp-registry')
149
+ return registry(m);
150
+ if (s === 'claude-connectors')
151
+ return claudeConnectors(m, opts.token);
152
+ if (s === 'claude-plugins')
153
+ return claudePlugins(m, head);
154
+ if (s === 'cursor')
155
+ return cursor(m);
156
+ if (s === 'grok')
157
+ return grok(m, head);
158
+ if (s === 'glama')
159
+ return glama(m);
160
+ if (s === 'awesome-mcp-servers')
161
+ return awesome(m, s, 'punkpeye/awesome-mcp-servers');
162
+ if (s === 'awesome-remote-mcp-servers')
163
+ return awesome(m, s, 'punkpeye/awesome-remote-mcp-servers');
164
+ if (s === 'docker')
165
+ return docker(m);
166
+ return Promise.resolve({ store: s, state: 'unknown', issues: [], note: MANUAL[s] });
167
+ });
168
+ return Promise.all(jobs);
169
+ }
@@ -11,7 +11,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
11
11
  import { z } from 'zod';
12
12
  import { drift } from './drift.js';
13
13
  import { fleet } from './fleet.js';
14
- import { pull, tryLinks } from './extras.js';
14
+ import { tryLinks } from './extras.js';
15
+ import { listings } from './listings.js';
15
16
  import { MANIFEST_FILE, draftManifest, loadManifest, storesOf } from './manifest.js';
16
17
  import { chatgptPack, claudePack, directoriesPack, simplePack, writePack } from './packs.js';
17
18
  import { preflight } from './preflight.js';
@@ -110,12 +111,16 @@ export async function serve() {
110
111
  inputSchema: { project: PROJECT },
111
112
  annotations: { title: 'Try', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
112
113
  }, async ({ project }) => reply({ links: tryLinks(await loadManifest(dirOf(project))) }));
113
- server.registerTool('pull_listings', {
114
- title: 'Compare live listings with mcplane.json',
115
- description: 'Reads the store listings that have an open feed (Claude directory, Cursor marketplace) and returns fields that differ from mcplane.json.',
116
- inputSchema: { project: PROJECT },
117
- annotations: { title: 'Pull', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
118
- }, async ({ project }) => reply({ listings: await pull(await loadManifest(dirOf(project))) }));
114
+ server.registerTool('listings', {
115
+ title: 'Where you’re listed, and whether it matches',
116
+ description: 'Reads every store with a public record (MCP Registry, Claude connectors and plugins, Cursor, Grok, Glama, the awesome lists, Docker) and returns, per store: listed, missing or unknown, plus differences from your live server and mcplane.json, such as tools missing from Claude’s synced inventory or a plugin pinned to an old commit.',
117
+ inputSchema: { project: PROJECT, stores: z.array(STORE).optional() },
118
+ annotations: { title: 'Listings', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
119
+ }, async ({ project, stores }) => {
120
+ const m = await loadManifest(dirOf(project));
121
+ const rows = await listings(m, stores?.length ? stores : storesOf(m), { token: token() });
122
+ return reply({ listings: rows }, `Listed on ${rows.filter((r) => r.state === 'listed').length} of ${rows.length}; ${rows.filter((r) => r.issues.length).length} need a fix.`);
123
+ });
119
124
  server.registerTool('init', {
120
125
  title: 'Create mcplane.json',
121
126
  description: 'Writes a starting mcplane.json from what the live server and its domain reveal (name, auth, privacy, support, terms and icon URLs). Refuses to overwrite an existing file.',
@@ -131,12 +136,12 @@ export async function serve() {
131
136
  });
132
137
  server.registerTool('fleet', {
133
138
  title: 'Every project at a glance',
134
- description: 'Finds every mcplane.json under a folder (three levels down) and returns, per project: blocking preflight checks, store updates needed since the last submission, reviews waiting and stores it is live on. quick true skips the live checks.',
139
+ description: 'Finds every mcplane.json under a folder (three levels down) and returns, per project: blocking preflight checks, store updates needed since the last submission, reviews waiting, where it is listed and listings that no longer match. quick true skips the live checks.',
135
140
  inputSchema: { root: z.string().optional().describe('Folder to search. Defaults to where mcplane was started.'), quick: z.boolean().default(false) },
136
141
  annotations: { title: 'Fleet', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
137
142
  }, async ({ root, quick }) => {
138
143
  const rows = await fleet(dirOf(root), { checks: !quick, token: token() });
139
- const bad = rows.filter((r) => r.error || r.blocking || r.driftActions?.length).length;
144
+ const bad = rows.filter((r) => r.error || r.blocking || r.driftActions?.length || r.listingIssues?.length).length;
140
145
  return reply({ projects: rows }, `${rows.length} projects, ${bad} need attention.`);
141
146
  });
142
147
  // Prompts are the agent-era "fastlane init": the agent in your repo writes the listing, you approve it.
package/dist/packs.js CHANGED
@@ -237,9 +237,9 @@ export function directoriesPack(m) {
237
237
  const repo = m.repository?.replace(/\.git$/, '');
238
238
  const desc = m.oneLiner ?? m.description ?? m.title;
239
239
  const rows = [
240
- ['MCP.Directory', 'https://mcp.directory/submit', 'Reads the official registry; claim the listing there, or submit a GitHub URL (reviewed within a day).'],
241
- ['mcp.so', 'https://mcp.so/submit', 'Repo URL and name. Paid option skips review. No update mechanism: edit by hand.'],
242
- ['MCP Market', 'https://mcpmarket.com/submit', 'GitHub URL, email, type. The free queue runs to weeks; paid lists within a day.'],
240
+ ['MCP.Directory', 'https://mcp.directory/submit', 'Says it auto-discovers from the official registry; its form wants a GitHub repo first, so hosted servers without public code can stall.'],
241
+ ['mcp.so', 'https://mcp.so/submit', 'Repo URL and name. Edits can return HTTP 200 with a hidden validation error (a tagline that is too long): check the public page after saving.'],
242
+ ['MCP Market', 'https://mcpmarket.com/submit', 'GitHub URL, email, type. The free queue runs to weeks; paid lists within a day. Check where its Try Now button sends people.'],
243
243
  ['mcpservers.org', 'https://mcpservers.org/submit', 'Has a field for your Official MCP Registry name. Free takes about two weeks.'],
244
244
  ['cursor.directory', 'https://cursor.directory', 'Submit from its MCP section.'],
245
245
  ];
@@ -260,6 +260,8 @@ Values to paste:
260
260
  |---|---|---|
261
261
  ${rows.map((r) => `| ${r[0]} | ${r[1]} | ${r[2]} |`).join('\n')}
262
262
 
263
+ After any form: a save that returned 200 is a claim. Open the public page and check the endpoint, auth and every link.
264
+
263
265
  Directories send little traffic on their own; the registry, the big stores and your own docs matter more. Do these once, and don't pay to skip a queue unless a launch date depends on it.
264
266
  `;
265
267
  return { files: [{ path: 'directories.md', content: md }], problems: [], todo: [] };
package/dist/preflight.js CHANGED
@@ -7,9 +7,12 @@ import { storesOf } from './manifest.js';
7
7
  function reminders(m, stores) {
8
8
  const r = [];
9
9
  if (m.server.auth === 'oauth') {
10
- r.push('Give reviewers a demo account: email and password, no 2FA, SMS or email confirmation, seeded with data that matches every test case. Re-check the seed right before submitting.');
10
+ 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.');
11
11
  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.');
12
12
  }
13
+ if (stores.some((s) => s === 'chatgpt' || s === 'claude-connectors')) {
14
+ 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.');
15
+ }
13
16
  if (stores.includes('chatgpt')) {
14
17
  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.');
15
18
  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.');
@@ -17,6 +20,8 @@ function reminders(m, stores) {
17
20
  r.push('ChatGPT allows one version in review at a time. To change a submission, Cancel Review and resubmit the same draft.');
18
21
  }
19
22
  if (stores.includes('claude-connectors') || stores.includes('claude-plugins')) {
23
+ if (stores.includes('claude-connectors'))
24
+ 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.');
20
25
  if (stores.includes('claude-plugins'))
21
26
  r.push('Run "claude plugin validate --strict" in the plugin repo; the portal validates every commit on the branch it tracks.');
22
27
  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.');
package/dist/publish.js CHANGED
@@ -59,7 +59,7 @@ async function registryVersions(name) {
59
59
  }
60
60
  /** 1.2.0 → 1.2.0-1, 1.2.0-1 → 1.2.0-2: the registry's way to change metadata without a new release. */
61
61
  const nextPrerelease = (v) => (/-(\d+)$/.test(v) ? v.replace(/-(\d+)$/, (_, n) => `-${Number(n) + 1}`) : `${v}-1`);
62
- const REGISTRY_READERS = 'GitHub’s MCP Registry (and VS Code), PulseMCP and MCP.Directory read from the official registry, so they pick this up on their own.';
62
+ const REGISTRY_READERS = 'Glama and PulseMCP import from the official registry. GitHub’s MCP gallery (VS Code) needs a one-time manual onboarding, then syncs new versions. Run "mcplane listings" in a day or two to see where it landed.';
63
63
  async function publishRegistry(m, yes, dir) {
64
64
  const json = serverJson(m);
65
65
  const file = m.registry?.file ?? 'server.json';
@@ -201,6 +201,22 @@ async function publishGrok(m, yes) {
201
201
  function prepared(m, store) {
202
202
  const gh = ghRepo(m);
203
203
  const desc = m.oneLiner ?? m.subtitle ?? m.title;
204
+ if (store === 'awesome-remote-mcp-servers') {
205
+ const auth = m.server.auth === 'oauth' ? '🔐' : '🔓';
206
+ const reg = registryName(m);
207
+ const sentence = (desc.endsWith('.') ? desc : `${desc}.`).slice(0, 120);
208
+ return {
209
+ store,
210
+ done: false,
211
+ lines: [
212
+ 'For hosted servers. Add this under the right category in punkpeye/awesome-remote-mcp-servers README.md (alphabetical):',
213
+ `- [${m.title}](${m.links?.website ?? m.server.url}) \`${m.server.url}\``,
214
+ ` [![${m.title} MCP connector](https://glama.ai/mcp/connectors/${reg}/badges/score.svg)](https://glama.ai/mcp/connectors/${reg})`,
215
+ ` ${auth} - ${sentence}`,
216
+ 'Star the repo first (PRs from accounts that haven’t aren’t merged). CI sends an unauthenticated initialize: a 401 with WWW-Authenticate passes as OAuth. The Glama connector must exist ("mcplane listings"). Agent-opened PRs can end the title with 🤖🤖🤖 for the fast track.',
217
+ ],
218
+ };
219
+ }
204
220
  if (store === 'awesome-mcp-servers') {
205
221
  const link = gh ? `[${gh.owner}/${gh.repo}](https://github.com/${gh.owner}/${gh.repo})` : `[${m.title}](${m.links?.website ?? m.server.url})`;
206
222
  return {
@@ -222,10 +238,11 @@ function prepared(m, store) {
222
238
  store,
223
239
  done: false,
224
240
  lines: [
225
- `Add ${gh ? `https://github.com/${gh.owner}/${gh.repo}` : 'your GitHub repo'} at https://glama.ai/mcp/servers ("Add MCP Server"). Glama runs its own checks (license, security, health) and scores the server.`,
226
- 'To claim the listing, commit glama.json at the repo root:',
241
+ `Hosted servers: Glama imports a connector from the official registry at https://glama.ai/mcp/connectors/${registryName(m)}. Publish there first, then claim it.`,
242
+ `Code on GitHub: add ${gh ? `https://github.com/${gh.owner}/${gh.repo}` : 'the repo'} at https://glama.ai/mcp/servers ("Add MCP Server"); Glama checks license, security and health, and scores it.`,
243
+ 'To claim either, commit glama.json at the repo root:',
227
244
  JSON.stringify({ $schema: 'https://glama.ai/mcp/schemas/server.json', maintainers: [owner] }, null, 2),
228
- 'awesome-mcp-servers asks for the Glama score badge on each entry, so do this first.',
245
+ 'Both awesome lists require the Glama badge on each entry, so do this first.',
229
246
  ],
230
247
  };
231
248
  }
package/dist/types.js CHANGED
@@ -12,4 +12,5 @@ export const STORE_NAMES = {
12
12
  cline: 'Cline MCP Marketplace',
13
13
  lobehub: 'LobeHub Market',
14
14
  'awesome-mcp-servers': 'awesome-mcp-servers',
15
+ 'awesome-remote-mcp-servers': 'awesome-remote-mcp-servers',
15
16
  };
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.1.0';
1
+ export const VERSION = '0.2.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcplane",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "fastlane for MCP servers: preflight against every store's rejection causes, publish where you can, prepare the rest, track every review.",
5
5
  "type": "module",
6
6
  "bin": {
package/schema.json CHANGED
@@ -25,7 +25,8 @@
25
25
  "glama",
26
26
  "cline",
27
27
  "lobehub",
28
- "awesome-mcp-servers"
28
+ "awesome-mcp-servers",
29
+ "awesome-remote-mcp-servers"
29
30
  ]
30
31
  },
31
32
  "url": {