@shipstatic/mcp 1.0.0-beta.3 → 1.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,21 +1,34 @@
1
1
  /**
2
2
  * The library entry — importing this file has NO side effects.
3
3
  *
4
- * That is the whole point of it: `bin.ts` is the executable, and this is what
5
- * a consumer imports. Today there is exactly one such consumer, the hosted
6
- * Streamable-HTTP transport in the platform's private `cloudflare/mcp`, which
7
- * takes the vocabulary below rather than re-authoring the strings an agent
8
- * reads. "One product, two transports" is only true if one of them can import
9
- * the other.
4
+ * `bin.ts` is the executable; this is what a consumer imports. Today there is
5
+ * one such consumer, the hosted Streamable-HTTP transport in the platform's
6
+ * private `cloudflare/mcp`, and both transports are converging on the same
7
+ * product: the complete toolset for authenticated callers, the anonymous
8
+ * deploy for everyone else. "One product, two transports" is only true if one
9
+ * of them can import the other.
10
10
  *
11
- * **The surface is curated, not swept.** Every export here is a deliberate
12
- * public commitment under semver; the modules behind it hold plenty that is
13
- * not (`toErrorResult`, `safeStringify`, the INSTRUCTIONS template, every tool
14
- * registration). `export *` would publish implementation detail and make the
15
- * next refactor a breaking change. `tests/index.test.ts` fences both
16
- * directions nothing missing, nothing extra.
11
+ * **The surface is exactly what a SECOND TRANSPORT needs nothing more.**
12
+ * That is the rule, and it is stricter than "curated". `createServer` and the
13
+ * configured `call` are deliberately NOT here: they are stdio's own
14
+ * composition, they have no consumer outside `bin.ts`, and `createServer` in
15
+ * particular builds a tool whose input is a filesystem PATH — a footgun to
16
+ * offer a Worker. Exporting them would make this package's API "stdio's
17
+ * internals, plus some shared bits" instead of a contract.
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
+ *
28
+ * `tests/index.test.ts` fences both directions — nothing missing, nothing
29
+ * extra — because adding an export is the quiet failure: everything published
30
+ * becomes a breaking change to remove.
17
31
  */
18
- export { type CallFn, type CallOptions, call, createCall, type ErrorHints } from './call.js';
19
- export { createServer } from './server.js';
20
- export { registerAccountTools } from './tools.js';
21
- export { ANNOTATIONS, PARAM_DESCRIPTIONS } from './vocabulary.js';
32
+ export { type CallFn, type CallOptions, createCall, type ErrorHints } from './call.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
@@ -1,21 +1,34 @@
1
1
  /**
2
2
  * The library entry — importing this file has NO side effects.
3
3
  *
4
- * That is the whole point of it: `bin.ts` is the executable, and this is what
5
- * a consumer imports. Today there is exactly one such consumer, the hosted
6
- * Streamable-HTTP transport in the platform's private `cloudflare/mcp`, which
7
- * takes the vocabulary below rather than re-authoring the strings an agent
8
- * reads. "One product, two transports" is only true if one of them can import
9
- * the other.
4
+ * `bin.ts` is the executable; this is what a consumer imports. Today there is
5
+ * one such consumer, the hosted Streamable-HTTP transport in the platform's
6
+ * private `cloudflare/mcp`, and both transports are converging on the same
7
+ * product: the complete toolset for authenticated callers, the anonymous
8
+ * deploy for everyone else. "One product, two transports" is only true if one
9
+ * of them can import the other.
10
10
  *
11
- * **The surface is curated, not swept.** Every export here is a deliberate
12
- * public commitment under semver; the modules behind it hold plenty that is
13
- * not (`toErrorResult`, `safeStringify`, the INSTRUCTIONS template, every tool
14
- * registration). `export *` would publish implementation detail and make the
15
- * next refactor a breaking change. `tests/index.test.ts` fences both
16
- * directions nothing missing, nothing extra.
11
+ * **The surface is exactly what a SECOND TRANSPORT needs nothing more.**
12
+ * That is the rule, and it is stricter than "curated". `createServer` and the
13
+ * configured `call` are deliberately NOT here: they are stdio's own
14
+ * composition, they have no consumer outside `bin.ts`, and `createServer` in
15
+ * particular builds a tool whose input is a filesystem PATH — a footgun to
16
+ * offer a Worker. Exporting them would make this package's API "stdio's
17
+ * internals, plus some shared bits" instead of a contract.
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
+ *
28
+ * `tests/index.test.ts` fences both directions — nothing missing, nothing
29
+ * extra — because adding an export is the quiet failure: everything published
30
+ * becomes a breaking change to remove.
17
31
  */
18
- export { call, createCall } from './call.js';
19
- export { createServer } from './server.js';
20
- export { registerAccountTools } from './tools.js';
21
- export { ANNOTATIONS, PARAM_DESCRIPTIONS } from './vocabulary.js';
32
+ export { createCall } from './call.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,26 +1,31 @@
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, 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
- const INSTRUCTIONS = `ShipStatic deploys static websites instantly. Free, no account required.
10
+ const B = INSTRUCTION_BLOCKS;
11
+ const D = DESCRIPTION_BLOCKS;
12
+ // Composed from the shared blocks plus the two sentences that are genuinely
13
+ // stdio's: how files arrive (a filesystem path) and how a caller
14
+ // authenticates (`SHIP_TOKEN`). The hosted transport composes the same blocks
15
+ // around its own two.
16
+ const INSTRUCTIONS = `${B.opening}
12
17
 
