mcplane 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +108 -18
  3. package/dist/agent-plugins.d.ts +6 -0
  4. package/dist/agent-plugins.js +76 -0
  5. package/dist/cdp.d.ts +49 -0
  6. package/dist/cdp.js +253 -0
  7. package/dist/checks/ard.d.ts +67 -0
  8. package/dist/checks/ard.js +215 -0
  9. package/dist/checks/auth.js +2 -1
  10. package/dist/checks/chain.d.ts +45 -0
  11. package/dist/checks/chain.js +171 -0
  12. package/dist/checks/listing.js +23 -14
  13. package/dist/checks/oauth.d.ts +45 -0
  14. package/dist/checks/oauth.js +64 -0
  15. package/dist/checks/readiness.d.ts +58 -0
  16. package/dist/checks/readiness.js +230 -0
  17. package/dist/checks/tools.js +38 -7
  18. package/dist/checks/vercel.d.ts +27 -0
  19. package/dist/checks/vercel.js +153 -0
  20. package/dist/cli.js +54 -6
  21. package/dist/core.d.ts +1 -0
  22. package/dist/core.js +20 -4
  23. package/dist/demo.d.ts +60 -0
  24. package/dist/demo.js +573 -0
  25. package/dist/drift.js +10 -3
  26. package/dist/evals.d.ts +34 -0
  27. package/dist/evals.js +75 -0
  28. package/dist/extras.js +1 -1
  29. package/dist/images.d.ts +21 -0
  30. package/dist/images.js +99 -0
  31. package/dist/listings.d.ts +14 -0
  32. package/dist/listings.js +40 -1
  33. package/dist/mcp-client.d.ts +1 -5
  34. package/dist/mcp-client.js +8 -1
  35. package/dist/mcp-server.js +21 -11
  36. package/dist/packs.d.ts +48 -18
  37. package/dist/packs.js +380 -90
  38. package/dist/publish.js +13 -4
  39. package/dist/report.js +30 -13
  40. package/dist/skills.d.ts +21 -0
  41. package/dist/skills.js +180 -0
  42. package/dist/submissions.js +2 -1
  43. package/dist/types.d.ts +37 -4
  44. package/dist/types.js +1 -0
  45. package/dist/version.d.ts +1 -1
  46. package/dist/version.js +1 -1
  47. package/dist/video.d.ts +58 -0
  48. package/dist/video.js +190 -0
  49. package/dist/zip.d.ts +16 -2
  50. package/dist/zip.js +65 -5
  51. package/package.json +4 -1
  52. package/schema.json +86 -12
  53. package/schemas/agent-plugins-1.0.0/LICENSE +202 -0
  54. package/schemas/agent-plugins-1.0.0/mcp.schema.json +120 -0
  55. package/schemas/agent-plugins-1.0.0/plugin.schema.json +65 -0
