@shipstatic/mcp 1.0.0-beta.4 → 1.0.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -85,7 +85,7 @@ Same config format — `npx @shipstatic/mcp`. Works with any MCP-compatible clie
85
85
 
86
86
  ## Free API key — permanent deployments
87
87
 
88
- `SHIP_TOKEN` is optional. Without it, deploys behave like the hosted endpoint (public, claim URL, 3-day expiry). With it, you get permanent deployments, the full toolset, and bigger limits.
88
+ `SHIP_TOKEN` is optional. Without it, deploys behave like the hosted endpoint (public, claim URL, expire in 3 days). With it, you get permanent deployments, the full toolset, and bigger limits.
89
89
 
90
90
  Get a free API key at [my.shipstatic.com/api-key](https://my.shipstatic.com/api-key):
91
91
 
package/dist/index.d.ts CHANGED
@@ -16,10 +16,19 @@
16
16
  * offer a Worker. Exporting them would make this package's API "stdio's
17
17
  * internals, plus some shared bits" instead of a contract.
18
18
  *
19
+ * Every name below answers a question the second transport must otherwise
20
+ * answer for itself, and each admission was a restatement deleted, not a
21
+ * convenience added: `SERVER_NAME` and `UPLOAD_TOOL_NAME` were literals in two
22
+ * repos (the first also correlates the Apps-SDK widget to the connector),
23
+ * `PUBLIC_EXPIRY` was the same duration written out eight times,
24
+ * `DESCRIPTION_BLOCKS` the fragments two tool descriptions genuinely share,
25
+ * and `ACCOUNT_TOOL_NAMES` is what lets the hosted catalogue fence name the
26
+ * fourteen without counting them again.
27
+ *
19
28
  * `tests/index.test.ts` fences both directions — nothing missing, nothing
20
29
  * extra — because adding an export is the quiet failure: everything published
21
30
  * becomes a breaking change to remove.
22
31
  */
23
32
  export { type CallFn, type CallOptions, createCall, type ErrorHints } from './call.js';
24
- export { registerAccountTools } from './tools.js';
25
- export { ANNOTATIONS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS } from './vocabulary.js';
33
+ export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
34
+ export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, } from './vocabulary.js';
package/dist/index.js CHANGED
@@ -16,10 +16,19 @@
16
16
  * offer a Worker. Exporting them would make this package's API "stdio's
17
17
  * internals, plus some shared bits" instead of a contract.
18
18
  *
19
+ * Every name below answers a question the second transport must otherwise
20
+ * answer for itself, and each admission was a restatement deleted, not a
21
+ * convenience added: `SERVER_NAME` and `UPLOAD_TOOL_NAME` were literals in two
22
+ * repos (the first also correlates the Apps-SDK widget to the connector),
23
+ * `PUBLIC_EXPIRY` was the same duration written out eight times,
24
+ * `DESCRIPTION_BLOCKS` the fragments two tool descriptions genuinely share,
25
+ * and `ACCOUNT_TOOL_NAMES` is what lets the hosted catalogue fence name the
26
+ * fourteen without counting them again.
27
+ *
19
28
  * `tests/index.test.ts` fences both directions — nothing missing, nothing
20
29
  * extra — because adding an export is the quiet failure: everything published
21
30
  * becomes a breaking change to remove.
22
31
  */
23
32
  export { createCall } from './call.js';
24
- export { registerAccountTools } from './tools.js';
25
- export { ANNOTATIONS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS } from './vocabulary.js';
33
+ export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
34
+ export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, } from './vocabulary.js';
package/dist/server.js CHANGED
@@ -1,23 +1,23 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import { IDEMPOTENCY_KEY_CONSTRAINTS } from '@shipstatic/ship';
3
2
  import { z } from 'zod';
4
3
  import { call } from './call.js';
5
4
  import { registerAccountTools } from './tools.js';
6
- import { ANNOTATIONS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS } from './vocabulary.js';
5
+ import { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, } from './vocabulary.js';
7
6
  // Destructured so the fifteen registrations below read as they always have.
8
7
  // The definitions live in `vocabulary.ts` because the hosted transport speaks
9
8
  // the same ones — that file records what is shared, what is not, and why.
10
9
  const { CREATE } = ANNOTATIONS;
