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/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
- }
@@ -0,0 +1,40 @@
1
+ import type { Manifest, StoreId } from './types.js';
2
+ export interface Project {
3
+ dir: string;
4
+ path: string;
5
+ manifest?: Manifest;
6
+ error?: string;
7
+ }
8
+ /** Folders under root that hold an mcplane.json, depth-first, a few levels down. */
9
+ export declare function findProjects(root?: string, depth?: number): Promise<Project[]>;
10
+ export interface FleetRow {
11
+ project: string;
12
+ name?: string;
13
+ url?: string;
14
+ blocking?: number;
15
+ toReview?: number;
16
+ driftActions?: {
17
+ store: StoreId;
18
+ what: string;
19
+ }[];
20
+ noSnapshot?: StoreId[];
21
+ waiting: {
22
+ store: StoreId;
23
+ since: string;
24
+ }[];
25
+ live: StoreId[];
26
+ listedOn?: StoreId[];
27
+ listingIssues?: {
28
+ store: StoreId;
29
+ issue: string;
30
+ }[];
31
+ error?: string;
32
+ }
33
+ /** Preflight, drift, listings and submissions for every project under root. Checks run four projects at a time. */
34
+ export declare function fleet(root?: string, opts?: {
35
+ checks?: boolean;
36
+ token?: string;
37
+ }): Promise<FleetRow[]>;
38
+ export declare function printFleet(rows: FleetRow[]): void;
39
+ /** Runs one mcplane command in every project, one after another. */
40
+ export declare function fleetRun(root: string, args: string[]): Promise<number>;
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,13 @@
1
+ import { type Manifest, type StoreId } from './types.js';
2
+ export interface Listing {
3
+ store: StoreId;
4
+ state: 'listed' | 'missing' | 'unknown';
5
+ url?: string;
6
+ /** Differences between the live record and your server or mcplane.json. */
7
+ issues: string[];
8
+ note?: string;
9
+ }
10
+ export declare function printListings(rows: Listing[]): void;
11
+ export declare function listings(m: Manifest, stores: StoreId[], opts?: {
12
+ token?: string;
13
+ }): Promise<Listing[]>;
@@ -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
+ }
@@ -0,0 +1,10 @@
1
+ import { type Manifest } from './types.js';
2
+ export { draftManifest, storesOf } from './draft.js';
3
+ export declare const MANIFEST_FILE = "mcplane.json";
4
+ export { SCHEMA_URL } from './draft.js';
5
+ export declare function loadManifest(dir?: string): Promise<Manifest>;
6
+ /**
7
+ * Creates .mcplane with its own .gitignore: snapshots are committed (CI reads
8
+ * them for drift), while submissions (private Review Times tokens) and packs stay local.
9
+ */
10
+ export declare function ensureLocalDir(dir?: string): Promise<string>;
package/dist/manifest.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { resolve } from 'node:path';
3
- import { McpClient } from './mcp-client.js';
4
3
  import { STORE_NAMES } from './types.js';
4
+ export { draftManifest, storesOf } from './draft.js';
5
5
  export const MANIFEST_FILE = 'mcplane.json';