package/dist/evals.js ADDED
@@ -0,0 +1,75 @@
1
+ const safe = (s) => s.replace(/[^a-zA-Z0-9_-]/g, '_');
2
+ /** How Claude Code names a plugin's MCP tools. */
3
+ export const toolId = (p, tool) => `mcp__plugin_${safe(p.plugin)}_${safe(p.server)}__${tool}`;
4
+ /** Case directory names: stable, ordered and readable, cut at a word. */
5
+ export const caseSlug = (s) => {
6
+ const slug = s
7
+ .toLowerCase()
8
+ .replace(/['’]/g, '')
9
+ .replace(/[^a-z0-9]+/g, '-')
10
+ .replace(/^-|-$/g, '');
11
+ return slug.length <= 48 ? slug : slug.slice(0, 49).replace(/-[^-]*$/, '');
12
+ };
13
+ /** Markdown with YAML frontmatter. Strings go out as JSON, which YAML reads as quoted scalars, so quotes and colons can't break it. */
14
+ export function frontmatter(fields, body = '') {
15
+ const value = (v) => (Array.isArray(v) ? `[${v.map(value).join(', ')}]` : typeof v === 'string' ? JSON.stringify(v) : String(v));
16
+ const lines = Object.entries(fields)
17
+ .filter(([, v]) => v !== undefined)
18
+ .map(([k, v]) => `${k}: ${value(v)}`);
19
+ return `---\n${lines.join('\n')}\n---\n${body.trim() ? `\n${body.trim()}\n` : ''}`;
20
+ }
21
+ /** An agent mock: a small model answers as the tool, from its description and output schema. */
22
+ function mock(m, name, tool, test) {
23
+ const lines = [
24
+ `You are the ${name} tool of ${m.title}'s MCP server (${m.server.url}).`,
25
+ tool?.description ? `What it does: ${tool.description}` : '',
26
+ tool?.outputSchema ? `Reply with JSON that matches this output schema, with realistic values: ${JSON.stringify(tool.outputSchema)}` : 'Reply the way the real tool would, with realistic values.',
27
+ test ? `In this case the user asked: "${test.prompt}". Return what lets the assistant give this: ${test.expected}` : '',
28
+ 'Never say that you are a stand-in.',
29
+ ];
30
+ return frontmatter({ type: 'agent' }, lines.filter(Boolean).join('\n'));
31
+ }
32
+ export function evalSuite(m, p, tools) {
33
+ const files = [];
34
+ const problems = [];
35
+ const pos = m.tests?.positive ?? [];
36
+ const neg = m.tests?.negative ?? [];
37
+ if (!pos.length && !neg.length)
38
+ problems.push('mcplane.json has no tests.positive or tests.negative to turn into cases');
39
+ const live = new Set(tools.map((t) => t.name));
40
+ // Without a live tool list (sign-in), the tools the tests name are all we know.
41
+ const names = tools.length ? tools.map((t) => t.name) : [...new Set(pos.flatMap((t) => t.tools))];
42
+ const toolNamed = (n) => tools.find((t) => t.name === n);
43
+ pos.forEach((t, i) => {
44
+ const dir = `evals/positive-${i + 1}-${caseSlug(t.scenario)}`;
45
+ files.push({ path: `${dir}/prompt.md`, content: frontmatter({ description: t.scenario, tags: ['mcplane', 'positive'], expected_outcome: t.expected, max_turns: 10, allowed_tools: ['Skill'] }, t.prompt) });
46
+ for (const name of t.tools) {
47
+ if (tools.length && !live.has(name))
48
+ problems.push(`test "${t.scenario}" names a tool the server doesn't have: ${name}`);
49
+ // Not scored: it can't pass without the plugin, and counting it would inflate the plugin's delta.
50
+ files.push({ path: `${dir}/graders/calls-${name}.md`, content: frontmatter({ type: 'tool_used', tool: toolId(p, name), arm: 'with-only' }) });
51
+ files.push({ path: `${dir}/mocks/${p.server}/${name}.md`, content: mock(m, name, toolNamed(name), t) });
52
+ }
53
+ files.push({
54
+ path: `${dir}/graders/answer.md`,
55
+ content: frontmatter({ type: 'llm' }, `PASS if the reply gives the user what this request should get: ${t.expected}\nFAIL if it doesn't, or if it says a tool failed or wasn't available.`),
56
+ });
57
+ });
58
+ // A case with no grader fails to load, and a negative case's graders are the tool names.
59
+ if (neg.length && !names.length)
60
+ problems.push('No tool names for the negative cases: tools/list returned none (pass --token for a server behind sign-in) and no positive test names a tool');
61
+ if (names.length)
62
+ neg.forEach((t, i) => {
63
+ const dir = `evals/negative-${i + 1}-${caseSlug(t.scenario)}`;
64
+ files.push({ path: `${dir}/prompt.md`, content: frontmatter({ description: t.scenario, tags: ['mcplane', 'negative'], max_turns: 6, allowed_tools: ['Skill'] }, t.prompt) });
65
+ // Scored in both arms: staying quiet is the point.
66
+ for (const name of names)
67
+ files.push({ path: `${dir}/graders/no-${name}.md`, content: frontmatter({ type: 'tool_used', tool: toolId(p, name), min: 0, max: 0, arm: 'both' }) });
68
+ });
69
+ // Suite-wide mocks for every tool, so a call in any case gets an answer rather than a missing tool.
70
+ if (tools.length)
71
+ files.push({ path: `evals/mocks/${p.server}/_tools.json`, content: JSON.stringify({ tools }, null, 2) + '\n' });
72
+ for (const name of names)
73
+ files.push({ path: `evals/mocks/${p.server}/${name}.md`, content: mock(m, name, toolNamed(name)) });
74
+ return { files, problems };
75
+ }
package/dist/extras.js CHANGED
@@ -18,7 +18,7 @@ export const DEFAULT_LANES = {
18
18
  'pack claude-connectors',
19
19
  'publish mcp-registry',
20
20
  'publish grok',
21
- 'handoff: Upload the ZIP in .mcplane/packs/chatgpt/ at platform.openai.com/plugins, add reviewer credentials and hint justifications (see chatgpt.md), make the attestations and submit. Then: mcplane submitted chatgpt --app-id <asdk_app_…>',
21
+ 'handoff: Upload the ZIP in .mcplane/packs/chatgpt/ at platform.openai.com/plugins, add reviewer credentials (see chatgpt.md), make the attestations and submit. Then: mcplane submitted chatgpt --app-id <asdk_app_…>',
22
22
  'handoff: Paste .mcplane/packs/claude-connectors.md into claude.ai/directory/manage/new, make the compliance statements yourself and submit. Then: mcplane submitted claude-connectors',
23
23
  ],
24
24
  };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Image facts the stores check, read from the bytes rather than the name or content type:
3
+ * the format, and pixel (or SVG viewBox) dimensions. Enough of PNG, JPEG, WebP and SVG to say
4
+ * whether an icon is square and big enough. No dependencies, so it runs anywhere.
5
+ */
6
+ export type ImageFormat = 'png' | 'jpeg' | 'webp' | 'svg';
7
+ export interface ImageInfo {
8
+ format: ImageFormat | null;
9
+ width?: number;
10
+ height?: number;
11
+ /** Why the dimensions couldn't be read. */
12
+ error?: string;
13
+ }
14
+ export declare const EXTENSION: Record<ImageFormat, string>;
15
+ export declare function sniff(b: Uint8Array): ImageFormat | null;
16
+ export declare function imageInfo(bytes: Uint8Array): ImageInfo;
17
+ /** What's wrong with an image as a store icon: square, and 48 to 4096 px for raster formats. Empty when it's fine. */
18
+ export declare function iconProblems(info: ImageInfo, label: string, opts?: {
19
+ min?: number;
20
+ max?: number;
21
+ }): string[];
package/dist/images.js ADDED
@@ -0,0 +1,99 @@
1
+ export const EXTENSION = { png: 'png', jpeg: 'jpg', webp: 'webp', svg: 'svg' };
2
+ const ascii = (b, from, to) => String.fromCharCode(...b.subarray(from, to));
3
+ export function sniff(b) {
4
+ if (b.length >= 8 && [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((v, i) => b[i] === v))
5
+ return 'png';
6
+ if (b.length >= 3 && b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
7
+ return 'jpeg';
8
+ if (b.length >= 12 && ascii(b, 0, 4) === 'RIFF' && ascii(b, 8, 12) === 'WEBP')
9
+ return 'webp';
10
+ const head = new TextDecoder().decode(b.subarray(0, 8192)).replace(/^/, '').trimStart();
11
+ if (/^(<\?xml[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*(<!DOCTYPE[^>]*>\s*)?(<!--[\s\S]*?-->\s*)*<svg[\s>]/i.test(head))
12
+ return 'svg';
13
+ return null;
14
+ }
15
+ function png(b, v) {
16
+ if (b.length < 24 || ascii(b, 12, 16) !== 'IHDR')
17
+ throw new Error('PNG has no IHDR chunk');
18
+ return { width: v.getUint32(16), height: v.getUint32(20) };
19
+ }
20
+ function jpeg(b, v) {
21
+ let p = 2;
22
+ while (p + 9 < b.length) {
23
+ if (b[p] !== 0xff)
24
+ throw new Error('JPEG segment marker missing');
25
+ const marker = b[p + 1];
26
+ if (marker === 0xd9 || marker === 0xda)
27
+ break;
28
+ // Start-of-frame markers carry the size; C4, C8 and CC are other segments in the same range.
29
+ if (marker >= 0xc0 && marker <= 0xcf && ![0xc4, 0xc8, 0xcc].includes(marker))
30
+ return { height: v.getUint16(p + 5), width: v.getUint16(p + 7) };
31
+ p += 2 + v.getUint16(p + 2);
32
+ }
33
+ throw new Error('JPEG has no frame header');
34
+ }
35
+ function webp(b, v) {
36
+ const chunk = ascii(b, 12, 16);
37
+ const u24 = (at) => b[at] | (b[at + 1] << 8) | (b[at + 2] << 16);
38
+ if (chunk === 'VP8 ' && b.length >= 30)
39
+ return { width: v.getUint16(26, true) & 0x3fff, height: v.getUint16(28, true) & 0x3fff };
40
+ if (chunk === 'VP8L' && b.length >= 25) {
41
+ const bits = v.getUint32(21, true);
42
+ return { width: (bits & 0x3fff) + 1, height: ((bits >>> 14) & 0x3fff) + 1 };
43
+ }
44
+ if (chunk === 'VP8X' && b.length >= 30)
45
+ return { width: u24(24) + 1, height: u24(27) + 1 };
46
+ throw new Error(`unknown WebP chunk "${chunk}"`);
47
+ }
48
+ function svg(src) {
49
+ const root = /<svg\b([^>]*)>/i.exec(src.replace(/<!--[\s\S]*?-->/g, ''));
50
+ if (!root)
51
+ throw new Error('no <svg> element');
52
+ const attr = (n) => new RegExp(`\\s${n}\\s*=\\s*["']([^"']*)["']`, 'i').exec(root[1])?.[1];
53
+ const vb = attr('viewBox');
54
+ if (vb) {
55
+ const parts = vb.trim().split(/[\s,]+/).map(Number);
56
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n)))
57
+ throw new Error(`viewBox "${vb}" isn't four numbers`);
58
+ return { width: parts[2], height: parts[3] };
59
+ }
60
+ const [w, h] = [attr('width'), attr('height')];
61
+ if (!w || !h)
62
+ throw new Error('the SVG has neither a viewBox nor a width and height');
63
+ if (!/^\d+(\.\d+)?(px)?$/.test(w) || !/^\d+(\.\d+)?(px)?$/.test(h))
64
+ throw new Error(`width "${w}" and height "${h}" aren't plain numbers`);
65
+ return { width: parseFloat(w), height: parseFloat(h) };
66
+ }
67
+ export function imageInfo(bytes) {
68
+ const format = sniff(bytes);
69
+ if (!format)
70
+ return { format, error: 'not a PNG, JPEG, WebP or SVG file' };
71
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
72
+ try {
73
+ if (format === 'png')
74
+ return { format, ...png(bytes, v) };
75
+ if (format === 'jpeg')
76
+ return { format, ...jpeg(bytes, v) };
77
+ if (format === 'webp')
78
+ return { format, ...webp(bytes, v) };
79
+ return { format, ...svg(new TextDecoder().decode(bytes)) };
80
+ }
81
+ catch (e) {
82
+ return { format, error: e.message };
83
+ }
84
+ }
85
+ /** What's wrong with an image as a store icon: square, and 48 to 4096 px for raster formats. Empty when it's fine. */
86
+ export function iconProblems(info, label, opts = {}) {
87
+ const min = opts.min ?? 48;
88
+ const max = opts.max ?? 4096;
89
+ if (!info.format || info.error || info.width === undefined || info.height === undefined)
90
+ return [`${label} ${info.error ?? "can't be read"}`];
91
+ const out = [];
92
+ if (info.width !== info.height)
93
+ out.push(`${label} is ${info.width}×${info.height}; it must be square`);
94
+ else if (info.format !== 'svg' && (info.width < min || info.width > max))
95
+ out.push(`${label} is ${info.width} px; it must be ${min} to ${max} px`);
96
+ else if (info.format === 'svg' && info.width < min)
97
+ out.push(`${label}'s viewBox is ${info.width} units; make it at least ${min}`);
98
+ return out;
99
+ }
@@ -7,6 +7,20 @@ export interface Listing {
7
7
  issues: string[];
8
8
  note?: string;
9
9
  }
10
+ export interface ConnectEntry {
11
+ slug: string;
12
+ name: string;
13
+ description: string;
14
+ /** Managed, OAuth, MCP, API Key, Beta. */
15
+ labels: string[];
16
+ /** "Experimental. This connector provider has not been verified by Vercel." */
17
+ experimental: boolean;
18
+ }
19
+ /**
20
+ * Vercel's directory has no feed, but serves an agent-readable copy of the page at /connect/browse.md:
21
+ * each service is **Name** and a description, its labels one per line, then [Learn more](/connect/<slug>).
22
+ */
23
+ export declare function parseConnectDirectory(md: string): ConnectEntry[];
10
24
  export declare function printListings(rows: Listing[]): void;
11
25
  export declare function listings(m: Manifest, stores: StoreId[], opts?: {
12
26
  token?: string;
package/dist/listings.js CHANGED
@@ -107,7 +107,7 @@ async function glama(m) {
107
107
  const url = `https://glama.ai/mcp/connectors/${registryName(m)}`;
108
108
  const code = await status(url);
109
109
  if (code === 200)
110
- return { store: 'glama', state: 'listed', url, issues: [], note: 'Imported from the registry. Claim it with glama.json so the score badge is yours.' };
110
+ return { store: 'glama', state: 'listed', url, issues: [], note: 'Imported from the registry. Claim it (Claim ownership on that page; "mcplane publish glama" has the steps) so the listing and badge are yours.' };
111
111
  if (code === 404)
112
112
  return { store: 'glama', state: 'missing', issues: [], note: 'Glama imports connectors from the official registry; publish there first.' };
113
113
  return { store: 'glama', state: 'unknown', issues: [] };
@@ -124,6 +124,43 @@ async function docker(m) {
124
124
  const code = await status(`https://github.com/docker/mcp-registry/tree/main/servers/${m.name}`);
125
125
  return { store: 'docker', state: code === 200 ? 'listed' : code === 404 ? 'missing' : 'unknown', issues: [] };
126
126
  }
127
+ const CONNECT_LABELS = ['Managed', 'OAuth', 'MCP', 'API Key', 'Beta'];
128
+ /**
129
+ * Vercel's directory has no feed, but serves an agent-readable copy of the page at /connect/browse.md:
130
+ * each service is **Name** and a description, its labels one per line, then [Learn more](/connect/<slug>).
131
+ */
132
+ export function parseConnectDirectory(md) {
133
+ const parts = md.split(/\[Learn more\]\(\/connect\/([a-z0-9-]+)\)/);
134
+ const out = [];
135
+ for (let i = 0; i + 1 < parts.length; i += 2) {
136
+ const name = [...parts[i].matchAll(/\*\*([^*\n]+)\*\*/g)].pop();
137
+ if (!name)
138
+ continue;
139
+ const lines = parts[i]
140
+ .slice(name.index + name[0].length)
141
+ .split('\n')
142
+ .map((l) => l.trim())
143
+ .filter(Boolean);
144
+ const text = lines.filter((l) => !CONNECT_LABELS.includes(l)).join(' ');
145
+ const experimental = /^Experimental\./.test(text);
146
+ out.push({ slug: parts[i + 1], name: name[1].trim(), description: text.replace(/^Experimental\.[^.]*\./, '').trim(), labels: lines.filter((l) => CONNECT_LABELS.includes(l)), experimental });
147
+ }
148
+ return out;
149
+ }
150
+ async function vercelConnect(m) {
151
+ m = forStore(m, 'vercel-connect');
152
+ const browse = 'https://vercel.com/connect/browse';
153
+ const entries = parseConnectDirectory((await getText(`${browse}.md`)) ?? '');
154
+ // It's a page, not an API: if it stops parsing, say so rather than report "missing".
155
+ if (entries.length < 100)
156
+ return { store: 'vercel-connect', state: 'unknown', issues: [], note: `Couldn't read Vercel's directory. Check your listing at ${browse}.` };
157
+ const slug = m.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
158
+ const hit = entries.find((e) => e.slug === m.name || e.slug === slug || e.name.toLowerCase() === m.title.toLowerCase());
159
+ if (!hit)
160
+ return { store: 'vercel-connect', state: 'missing', issues: [], note: `Not among its ${entries.length} services, read from the agent-readable copy of ${browse}.` };
161
+ const note = [hit.labels.join(', '), hit.experimental && 'marked Experimental: not verified by Vercel'].filter(Boolean).join('; ');
162
+ return { store: 'vercel-connect', state: 'listed', url: `https://vercel.com/connect/${hit.slug}`, issues: [], note: note || undefined };
163
+ }
127
164
  export function printListings(rows) {
128
165
  const mark = { listed: '✓', missing: '·', unknown: '?' };
129
166
  for (const r of rows) {
@@ -165,6 +202,8 @@ export async function listings(m, stores, opts = {}) {
165
202
  return awesome(m, s, 'punkpeye/awesome-remote-mcp-servers');
166
203
  if (s === 'docker')
167
204
  return docker(m);
205
+ if (s === 'vercel-connect')
206
+ return vercelConnect(m);
168
207
  return Promise.resolve({ store: s, state: 'unknown', issues: [], note: MANUAL[s] });
169
208
  });
170
209
  return Promise.all(jobs);
@@ -1,8 +1,3 @@
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
1
  export interface RpcReply {
7
2
  status: number;
8
3
  contentType: string;
@@ -19,6 +14,7 @@ export declare class McpClient {
19
14
  request(method: string, params?: unknown, opts?: {
20
15
  notification?: boolean;
21
16
  url?: string;
17
+ headers?: Record<string, string>;
22
18
  }): Promise<RpcReply>;
23
19
  initialize(): Promise<RpcReply>;
24
20
  }
@@ -1,3 +1,9 @@
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
+ import { VERSION } from './version.js';
1
7
  const UA = 'mcplane (+https://github.com/stevysmith/mcplane)';
2
8
  export class McpClient {
3
9
  url;
@@ -18,6 +24,7 @@ export class McpClient {
18
24
  'mcp-protocol-version': '2025-06-18',
19
25
  'user-agent': UA,
20
26
  ...this.extraHeaders,
27
+ ...opts.headers,
21
28
  };
22
29
  if (this.session)
23
30
  headers['mcp-session-id'] = this.session;
@@ -38,7 +45,7 @@ export class McpClient {
38
45
  const r = await this.request('initialize', {
39
46
  protocolVersion: '2025-06-18',
40
47
  capabilities: {},
41
- clientInfo: { name: 'mcplane', version: '0.1.0' },
48
+ clientInfo: { name: 'mcplane', version: VERSION },
42
49
  });
43
50
  if (r.status < 300)
44
51
  await this.request('notifications/initialized', undefined, { notification: true }).catch(() => null);
@@ -14,7 +14,7 @@ import { fleet } from './fleet.js';
14
14
  import { tryLinks } from './extras.js';
15
15
  import { listings } from './listings.js';
16
16
  import { MANIFEST_FILE, draftManifest, loadManifest, storesOf } from './manifest.js';
17
- import { chatgptPack, claudePack, directoriesPack, simplePack, writePack } from './packs.js';
17
+ import { PACKS, packFor, writePack } from './packs.js';
18
18
  import { preflight } from './preflight.js';
19
19
  import { publish } from './publish.js';
20
20
  import { recordDecision, recordSubmitted, status } from './submissions.js';
@@ -32,14 +32,21 @@ export async function serve() {
32
32
  const server = new McpServer({ name: 'mcplane', title: 'mcplane', version: VERSION });
33
33
  server.registerTool('preflight', {
34
34
  title: 'Check an MCP server against store review rules',
35
- description: 'Checks a live MCP server, its listing links, icon and plugin repo against the known rejection causes of ChatGPT, Claude, Cursor, Grok, Muse and the MCP Registry. Returns each check with pass, warn, fail or skip, what was found and how to fix it, plus reminders that no script can verify. Uses mcplane.json, or a url for a server without one.',
36
- inputSchema: { project: PROJECT, url: z.string().url().optional(), stores: z.array(STORE).optional() },
35
+ description: 'Checks a live MCP server, its listing links, icon and plugin repo against the known rejection causes of ChatGPT, Claude, Cursor, Grok, Muse, Vercel Connect and the MCP Registry, plus its ARD manifest and its readiness for MCP 2026-07-28 (checks whose id starts with "readiness."; never blocking). For OAuth servers it also walks the sign-in chain with plain GETs (the authorization endpoint, then the sign-in pages in sign_in or server.signIn) looking for bot challenges and 403s. Returns each check with pass, warn, fail, skip or info (a note that never blocks), what was found and how to fix it, plus reminders that no script can verify. Uses mcplane.json, or a url for a server without one.',
36
+ inputSchema: {
37
+ project: PROJECT,
38
+ url: z.string().url().optional(),
39
+ stores: z.array(STORE).optional(),
40
+ sign_in: z.array(z.string().url()).optional().describe('Sign-in pages or identity-provider URLs the consent screen uses, added to server.signIn.'),
41
+ },
37
42
  annotations: { title: 'Preflight', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
38
- }, async ({ project, url, stores }) => {
43
+ }, async ({ project, url, stores, sign_in }) => {
39
44
  const m = url ? await draftManifest(url) : await loadManifest(dirOf(project));
45
+ if (sign_in?.length)
46
+ m.server.signIn = [...(m.server.signIn ?? []), ...sign_in];
40
47
  const r = await preflight(m, { stores, token: token() });
41
48
  const fails = r.checks.filter((c) => c.level === 'fail').length;
42
- return reply(r, `${fails} blocking, ${r.checks.filter((c) => c.level === 'warn').length} to review, ${r.checks.filter((c) => c.level === 'pass').length} passed.`);
49
+ return reply(r, `${fails} blocking, ${r.checks.filter((c) => c.level === 'warn').length} to review, ${r.checks.filter((c) => c.level === 'info').length} notes, ${r.checks.filter((c) => c.level === 'pass').length} passed.`);
43
50
  });
44
51
  server.registerTool('drift', {
45
52
  title: 'What each store needs since your last submission',
@@ -53,12 +60,16 @@ export async function serve() {
53
60
  });
54
61
  server.registerTool('pack', {
55
62
  title: 'Write a submission pack',
56
- description: 'Writes everything a store’s submission form asks for into .mcplane/packs: the plugin ZIP ChatGPT’s portal uploads (listing, icons, 5+3 tests, release notes) with a checklist for the dashboard-only steps, markdown packs for claude-connectors, cursor and muse, and one sheet for the form-only directories. Returns the files written and any problems to fix first.',
57
- inputSchema: { project: PROJECT, store: z.enum(['chatgpt', 'claude-connectors', 'cursor', 'muse', 'directories']) },
63
+ description: 'Writes everything a store’s submission form asks for into .mcplane/packs: the plugin ZIP ChatGPT’s portal uploads (listing, icons, 5+3 tests, release notes, and the skills in chatgpt.skills) with a checklist for the dashboard-only steps, markdown packs for claude-connectors, vercel-connect (Submit a Service values, OAuth method and token test), cursor and muse, and one sheet for the form-only directories. Also ard (a manifest for /.well-known/ard.json) and claude-eval (a "claude plugin eval" suite from the review tests). Returns the files written and any problems to fix first.',
64
+ inputSchema: {
65
+ project: PROJECT,
66
+ store: z.enum(PACKS),
67
+ tools_file: z.string().optional().describe('A saved tools/list result, relative to the project folder, for a server behind sign-in.'),
68
+ },
58
69
  annotations: { title: 'Pack', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
59
- }, async ({ project, store }) => {
70
+ }, async ({ project, store, tools_file }) => {
60
71
  const m = await loadManifest(dirOf(project));
61
- const pack = store === 'directories' ? directoriesPack(m) : store === 'chatgpt' ? await chatgptPack(m, token()) : store === 'claude-connectors' ? await claudePack(m, token()) : await simplePack(m, store);
72
+ const pack = await packFor(m, store, { token: token(), tools: tools_file, dir: dirOf(project) });
62
73
  const files = await writePack(pack, dirOf(project));
63
74
  return reply({ files, problems: pack.problems, todo: pack.todo });
64
75
  });
@@ -113,7 +124,7 @@ export async function serve() {
113
124
  }, async ({ project }) => reply({ links: tryLinks(await loadManifest(dirOf(project))) }));
114
125
  server.registerTool('listings', {
115
126
  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.',
127
+ description: 'Reads every store with a public record (MCP Registry, Claude connectors and plugins, Cursor, Grok, Glama, the awesome lists, Docker, Vercel Connect) 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
128
  inputSchema: { project: PROJECT, stores: z.array(STORE).optional() },
118
129
  annotations: { title: 'Listings', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
119
130
  }, async ({ project, stores }) => {
@@ -157,7 +168,6 @@ export async function serve() {
157
168
  - title; subtitle (30 characters max, no "AI" filler); oneLiner (one plain sentence: what it does); description (what it does, who it's for, what it can't do).
158
169
  - category (a ChatGPT category such as DEVELOPER_TOOLS or PRODUCTIVITY), stores (where it should list), repository if there's a public plugin repo, version.
159
170
  - tests: exactly 5 positive cases (scenario, a realistic user prompt, the tools it should call, the expected result) and 3 negative cases (prompts that should NOT trigger this app). Base them on real tool behaviour, not hopes.
160
- - justifications, only where the drafts would be wrong: one factual sentence per hint, under 200 characters.
161
171
  Write in plain language. No marketing words, no instructions to the model, no pricing or upgrade copy.
162
172
  3. Call preflight. Fix what you can in the code (annotations, titles, output schemas, descriptions) and list what needs me (legal pages, icon, OAuth setup).
163
173
  4. Show me the final listing text and the tests before anything is submitted.`));
package/dist/packs.d.ts CHANGED
@@ -7,37 +7,67 @@ export interface Pack {
7
7
  problems: string[];
8
8
  todo: string[];
9
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
- };
10
+ export declare function listTools(m: Manifest, token?: string): Promise<Tool[]>;
11
+ export interface PackOptions {
12
+ /** Access token for a server behind sign-in. */
13
+ token?: string;
14
+ /** A saved tools/list result to use instead of asking the server. */
15
+ tools?: string;
16
+ /** The folder holding mcplane.json, which local paths are relative to. */
17
+ dir?: string;
18
+ }
19
+ /** A saved tools/list: the result ({ "tools": [...] }), the whole JSON-RPC reply, or just the array. */
20
+ export declare function parseToolsFile(text: string, file?: string): Tool[];
21
+ /**
22
+ * The tool list a pack checks against: a saved file when given, else the live server. A pack that
23
+ * doesn't need the list to build (the ChatGPT ZIP, the Claude form) still builds when neither works.
24
+ */
25
+ declare function toolsFor(m: Manifest, opts: PackOptions): Promise<{
26
+ tools: Tool[] | null;
27
+ why?: string;
28
+ }>;
29
+ /**
30
+ * OpenAI no longer asks for hint justifications: its automated review checks the hints and you appeal
31
+ * if it flags one. Justifications already written in mcplane.json are kept as notes for that appeal.
32
+ * With no tool list, every note is kept and the hint values are left out.
33
+ */
34
+ declare function appealNotes(m: Manifest, tools: Tool[] | null): string;
22
35
  declare function category(raw?: string): string | null;
23
36
  /** WCAG contrast ratio between two #RRGGBB colours. */
24
37
  declare function contrast(a: string, b: string): number;
25
38
  /**
26
39
  * ChatGPT: the plugin ZIP the portal takes since DevDay (27 Sep 2026). plugin.json in the
27
40
  * Agent Plugins format carries the listing, the 5+3 test cases, demo video, release notes
28
- * and translations; mcp.json points at the server; icons are bundled. Reviewer credentials,
29
- * hint justifications and the policy attestations stay in the dashboard.
41
+ * and translations; mcp.json points at the server; icons are bundled. Reviewer credentials
42
+ * and the policy attestations stay in the dashboard.
43
+ */
44
+ export declare function chatgptPack(m: Manifest, opts?: PackOptions): Promise<Pack>;
45
+ export declare function claudePack(m: Manifest, opts?: PackOptions): Promise<Pack>;
46
+ /**
47
+ * Vercel Connect: every value for Submit a Service, the OAuth method as Connect will discover it,
48
+ * and the token test each OAuth method needs before the form lets you submit.
49
+ * https://vercel.com/docs/connect/providers#submit-your-service-to-vercel-connect
50
+ */
51
+ export declare function vercelConnectPack(m: Manifest): Promise<Pack>;
52
+ /** ARD: one manifest for /.well-known/ard.json that Lighthouse 13.5 also accepts at ai-catalog.json. */
53
+ export declare function ardPack(m: Manifest, opts?: PackOptions): Promise<Pack>;
54
+ /**
55
+ * Claude Code plugin evals: mcplane.json's review tests as a "claude plugin eval" suite, with mocks
56
+ * from your live tools/list, to copy into the plugin repo.
57
+ * https://code.claude.com/docs/en/plugin-evals
30
58
  */
31
- export declare function chatgptPack(m: Manifest, token?: string): Promise<Pack>;
32
- export declare function claudePack(m: Manifest, token?: string): Promise<Pack>;
59
+ export declare function claudeEvalPack(m: Manifest, opts?: PackOptions): Promise<Pack>;
60
+ /** Every pack by name: a store with a form, or one of the extras. */
61
+ export declare const PACKS: readonly ["chatgpt", "claude-connectors", "vercel-connect", "cursor", "muse", "directories", "ard", "claude-eval"];
62
+ export declare function packFor(m: Manifest, name: string, opts?: PackOptions): Promise<Pack>;
33
63
  export declare function simplePack(m: Manifest, store: StoreId): Promise<Pack>;
34
64
  /** The long tail: directories that only take a web form. One sheet with every value to paste. */
35
65
  export declare function directoriesPack(m: Manifest): Pack;
36
66
  export declare function writePack(pack: Pack, dir?: string): Promise<string[]>;
37
67
  export declare const _test: {
38
- thirdPerson: typeof thirdPerson;
39
- justify: typeof justify;
68
+ appealNotes: typeof appealNotes;
40
69
  contrast: typeof contrast;
41
70
  category: typeof category;
71
+ toolsFor: typeof toolsFor;
42
72
  };
43
73
  export {};