mcplane 0.5.1 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ New ChatGPT checks
6
+ - `server.widget-domain` (blocks): tools with a UI name resources (`_meta.ui.resourceUri` or `openai/outputTemplate`), and each one must set a widget domain, which OpenAI requires before you can submit a plugin with UI. Preflight now reads those resources; one that needs sign-in is skipped unless you pass `--token`. `openai/widgetDomain` and `ui.domain` both count.
7
+ - `tools.file-viewer-wording` (warning): a tool with a file entrypoint described as a command ("Opens an HTML file…"). The model then calls it with a file path and ChatGPT refuses the read ("MCP app cannot read resource outside its widget scope"), as Stacktree's viewer did on 6 October. Say the user opens the file and name the tool for content the model can read.
8
+ - `tools.sensitive-inputs`: an input called `password` that's really a passcode for something the tool makes gets the advice to call it `passcode`. ChatGPT's approval panel files `password` under "Account passwords".
9
+
10
+ ChatGPT pack
11
+ - Upload a new version from the plugin's own page (**Upload new version**, or **Upload plugin to make changes**). The Plugins page's upload area creates a second plugin even when the package name matches, which is how Stacktree ended up with two.
12
+ - Published tool changes don't arrive on cue: on 6 October a Rescan left a published plugin's tool table on its reviewed list, and a published plugin's MCP card has no Reconnect. Check the MCPs tab lists new tools before submitting test cases that use them.
13
+ - Don't fix a duplicate by deleting and uploading again: deleting can reserve the plugin's name (openai/codex#39508).
14
+
15
+ Fewer false alarms
16
+ - `tools.destructive-overwrite` skips verbs whose subject is a sibling tool ("update_site replaces a page in place" in another tool's description) and, like `tools.destructive-option`, expiry on a tool that creates. Stacktree's publish_html, which passed ChatGPT's scan, no longer warns.
17
+
18
+ README
19
+ - 2026-07-28 cache hints: a `tools/list` that differs by caller should be `cacheScope: "private"`.
20
+
3
21
  ## 0.5.1
4
22
 
5
23
  New ChatGPT checks (warnings)
package/README.md CHANGED
@@ -89,10 +89,10 @@ npx mcplane fleet --root ~/Projects
89
89
 
90
90
  Checks come from real rejections, most of them ours:
91
91
 
92
- - **Server**: `initialize`, `tools/list` and `ping` work. An OAuth server that initializes without a token also lists its tools without one (the half-way state leaves a new ChatGPT connector stuck on "no actions available"). Unknown methods return `-32601` (OpenAI's tool scan probes `server/discover` and gives up on a crash). The URL works with a trailing slash. CORS preflight answers. Automated clients aren't blocked by a bot filter. A TLS 1.2 client connects. DNS doesn't advertise Encrypted ClientHello, which review proxies reset without a trace in your logs.
92
+ - **Server**: `initialize`, `tools/list` and `ping` work. An OAuth server that initializes without a token also lists its tools without one (the half-way state leaves a new ChatGPT connector stuck on "no actions available"). Unknown methods return `-32601` (OpenAI's tool scan probes `server/discover` and gives up on a crash). The URL works with a trailing slash. CORS preflight answers. Automated clients aren't blocked by a bot filter. A TLS 1.2 client connects. DNS doesn't advertise Encrypted ClientHello, which review proxies reset without a trace in your logs. Tools with a UI name resources that set a widget domain, which OpenAI requires before you can submit a plugin with UI.
93
93
  - **OAuth**: unauthenticated calls get `401` with `resource_metadata`. Protected-resource and authorization-server metadata resolve, and PKCE S256 is supported. With `--register`, Dynamic Client Registration is tested, including the `cursor://` and other native redirect schemes that locked out every desktop client for us.
94
94
  - **Sign-in chain**: plain GETs, the way a reviewer's browser goes: the authorization endpoint, the sign-in page it sends people to, and the identity-provider hosts that page loads. A bot challenge (`cf-mitigated: challenge`) or a 403 anywhere on it is flagged: every MCP check can pass while reviewers never reach your sign-in page. Nothing signs in and no client is registered, so an authorization server that wants a registered client stops the walk at its error page: name your sign-in pages in `server.signIn` (or `--sign-in <url>`) and they're checked too. A pass from your own network proves less than one from a cloud VM or CI.
95
- - **Tools**: every tool has a title and explicit `readOnlyHint`, `destructiveHint` and `openWorldHint`, and the hints are consistent. Tools declare `outputSchema`. Descriptions don't instruct the model. Writes aren't authorised by a token passed through the chat (Anthropic rejected exactly that). Hints follow OpenAI's current definitions: `openWorldHint` for public or open-ended destinations (read-only web search included; connectivity alone doesn't decide it), `destructiveHint` for messages that can't be unsent, for overwriting, revoking access or deleting, and for destructive options in the input schema (ChatGPT's scan flagged a non-destructive publish tool for offering `burn_after_read`). No catch-all request tools, no tool names over 64 characters, no sensitive inputs, no upgrade or pricing copy. Tools that move money or handle crypto wallets are flagged for ChatGPT, whose submit step has you attest to neither.
95
+ - **Tools**: every tool has a title and explicit `readOnlyHint`, `destructiveHint` and `openWorldHint`, and the hints are consistent. Tools declare `outputSchema`. Descriptions don't instruct the model. Writes aren't authorised by a token passed through the chat (Anthropic rejected exactly that). Hints follow OpenAI's current definitions: `openWorldHint` for public or open-ended destinations (read-only web search included; connectivity alone doesn't decide it), `destructiveHint` for messages that can't be unsent, for overwriting, revoking access or deleting, and for destructive options in the input schema (ChatGPT's scan flagged a non-destructive publish tool for offering `burn_after_read`). No catch-all request tools, no tool names over 64 characters, no sensitive inputs, no upgrade or pricing copy. Tools that move money or handle crypto wallets are flagged for ChatGPT, whose submit step has you attest to neither. File viewers say the user opens the file: described as a command, the model calls one with a path and ChatGPT refuses the read.
96
96
  - **Listing**: the icon is square and at least 512px behind a direct link (PNG, JPEG, WebP or SVG, measured from the bytes), and `/favicon.ico` resolves. The privacy policy covers collection, retention and user controls. Support is a web page. Name, subtitle, one-liner, description and starter prompts fit each store's limits, including the 1,024 characters the ChatGPT package allows for its root description. Every test case uses tools the live server actually has. The ChatGPT domain challenge is served.