6
- export const SCHEMA_URL = 'https://unpkg.com/mcplane/schema.json';
6
+ export { SCHEMA_URL } from './draft.js';
7
7
  export async function loadManifest(dir = process.cwd()) {
8
8
  const path = resolve(dir, MANIFEST_FILE);
9
9
  let raw;
@@ -44,54 +44,3 @@ export async function ensureLocalDir(dir = process.cwd()) {
44
44
  await writeFile(resolve(d, '.gitignore'), 'submissions.json\npacks/\n', { flag: 'wx' }).catch(() => null);
45
45
  return d;
46
46
  }
47
- export function storesOf(m) {
48
- return m.stores?.length ? m.stores : Object.keys(STORE_NAMES);
49
- }
50
- /** A starting manifest from what the live server and its domain reveal. */
51
- export async function draftManifest(url) {
52
- const client = new McpClient(url);
53
- const init = await client.initialize().catch(() => null);
54
- const auth = init?.status === 401 ? 'oauth' : 'none';
55
- const info = init?.body?.result?.serverInfo ?? {};
56
- const host = new URL(url).hostname;
57
- const labels = host.split('.');
58
- // api.example.com and mcp.example.com usually keep their pages on example.com.
59
- const origins = [`https://${host}`, ...(labels.length > 2 ? [`https://${labels.slice(1).join('.')}`] : [])];
60
- const origin = origins[origins.length - 1];
61
- const found = async (p) => {
62
- for (const o of origins) {
63
- const r = await fetch(o + p, { method: 'GET', redirect: 'follow' }).catch(() => null);
64
- if (r?.ok && !(r.headers.get('content-type') ?? '').includes('json'))
65
- return o + p;
66
- }
67
- return undefined;
68
- };
69
- const first = async (...paths) => {
70
- for (const p of paths) {
71
- const hit = await found(p);
72
- if (hit)
73
- return hit;
74
- }
75
- return undefined;
76
- };
77
- const [privacy, support, terms, docs, icon] = await Promise.all([
78
- first('/privacy', '/privacy-policy', '/legal/privacy'),
79
- first('/support', '/contact', '/help'),
80
- first('/terms', '/terms-of-service', '/legal/terms'),
81
- first('/llms.txt', '/docs'),
82
- first('/icon-512.png', '/icon.png', '/apple-touch-icon.png'),
83
- ]);
84
- return {
85
- $schema: SCHEMA_URL,
86
- name: String(info.name ?? new URL(url).hostname.split('.')[0]),
87
- title: String(info.title ?? info.name ?? ''),
88
- subtitle: '',
89
- oneLiner: '',
90
- description: '',
91
- server: { url, auth },
92
- author: { name: '', url: origin },
93
- links: { website: origin, support, privacy, terms, docs },
94
- icon,
95
- stores: ['mcp-registry', 'chatgpt', 'claude-connectors', 'cursor', 'muse'],
96
- };
97
- }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Just enough of a streamable-HTTP MCP client to inspect a server the way a
3
+ * store's scanner does: plain POSTs, JSON or SSE replies, a session id if the
4
+ * server hands one out.
5
+ */
6
+ export interface RpcReply {
7
+ status: number;
8
+ contentType: string;
9
+ body: any;
10
+ headers: Headers;
11
+ }
12
+ export declare class McpClient {
13
+ readonly url: string;
14
+ private extraHeaders;
15
+ private timeoutMs;
16
+ private session;
17
+ private id;
18
+ constructor(url: string, extraHeaders?: Record<string, string>, timeoutMs?: number);
19
+ request(method: string, params?: unknown, opts?: {
20
+ notification?: boolean;
21
+ url?: string;
22
+ }): Promise<RpcReply>;
23
+ initialize(): Promise<RpcReply>;
24
+ }
@@ -0,0 +1 @@
1
+ export declare function serve(): Promise<void>;
@@ -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.
@@ -0,0 +1,32 @@
1
+ import type { Manifest, StoreId, Tool } from './types.js';
2
+ export interface Pack {
3
+ files: {
4
+ path: string;
5
+ content: string;
6
+ }[];
7
+ problems: string[];
8
+ todo: string[];
9
+ }
10
+ /** "Add the user's…" → "adds the user's…"; leaves non-verb openings alone. */
11
+ declare function thirdPerson(s: string): string | null;
12
+ /** One-sentence drafts from the declared hints and the tool's own description. Review them: they are a start, not an answer. */
13
+ declare function justify(t: Tool, product: string, own?: {
14
+ readOnly?: string;
15
+ openWorld?: string;
16
+ destructive?: string;
17
+ }): {
18
+ read_only_justification: string;
19
+ open_world_justification: string;
20
+ destructive_justification: string;
21
+ };
22
+ export declare function chatgptPack(m: Manifest, token?: string): Promise<Pack>;
23
+ export declare function claudePack(m: Manifest, token?: string): Promise<Pack>;
24
+ export declare function simplePack(m: Manifest, store: StoreId): Promise<Pack>;
25
+ /** The long tail: directories that only take a web form. One sheet with every value to paste. */
26
+ export declare function directoriesPack(m: Manifest): Pack;
27
+ export declare function writePack(pack: Pack, dir?: string): Promise<string[]>;
28
+ export declare const _test: {
29
+ thirdPerson: typeof thirdPerson;
30
+ justify: typeof justify;
31
+ };
32
+ export {};
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: [] };
@@ -0,0 +1,9 @@
1
+ import { type PreflightResult } from './core.js';
2
+ import type { Manifest, StoreId } from './types.js';
3
+ export type { PreflightResult } from './core.js';
4
+ /** Preflight with everything Node can check, the TLS 1.2 handshake included. */
5
+ export declare function preflight(m: Manifest, opts?: {
6
+ stores?: StoreId[];
7
+ register?: boolean;
8
+ token?: string;
9
+ }): Promise<PreflightResult>;
package/dist/preflight.js CHANGED
@@ -1,39 +1,6 @@
1
- import { authChecks } from './checks/auth.js';
2
- import { listingChecks, repoChecks } from './checks/listing.js';
3
- import { serverChecks } from './checks/server.js';
4
- import { toolChecks } from './checks/tools.js';
5
- import { storesOf } from './manifest.js';
6
- /** Things no script can see, which have still cost real submissions. */
7
- function reminders(m, stores) {
8
- const r = [];
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.');
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
- }
13
- if (stores.includes('chatgpt')) {
14
- 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
- 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.');
16
- r.push('Tool-annotation justifications are cut at 200 characters without warning.');
17
- r.push('ChatGPT allows one version in review at a time. To change a submission, Cancel Review and resubmit the same draft.');
18
- }
19
- if (stores.includes('claude-connectors') || stores.includes('claude-plugins')) {
20
- if (stores.includes('claude-plugins'))
21
- r.push('Run "claude plugin validate --strict" in the plugin repo; the portal validates every commit on the branch it tracks.');
22
- 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.');
23
- }
24
- return r;
25
- }
26
- export async function preflight(m, opts = {}) {
27
- const stores = opts.stores?.length ? opts.stores : storesOf(m);
28
- const auth = m.server.auth ?? 'none';
29
- const server = await serverChecks(m.server.url, auth, opts.token);
30
- const [authC, listing, repo] = await Promise.all([
31
- auth === 'oauth' ? authChecks(m.server.url, { register: opts.register }) : Promise.resolve([]),
32
- listingChecks(m, server.tools, stores),
33
- repoChecks(m, stores),
34
- ]);
35
- const all = [...server.checks, ...authC, ...toolChecks(server.tools), ...listing, ...repo];
36
- // Keep checks that apply to every store or to one we're submitting to.
37
- const checks = all.filter((c) => !c.stores?.length || c.stores.some((s) => stores.includes(s)));
38
- return { checks, reminders: reminders(m, stores), stores };
1
+ import { tls12Check } from './checks/tls.js';
2
+ import { preflight as core } from './core.js';
3
+ /** Preflight with everything Node can check, the TLS 1.2 handshake included. */
4
+ export function preflight(m, opts = {}) {
5
+ return core(m, { ...opts, extraChecks: [tls12Check] });
39
6
  }