11
10
  const B = INSTRUCTION_BLOCKS;
11
+ const D = DESCRIPTION_BLOCKS;
12
12
  // Composed from the shared blocks plus the two sentences that are genuinely
13
13
  // stdio's: how files arrive (a filesystem path) and how a caller
14
14
  // authenticates (`SHIP_TOKEN`). The hosted transport composes the same blocks
15
15
  // around its own two.
16
16
  const INSTRUCTIONS = `${B.opening}
17
17
 
18
- To deploy: call deployments_upload with the build output directory path. ${B.liveAndPassword}
18
+ To deploy: call ${UPLOAD_TOOL_NAME} with the build output directory path. ${B.liveAndPassword}
19
19
 
20
- Without SHIP_TOKEN, deployments are public and expire in 3 days. ${B.claim}
20
+ Without SHIP_TOKEN, deployments are public and expire in ${PUBLIC_EXPIRY}. ${B.claim}
21
21
 
22
22
  With SHIP_TOKEN configured, deployments go to the user's account and never expire. Listing, managing, and domain operations also require SHIP_TOKEN.
23
23
 
@@ -36,14 +36,14 @@ ${B.domainWorkflow}`;
36
36
  */
37
37
  export function createServer(ship, version) {
38
38
  const server = new McpServer({
39
- name: 'shipstatic',
39
+ name: SERVER_NAME,
40
40
  version,
41
41
  }, {
42
42
  instructions: INSTRUCTIONS,
43
43
  });
44
44
  // Deployments
45
- server.registerTool('deployments_upload', {
46
- description: 'Deploy a static site instantly — free, no account or API key required. Returns the live URL, file count, and size. Without SHIP_TOKEN, the response includes a claim URL (site expires in 3 days) — always show both the deployment URL and claim URL to the user. To make the site private, pass `password`; always show the password to the user if you set one.',
45
+ server.registerTool(UPLOAD_TOOL_NAME, {
46
+ description: `Deploy a static site instantly — ${D.free}. Returns the live URL, file count, and size. Without SHIP_TOKEN, the response includes a claim URL (site expires in ${PUBLIC_EXPIRY}) — always show both the deployment URL and claim URL to the user. ${D.password}`,
47
47
  annotations: CREATE,
48
48
  inputSchema: {
49
49
  path: z
@@ -51,10 +51,7 @@ export function createServer(ship, version) {
51
51
  .describe('Absolute path to the build output directory to deploy (e.g. "/Users/me/project/dist")'),
52
52
  labels: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.labels),
53
53
  password: z.string().optional().describe(PARAM_DESCRIPTIONS.password),
54
- idempotencyKey: z
55
- .string()
56
- .optional()
57
- .describe(`Makes this deploy replayable instead of repeatable. A deploy is not naturally idempotent: if a call times out you cannot tell "it never landed" from "it landed and the response was lost", and retrying creates a second deployment. Send the same key on the retry and the original deployment is replayed instead (within ${IDEMPOTENCY_KEY_CONSTRAINTS.WINDOW_SECONDS / 3600} hours). Key the ATTEMPT — a run id, a commit sha, a uuid minted before the first try — never one minted fresh on each retry, which would defeat the point.`),
54
+ idempotencyKey: z.string().optional().describe(PARAM_DESCRIPTIONS.idempotencyKey),
58
55
  },
59
56
  }, ({ path, labels, password, idempotencyKey }) => call(() => ship.deployments.upload(path, { labels, password, idempotencyKey, via: 'mcp' })));
60
57
  // The other fourteen. Identical on every transport, so they live in the
package/dist/tools.d.ts CHANGED
@@ -30,4 +30,24 @@
30
30
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
31
31
  import type Ship from '@shipstatic/ship';
32
32
  import type { CallFn } from './call.js';
33
+ /**
34
+ * The fourteen, by name, in registration order.
35
+ *
36
+ * Exported so a second transport can state its expected catalogue as
37
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` instead of listing fifteen
38
+ * strings it would then have to keep in agreement with this file — the hosted
39
+ * parity fence is the consumer, and "fifteen" is otherwise a number two repos
40
+ * count separately.
41
+ *
42
+ * **Deliberately a list beside the registrations rather than a table they are
43
+ * generated from.** A `Record<name, factory>` would make the pairing
44
+ * structural, and it would cost the zod→handler inference every one of the
45
+ * fourteen one-liners below relies on: `({ deployment }) => …` is typed today
46
+ * from the `inputSchema` literal in the same call, and a loop over a
47
+ * heterogeneous table cannot correlate the two. The same guarantee costs
48
+ * nothing as a set comparison, and `tests/server.test.ts` makes it — through a
49
+ * real `tools/list`, so a registration without a name, a name without a
50
+ * registration, and a typo in either all turn it red.
51
+ */
52
+ export declare const ACCOUNT_TOOL_NAMES: readonly ["deployments_list", "deployments_get", "deployments_set", "deployments_delete", "domains_set", "domains_list", "domains_get", "domains_records", "domains_dns", "domains_share", "domains_validate", "domains_verify", "domains_delete", "whoami"];
33
53
  export declare function registerAccountTools(server: McpServer, ship: Ship, call: CallFn): void;
package/dist/tools.js CHANGED
@@ -30,6 +30,41 @@
30
30
  import { z } from 'zod';
31
31
  import { ANNOTATIONS } from './vocabulary.js';
32
32
  const { READ, WRITE, DESTRUCTIVE } = ANNOTATIONS;
33
+ /**
34
+ * The fourteen, by name, in registration order.
35
+ *
36
+ * Exported so a second transport can state its expected catalogue as
37
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` instead of listing fifteen
38
+ * strings it would then have to keep in agreement with this file — the hosted
39
+ * parity fence is the consumer, and "fifteen" is otherwise a number two repos
40
+ * count separately.
41
+ *
42
+ * **Deliberately a list beside the registrations rather than a table they are
43
+ * generated from.** A `Record<name, factory>` would make the pairing
44
+ * structural, and it would cost the zod→handler inference every one of the
45
+ * fourteen one-liners below relies on: `({ deployment }) => …` is typed today
46
+ * from the `inputSchema` literal in the same call, and a loop over a
47
+ * heterogeneous table cannot correlate the two. The same guarantee costs
48
+ * nothing as a set comparison, and `tests/server.test.ts` makes it — through a
49
+ * real `tools/list`, so a registration without a name, a name without a
50
+ * registration, and a typo in either all turn it red.
51
+ */
52
+ export const ACCOUNT_TOOL_NAMES = [
53
+ 'deployments_list',
54
+ 'deployments_get',
55
+ 'deployments_set',
56
+ 'deployments_delete',
57
+ 'domains_set',
58
+ 'domains_list',
59
+ 'domains_get',
60
+ 'domains_records',
61
+ 'domains_dns',
62
+ 'domains_share',
63
+ 'domains_validate',
64
+ 'domains_verify',
65
+ 'domains_delete',
66
+ 'whoami',
67
+ ];
33
68
  /**
34
69
  * The pagination surface, shared by every list tool because it is one
35
70
  * contract, not two. A list answers `{<collection>, cursor}` and nothing
@@ -16,9 +16,10 @@
16
16
  *
17
17
  * - **The file-input schema.** A filesystem path here, inline content there:
18
18
  * Workers has no filesystem. Structurally forced apart.
19
- * - **Tool descriptions.** Deliberately rewritten hosted-side for an
20
- * Apps-SDK caller that must be told not to base64-encode text — a failure
21
- * mode the filesystem path does not have.
19
+ * - **Tool descriptions**, as whole strings. They are deliberately rewritten
20
+ * hosted-side for an Apps-SDK caller that must be told not to base64-encode
21
+ * text — a failure mode the filesystem path does not have. What genuinely
22
+ * overlaps is exported as `DESCRIPTION_BLOCKS` and composed per transport.
22
23
  * - **Anything Apps-SDK** (widget, `_meta`, `outputSchema`): hosted-only by
23
24
  * nature.
24
25
  *
@@ -26,6 +27,37 @@
26
27
  * Everything else should be here, and adding a shared fact anywhere else is
27
28
  * how the next year's drift starts.
28
29
  */
30
+ /**
31
+ * The server name every transport reports in `serverInfo`.
32
+ *
33
+ * Shared because it is not only prose: the Apps-SDK widget's bridge handshake
34
+ * sends `appInfo.name`, which the HOST correlates against `serverInfo.name` to
35
+ * tie the rendered view to the connector. Two literals kept equal by comment
36
+ * is exactly the shape this package exists to delete.
37
+ */
38
+ export declare const SERVER_NAME = "shipstatic";
39
+ /**
40
+ * The one tool authored per transport — its INPUT differs (a filesystem path
41
+ * over stdio, inline bytes over HTTP), its NAME must not. Exported so the
42
+ * hosted parity fence can build the expected catalogue as
43
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen.
44
+ */
45
+ export declare const UPLOAD_TOOL_NAME = "deployments_upload";
46
+ /**
47
+ * How long an anonymous deployment lives, in the words an agent reads.
48
+ *
49
+ * **The fact is owned by `cloudflare/api` (`DEPLOYMENT.PUBLIC_TTL`), which
50
+ * `@shipstatic/types` does not yet export** — so this is a restatement, and it
51
+ * is deliberately the ONLY one. It previously appeared in eight places across
52
+ * the two servers and the widget; a TTL change had to find all eight. When
53
+ * types exports the constant, this line becomes a derivation and nothing above
54
+ * it moves.
55
+ *
56
+ * A phrase rather than a number because every consumer is prose: the value has
57
+ * to carry its own unit, and `PUBLIC_TTL / 86400` interpolated at eight sites
58
+ * would restate the unit eight times instead of the number.
59
+ */
60
+ export declare const PUBLIC_EXPIRY = "3 days";
29
61
  /**
30
62
  * MCP tool annotations by kind of operation. An agent reads these to decide
31
63
  * whether it may call speculatively (`readOnlyHint`), whether a retry is free
@@ -65,14 +97,6 @@ export declare const ANNOTATIONS: {
65
97
  readonly idempotentHint: true;
66
98
  };
67
99
  };
68
- /**
69
- * Deploy-parameter descriptions shared by both transports.
70
- *
71
- * The numbers interpolate from `@shipstatic/types` rather than being written
72
- * out, so a platform constraint change reaches every agent-facing string
73
- * without anyone editing prose — the same reason the API and the SDK import
74
- * them instead of restating them.
75
- */
76
100
  /**
77
101
  * INSTRUCTIONS sentences both transports say.
78
102
  *
@@ -95,7 +119,41 @@ export declare const INSTRUCTION_BLOCKS: {
95
119
  readonly domainConcept: "- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.";
96
120
  readonly domainWorkflow: "To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.";
97
121
  };
122
+ /**
123
+ * The fragments of the upload tool's description that both transports say.
124
+ *
125
+ * The surrounding descriptions diverge on purpose — hosted opens for an
126
+ * Apps-SDK caller and spends a paragraph on plain-text-vs-base64, stdio takes
127
+ * a filesystem path and has no such hazard — so what is shared is smaller than
128
+ * a sentence in one case and exactly a sentence in the other. Both were pinned
129
+ * by a `toContain` on each side, which meant three copies of each fragment
130
+ * (two sources and a test literal) held equal by nobody.
131
+ *
132
+ * Same reasoning as `INSTRUCTION_BLOCKS`, one altitude down: blocks are shared,
133
+ * composition is per transport.
134
+ */
135
+ export declare const DESCRIPTION_BLOCKS: {
136
+ /** The no-account promise, mid-sentence in both openings. */
137
+ readonly free: "free, no account or API key required";
138
+ /** The password read-back rule — a password the user never sees locks them out. */
139
+ readonly password: "To make the site private, pass `password`; always show the password to the user if you set one.";
140
+ };
141
+ /**
142
+ * Deploy-parameter descriptions shared by both transports.
143
+ *
144
+ * The numbers interpolate from `@shipstatic/types` rather than being written
145
+ * out, so a platform constraint change reaches every agent-facing string
146
+ * without anyone editing prose — the same reason the API and the SDK import
147
+ * them instead of restating them.
148
+ */
98
149
  export declare const PARAM_DESCRIPTIONS: {
99
150
  readonly labels: "Labels for organizing deployments (e.g. [\"production\", \"v1.2\"]). Lowercase, 3-25 chars, allows . _ - separators. Up to 10.";
100
151
  readonly password: "Optional password to gate the deployment behind an unlock prompt (6–128 characters; whitespace significant). Visitors must enter this password before viewing the site, including on any custom domains pointing at it.";
152
+ /**
153
+ * Shared even though only stdio offers the option today: the hosted door
154
+ * gains it with OAuth (it can scope a replay per user once callers have an
155
+ * identity), and the law this teaches — key the ATTEMPT, never the try — is
156
+ * the same one on both. The window is derived, never typed out.
157
+ */
158
+ readonly idempotencyKey: `Makes this deploy replayable instead of repeatable. A deploy is not naturally idempotent: if a call times out you cannot tell "it never landed" from "it landed and the response was lost", and retrying creates a second deployment. Send the same key on the retry and the original deployment is replayed instead (within ${number} hours). Key the ATTEMPT \u2014 a run id, a commit sha, a uuid minted before the first try \u2014 never one minted fresh on each retry, which would defeat the point.`;
101
159
  };
@@ -16,9 +16,10 @@
16
16
  *
17
17
  * - **The file-input schema.** A filesystem path here, inline content there:
18
18
  * Workers has no filesystem. Structurally forced apart.
19
- * - **Tool descriptions.** Deliberately rewritten hosted-side for an
20
- * Apps-SDK caller that must be told not to base64-encode text — a failure
21
- * mode the filesystem path does not have.
19
+ * - **Tool descriptions**, as whole strings. They are deliberately rewritten
20
+ * hosted-side for an Apps-SDK caller that must be told not to base64-encode
21
+ * text — a failure mode the filesystem path does not have. What genuinely
22
+ * overlaps is exported as `DESCRIPTION_BLOCKS` and composed per transport.
22
23
  * - **Anything Apps-SDK** (widget, `_meta`, `outputSchema`): hosted-only by
23
24
  * nature.
24
25
  *
@@ -26,7 +27,38 @@
26
27
  * Everything else should be here, and adding a shared fact anywhere else is
27
28
  * how the next year's drift starts.
28
29
  */
29
- import { LABEL_CONSTRAINTS, PASSWORD_CONSTRAINTS } from '@shipstatic/ship';
30
+ import { IDEMPOTENCY_KEY_CONSTRAINTS, LABEL_CONSTRAINTS, PASSWORD_CONSTRAINTS, } from '@shipstatic/ship';
31
+ /**
32
+ * The server name every transport reports in `serverInfo`.
33
+ *
34
+ * Shared because it is not only prose: the Apps-SDK widget's bridge handshake
35
+ * sends `appInfo.name`, which the HOST correlates against `serverInfo.name` to
36
+ * tie the rendered view to the connector. Two literals kept equal by comment
37
+ * is exactly the shape this package exists to delete.
38
+ */
39
+ export const SERVER_NAME = 'shipstatic';
40
+ /**
41
+ * The one tool authored per transport — its INPUT differs (a filesystem path
42
+ * over stdio, inline bytes over HTTP), its NAME must not. Exported so the
43
+ * hosted parity fence can build the expected catalogue as
44
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen.
45
+ */
46
+ export const UPLOAD_TOOL_NAME = 'deployments_upload';
47
+ /**
48
+ * How long an anonymous deployment lives, in the words an agent reads.
49
+ *
50
+ * **The fact is owned by `cloudflare/api` (`DEPLOYMENT.PUBLIC_TTL`), which
51
+ * `@shipstatic/types` does not yet export** — so this is a restatement, and it
52
+ * is deliberately the ONLY one. It previously appeared in eight places across
53
+ * the two servers and the widget; a TTL change had to find all eight. When
54
+ * types exports the constant, this line becomes a derivation and nothing above
55
+ * it moves.
56
+ *
57
+ * A phrase rather than a number because every consumer is prose: the value has
58
+ * to carry its own unit, and `PUBLIC_TTL / 86400` interpolated at eight sites
59
+ * would restate the unit eight times instead of the number.
60
+ */
61
+ export const PUBLIC_EXPIRY = '3 days';
30
62
  const OPEN_WORLD = { openWorldHint: true };
31
63
  /**
32
64
  * MCP tool annotations by kind of operation. An agent reads these to decide
@@ -48,14 +80,6 @@ export const ANNOTATIONS = {
48
80
  WRITE: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, ...OPEN_WORLD },
49
81
  DESTRUCTIVE: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...OPEN_WORLD },
50
82
  };
51
- /**
52
- * Deploy-parameter descriptions shared by both transports.
53
- *
54
- * The numbers interpolate from `@shipstatic/types` rather than being written
55
- * out, so a platform constraint change reaches every agent-facing string
56
- * without anyone editing prose — the same reason the API and the SDK import
57
- * them instead of restating them.
58
- */
59
83
  /**
60
84
  * INSTRUCTIONS sentences both transports say.
61
85
  *
@@ -78,7 +102,41 @@ export const INSTRUCTION_BLOCKS = {
78
102
  domainConcept: '- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.',
79
103
  domainWorkflow: 'To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.',
80
104
  };
105
+ /**
106
+ * The fragments of the upload tool's description that both transports say.
107
+ *
108
+ * The surrounding descriptions diverge on purpose — hosted opens for an
109
+ * Apps-SDK caller and spends a paragraph on plain-text-vs-base64, stdio takes
110
+ * a filesystem path and has no such hazard — so what is shared is smaller than
111
+ * a sentence in one case and exactly a sentence in the other. Both were pinned
112
+ * by a `toContain` on each side, which meant three copies of each fragment
113
+ * (two sources and a test literal) held equal by nobody.
114
+ *
115
+ * Same reasoning as `INSTRUCTION_BLOCKS`, one altitude down: blocks are shared,
116
+ * composition is per transport.
117
+ */
118
+ export const DESCRIPTION_BLOCKS = {
119
+ /** The no-account promise, mid-sentence in both openings. */
120
+ free: 'free, no account or API key required',
121
+ /** The password read-back rule — a password the user never sees locks them out. */
122
+ password: 'To make the site private, pass `password`; always show the password to the user if you set one.',
123
+ };
124
+ /**
125
+ * Deploy-parameter descriptions shared by both transports.
126
+ *
127
+ * The numbers interpolate from `@shipstatic/types` rather than being written
128
+ * out, so a platform constraint change reaches every agent-facing string
129
+ * without anyone editing prose — the same reason the API and the SDK import
130
+ * them instead of restating them.
131
+ */
81
132
  export const PARAM_DESCRIPTIONS = {
82
133
  labels: `Labels for organizing deployments (e.g. ["production", "v1.2"]). Lowercase, ${LABEL_CONSTRAINTS.MIN_LENGTH}-${LABEL_CONSTRAINTS.MAX_LENGTH} chars, allows . _ - separators. Up to ${LABEL_CONSTRAINTS.MAX_COUNT}.`,
83
134
  password: `Optional password to gate the deployment behind an unlock prompt (${PASSWORD_CONSTRAINTS.MIN_LENGTH}–${PASSWORD_CONSTRAINTS.MAX_LENGTH} characters; whitespace significant). Visitors must enter this password before viewing the site, including on any custom domains pointing at it.`,
135
+ /**
136
+ * Shared even though only stdio offers the option today: the hosted door
137
+ * gains it with OAuth (it can scope a replay per user once callers have an
138
+ * identity), and the law this teaches — key the ATTEMPT, never the try — is
139
+ * the same one on both. The window is derived, never typed out.
140
+ */
141
+ idempotencyKey: `Makes this deploy replayable instead of repeatable. A deploy is not naturally idempotent: if a call times out you cannot tell "it never landed" from "it landed and the response was lost", and retrying creates a second deployment. Send the same key on the retry and the original deployment is replayed instead (within ${IDEMPOTENCY_KEY_CONSTRAINTS.WINDOW_SECONDS / 3600} hours). Key the ATTEMPT — a run id, a commit sha, a uuid minted before the first try — never one minted fresh on each retry, which would defeat the point.`,
84
142
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "mcpName": "com.shipstatic/mcp",
5
5
  "description": "ShipStatic MCP — deploy static websites from AI agents. Full toolset incl. custom domains. Free hosted endpoint at mcp.shipstatic.com — no install.",
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "typecheck": "tsc -p tsconfig.check.json --noEmit",
25
25
  "lint": "biome check .",
26
26
  "format": "biome format --write .",
27
+ "smoke": "node smoke.mjs",
27
28
  "coverage": "vitest --run --coverage"
28
29
  },
29
30
  "files": [