mcplane 0.3.1 → 0.4.1
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 +28 -1
- package/dist/checks/listing.js +17 -7
- package/dist/checks/tools.js +4 -0
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/draft.d.ts +2 -0
- package/dist/draft.js +8 -0
- package/dist/drift.d.ts +2 -0
- package/dist/drift.js +13 -2
- package/dist/extras.js +1 -1
- package/dist/listings.js +5 -3
- package/dist/manifest.d.ts +1 -1
- package/dist/manifest.js +1 -1
- package/dist/mcp-server.js +1 -1
- package/dist/packs.d.ts +12 -1
- package/dist/packs.js +201 -66
- package/dist/publish.js +13 -4
- package/dist/types.d.ts +35 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/zip.d.ts +4 -0
- package/dist/zip.js +75 -0
- package/package.json +1 -1
- package/schema.json +106 -1
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ npx mcplane fleet --root ~/Projects
|
|
|
72
72
|
|---|---|---|
|
|
73
73
|
| Official MCP Registry | **Publishes**: writes `server.json`, runs `mcp-publisher`, signs in with GitHub OIDC in Actions. Glama and PulseMCP import from it; GitHub's MCP gallery (VS Code) syncs it after a one-time manual onboarding | Versions can't change: bump, or a prerelease like `1.2.0-1` for listing-only edits. mcplane checks what's already published |
|
|
74
74
|
| Grok plugins | **Publishes**: opens the pull request to `xai-org/plugin-marketplace`, validated with xAI's own scripts | Opens a pin-bump PR when xAI's daily bump lags |
|
|
75
|
-
| ChatGPT | **
|
|
75
|
+
| ChatGPT | **Builds the plugin ZIP** the portal uploads since DevDay: `plugin.json` (Agent Plugins format) with listing, icons, 5+3 tests, demo video, release notes and translations, plus `mcp.json`. Checked against OpenAI's submission rules | Tool changes roll out after OpenAI's scans. Listing changes need a new ZIP and review, and drift says when |
|
|
76
76
|
| Claude connectors | **Prepares** every field of the directory form | Tool changes are live on deploy; listing edits (tool names included) are reviewed |
|
|
77
77
|
| Claude plugins | **Prepares** and checks the plugin repo | The portal tracks your branch; drift tells you a new version is waiting for review |
|
|
78
78
|
| Cursor | **Prepares** and checks the plugin repo | Pinned to the commit first added; drift flags when you've moved on |
|
|
@@ -122,6 +122,21 @@ A form that returned 200 is a claim; the public record is the result. `listings`
|
|
|
122
122
|
|
|
123
123
|
Coverage decays quietly, so run it on a schedule (`--ci` exits 1 on a mismatch). `fleet` runs it for every project.
|
|
124
124
|
|
|
125
|
+
## ChatGPT's plugin ZIP
|
|
126
|
+
|
|
127
|
+
Since DevDay (September 2026) ChatGPT takes a plugin ZIP. `mcplane pack chatgpt` builds it from `mcplane.json`: `plugin.json` in the Agent Plugins format, with listing, icons, the 5 positive and 3 negative test cases, demo video, release notes and translations, plus `mcp.json`. It checks the package against OpenAI's submission rules first: name and description lengths, category, the four https URLs, at most 3 starter prompts, square icons of 48 to 4096 px, brand-colour contrast.
|
|
128
|
+
|
|
129
|
+
```json
|
|
130
|
+
"chatgpt": {
|
|
131
|
+
"demoVideo": "https://example.com/demo.mp4",
|
|
132
|
+
"releaseNotes": "Adds hourly forecasts.",
|
|
133
|
+
"capabilities": ["Search forecasts", "Compare cities"],
|
|
134
|
+
"brandColor": "#2357C6"
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
What stays with you: upload the ZIP, enter reviewer credentials and the per-tool hint justifications (drafted in `chatgpt.md`), and make the attestations. Bump `version` for every upload. Tool changes don't need a new ZIP; OpenAI's scans pick them up, and `drift` tracks extension metadata (sidebar, panels, file viewers) as part of each tool.
|
|
139
|
+
|
|
125
140
|
## Keeping listings current
|
|
126
141
|
|
|
127
142
|
Every store calls your live server, so fixes reach users on their own. What doesn't: listing text, tool names in Claude's listing, 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.
|
|
@@ -237,6 +252,18 @@ It's what powers [reviewtimes.fyi/check](https://reviewtimes.fyi/check). The TLS
|
|
|
237
252
|
|
|
238
253
|
One file describes your server and its listings. [`schema.json`](schema.json) gives editors completion and validation; `mcplane init` adds the `$schema` line. [`examples/mcplane.json`](examples/mcplane.json) is a complete one, from [Review Times](https://reviewtimes.fyi)' own ChatGPT and Claude submissions.
|
|
239
254
|
|
|
255
|
+
|
|
256
|
+
Each store can have its own listing text, the way fastlane keeps metadata per language. Anything you leave out falls back to the shared fields:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
"description": "Shows how long AI app stores are taking to review submissions…",
|
|
260
|
+
"listing": {
|
|
261
|
+
"claude-connectors": { "description": "Ask Claude before you submit (\"How long is Claude plugin review taking?\")…" }
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Packs, checks, `drift` and `listings` all use the right text for each store.
|
|
266
|
+
|
|
240
267
|
## fastlane, mapped
|
|
241
268
|
|
|
242
269
|
| fastlane | mcplane |
|
package/dist/checks/listing.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { forStore } from '../draft.js';
|
|
1
2
|
const get = (url) => fetch(url, { redirect: 'follow', signal: AbortSignal.timeout(12_000), headers: { 'user-agent': 'mcplane' } }).catch(() => null);
|
|
2
3
|
/** Width and height from a PNG's IHDR chunk. */
|
|
3
4
|
function pngSize(buf) {
|
|
@@ -89,23 +90,32 @@ export async function listingChecks(m, tools, stores) {
|
|
|
89
90
|
checks.push({ id, level: 'pass', title: `${label} fits (${max} characters)`, stores: s });
|
|
90
91
|
};
|
|
91
92
|
if (has(stores, 'chatgpt')) {
|
|
92
|
-
|
|
93
|
-
len(
|
|
94
|
-
|
|
93
|
+
const g = forStore(m, 'chatgpt');
|
|
94
|
+
len(g.title, 30, 'listing.name', 'Display name', ['chatgpt']);
|
|
95
|
+
len(g.subtitle, 30, 'listing.subtitle', 'Subtitle', ['chatgpt']);
|
|
96
|
+
const prompts = g.prompts ?? [];
|
|
95
97
|
const bad = prompts.filter((p) => p.length > 128 || /(^|\s)@\w/.test(p));
|
|
98
|
+
if (prompts.length > 3)
|
|
99
|
+
bad.push(`${prompts.length} prompts (at most 3)`);
|
|
96
100
|
const dupes = prompts.filter((p, i) => prompts.findIndex((q) => q.trim().toLowerCase() === p.trim().toLowerCase()) !== i);
|
|
97
101
|
if (bad.length || dupes.length) {
|
|
98
|
-
checks.push({ id: 'listing.prompts', level: 'fail', title: 'Starter prompts are short, unique and free of @mentions', detail: [...bad.map((p) => `"${p.slice(0, 40)}…"`), ...dupes.map((p) => `duplicate: "${p.slice(0, 40)}"`)].join('; '), fix: 'ChatGPT
|
|
102
|
+
checks.push({ id: 'listing.prompts', level: 'fail', title: 'Starter prompts are short, unique and free of @mentions', detail: [...bad.map((p) => `"${p.slice(0, 40)}…"`), ...dupes.map((p) => `duplicate: "${p.slice(0, 40)}"`)].join('; '), fix: 'ChatGPT takes at most 3 starter prompts, each one line of up to 128 characters, unique, with no @mentions.', stores: ['chatgpt'] });
|
|
99
103
|
}
|
|
100
104
|
else if (prompts.length) {
|
|
101
105
|
checks.push({ id: 'listing.prompts', level: 'pass', title: 'Starter prompts are short, unique and free of @mentions', stores: ['chatgpt'] });
|
|
102
106
|
}
|
|
107
|
+
len(g.description, 4000, 'listing.chatgpt-description', 'Long description for ChatGPT', ['chatgpt']);
|
|
108
|
+
// The plugin package requires all four listing URLs for MCP review.
|
|
109
|
+
const missing = ['website', 'support', 'privacy', 'terms'].filter((k) => !g.links?.[k]?.startsWith('https://'));
|
|
110
|
+
checks.push(missing.length
|
|
111
|
+
? { id: 'listing.chatgpt-urls', level: 'fail', title: 'Website, support, privacy and terms URLs are set', detail: `missing or not https: ${missing.join(', ')}`, fix: 'ChatGPT’s plugin package requires all four as https URLs for MCP review.', stores: ['chatgpt'] }
|
|
112
|
+
: { id: 'listing.chatgpt-urls', level: 'pass', title: 'Website, support, privacy and terms URLs are set', stores: ['chatgpt'] });
|
|
103
113
|
}
|
|
104
114
|
if (has(stores, 'claude-connectors', 'claude-plugins'))
|
|
105
|
-
len(m.title, 100, 'listing.claude-name', 'Name for Claude', ['claude-connectors', 'claude-plugins']);
|
|
115
|
+
len(forStore(m, 'claude-connectors').title, 100, 'listing.claude-name', 'Name for Claude', ['claude-connectors', 'claude-plugins']);
|
|
106
116
|
if (has(stores, 'claude-connectors', 'claude-plugins'))
|
|
107
|
-
len(m.oneLiner, 200, 'listing.one-liner', 'One-liner', ['claude-connectors', 'claude-plugins']);
|
|
108
|
-
len(m.description, 2000, 'listing.description', 'Description', []);
|
|
117
|
+
len(forStore(m, 'claude-connectors').oneLiner, 200, 'listing.one-liner', 'One-liner', ['claude-connectors', 'claude-plugins']);
|
|
118
|
+
len(forStore(m, 'claude-connectors').description, 2000, 'listing.description', 'Description', []);
|
|
109
119
|
// Test cases must name tools the server actually has; descriptions drift away from live servers.
|
|
110
120
|
const named = [...new Set((m.tests?.positive ?? []).flatMap((t) => t.tools))];
|
|
111
121
|
if (tools.length && named.length) {
|
package/dist/checks/tools.js
CHANGED
|
@@ -118,5 +118,9 @@ export function toolChecks(tools) {
|
|
|
118
118
|
checks.push(upsell.length
|
|
119
119
|
? { id: 'tools.no-upsell', level: 'fail', title: 'No upgrade or pricing copy in tools', detail: upsell.map((t) => t.name).join(', '), fix: 'OpenAI prohibits in-app commerce for digital subscriptions, including freemium upsells. Keep plans and pricing on your site; tool text and errors should not steer users to upgrade.', stores: ['chatgpt'] }
|
|
120
120
|
: { id: 'tools.no-upsell', level: 'pass', title: 'No upgrade or pricing copy in tools' });
|
|
121
|
+
// ChatGPT extensions (DevDay 2026): entrypoints declared in _meta["openai/ui"]. Reported so they show up in reviews and drift.
|
|
122
|
+
const ext = tools.flatMap((t) => ((t._meta?.['openai/ui']?.entrypoints) ?? []).map((e) => `${t.name}: ${e.type}${e.extensions?.length ? ` (${e.extensions.join(', ')})` : ''}`));
|
|
123
|
+
if (ext.length)
|
|
124
|
+
checks.push({ id: 'tools.extensions', level: 'pass', title: 'ChatGPT extensions declared', detail: ext.join('; '), stores: ['chatgpt'] });
|
|
121
125
|
return checks;
|
|
122
126
|
}
|
package/dist/core.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare function preflight(m: Manifest, opts?: {
|
|
|
12
12
|
token?: string;
|
|
13
13
|
extraChecks?: SocketCheck[];
|
|
14
14
|
}): Promise<PreflightResult>;
|
|
15
|
-
export { draftManifest, storesOf, SCHEMA_URL } from './draft.js';
|
|
15
|
+
export { draftManifest, forStore, storesOf, SCHEMA_URL } from './draft.js';
|
|
16
16
|
export { toolChecks } from './checks/tools.js';
|
|
17
17
|
export { STORE_NAMES } from './types.js';
|
|
18
18
|
export type { Check, Level, Manifest, StoreId, Tool } from './types.js';
|
package/dist/core.js
CHANGED
|
@@ -47,6 +47,6 @@ export async function preflight(m, opts = {}) {
|
|
|
47
47
|
const checks = all.filter((c) => !c.stores?.length || c.stores.some((s) => stores.includes(s)));
|
|
48
48
|
return { checks, reminders: reminders(m, stores), stores };
|
|
49
49
|
}
|
|
50
|
-
export { draftManifest, storesOf, SCHEMA_URL } from './draft.js';
|
|
50
|
+
export { draftManifest, forStore, storesOf, SCHEMA_URL } from './draft.js';
|
|
51
51
|
export { toolChecks } from './checks/tools.js';
|
|
52
52
|
export { STORE_NAMES } from './types.js';
|
package/dist/draft.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type Manifest, type StoreId } from './types.js';
|
|
2
2
|
export declare const SCHEMA_URL = "https://unpkg.com/mcplane/schema.json";
|
|
3
|
+
/** The manifest as one store sees it: that store's listing text over the shared fields. */
|
|
4
|
+
export declare function forStore(m: Manifest, store: StoreId): Manifest;
|
|
3
5
|
export declare function storesOf(m: Manifest): StoreId[];
|
|
4
6
|
/** A starting manifest from what the live server and its domain reveal. */
|
|
5
7
|
export declare function draftManifest(url: string): Promise<Manifest>;
|
package/dist/draft.js
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
import { McpClient } from './mcp-client.js';
|
|
3
3
|
import { STORE_NAMES } from './types.js';
|
|
4
4
|
export const SCHEMA_URL = 'https://unpkg.com/mcplane/schema.json';
|
|
5
|
+
/** The manifest as one store sees it: that store's listing text over the shared fields. */
|
|
6
|
+
export function forStore(m, store) {
|
|
7
|
+
const o = m.listing?.[store];
|
|
8
|
+
if (!o)
|
|
9
|
+
return m;
|
|
10
|
+
const set = Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined && v !== ''));
|
|
11
|
+
return { ...m, ...set };
|
|
12
|
+
}
|
|
5
13
|
export function storesOf(m) {
|
|
6
14
|
return m.stores?.length ? m.stores : Object.keys(STORE_NAMES);
|
|
7
15
|
}
|
package/dist/drift.d.ts
CHANGED
package/dist/drift.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { resolve } from 'node:path';
|
|
13
13
|
import { ensureLocalDir } from './manifest.js';
|
|
14
|
+
import { forStore } from './draft.js';
|
|
14
15
|
import { McpClient } from './mcp-client.js';
|
|
15
16
|
import { STORE_NAMES } from './types.js';
|
|
16
17
|
const DIR = '.mcplane/snapshots';
|
|
@@ -21,6 +22,12 @@ function canon(v) {
|
|
|
21
22
|
const sort = (x) => (Array.isArray(x) ? x.map(sort) : x && typeof x === 'object' ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, sort(x[k])])) : x);
|
|
22
23
|
return JSON.stringify(sort(v));
|
|
23
24
|
}
|
|
25
|
+
/** The parts of _meta that put a tool on screen: MCP Apps UI, ChatGPT extension entrypoints, output templates. */
|
|
26
|
+
function uiOf(t) {
|
|
27
|
+
const m = t._meta ?? {};
|
|
28
|
+
const picked = Object.fromEntries(Object.entries({ ui: m.ui, openai: m['openai/ui'], template: m['openai/outputTemplate'] }).filter(([, v]) => v !== undefined));
|
|
29
|
+
return Object.keys(picked).length ? canon(picked) : '';
|
|
30
|
+
}
|
|
24
31
|
function shape(t) {
|
|
25
32
|
return {
|
|
26
33
|
name: t.name,
|
|
@@ -29,6 +36,7 @@ function shape(t) {
|
|
|
29
36
|
hints: { readOnly: t.annotations?.readOnlyHint, destructive: t.annotations?.destructiveHint, openWorld: t.annotations?.openWorldHint },
|
|
30
37
|
input: canon(t.inputSchema),
|
|
31
38
|
output: canon(t.outputSchema),
|
|
39
|
+
ui: uiOf(t),
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
function listingOf(m) {
|
|
@@ -69,7 +77,7 @@ export async function takeSnapshot(m, store, opts, dir = process.cwd()) {
|
|
|
69
77
|
version: opts.version,
|
|
70
78
|
state: opts.state,
|
|
71
79
|
tools: tools.map(shape),
|
|
72
|
-
listing: listingOf(m),
|
|
80
|
+
listing: listingOf(forStore(m, store)),
|
|
73
81
|
repoSha,
|
|
74
82
|
};
|
|
75
83
|
await ensureLocalDir(dir);
|
|
@@ -111,6 +119,9 @@ export function diffTools(before, now) {
|
|
|
111
119
|
fields.push('inputs');
|
|
112
120
|
if (o.output !== t.output)
|
|
113
121
|
fields.push('outputSchema');
|
|
122
|
+
// Snapshots from before 0.4 have no ui field; don't report that as a change.
|
|
123
|
+
if (o.ui !== undefined && o.ui !== t.ui)
|
|
124
|
+
fields.push('UI/extensions');
|
|
114
125
|
if ((o.title ?? '') !== (t.title ?? ''))
|
|
115
126
|
fields.push('title');
|
|
116
127
|
if ((o.description ?? '') !== (t.description ?? ''))
|
|
@@ -131,8 +142,8 @@ export async function drift(m, stores, opts = {}, dir = process.cwd()) {
|
|
|
131
142
|
const missing = [];
|
|
132
143
|
const [live, head] = await Promise.all([liveTools(m, opts.token), repoHead(m.repository)]);
|
|
133
144
|
const now = live.map(shape);
|
|
134
|
-
const listing = listingOf(m);
|
|
135
145
|
for (const store of stores) {
|
|
146
|
+
const listing = listingOf(forStore(m, store));
|
|
136
147
|
const snap = await readSnapshot(store, dir);
|
|
137
148
|
if (!snap) {
|
|
138
149
|
missing.push(store);
|
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 .mcplane/packs/chatgpt
|
|
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_…>',
|
|
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
|
};
|
package/dist/listings.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* public record is the result. Coverage decays quietly, so this is meant to
|
|
5
5
|
* run on a schedule.
|
|
6
6
|
*/
|
|
7
|
+
import { forStore } from './draft.js';
|
|
7
8
|
import { liveTools, repoHead } from './drift.js';
|
|
8
9
|
import { registryName } from './publish.js';
|
|
9
10
|
import { STORE_NAMES } from './types.js';
|
|
@@ -44,6 +45,7 @@ async function registry(m) {
|
|
|
44
45
|
return { store: 'mcp-registry', state: 'listed', url: `https://registry.modelcontextprotocol.io/v0/servers?search=${encodeURIComponent(name)}`, issues };
|
|
45
46
|
}
|
|
46
47
|
async function claudeConnectors(m, token) {
|
|
48
|
+
m = forStore(m, 'claude-connectors');
|
|
47
49
|
const data = await getJson('https://api.anthropic.com/api/directory/servers?verified_tier=anthropic,partner,community&visibility=commercial&limit=5000');
|
|
48
50
|
if (!data)
|
|
49
51
|
return { store: 'claude-connectors', state: 'unknown', issues: [], note: 'Anthropic’s directory feed did not answer.' };
|
|
@@ -52,9 +54,9 @@ async function claudeConnectors(m, token) {
|
|
|
52
54
|
return { store: 'claude-connectors', state: 'missing', issues: [] };
|
|
53
55
|
const issues = [];
|
|
54
56
|
if (m.oneLiner && norm(hit.one_liner) !== norm(m.oneLiner))
|
|
55
|
-
issues.push('one-liner differs from mcplane.json');
|
|
57
|
+
issues.push('one-liner differs from mcplane.json (edit the listing, or set listing["claude-connectors"] if the difference is intended)');
|
|
56
58
|
if (m.description && norm(hit.description) !== norm(m.description))
|
|
57
|
-
issues.push('description differs from mcplane.json');
|
|
59
|
+
issues.push('description differs from mcplane.json (edit the listing, or set listing["claude-connectors"] if the difference is intended)');
|
|
58
60
|
// The tool list is synced from your server at submission and isn't an editable field: compare it with what you serve now.
|
|
59
61
|
const published = hit.tool_names ?? [];
|
|
60
62
|
const live = (await liveTools(m, token).catch(() => [])).map((t) => t.name);
|
|
@@ -105,7 +107,7 @@ async function glama(m) {
|
|
|
105
107
|
const url = `https://glama.ai/mcp/connectors/${registryName(m)}`;
|
|
106
108
|
const code = await status(url);
|
|
107
109
|
if (code === 200)
|
|
108
|
-
return { store: 'glama', state: 'listed', url, issues: [], note: 'Imported from the registry. Claim it
|
|
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.' };
|
|
109
111
|
if (code === 404)
|
|
110
112
|
return { store: 'glama', state: 'missing', issues: [], note: 'Glama imports connectors from the official registry; publish there first.' };
|
|
111
113
|
return { store: 'glama', state: 'unknown', issues: [] };
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Manifest } from './types.js';
|
|
2
|
-
export { draftManifest, storesOf } from './draft.js';
|
|
2
|
+
export { draftManifest, forStore, storesOf } from './draft.js';
|
|
3
3
|
export declare const MANIFEST_FILE = "mcplane.json";
|
|
4
4
|
export { SCHEMA_URL } from './draft.js';
|
|
5
5
|
export declare function loadManifest(dir?: string): Promise<Manifest>;
|
package/dist/manifest.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { STORE_NAMES } from './types.js';
|
|
4
|
-
export { draftManifest, storesOf } from './draft.js';
|
|
4
|
+
export { draftManifest, forStore, storesOf } from './draft.js';
|
|
5
5
|
export const MANIFEST_FILE = 'mcplane.json';
|
|
6
6
|
export { SCHEMA_URL } from './draft.js';
|
|
7
7
|
export async function loadManifest(dir = process.cwd()) {
|
package/dist/mcp-server.js
CHANGED
|
@@ -53,7 +53,7 @@ export async function serve() {
|
|
|
53
53
|
});
|
|
54
54
|
server.registerTool('pack', {
|
|
55
55
|
title: 'Write a submission pack',
|
|
56
|
-
description: 'Writes everything a store’s submission form asks for into .mcplane/packs:
|
|
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
57
|
inputSchema: { project: PROJECT, store: z.enum(['chatgpt', 'claude-connectors', 'cursor', 'muse', 'directories']) },
|
|
58
58
|
annotations: { title: 'Pack', readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
59
59
|
}, async ({ project, store }) => {
|
package/dist/packs.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Manifest, StoreId, Tool } from './types.js';
|
|
|
2
2
|
export interface Pack {
|
|
3
3
|
files: {
|
|
4
4
|
path: string;
|
|
5
|
-
content: string;
|
|
5
|
+
content: string | Uint8Array;
|
|
6
6
|
}[];
|
|
7
7
|
problems: string[];
|
|
8
8
|
todo: string[];
|
|
@@ -19,6 +19,15 @@ declare function justify(t: Tool, product: string, own?: {
|
|
|
19
19
|
open_world_justification: string;
|
|
20
20
|
destructive_justification: string;
|
|
21
21
|
};
|
|
22
|
+
declare function category(raw?: string): string | null;
|
|
23
|
+
/** WCAG contrast ratio between two #RRGGBB colours. */
|
|
24
|
+
declare function contrast(a: string, b: string): number;
|
|
25
|
+
/**
|
|
26
|
+
* ChatGPT: the plugin ZIP the portal takes since DevDay (27 Sep 2026). plugin.json in the
|
|
27
|
+
* 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.
|
|
30
|
+
*/
|
|
22
31
|
export declare function chatgptPack(m: Manifest, token?: string): Promise<Pack>;
|
|
23
32
|
export declare function claudePack(m: Manifest, token?: string): Promise<Pack>;
|
|
24
33
|
export declare function simplePack(m: Manifest, store: StoreId): Promise<Pack>;
|
|
@@ -28,5 +37,7 @@ export declare function writePack(pack: Pack, dir?: string): Promise<string[]>;
|
|
|
28
37
|
export declare const _test: {
|
|
29
38
|
thirdPerson: typeof thirdPerson;
|
|
30
39
|
justify: typeof justify;
|
|
40
|
+
contrast: typeof contrast;
|
|
41
|
+
category: typeof category;
|
|
31
42
|
};
|
|
32
43
|
export {};
|
package/dist/packs.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Submission packs: everything a store
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* rest get a markdown pack to paste from.
|
|
2
|
+
* Submission packs: everything a store asks for, checked against its limits.
|
|
3
|
+
* ChatGPT gets the plugin ZIP its portal uploads; the rest get a markdown pack
|
|
4
|
+
* to paste from.
|
|
6
5
|
*/
|
|
7
6
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
8
|
-
import { resolve } from 'node:path';
|
|
7
|
+
import { dirname, resolve } from 'node:path';
|
|
9
8
|
import { ensureLocalDir } from './manifest.js';
|
|
9
|
+
import { forStore } from './draft.js';
|
|
10
10
|
import { McpClient } from './mcp-client.js';
|
|
11
|
-
|
|
11
|
+
import { zip } from './zip.js';
|
|
12
12
|
const MAX_JUSTIFICATION = 200; // the ChatGPT form cuts longer ones without warning
|
|
13
13
|
async function listTools(m, token) {
|
|
14
14
|
const c = new McpClient(m.server.url, token ? { authorization: `Bearer ${token}` } : {});
|
|
@@ -45,34 +45,105 @@ function justify(t, product, own) {
|
|
|
45
45
|
(a.destructiveHint ? `Can delete or overwrite data: it ${what}.` : a.readOnlyHint ? 'Read-only, so it cannot delete or overwrite anything.' : 'Only adds or updates the user’s own data; it never deletes or overwrites existing records.'), MAX_JUSTIFICATION),
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
+
/** The categories ChatGPT's package validator accepts, and the old form's names mapped onto them. */
|
|
49
|
+
const CATEGORIES = ['Productivity', 'Creativity', 'Developer Tools', 'Business & Operations', 'Data & Analytics', 'Communication', 'Education & Research', 'Security', 'Finance', 'Healthcare', 'Travel', 'Entertainment', 'Other'];
|
|
50
|
+
const OLD_CATEGORY = {
|
|
51
|
+
BUSINESS: 'Business & Operations', COLLABORATION: 'Communication', DESIGN: 'Creativity', DEVELOPER_TOOLS: 'Developer Tools', EDUCATION: 'Education & Research',
|
|
52
|
+
ENTERTAINMENT: 'Entertainment', FINANCE: 'Finance', PRODUCTIVITY: 'Productivity', TRAVEL: 'Travel', FOOD: 'Other', LIFESTYLE: 'Other', NEWS: 'Other', SHOPPING: 'Other',
|
|
53
|
+
};
|
|
54
|
+
function category(raw) {
|
|
55
|
+
if (!raw)
|
|
56
|
+
return null;
|
|
57
|
+
const hit = CATEGORIES.find((c) => c.toLowerCase() === raw.toLowerCase());
|
|
58
|
+
return hit ?? OLD_CATEGORY[raw.toUpperCase().replace(/[^A-Z]+/g, '_')] ?? null;
|
|
59
|
+
}
|
|
60
|
+
/** WCAG contrast ratio between two #RRGGBB colours. */
|
|
61
|
+
function contrast(a, b) {
|
|
62
|
+
const lum = (hex) => {
|
|
63
|
+
const [r, g, bl] = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((c) => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4));
|
|
64
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
|
|
65
|
+
};
|
|
66
|
+
const [x, y] = [lum(a), lum(b)].sort((p, q) => q - p);
|
|
67
|
+
return (x + 0.05) / (y + 0.05);
|
|
68
|
+
}
|
|
69
|
+
/** Downloads an icon and checks it against the package rules: square, 48 to 4096 px, 5 MiB, PNG/JPEG/WebP/SVG. */
|
|
70
|
+
async function fetchIcon(url, label, problems) {
|
|
71
|
+
const res = await fetch(url, { headers: { 'user-agent': 'mcplane' } }).catch(() => null);
|
|
72
|
+
if (!res?.ok) {
|
|
73
|
+
problems.push(`${label} ${url} didn't load (HTTP ${res?.status ?? 'error'})`);
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const type = res.headers.get('content-type') ?? '';
|
|
77
|
+
const data = new Uint8Array(await res.arrayBuffer());
|
|
78
|
+
const ext = type.includes('svg') || url.endsWith('.svg') ? 'svg' : type.includes('jpeg') || /\.jpe?g$/.test(url) ? 'jpg' : type.includes('webp') || url.endsWith('.webp') ? 'webp' : 'png';
|
|
79
|
+
if (data.length > 5 * 1024 * 1024)
|
|
80
|
+
problems.push(`${label} is ${(data.length / 1048576).toFixed(1)} MiB; the limit is 5 MiB`);
|
|
81
|
+
if (ext === 'png' && data[0] === 0x89) {
|
|
82
|
+
const v = new DataView(data.buffer, data.byteOffset);
|
|
83
|
+
const [w, h] = [v.getUint32(16), v.getUint32(20)];
|
|
84
|
+
if (w !== h)
|
|
85
|
+
problems.push(`${label} is ${w}×${h}; it must be square`);
|
|
86
|
+
else if (w < 48 || w > 4096)
|
|
87
|
+
problems.push(`${label} is ${w} px; it must be 48 to 4096 px`);
|
|
88
|
+
}
|
|
89
|
+
return { data, ext };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* ChatGPT: the plugin ZIP the portal takes since DevDay (27 Sep 2026). plugin.json in the
|
|
93
|
+
* Agent Plugins format carries the listing, the 5+3 test cases, demo video, release notes
|
|
94
|
+
* and translations; mcp.json points at the server; icons are bundled. Reviewer credentials,
|
|
95
|
+
* hint justifications and the policy attestations stay in the dashboard.
|
|
96
|
+
*/
|
|
48
97
|
export async function chatgptPack(m, token) {
|
|
98
|
+
m = forStore(m, 'chatgpt');
|
|
99
|
+
const g = m.chatgpt ?? {};
|
|
49
100
|
const tools = await listTools(m, token);
|
|
50
101
|
const problems = [];
|
|
51
102
|
const todo = [];
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
103
|
+
const need = (ok, msg) => {
|
|
104
|
+
if (!ok)
|
|
105
|
+
problems.push(msg);
|
|
106
|
+
};
|
|
107
|
+
const oneLine = (v, max, label) => {
|
|
108
|
+
need(v, `${label} is missing`);
|
|
109
|
+
if (v && v.length > max)
|
|
110
|
+
problems.push(`${label} is ${v.length} characters; the limit is ${max}`);
|
|
111
|
+
if (v && /\n/.test(v))
|
|
112
|
+
problems.push(`${label} must be one line`);
|
|
113
|
+
};
|
|
114
|
+
need(/^[a-z0-9]+(-[a-z0-9]+)*$/.test(m.name) && m.name.length <= 64, `name "${m.name}" must be lowercase letters, digits and single hyphens, at most 64 characters`);
|
|
115
|
+
need(m.version && /^\d+\.\d+\.\d+/.test(m.version), 'version must be a semantic version (e.g. 1.0.0); every upload needs a new one');
|
|
116
|
+
oneLine(m.title, 30, 'Display name (title)');
|
|
117
|
+
oneLine(m.subtitle, 30, 'Short description (subtitle)');
|
|
118
|
+
need(m.description, 'Long description is missing');
|
|
119
|
+
if ((m.description ?? '').length > 4000)
|
|
120
|
+
problems.push(`Long description is ${m.description.length} characters; the limit is 4000`);
|
|
121
|
+
const developerName = g.developerName ?? m.author?.name;
|
|
122
|
+
oneLine(developerName, 80, 'Developer name (chatgpt.developerName or author.name)');
|
|
123
|
+
const cat = category(m.category);
|
|
124
|
+
need(cat, `category "${m.category ?? ''}" isn't one of: ${CATEGORIES.join(', ')}`);
|
|
125
|
+
for (const [k, v] of Object.entries({ website: m.links?.website, support: m.links?.support, privacy: m.links?.privacy, terms: m.links?.terms }))
|
|
126
|
+
need(v && v.startsWith('https://'), `links.${k} must be an https URL (all four are required for review)`);
|
|
127
|
+
const prompts = m.prompts ?? [];
|
|
128
|
+
if (prompts.length > 3)
|
|
129
|
+
problems.push(`${prompts.length} starter prompts; the limit is 3`);
|
|
130
|
+
for (const p of prompts)
|
|
131
|
+
if (p.length > 128 || /\n/.test(p) || /(^|\s)@\w/.test(p))
|
|
132
|
+
problems.push(`starter prompt "${p.slice(0, 40)}…" must be one line, at most 128 characters, with no @mention`);
|
|
133
|
+
for (const c of g.capabilities ?? [])
|
|
134
|
+
if (c.length > 120)
|
|
135
|
+
problems.push(`capability "${c.slice(0, 40)}…" is over 120 characters`);
|
|
136
|
+
if ((g.capabilities ?? []).length > 20)
|
|
137
|
+
problems.push('at most 20 capabilities');
|
|
138
|
+
if (g.brandColor && (!/^#[0-9a-fA-F]{6}$/.test(g.brandColor) || contrast(g.brandColor, '#FFFFFF') < 2))
|
|
139
|
+
problems.push(`brandColor ${g.brandColor} needs 2:1 contrast against white`);
|
|
140
|
+
if (g.brandColorDark && (!/^#[0-9a-fA-F]{6}$/.test(g.brandColorDark) || contrast(g.brandColorDark, '#212121') < 2))
|
|
141
|
+
problems.push(`brandColorDark ${g.brandColorDark} needs 2:1 contrast against #212121`);
|
|
142
|
+
need(g.demoVideo, 'chatgpt.demoVideo is missing: a reviewer-accessible video URL showing the main use cases (required for review)');
|
|
143
|
+
need(g.releaseNotes, 'chatgpt.releaseNotes is missing (required for review)');
|
|
59
144
|
const missingHints = tools.filter((t) => ['readOnlyHint', 'openWorldHint', 'destructiveHint'].some((h) => typeof t.annotations?.[h] !== 'boolean'));
|
|
60
145
|
for (const t of missingHints)
|
|
61
146
|
problems.push(`${t.name} doesn't set all three hints; ChatGPT treats that as a blocker`);
|
|
62
|
-
const toolsOut = Object.fromEntries(tools.map((t) => [
|
|
63
|
-
t.name,
|
|
64
|
-
{
|
|
65
|
-
annotations: {
|
|
66
|
-
readOnlyHint: !!t.annotations?.readOnlyHint,
|
|
67
|
-
openWorldHint: !!t.annotations?.openWorldHint,
|
|
68
|
-
destructiveHint: !!t.annotations?.destructiveHint,
|
|
69
|
-
},
|
|
70
|
-
justifications: justify(t, m.title, m.justifications?.[t.name]),
|
|
71
|
-
},
|
|
72
|
-
]));
|
|
73
|
-
const drafted = tools.filter((t) => !m.justifications?.[t.name]).map((t) => t.name);
|
|
74
|
-
if (drafted.length)
|
|
75
|
-
todo.push(`Read the drafted justifications for ${drafted.join(', ')}: they come from your hints and tool descriptions. Once they say exactly what each tool changes, save them under "justifications" in mcplane.json so every version reuses them.`);
|
|
76
147
|
for (const [name, j] of Object.entries(m.justifications ?? {}))
|
|
77
148
|
for (const [k, v] of Object.entries(j))
|
|
78
149
|
if (v && v.length > MAX_JUSTIFICATION)
|
|
@@ -92,54 +163,115 @@ export async function chatgptPack(m, token) {
|
|
|
92
163
|
for (const t of tools)
|
|
93
164
|
if (!covered.has(t.name))
|
|
94
165
|
todo.push(`No positive test uses ${t.name}. Reviewers check every tool.`);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
166
|
+
// Icons, bundled into the package.
|
|
167
|
+
const assets = [];
|
|
168
|
+
const icon = async (url, file, label) => {
|
|
169
|
+
if (!url)
|
|
170
|
+
return undefined;
|
|
171
|
+
const got = await fetchIcon(url, label, problems);
|
|
172
|
+
if (!got)
|
|
173
|
+
return undefined;
|
|
174
|
+
const path = `assets/${file}.${got.ext}`;
|
|
175
|
+
assets.push({ path, data: got.data });
|
|
176
|
+
return `./${path}`;
|
|
177
|
+
};
|
|
178
|
+
need(m.icon, 'icon is missing: a direct URL to a square image, 48 to 4096 px');
|
|
179
|
+
const logo = await icon(m.icon, 'logo', 'icon');
|
|
180
|
+
const composerIcon = (await icon(g.composerIcon, 'composer-icon', 'chatgpt.composerIcon')) ?? logo;
|
|
181
|
+
const logoDark = await icon(g.logoDark, 'logo-dark', 'chatgpt.logoDark');
|
|
182
|
+
const plugin = {
|
|
183
|
+
$schema: 'https://agent-plugins.org/schemas/1.0.0/plugin.schema.json',
|
|
184
|
+
name: m.name,
|
|
185
|
+
version: m.version ?? '1.0.0',
|
|
186
|
+
description: m.oneLiner ?? m.subtitle ?? m.title,
|
|
187
|
+
author: { name: developerName ?? m.title, ...(m.author?.url ? { url: m.author.url } : {}) },
|
|
188
|
+
...(m.links?.website ? { homepage: m.links.website } : {}),
|
|
189
|
+
...(m.repository ? { repository: m.repository } : {}),
|
|
190
|
+
extensions: {
|
|
191
|
+
'com.openai': {
|
|
192
|
+
interface: {
|
|
193
|
+
displayName: m.title,
|
|
194
|
+
shortDescription: m.subtitle ?? '',
|
|
195
|
+
longDescription: m.description ?? '',
|
|
196
|
+
developerName: developerName ?? '',
|
|
197
|
+
category: cat ?? 'Other',
|
|
198
|
+
capabilities: g.capabilities ?? [],
|
|
199
|
+
websiteURL: m.links?.website,
|
|
200
|
+
supportURL: m.links?.support,
|
|
201
|
+
privacyPolicyURL: m.links?.privacy,
|
|
202
|
+
termsOfServiceURL: m.links?.terms,
|
|
203
|
+
...(prompts.length ? { defaultPrompt: prompts.slice(0, 3) } : {}),
|
|
204
|
+
...(g.brandColor ? { brandColor: g.brandColor } : {}),
|
|
205
|
+
...(g.brandColorDark ? { brandColorDark: g.brandColorDark } : {}),
|
|
206
|
+
...(logo ? { logo } : {}),
|
|
207
|
+
...(composerIcon ? { composerIcon } : {}),
|
|
208
|
+
...(logoDark ? { logoDark } : {}),
|
|
209
|
+
},
|
|
210
|
+
review: {
|
|
211
|
+
test_cases: {
|
|
212
|
+
positive: pos.map((p) => ({ description: p.scenario, prompt: p.prompt, tools_triggered: p.tools.join(', '), expected_behavior: p.expected })),
|
|
213
|
+
negative: neg.map((n) => ({ description: n.scenario, prompt: n.prompt })),
|
|
214
|
+
},
|
|
215
|
+
...(g.demoVideo ? { demo_recording_url: g.demoVideo } : {}),
|
|
216
|
+
...(g.commerce !== undefined ? { commerce: g.commerce } : {}),
|
|
217
|
+
...(g.commerceDescription ? { commerce_description: g.commerceDescription } : {}),
|
|
218
|
+
},
|
|
219
|
+
publication: {
|
|
220
|
+
...(g.countries ? { countries: g.countries } : {}),
|
|
221
|
+
...(g.releaseNotes ? { release_notes: g.releaseNotes } : {}),
|
|
222
|
+
...(g.translations ? { translations: g.translations } : {}),
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
},
|
|
110
226
|
};
|
|
111
|
-
const
|
|
227
|
+
const mcp = { $schema: 'https://agent-plugins.org/schemas/1.0.0/mcp.schema.json', mcpServers: { [m.name]: { type: 'streamable-http', url: m.server.url } } };
|
|
228
|
+
const pkg = zip([
|
|
229
|
+
{ path: 'plugin.json', data: JSON.stringify(plugin, null, 2) + '\n' },
|
|
230
|
+
{ path: 'mcp.json', data: JSON.stringify(mcp, null, 2) + '\n' },
|
|
231
|
+
...assets,
|
|
232
|
+
]);
|
|
233
|
+
const zipName = `${m.name}-${m.version ?? '1.0.0'}.zip`;
|
|
234
|
+
// Justifications still go in the dashboard, one per hint, per tool.
|
|
235
|
+
const justified = tools
|
|
236
|
+
.map((t) => {
|
|
237
|
+
const j = justify(t, m.title, m.justifications?.[t.name]);
|
|
238
|
+
const a = t.annotations ?? {};
|
|
239
|
+
return `### ${t.name}\n- readOnlyHint ${!!a.readOnlyHint}: ${j.read_only_justification}\n- openWorldHint ${!!a.openWorldHint}: ${j.open_world_justification}\n- destructiveHint ${!!a.destructiveHint}: ${j.destructive_justification}`;
|
|
240
|
+
})
|
|
241
|
+
.join('\n\n');
|
|
242
|
+
const drafted = tools.filter((t) => !m.justifications?.[t.name]).map((t) => t.name);
|
|
243
|
+
if (drafted.length)
|
|
244
|
+
todo.push(`Read the drafted justifications for ${drafted.join(', ')} in chatgpt.md. Once they say exactly what each tool changes, save them under "justifications" in mcplane.json.`);
|
|
245
|
+
const steps = `# ChatGPT submission: ${m.title} ${m.version ?? ''}
|
|
112
246
|
|
|
113
|
-
|
|
247
|
+
## 1. Upload the package
|
|
248
|
+
platform.openai.com/plugins → **Upload new or existing plugin** → choose your verified developer identity → upload \`${zipName}\`.
|
|
249
|
+
It fills the listing, icons, test cases, demo video, release notes and translations. To change any of them later, edit mcplane.json, bump "version" and run \`mcplane pack chatgpt\` again.
|
|
114
250
|
|
|
115
|
-
##
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
- A demo video recorded in ChatGPT developer mode showing the tools your tests use, hosted as an .mp4 URL.
|
|
119
|
-
- Icons: a directory icon (square PNG, 256 px or more) and a composer icon (48 px or more; the developer-mode dialog caps uploads at 10 KB).
|
|
251
|
+
## 2. Resolve findings
|
|
252
|
+
Open **Metadata & Skills** and **MCPs**, wait for the checks, fix anything listed (Copy issues is handy), and upload again if the package changes.
|
|
253
|
+
Domain verification: serve the token it gives you as plain text at ${new URL(m.server.url).origin}/.well-known/openai-apps-challenge, and keep it there.
|
|
120
254
|
|
|
121
|
-
##
|
|
122
|
-
-
|
|
123
|
-
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
- Prompts (up to 3): ${(m.prompts ?? []).slice(0, 3).map((p) => `"${p}"`).join(', ') || 'none set'}
|
|
255
|
+
## 3. What only you can enter
|
|
256
|
+
- **Review details:** reviewer credentials${m.server.auth === 'oauth' ? ` (${m.reviewerAccess ?? 'an email-and-password account with no 2FA, SMS or email confirmation, seeded for every test case'})` : ' (none needed: no sign-in)'}. Credentials never go in the ZIP.
|
|
257
|
+
- **Hint justifications**, one per value on every tool (200 characters each, cut without warning):
|
|
258
|
+
|
|
259
|
+
${justified}
|
|
260
|
+
|
|
261
|
+
- **Submit for review** and the policy attestations.
|
|
129
262
|
|
|
130
263
|
## Traps
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
- "Submit for Review" shows nothing for about 20 seconds. Confirm on the plugins list that the version reads "Review".
|
|
135
|
-
- One version can be in review at a time. Changing tools on a published plugin needs a new version.
|
|
264
|
+
- One review can be active per plugin. To replace a package in review, cancel the review first.
|
|
265
|
+
- "Submit for review" can show nothing for a while; confirm the status on the Plugins page.
|
|
266
|
+
- Tool changes don't need a new package: OpenAI's scans pick them up. Listing changes do.
|
|
136
267
|
|
|
137
|
-
When it's in: \`mcplane submitted chatgpt --app-id <asdk_app_… from the URL>\`
|
|
268
|
+
When it's in: \`mcplane submitted chatgpt --version ${m.version ?? '1.0.0'} --app-id <asdk_app_… from the URL>\`
|
|
138
269
|
`;
|
|
139
270
|
return {
|
|
140
271
|
files: [
|
|
141
|
-
{ path:
|
|
142
|
-
{ path: 'chatgpt.
|
|
272
|
+
{ path: `chatgpt/${zipName}`, content: pkg },
|
|
273
|
+
{ path: 'chatgpt/plugin.json', content: JSON.stringify(plugin, null, 2) + '\n' },
|
|
274
|
+
{ path: 'chatgpt/chatgpt.md', content: steps },
|
|
143
275
|
],
|
|
144
276
|
problems,
|
|
145
277
|
todo,
|
|
@@ -152,6 +284,7 @@ function readWrite(tools) {
|
|
|
152
284
|
return writes.length === tools.length ? 'Write only (check: most servers also read)' : 'Read and write';
|
|
153
285
|
}
|
|
154
286
|
export async function claudePack(m, token) {
|
|
287
|
+
m = forStore(m, 'claude-connectors');
|
|
155
288
|
const tools = await listTools(m, token);
|
|
156
289
|
const problems = [];
|
|
157
290
|
if ((m.oneLiner ?? '').length > 200)
|
|
@@ -217,6 +350,7 @@ When it's in: \`mcplane submitted claude-connectors\`
|
|
|
217
350
|
return { files: [{ path: 'claude-connectors.md', content: md }], problems, todo: [] };
|
|
218
351
|
}
|
|
219
352
|
export async function simplePack(m, store) {
|
|
353
|
+
m = forStore(m, store);
|
|
220
354
|
const problems = [];
|
|
221
355
|
let md = '';
|
|
222
356
|
if (store === 'cursor') {
|
|
@@ -272,9 +406,10 @@ export async function writePack(pack, dir = process.cwd()) {
|
|
|
272
406
|
await mkdir(out, { recursive: true });
|
|
273
407
|
const written = [];
|
|
274
408
|
for (const f of pack.files) {
|
|
409
|
+
await mkdir(dirname(resolve(out, f.path)), { recursive: true });
|
|
275
410
|
await writeFile(resolve(out, f.path), f.content);
|
|
276
411
|
written.push(`.mcplane/packs/${f.path}`);
|
|
277
412
|
}
|
|
278
413
|
return written;
|
|
279
414
|
}
|
|
280
|
-
export const _test = { thirdPerson, justify };
|
|
415
|
+
export const _test = { thirdPerson, justify, contrast, category };
|
package/dist/publish.js
CHANGED
|
@@ -233,15 +233,24 @@ function prepared(m, store) {
|
|
|
233
233
|
return { store, done: false, lines: [`smithery mcp publish ${m.server.url} -n @<your-org>/${m.name}`, 'Or at https://smithery.ai/new. If Smithery’s scan can’t get past your sign-in, serve /.well-known/mcp/server-card.json describing your tools.'] };
|
|
234
234
|
}
|
|
235
235
|
if (store === 'glama') {
|
|
236
|
+
const reg = registryName(m);
|
|
236
237
|
const owner = gh?.owner ?? '<your-github-user>';
|
|
238
|
+
const domainNs = !reg.startsWith('io.github.');
|
|
239
|
+
const domain = reg.split('/')[0].split('.').reverse().join('.');
|
|
237
240
|
return {
|
|
238
241
|
store,
|
|
239
242
|
done: false,
|
|
240
243
|
lines: [
|
|
241
|
-
`Hosted
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
244
|
+
`Hosted server: Glama imports it from the official registry as a connector, https://glama.ai/mcp/connectors/${reg}. Publish there first.`,
|
|
245
|
+
domainNs
|
|
246
|
+
? `To claim it: open that page, choose Claim ownership, sign in, and verify ${domain} with the token Glama shows, either as a TXT record at _glama-claim.${domain} or as https://${domain}/.well-known/glama.json ({"$schema":"https://glama.ai/mcp/schemas/connector.json","claim":"glama_claim_…"}). One verification covers every connector under the domain.`
|
|
247
|
+
: `To claim it: open that page, choose Claim ownership and "Claim with GitHub" as ${reg.split('/')[0].slice('io.github.'.length)}. An organisation also needs the Glama GitHub App installed.`,
|
|
248
|
+
...(gh
|
|
249
|
+
? [
|
|
250
|
+
`Server code on GitHub is a separate listing: add https://github.com/${gh.owner}/${gh.repo} at https://glama.ai/mcp/servers ("Add MCP Server") and claim it with glama.json at the repo root:`,
|
|
251
|
+
JSON.stringify({ $schema: 'https://glama.ai/mcp/schemas/server.json', maintainers: [owner] }, null, 2),
|
|
252
|
+
]
|
|
253
|
+
: []),
|
|
245
254
|
'Both awesome lists require the Glama badge on each entry, so do this first.',
|
|
246
255
|
],
|
|
247
256
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface Manifest {
|
|
|
39
39
|
icon?: string;
|
|
40
40
|
prompts?: string[];
|
|
41
41
|
stores?: StoreId[];
|
|
42
|
+
/** Per-store listing text, like fastlane's per-language metadata. Each store falls back to the top-level fields. */
|
|
43
|
+
listing?: Partial<Record<StoreId, ListingText>>;
|
|
42
44
|
/** Review test cases: ChatGPT wants exactly 5 positive and 3 negative. */
|
|
43
45
|
tests?: {
|
|
44
46
|
positive?: {
|
|
@@ -66,6 +68,31 @@ export interface Manifest {
|
|
|
66
68
|
description?: string;
|
|
67
69
|
file?: string;
|
|
68
70
|
};
|
|
71
|
+
/** ChatGPT plugin package fields (plugin.json) that have no shared equivalent. */
|
|
72
|
+
chatgpt?: {
|
|
73
|
+
/** Publisher name shown in the listing (80 max). Defaults to author.name. */
|
|
74
|
+
developerName?: string;
|
|
75
|
+
/** Capability labels (20 max, 120 characters each). */
|
|
76
|
+
capabilities?: string[];
|
|
77
|
+
/** #RRGGBB with 2:1 contrast against white; the dark one against #212121. */
|
|
78
|
+
brandColor?: string;
|
|
79
|
+
brandColorDark?: string;
|
|
80
|
+
/** Direct image URLs; default to "icon". */
|
|
81
|
+
composerIcon?: string;
|
|
82
|
+
logoDark?: string;
|
|
83
|
+
/** Reviewer-accessible video URL showing the main use cases (required for review). */
|
|
84
|
+
demoVideo?: string;
|
|
85
|
+
commerce?: boolean;
|
|
86
|
+
commerceDescription?: string;
|
|
87
|
+
/** Uppercase country codes; leave out to keep current targeting. */
|
|
88
|
+
countries?: string[];
|
|
89
|
+
/** What changed in this version (required for review). */
|
|
90
|
+
releaseNotes?: string;
|
|
91
|
+
translations?: Record<string, {
|
|
92
|
+
subtitle?: string;
|
|
93
|
+
description?: string;
|
|
94
|
+
}>;
|
|
95
|
+
};
|
|
69
96
|
/** Grok marketplace entry overrides. */
|
|
70
97
|
grok?: {
|
|
71
98
|
description?: string;
|
|
@@ -78,6 +105,13 @@ export interface Manifest {
|
|
|
78
105
|
/** How reviewers get in, for servers behind sign-in. Never put the password here; say where it lives. */
|
|
79
106
|
reviewerAccess?: string;
|
|
80
107
|
}
|
|
108
|
+
export interface ListingText {
|
|
109
|
+
title?: string;
|
|
110
|
+
subtitle?: string;
|
|
111
|
+
oneLiner?: string;
|
|
112
|
+
description?: string;
|
|
113
|
+
prompts?: string[];
|
|
114
|
+
}
|
|
81
115
|
export interface Tool {
|
|
82
116
|
name: string;
|
|
83
117
|
title?: string;
|
|
@@ -87,6 +121,7 @@ export interface Tool {
|
|
|
87
121
|
required?: string[];
|
|
88
122
|
};
|
|
89
123
|
outputSchema?: unknown;
|
|
124
|
+
_meta?: Record<string, unknown>;
|
|
90
125
|
annotations?: {
|
|
91
126
|
title?: string;
|
|
92
127
|
readOnlyHint?: boolean;
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.4.1";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.
|
|
1
|
+
export const VERSION = '0.4.1';
|
package/dist/zip.d.ts
ADDED
package/dist/zip.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal ZIP writer (deflate, no dependencies): enough to build the plugin
|
|
3
|
+
* package ChatGPT's submission portal takes.
|
|
4
|
+
*/
|
|
5
|
+
import { deflateRawSync } from 'node:zlib';
|
|
6
|
+
const CRC = (() => {
|
|
7
|
+
const t = new Uint32Array(256);
|
|
8
|
+
for (let n = 0; n < 256; n++) {
|
|
9
|
+
let c = n;
|
|
10
|
+
for (let k = 0; k < 8; k++)
|
|
11
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
12
|
+
t[n] = c >>> 0;
|
|
13
|
+
}
|
|
14
|
+
return t;
|
|
15
|
+
})();
|
|
16
|
+
function crc32(buf) {
|
|
17
|
+
let c = 0xffffffff;
|
|
18
|
+
for (const b of buf)
|
|
19
|
+
c = CRC[(c ^ b) & 0xff] ^ (c >>> 8);
|
|
20
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
21
|
+
}
|
|
22
|
+
export function zip(files) {
|
|
23
|
+
const enc = new TextEncoder();
|
|
24
|
+
const locals = [];
|
|
25
|
+
const central = [];
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const f of files) {
|
|
28
|
+
const name = enc.encode(f.path);
|
|
29
|
+
const raw = typeof f.data === 'string' ? enc.encode(f.data) : f.data;
|
|
30
|
+
const body = deflateRawSync(raw);
|
|
31
|
+
const crc = crc32(raw);
|
|
32
|
+
const local = new Uint8Array(30 + name.length);
|
|
33
|
+
const lv = new DataView(local.buffer);
|
|
34
|
+
lv.setUint32(0, 0x04034b50, true);
|
|
35
|
+
lv.setUint16(4, 20, true);
|
|
36
|
+
lv.setUint16(6, 0x0800, true); // UTF-8 names
|
|
37
|
+
lv.setUint16(8, 8, true); // deflate
|
|
38
|
+
lv.setUint32(14, crc, true);
|
|
39
|
+
lv.setUint32(18, body.length, true);
|
|
40
|
+
lv.setUint32(22, raw.length, true);
|
|
41
|
+
lv.setUint16(26, name.length, true);
|
|
42
|
+
local.set(name, 30);
|
|
43
|
+
const cen = new Uint8Array(46 + name.length);
|
|
44
|
+
const cv = new DataView(cen.buffer);
|
|
45
|
+
cv.setUint32(0, 0x02014b50, true);
|
|
46
|
+
cv.setUint16(4, 20, true);
|
|
47
|
+
cv.setUint16(6, 20, true);
|
|
48
|
+
cv.setUint16(8, 0x0800, true);
|
|
49
|
+
cv.setUint16(10, 8, true);
|
|
50
|
+
cv.setUint32(16, crc, true);
|
|
51
|
+
cv.setUint32(20, body.length, true);
|
|
52
|
+
cv.setUint32(24, raw.length, true);
|
|
53
|
+
cv.setUint16(28, name.length, true);
|
|
54
|
+
cv.setUint32(42, offset, true);
|
|
55
|
+
cen.set(name, 46);
|
|
56
|
+
locals.push(local, body);
|
|
57
|
+
central.push(cen);
|
|
58
|
+
offset += local.length + body.length;
|
|
59
|
+
}
|
|
60
|
+
const cenSize = central.reduce((n, c) => n + c.length, 0);
|
|
61
|
+
const end = new Uint8Array(22);
|
|
62
|
+
const ev = new DataView(end.buffer);
|
|
63
|
+
ev.setUint32(0, 0x06054b50, true);
|
|
64
|
+
ev.setUint16(8, files.length, true);
|
|
65
|
+
ev.setUint16(10, files.length, true);
|
|
66
|
+
ev.setUint32(12, cenSize, true);
|
|
67
|
+
ev.setUint32(16, offset, true);
|
|
68
|
+
const out = new Uint8Array(offset + cenSize + 22);
|
|
69
|
+
let p = 0;
|
|
70
|
+
for (const part of [...locals, ...central, end]) {
|
|
71
|
+
out.set(part, p);
|
|
72
|
+
p += part.length;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
},
|
|
63
63
|
"category": {
|
|
64
64
|
"type": "string",
|
|
65
|
-
"description": "ChatGPT category,
|
|
65
|
+
"description": "ChatGPT category: Productivity, Creativity, Developer Tools, Business & Operations, Data & Analytics, Communication, Education & Research, Security, Finance, Healthcare, Travel, Entertainment or Other (the old form's names like DEVELOPER_TOOLS are mapped)."
|
|
66
66
|
},
|
|
67
67
|
"server": {
|
|
68
68
|
"type": "object",
|
|
@@ -279,6 +279,111 @@
|
|
|
279
279
|
"reviewerAccess": {
|
|
280
280
|
"type": "string",
|
|
281
281
|
"description": "How reviewers get in, for servers behind sign-in. Say where credentials live; never put a password here."
|
|
282
|
+
},
|
|
283
|
+
"listing": {
|
|
284
|
+
"type": "object",
|
|
285
|
+
"description": "Per-store listing text, like fastlane's per-language metadata. Each store falls back to the top-level title, subtitle, oneLiner, description and prompts.",
|
|
286
|
+
"propertyNames": {
|
|
287
|
+
"$ref": "#/$defs/store"
|
|
288
|
+
},
|
|
289
|
+
"additionalProperties": {
|
|
290
|
+
"type": "object",
|
|
291
|
+
"additionalProperties": false,
|
|
292
|
+
"properties": {
|
|
293
|
+
"title": {
|
|
294
|
+
"type": "string"
|
|
295
|
+
},
|
|
296
|
+
"subtitle": {
|
|
297
|
+
"type": "string"
|
|
298
|
+
},
|
|
299
|
+
"oneLiner": {
|
|
300
|
+
"type": "string"
|
|
301
|
+
},
|
|
302
|
+
"description": {
|
|
303
|
+
"type": "string"
|
|
304
|
+
},
|
|
305
|
+
"prompts": {
|
|
306
|
+
"type": "array",
|
|
307
|
+
"items": {
|
|
308
|
+
"type": "string"
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"chatgpt": {
|
|
315
|
+
"type": "object",
|
|
316
|
+
"additionalProperties": false,
|
|
317
|
+
"description": "ChatGPT plugin package fields (plugin.json) with no shared equivalent.",
|
|
318
|
+
"properties": {
|
|
319
|
+
"developerName": {
|
|
320
|
+
"type": "string",
|
|
321
|
+
"description": "Publisher name (80 max). Defaults to author.name.",
|
|
322
|
+
"maxLength": 80
|
|
323
|
+
},
|
|
324
|
+
"capabilities": {
|
|
325
|
+
"type": "array",
|
|
326
|
+
"maxItems": 20,
|
|
327
|
+
"items": {
|
|
328
|
+
"type": "string",
|
|
329
|
+
"maxLength": 120
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
"brandColor": {
|
|
333
|
+
"type": "string",
|
|
334
|
+
"description": "#RRGGBB with 2:1 contrast against white.",
|
|
335
|
+
"pattern": "^#[0-9a-fA-F]{6}$"
|
|
336
|
+
},
|
|
337
|
+
"brandColorDark": {
|
|
338
|
+
"type": "string",
|
|
339
|
+
"description": "#RRGGBB with 2:1 contrast against #212121.",
|
|
340
|
+
"pattern": "^#[0-9a-fA-F]{6}$"
|
|
341
|
+
},
|
|
342
|
+
"composerIcon": {
|
|
343
|
+
"$ref": "#/$defs/url"
|
|
344
|
+
},
|
|
345
|
+
"logoDark": {
|
|
346
|
+
"$ref": "#/$defs/url"
|
|
347
|
+
},
|
|
348
|
+
"demoVideo": {
|
|
349
|
+
"$ref": "#/$defs/url",
|
|
350
|
+
"description": "Reviewer-accessible video URL showing the main use cases. Required for review."
|
|
351
|
+
},
|
|
352
|
+
"commerce": {
|
|
353
|
+
"type": "boolean"
|
|
354
|
+
},
|
|
355
|
+
"commerceDescription": {
|
|
356
|
+
"type": "string"
|
|
357
|
+
},
|
|
358
|
+
"countries": {
|
|
359
|
+
"type": "array",
|
|
360
|
+
"items": {
|
|
361
|
+
"type": "string",
|
|
362
|
+
"pattern": "^[A-Z]{2}$"
|
|
363
|
+
}
|
|
364
|
+
},
|
|
365
|
+
"releaseNotes": {
|
|
366
|
+
"type": "string",
|
|
367
|
+
"description": "What changed in this version. Required for review."
|
|
368
|
+
},
|
|
369
|
+
"translations": {
|
|
370
|
+
"type": "object",
|
|
371
|
+
"additionalProperties": {
|
|
372
|
+
"type": "object",
|
|
373
|
+
"additionalProperties": false,
|
|
374
|
+
"properties": {
|
|
375
|
+
"subtitle": {
|
|
376
|
+
"type": "string",
|
|
377
|
+
"maxLength": 30
|
|
378
|
+
},
|
|
379
|
+
"description": {
|
|
380
|
+
"type": "string",
|
|
381
|
+
"maxLength": 4000
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
282
387
|
}
|
|
283
388
|
}
|
|
284
389
|
}
|