97
97
  - **Plugin repo**: public, on GitHub, with `.claude-plugin/plugin.json`, and no root `SKILL.md` shadowing the plugin's skills. Grok plugins come from an organisation.
98
98
  - **Vercel Connect**: what its [For Service Providers](https://vercel.com/docs/connect/providers) page asks for, read from your discovery documents alone (nothing is registered). Required items block: metadata served as JSON (at the path-suffixed form when the issuer has a path), `token_endpoint`, `grant_types_supported`, and OAuth and OpenID Connect documents that agree. Recommended items warn: DCR or CIMD so Connect can create the client, a `token_endpoint_auth_methods_supported` it can use, PKCE S256, refresh tokens. Optional ones are notes: protected-resource metadata, `revocation_endpoint`, `scopes_supported`. What needs a real token (`expires_in`, refresh rotation, the `https://connect.vercel.com/callback` redirect) is on the list of things to check yourself.
@@ -125,6 +125,8 @@ Readiness for MCP 2026-07-28 (never blocking)
125
125
 
126
126
  Servers behind sign-in need `--token` for the probes; the authorization checks run from the discovery documents either way.
127
127
 
128
+ One thing preflight can't see: 2026-07-28 results carry cache hints, and `tools/list` is one of the results that must. If your list differs by caller, for example tools you hide from ChatGPT connections, mark it `cacheScope: "private"`, or a shared cache can hand one client another's list.
129
+
128
130
  ## ARD manifest
129
131
 
130
132
  `mcplane pack ard` writes `.mcplane/packs/ard/ard.json`: one entry for your server (`application/mcp-server-card+json`, pointing at your server card when you serve one), with representative queries from your prompts and tests and capabilities from your tool names. It validates against [ARD v0.91](https://agenticresourcediscovery.org/spec/) and against the older schema [Lighthouse 13.5](https://github.com/GoogleChrome/lighthouse/blob/v13.5.0/core/audits/agentic/ard-schema.js) audits. Serve it at `/.well-known/ard.json`, and either at `/.well-known/ai-catalog.json` too or from an `Agentmap:` line in robots.txt, which Lighthouse reads first. ARD is a proposal, so treat it as cheap insurance rather than a requirement.
@@ -197,7 +199,7 @@ Since DevDay (September 2026) ChatGPT takes a plugin ZIP. `mcplane pack chatgpt`
197
199
  - **Behind sign-in**: the ZIP doesn't need your tool list, so `pack chatgpt` builds without `--token` and says what it couldn't check. `--tools <file>` takes a saved `tools/list` result instead.
198
200
  - The ZIP is reproducible: entries are dated 1980-01-01 and carry Unix modes, so the same files give the same bytes.
199
201
 
200
- What stays with you: upload the ZIP, enter reviewer credentials 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.
202
+ What stays with you: upload the ZIP, enter reviewer credentials and make the attestations. Bump `version` for every upload, and upload a new version from the plugin's own page: the Plugins page's upload area makes a second plugin. Tool changes don't need a new ZIP; OpenAI says its scans pick them up after automated checks, though not on cue (check the MCPs tab before you submit test cases that use new tools), and `drift` tracks extension metadata (sidebar, panels, file viewers) as part of each tool.
201
203
 
202
204
  **Annotation rules.** OpenAI's [plugin guidelines](https://developers.openai.com/plugins/plugin-guidelines) now ask only for explicit `readOnlyHint`, `destructiveHint` and `openWorldHint` booleans on every tool; its automated review checks them, and you appeal if it flags one you believe is right. Justifications are no longer required, so the pack no longer drafts them: any you wrote in `mcplane.json` become optional appeal notes. Writes count as destructive unless they only add (overwriting, cancelling, revoking access and irreversible sends are all destructive, and an undo button doesn't change that), and preflight warns when a write that overwrites, revokes or deletes says `destructiveHint: false`, or offers an option that does. To keep such an option for other clients, stop advertising it in the schema you serve ChatGPT. `openWorldHint` is about where a tool reaches, not whether it calls an API. A hint change is a note in `drift`, not an action: OpenAI re-checks annotations itself, so `drift --ci` doesn't fail on one.
203
205
 
@@ -106,7 +106,7 @@ export function initializeCheck(r, era) {
106
106
  export function modernHeaderCheck(r) {
107
107
  const title = 'Accepts the Mcp-Method and Mcp-Name headers';
108
108
  return r && r.status < 300 && r.body?.result
109
- ? check('headers', 'pass', title, { detail: 'a 2026-07-28 tools/list with Mcp-Method works (Mcp-Name only goes on tools/call, resources/read and prompts/get, which mcplane doesn’t call)' })
109
+ ? check('headers', 'pass', title, { detail: 'a 2026-07-28 tools/list with Mcp-Method works (Mcp-Name only goes on tools/call, resources/read and prompts/get, which this probe doesn’t send)' })
110
110
  : check('headers', 'warn', title, { detail: `a 2026-07-28 tools/list with Mcp-Method got ${said(r)}`, fix: 'Mcp-Method is required on every 2026-07-28 POST and must match the body’s method.' });
111
111
  }
112
112
  /** Legacy servers: the same tools/list with and without the new headers should get the same answer. */
@@ -60,6 +60,38 @@ export async function serverChecks(url, auth, token, extra = []) {
60
60
  checks.push(tools.length
61
61
  ? { id: 'server.tools', level: 'pass', title: 'tools/list returns tools', detail: tools.map((t) => t.name).join(', ') }
62
62
  : { id: 'server.tools', level: 'fail', title: 'tools/list returns tools', detail: `HTTP ${list?.status ?? 'error'}`, fix: 'Stores scan tools/list; with no tools there is nothing to list.' });
63
+ // Tools with UI name an MCP App resource (_meta.ui.resourceUri, or openai/outputTemplate). OpenAI requires a
64
+ // dedicated widget domain on each one when you submit a plugin with UI, unique per plugin. Set it as
65
+ // _meta["openai/widgetDomain"] on the resource: ui.domain also counts, but its format differs by host (Claude
66
+ // expects its own hash-derived domain there).
67
+ const uiUris = [...new Set(tools.flatMap((t) => {
68
+ const meta = (t._meta ?? {});
69
+ return [meta.ui?.resourceUri, meta['openai/outputTemplate']].filter((u) => typeof u === 'string');
70
+ }))];
71
+ if (uiUris.length) {
72
+ const missing = [];
73
+ const unread = [];
74
+ let denied = 0;
75
+ for (const uri of uiUris) {
76
+ const read = await client.request('resources/read', { uri }).catch(() => null);
77
+ if (read?.status === 401 || read?.status === 403) {
78
+ denied++;
79
+ continue;
80
+ }
81
+ const content = read?.body?.result?.contents?.[0];
82
+ if (!content)
83
+ unread.push(uri);
84
+ else if (!content._meta?.['openai/widgetDomain'] && !content._meta?.ui?.domain)
85
+ missing.push(uri);
86
+ }
87
+ checks.push(denied === uiUris.length
88
+ ? { id: 'server.widget-domain', level: 'skip', title: 'UI resources set a widget domain', detail: 'resources/read needs sign-in; run preflight with --token to check', stores: ['chatgpt'] }
89
+ : missing.length
90
+ ? { id: 'server.widget-domain', level: 'fail', title: 'UI resources set a widget domain', detail: missing.join(', '), fix: 'Set _meta["openai/widgetDomain"] (for example "https://yourdomain.com") on each UI resource. OpenAI requires a dedicated, per-plugin widget domain to submit a plugin with UI.', stores: ['chatgpt'] }
91
+ : unread.length === uiUris.length
92
+ ? { id: 'server.widget-domain', level: 'warn', title: 'UI resources set a widget domain', detail: `resources/read gave nothing for ${unread.join(', ')}`, fix: 'Tools name UI resources that resources/read does not return. ChatGPT reads them the same way to show the UI.', stores: ['chatgpt'] }
93
+ : { id: 'server.widget-domain', level: 'pass', title: 'UI resources set a widget domain', detail: uiUris.filter((u) => !unread.includes(u)).join(', '), stores: ['chatgpt'] });
94
+ }
63
95
  const ping = await client.request('ping').catch(() => null);
64
96
  checks.push({
65
97
  id: 'server.ping',
@@ -34,18 +34,23 @@ const CRYPTO = /\b(crypto(?:currenc(?:y|ies))?|wallets?|on[\s-]?chain|blockchain
34
34
  * links aren't money movement, and neither is a purchase history or a hotel's check-out date.
35
35
  */
36
36
  const MONEY = /\b(transfer(?:s|ring)? (?:the |your )?(?:funds|money|payments?)|send(?:s|ing)? (?:a |the |your )?(?:money|funds|payments?)|(?:make|makes|making|complete|completes|process|processes|processing|submit|submits|execute|executes) (?:a |the )?(?:payments?|purchases?)|pay(?:s|ing)? (?:an? |the |your )?(?:invoices?|bills?)|charg(?:e|es|ing) (?:a |the |their |your )?(?:cards?|customers?)|refund(?:s|ing)? (?:an? |the |their |your )?(?:customers?|orders?|payments?|charges?)|issu(?:e|es|ing) (?:a )?refunds?|(?:withdraw|deposit)(?:s|ing)? (?:funds|money)|(?:purchas(?:e|es|ing)|buy(?:s|ing)?) (?:an?|the|this|it)|checkouts?(?![\s_-]*(?:date|time|day)))\b/gi;
37
- /** The words a pattern finds in text, skipping negated ones ("never overwrites"). */
38
- function mentions(text, re) {
37
+ /**
38
+ * The words a pattern finds in text, skipping negated ones ("never overwrites") and ones whose subject is another
39
+ * tool: "(update_site replaces a page in place)" describes update_site, not the tool being checked.
40
+ */
41
+ function mentions(text, re, others = []) {
39
42
  const found = new Set();
43
+ const subject = others.length ? new RegExp(`\\b(${others.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})\\b(\\s+\\w+){0,2}\\s*$`, 'i') : null;
40
44
  for (const m of text.matchAll(re)) {
41
- if (NEGATED.test(text.slice(Math.max(0, m.index - 40), m.index)))
45
+ const before = text.slice(Math.max(0, m.index - 40), m.index);
46
+ if (NEGATED.test(before) || subject?.test(before))
42
47
  continue;
43
48
  found.add(m[0].toLowerCase().replace(/\s+/g, ' '));
44
49
  }
45
50
  return [...found];
46
51
  }
47
- /** The destructive words in a tool's name, title and description. */
48
- const destructiveWords = (text) => mentions(text, DESTRUCTIVE);
52
+ /** The destructive words in a tool's name, title and description, given the names of its sibling tools. */
53
+ const destructiveWords = (text, others = []) => mentions(text, DESTRUCTIVE, others);
49
54
  /** A name as words: set_password and setPassword both read "set password". */
50
55
  const words = (name) => name.replace(/[_-]+/g, ' ').replace(/([a-z])([A-Z])/g, '$1 $2');
51
56
  /** A tool's input properties and the ones a level down (an object's properties, an array's items), by path. */
@@ -148,10 +153,14 @@ export function toolChecks(tools) {
148
153
  }
149
154
  // The same definition covers overwriting, revoking access and deleting; an undo doesn't change that.
150
155
  // Conservative: explicit write tools whose own words say so, negated mentions ("never overwrites") excluded.
156
+ // Expiry follows the destructive-option rule below: on a tool that creates, it bounds what the call adds.
157
+ // Stacktree's publish_html ("plan-aware expiry", "update_site replaces a page in place") passed ChatGPT's scan.
151
158
  const destroys = tools.flatMap((t) => {
152
159
  if (t.annotations?.readOnlyHint !== false || t.annotations?.destructiveHint !== false)
153
160
  return [];
154
- const found = destructiveWords(`${words(t.name)} ${t.title ?? ''} ${t.description ?? ''}`);
161
+ const creates = CREATES.test(words(t.name));
162
+ const others = tools.filter((o) => o !== t).map((o) => o.name);
163
+ const found = destructiveWords(`${words(t.name)} ${t.title ?? ''} ${t.description ?? ''}`, others).filter((w) => !(creates && w.startsWith('expir')));
155
164
  return found.length ? [`${t.name} (${found.join(', ')})`] : [];
156
165
  });
157
166
  if (destroys.length) {
@@ -225,7 +234,10 @@ export function toolChecks(tools) {
225
234
  .filter((k) => /(ssn|social_security|passport|national_id|tax_id|credit_card|card_number|cvv|iban|password|mfa|otp|2fa|diagnos|medical|health_record|biometric|fingerprint)/i.test(k))
226
235
  .map((k) => `${t.name}.${k}`));
227
236
  if (sensitive.length) {
228
- checks.push({ id: 'tools.sensitive-inputs', level: 'fail', title: 'Tools don’t ask for sensitive data', detail: sensitive.join(', '), fix: 'OpenAI flags inputs that request PHI, card data, SSNs, credentials, MFA codes, government IDs or biometrics unless strictly necessary. Remove them or justify them in the submission.', stores: ['chatgpt'] });
237
+ const passcode = sensitive.some((s) => /\.password$/i.test(s))
238
+ ? ' If a password input is really a passcode for a page or file the tool makes, name it passcode: ChatGPT’s approval panel files an argument called password under "Account passwords", which reads as a credential request. Accepting both names on the server keeps older clients working.'
239
+ : '';
240
+ checks.push({ id: 'tools.sensitive-inputs', level: 'fail', title: 'Tools don’t ask for sensitive data', detail: sensitive.join(', '), fix: `OpenAI flags inputs that request PHI, card data, SSNs, credentials, MFA codes, government IDs or biometrics unless strictly necessary. Remove them or justify them in the submission.${passcode}`, stores: ['chatgpt'] });
229
241
  }
230
242
  const upsell = tools.filter((t) => UPSELL.test(`${t.title ?? ''} ${t.annotations?.title ?? ''} ${t.description ?? ''}`));
231
243
  checks.push(upsell.length
@@ -235,5 +247,22 @@ export function toolChecks(tools) {
235
247
  const ext = tools.flatMap((t) => ((t._meta?.['openai/ui']?.entrypoints) ?? []).map((e) => `${t.name}: ${e.type}${e.extensions?.length ? ` (${e.extensions.join(', ')})` : ''}`));
236
248
  if (ext.length)
237
249
  checks.push({ id: 'tools.extensions', level: 'pass', title: 'ChatGPT extensions declared', detail: ext.join('; '), stores: ['chatgpt'] });
250
+ // A file viewer opens when the user opens a file, and ChatGPT passes it an opaque resource URI. Described as a command
251
+ // ("Opens an HTML file in…"), the model calls it itself with a file path, and ChatGPT refuses the read ("MCP app cannot
252
+ // read resource outside its widget scope"). Seen with Stacktree's open_html_file in ChatGPT desktop, 6 Oct 2026.
253
+ const fileViewers = tools.filter((t) => ((t._meta?.['openai/ui']?.entrypoints) ?? []).some((e) => e.type === 'file'));
254
+ if (fileViewers.length) {
255
+ const invites = fileViewers.filter((t) => !/(user opens|when (the |a )?user opens|path does not work|not a (file )?path|opaque)/i.test(t.description ?? ''));
256
+ checks.push(invites.length
257
+ ? {
258
+ id: 'tools.file-viewer-wording',
259
+ level: 'warn',
260
+ title: 'File viewers say the user opens the file',
261
+ detail: invites.map((t) => t.name).join(', '),
262
+ fix: 'Say that ChatGPT calls this when the user opens a file, that a file path does not work here, and which tool the model should use instead for a file it can read (Stacktree points at publish_html). Otherwise the model calls the viewer with a path and the user sees ChatGPT refuse the read.',
263
+ stores: ['chatgpt'],
264
+ }
265
+ : { id: 'tools.file-viewer-wording', level: 'pass', title: 'File viewers say the user opens the file', stores: ['chatgpt'] });
266
+ }
238
267
  return checks;
239
268
  }
package/dist/packs.js CHANGED
@@ -289,7 +289,8 @@ export async function chatgptPack(m, opts = {}) {
289
289
  const steps = `# ChatGPT submission: ${m.title} ${m.version ?? ''}
290
290
 
291
291
  ## 1. Upload the package
292
- platform.openai.com/plugins → **Upload new or existing plugin** → choose your verified developer identity → upload \`${zipName}\`.
292
+ - **First version:** platform.openai.com/plugins → **Upload new or existing plugin** → choose your verified developer identity → upload \`${zipName}\`.
293
+ - **A new version of a plugin you already have:** open that plugin's page and use **Upload new version** (or **Upload plugin to make changes**). Uploading from the Plugins page instead creates a second plugin, even when the package name matches.
293
294
  It fills the listing, icons, test cases, demo video, release notes and translations${skills.names.length ? `, and adds ${skills.names.length === 1 ? 'the skill' : `${skills.names.length} skills:`} ${skills.names.join(', ')}` : ''}. To change any of them later, edit mcplane.json, bump "version" and run \`mcplane pack chatgpt\` again.${skills.names.length ? '\nSkills are scanned too, which can take up to 2 hours. If one is flagged, take it out of chatgpt.skills and upload again; skills can come back in a later version.' : ''}
294
295
 
295
296
  ## 2. Resolve findings
@@ -308,7 +309,8 @@ Hints need no justification: OpenAI's automated review checks readOnlyHint, dest
308
309
  ## Traps
309
310
  - One review can be active per plugin. To replace a package in review, cancel the review first.
310
311
  - "Submit for review" can show nothing for a while; confirm the status on the Plugins page.
311
- - Tool changes don't need a new package: OpenAI's scans pick them up once you're published. Listing changes do.
312
+ - Tool changes don't need a new package: OpenAI says its scans pick them up once you're published, after automated checks. Listing changes do. Neither happens on cue: on 6 Oct 2026 a Rescan on a published plugin left its tools table at the reviewed list ("last checked yesterday") and the scanner never connected, and a published plugin's MCP card has no Reconnect. If a new version's test cases use tools added since the last review, wait until the MCPs tab lists them before you submit.
313
+ - Don't fix a duplicate plugin by deleting it and uploading again: deleting can reserve the plugin's name for an unknown time (openai/codex#39508).
312
314
  - A rescan doesn't update a submission already in review: it keeps the tools it was submitted with. To change them, cancel the review (back to Draft), reconnect so ChatGPT rediscovers the tools, then resubmit.
313
315
  - The package takes icons up to 5 MiB, but developer mode's own icon upload (when you add the server by hand to record the demo) takes 10 KB at most.
314
316
 
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.1";
1
+ export declare const VERSION = "0.6.0";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = '0.5.1';
1
+ export const VERSION = '0.6.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcplane",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "fastlane for MCP servers: preflight against every store's rejection causes, publish where you can, prepare the rest, track every review.",
5
5
  "type": "module",
6
6
  "bin": {