13
- To deploy: call deployments_upload with the build output directory path. The site is live immediately. To make the site private, pass \`password\` — visitors must unlock before viewing, including on any custom domains pointing at it.
18
+ To deploy: call deployments_upload with the build output directory path. ${B.liveAndPassword}
14
19
 
15
- Without SHIP_TOKEN, deployments are public and expire in 3 days. The response includes a claim URL — always show the deployment URL and the claim URL to the user so they can keep the site permanently.
20
+ Without SHIP_TOKEN, deployments are public and expire in ${PUBLIC_EXPIRY}. ${B.claim}
16
21
 
17
22
  With SHIP_TOKEN configured, deployments go to the user's account and never expire. Listing, managing, and domain operations also require SHIP_TOKEN.
18
23
 
19
- Concepts:
20
- - Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.
21
- - Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.
24
+ ${B.conceptsHeader}
25
+ ${B.deploymentConcept}
26
+ ${B.domainConcept}
22
27
 
23
- To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.`;
28
+ ${B.domainWorkflow}`;
24
29
  /**
25
30
  * Builds the stdio server's full 15-tool surface over an injected client.
26
31
  *
@@ -31,14 +36,14 @@ To add a custom domain: domains_validate → domains_set → domains_records (sh
31
36
  */
32
37
  export function createServer(ship, version) {
33
38
  const server = new McpServer({
34
- name: 'shipstatic',
39
+ name: SERVER_NAME,
35
40
  version,
36
41
  }, {
37
42
  instructions: INSTRUCTIONS,
38
43
  });
39
44
  // Deployments
40
- server.registerTool('deployments_upload', {
41
- 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}`,
42
47
  annotations: CREATE,
43
48
  inputSchema: {
44
49
  path: z
@@ -46,10 +51,7 @@ export function createServer(ship, version) {
46
51
  .describe('Absolute path to the build output directory to deploy (e.g. "/Users/me/project/dist")'),
47
52
  labels: z.array(z.string()).optional().describe(PARAM_DESCRIPTIONS.labels),
48
53
  password: z.string().optional().describe(PARAM_DESCRIPTIONS.password),
49
- idempotencyKey: z
50
- .string()
51
- .optional()
52
- .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),
53
55
  },
54
56
  }, ({ path, labels, password, idempotencyKey }) => call(() => ship.deployments.upload(path, { labels, password, idempotencyKey, via: 'mcp' })));
55
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,6 +97,47 @@ export declare const ANNOTATIONS: {
65
97
  readonly idempotentHint: true;
66
98
  };
67
99
  };
100
+ /**
101
+ * INSTRUCTIONS sentences both transports say.
102
+ *
103
+ * `initialize`'s instructions are the other half of what an agent reads
104
+ * before acting (the catalogue is the first). Each transport composes its own
105
+ * from these blocks plus the two things that are genuinely its own: how files
106
+ * arrive, and how a caller authenticates.
107
+ *
108
+ * Three of these are duplicated prose TODAY, kept equal by review. The last
109
+ * two are stdio-only only because the hosted transport has no domain tools
110
+ * yet — when it gains them with OAuth they become shared too, which is
111
+ * precisely when someone would otherwise copy them across.
112
+ */
113
+ export declare const INSTRUCTION_BLOCKS: {
114
+ readonly opening: "ShipStatic deploys static websites instantly. Free, no account required.";
115
+ readonly liveAndPassword: "The site is live immediately. To make the site private, pass `password` — visitors must unlock before viewing, including on any custom domains pointing at it.";
116
+ readonly claim: "The response includes a claim URL — always show the deployment URL and the claim URL to the user so they can keep the site permanently.";
117
+ readonly conceptsHeader: "Concepts:";
118
+ readonly deploymentConcept: "- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.";
119
+ readonly domainConcept: "- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.";
120
+ readonly domainWorkflow: "To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.";
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
+ };
68
141
  /**
69
142
  * Deploy-parameter descriptions shared by both transports.
70
143
  *
@@ -76,4 +149,11 @@ export declare const ANNOTATIONS: {
76
149
  export declare const PARAM_DESCRIPTIONS: {
77
150
  readonly labels: "Labels for organizing deployments (e.g. [\"production\", \"v1.2\"]). Lowercase, 3-25 chars, allows . _ - separators. Up to 10.";
78
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.`;
79
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,6 +80,47 @@ 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
  };
83
+ /**
84
+ * INSTRUCTIONS sentences both transports say.
85
+ *
86
+ * `initialize`'s instructions are the other half of what an agent reads
87
+ * before acting (the catalogue is the first). Each transport composes its own
88
+ * from these blocks plus the two things that are genuinely its own: how files
89
+ * arrive, and how a caller authenticates.
90
+ *
91
+ * Three of these are duplicated prose TODAY, kept equal by review. The last
92
+ * two are stdio-only only because the hosted transport has no domain tools
93
+ * yet — when it gains them with OAuth they become shared too, which is
94
+ * precisely when someone would otherwise copy them across.
95
+ */
96
+ export const INSTRUCTION_BLOCKS = {
97
+ opening: 'ShipStatic deploys static websites instantly. Free, no account required.',
98
+ liveAndPassword: 'The site is live immediately. To make the site private, pass `password` — visitors must unlock before viewing, including on any custom domains pointing at it.',
99
+ claim: 'The response includes a claim URL — always show the deployment URL and the claim URL to the user so they can keep the site permanently.',
100
+ conceptsHeader: 'Concepts:',
101
+ deploymentConcept: '- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.',
102
+ domainConcept: '- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.',
103
+ domainWorkflow: 'To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.',
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
+ };
51
124
  /**
52
125
  * Deploy-parameter descriptions shared by both transports.
53
126
  *
@@ -59,4 +132,11 @@ export const ANNOTATIONS = {
59
132
  export const PARAM_DESCRIPTIONS = {
60
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}.`,
61
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.`,
62
142
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.0.0-beta.3",
3
+ "version": "1.0.0-beta.5",
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": [