@shipstatic/mcp 1.0.0-beta.0 → 1.0.0-beta.10

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.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * The vocabulary both transports speak.
3
+ *
4
+ * `@shipstatic/mcp` (stdio) and the hosted Streamable-HTTP server are one
5
+ * product with two doors in. Everything an agent observes that is NOT forced
6
+ * apart by the transport lives here and is IMPORTED by both — because a fact
7
+ * with two owners is a fact that drifts. This pair kept ten such strings
8
+ * byte-identical by hand for a year, and the hand slipped: a tool description
9
+ * diverged unnoticed, a one-word correction had to be applied at three sites,
10
+ * and a test mock invented constraint numbers production never used. A
11
+ * coordination table written in prose is a specification for drift, not a
12
+ * defence against it.
13
+ *
14
+ * What belongs here: anything true of a ShipStatic deploy regardless of how
15
+ * the bytes arrived. What does not, and why:
16
+ *
17
+ * - **The file-input schema.** A filesystem path here, inline content there:
18
+ * Workers has no filesystem. Structurally forced apart.
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.
23
+ * - **Anything Apps-SDK** (widget, `_meta`, `outputSchema`): hosted-only by
24
+ * nature.
25
+ *
26
+ * Each of those is recorded in `cloudflare/mcp/CLAUDE.md`'s divergence table.
27
+ * Everything else should be here, and adding a shared fact anywhere else is
28
+ * how the next year's drift starts.
29
+ */
30
+ /**
31
+ * Two packages, and the split is a rule rather than an accident: **read a
32
+ * constant from whatever will act on it.**
33
+ *
34
+ * The label, password and idempotency-key constraints come from
35
+ * `@shipstatic/ship` because the SDK is what validates a value against them
36
+ * before it reaches the wire — describing a bound the client in the same
37
+ * process will not honour is the drift that matters, and reading both from one
38
+ * module makes it impossible. `@shipstatic/types` declares them, but ship
39
+ * bundles its own copy, so importing them from types here would let a describe
40
+ * advertise a limit the validator beside it rejects.
41
+ *
42
+ * The public-deploy lifetime is the other kind of fact. Ship never reads it —
43
+ * the API stamps it — so there is no validator to agree with, and taking it
44
+ * from the package that merely forwards it would mean a ship release every
45
+ * time the platform's own vocabulary grows.
46
+ */
47
+ /**
48
+ * The server name every transport reports in `serverInfo`.
49
+ *
50
+ * Shared because it is not only prose: the Apps-SDK widget's bridge handshake
51
+ * sends `appInfo.name`, which the HOST correlates against `serverInfo.name` to
52
+ * tie the rendered view to the connector. Two literals kept equal by comment
53
+ * is exactly the shape this package exists to delete.
54
+ */
55
+ export declare const SERVER_NAME = "shipstatic";
56
+ /**
57
+ * The one tool authored per transport — its INPUT differs (a filesystem path
58
+ * over stdio, inline bytes over HTTP), its NAME must not. Exported so the
59
+ * hosted parity fence can build the expected catalogue as
60
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen.
61
+ */
62
+ export declare const UPLOAD_TOOL_NAME = "deployments_upload";
63
+ /**
64
+ * How long an anonymous deployment lives, in the words an agent reads.
65
+ *
66
+ * **Derived, since `@shipstatic/types@2.5.0-beta.19`.** It was a restatement
67
+ * until then, and deliberately the only one — the duration had appeared in
68
+ * eight places across the two servers and the widget, so a TTL change had to
69
+ * find all eight. Both halves of the fix landed together: types declares the
70
+ * number and `cloudflare/api` imports it back, because exporting without the
71
+ * import-back would have given the fact two owners instead of ending the
72
+ * duplication.
73
+ *
74
+ * A phrase rather than a number because every consumer is prose: the value has
75
+ * to carry its own unit, and dividing by 86400 at eight sites would restate the
76
+ * unit eight times instead of the number.
77
+ *
78
+ * The unit stays literal, and that is the one assumption here: this reads
79
+ * correctly while the TTL is a whole number of days, which it has always been.
80
+ * A TTL of hours would need the prose reviewed anyway — the widget's own
81
+ * `formatExpires` speaks in days and hours too — so the honest failure is a
82
+ * sentence someone must rewrite, not a number that silently rounds.
83
+ */
84
+ export declare const PUBLIC_EXPIRY: string;
85
+ /**
86
+ * MCP tool annotations by kind of operation. An agent reads these to decide
87
+ * whether it may call speculatively (`readOnlyHint`), whether a retry is free
88
+ * (`idempotentHint`), and whether it must confirm with the user first
89
+ * (`destructiveHint`).
90
+ *
91
+ * **`CREATE` carries no `idempotentHint`, deliberately.** A deploy creates a
92
+ * new deployment on every call. `idempotencyKey` makes a retry replay the
93
+ * original instead — but that property is conditional on an argument the
94
+ * caller may not pass, while the annotation is static per tool. Advertising it
95
+ * would promise every agent that any retry is free, which is exactly false for
96
+ * the keyless caller, and an annotation an agent trusts wrongly is worse than
97
+ * one it never reads.
98
+ */
99
+ export declare const ANNOTATIONS: {
100
+ readonly READ: {
101
+ readonly openWorldHint: true;
102
+ readonly readOnlyHint: true;
103
+ readonly destructiveHint: false;
104
+ readonly idempotentHint: true;
105
+ };
106
+ readonly CREATE: {
107
+ readonly openWorldHint: true;
108
+ readonly readOnlyHint: false;
109
+ readonly destructiveHint: false;
110
+ };
111
+ readonly WRITE: {
112
+ readonly openWorldHint: true;
113
+ readonly readOnlyHint: false;
114
+ readonly destructiveHint: false;
115
+ readonly idempotentHint: true;
116
+ };
117
+ readonly DESTRUCTIVE: {
118
+ readonly openWorldHint: true;
119
+ readonly readOnlyHint: false;
120
+ readonly destructiveHint: true;
121
+ readonly idempotentHint: true;
122
+ };
123
+ };
124
+ /**
125
+ * INSTRUCTIONS sentences both transports say.
126
+ *
127
+ * `initialize`'s instructions are the other half of what an agent reads
128
+ * before acting (the catalogue is the first). Each transport composes its own
129
+ * from these blocks plus the two things that are genuinely its own: how files
130
+ * arrive, and how a caller authenticates.
131
+ *
132
+ * Three of these are duplicated prose TODAY, kept equal by review. The last
133
+ * two are stdio-only only because the hosted transport has no domain tools
134
+ * yet — when it gains them with OAuth they become shared too, which is
135
+ * precisely when someone would otherwise copy them across.
136
+ */
137
+ export declare const INSTRUCTION_BLOCKS: {
138
+ readonly opening: "ShipStatic deploys static websites instantly. Free, no account required.";
139
+ 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.";
140
+ 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.";
141
+ readonly conceptsHeader: "Concepts:";
142
+ readonly deploymentConcept: "- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.";
143
+ readonly domainConcept: "- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.";
144
+ readonly domainWorkflow: "To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.";
145
+ };
146
+ /**
147
+ * The fragments of the upload tool's description that both transports say.
148
+ *
149
+ * The surrounding descriptions diverge on purpose — hosted opens for an
150
+ * Apps-SDK caller and spends a paragraph on plain-text-vs-base64, stdio takes
151
+ * a filesystem path and has no such hazard — so what is shared is smaller than
152
+ * a sentence in one case and exactly a sentence in the other. Both were pinned
153
+ * by a `toContain` on each side, which meant three copies of each fragment
154
+ * (two sources and a test literal) held equal by nobody.
155
+ *
156
+ * Same reasoning as `INSTRUCTION_BLOCKS`, one altitude down: blocks are shared,
157
+ * composition is per transport.
158
+ */
159
+ export declare const DESCRIPTION_BLOCKS: {
160
+ /** The no-account promise, mid-sentence in both openings. */
161
+ readonly free: "free, no account or API key required";
162
+ /** The password read-back rule — a password the user never sees locks them out. */
163
+ readonly password: "To make the site private, pass `password`; always show the password to the user if you set one.";
164
+ };
165
+ /**
166
+ * Deploy-parameter descriptions shared by both transports.
167
+ *
168
+ * The numbers interpolate from `@shipstatic/types` rather than being written
169
+ * out, so a platform constraint change reaches every agent-facing string
170
+ * without anyone editing prose — the same reason the API and the SDK import
171
+ * them instead of restating them.
172
+ */
173
+ export declare const PARAM_DESCRIPTIONS: {
174
+ readonly labels: "Labels for organizing deployments (e.g. [\"production\", \"v1.2\"]). Lowercase, 3-25 chars, allows . _ - separators. Up to 10.";
175
+ 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.";
176
+ /**
177
+ * Shared even though only stdio offers the option today: the hosted door
178
+ * gains it with OAuth (it can scope a replay per user once callers have an
179
+ * identity), and the law this teaches — key the ATTEMPT, never the try — is
180
+ * the same one on both. The window is derived, never typed out.
181
+ */
182
+ 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.`;
183
+ };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * The vocabulary both transports speak.
3
+ *
4
+ * `@shipstatic/mcp` (stdio) and the hosted Streamable-HTTP server are one
5
+ * product with two doors in. Everything an agent observes that is NOT forced
6
+ * apart by the transport lives here and is IMPORTED by both — because a fact
7
+ * with two owners is a fact that drifts. This pair kept ten such strings
8
+ * byte-identical by hand for a year, and the hand slipped: a tool description
9
+ * diverged unnoticed, a one-word correction had to be applied at three sites,
10
+ * and a test mock invented constraint numbers production never used. A
11
+ * coordination table written in prose is a specification for drift, not a
12
+ * defence against it.
13
+ *
14
+ * What belongs here: anything true of a ShipStatic deploy regardless of how
15
+ * the bytes arrived. What does not, and why:
16
+ *
17
+ * - **The file-input schema.** A filesystem path here, inline content there:
18
+ * Workers has no filesystem. Structurally forced apart.
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.
23
+ * - **Anything Apps-SDK** (widget, `_meta`, `outputSchema`): hosted-only by
24
+ * nature.
25
+ *
26
+ * Each of those is recorded in `cloudflare/mcp/CLAUDE.md`'s divergence table.
27
+ * Everything else should be here, and adding a shared fact anywhere else is
28
+ * how the next year's drift starts.
29
+ */
30
+ import { IDEMPOTENCY_KEY_CONSTRAINTS, LABEL_CONSTRAINTS, PASSWORD_CONSTRAINTS, } from '@shipstatic/ship';
31
+ import { PUBLIC_DEPLOYMENT_TTL_SECONDS } from '@shipstatic/types';
32
+ /**
33
+ * Two packages, and the split is a rule rather than an accident: **read a
34
+ * constant from whatever will act on it.**
35
+ *
36
+ * The label, password and idempotency-key constraints come from
37
+ * `@shipstatic/ship` because the SDK is what validates a value against them
38
+ * before it reaches the wire — describing a bound the client in the same
39
+ * process will not honour is the drift that matters, and reading both from one
40
+ * module makes it impossible. `@shipstatic/types` declares them, but ship
41
+ * bundles its own copy, so importing them from types here would let a describe
42
+ * advertise a limit the validator beside it rejects.
43
+ *
44
+ * The public-deploy lifetime is the other kind of fact. Ship never reads it —
45
+ * the API stamps it — so there is no validator to agree with, and taking it
46
+ * from the package that merely forwards it would mean a ship release every
47
+ * time the platform's own vocabulary grows.
48
+ */
49
+ /**
50
+ * The server name every transport reports in `serverInfo`.
51
+ *
52
+ * Shared because it is not only prose: the Apps-SDK widget's bridge handshake
53
+ * sends `appInfo.name`, which the HOST correlates against `serverInfo.name` to
54
+ * tie the rendered view to the connector. Two literals kept equal by comment
55
+ * is exactly the shape this package exists to delete.
56
+ */
57
+ export const SERVER_NAME = 'shipstatic';
58
+ /**
59
+ * The one tool authored per transport — its INPUT differs (a filesystem path
60
+ * over stdio, inline bytes over HTTP), its NAME must not. Exported so the
61
+ * hosted parity fence can build the expected catalogue as
62
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen.
63
+ */
64
+ export const UPLOAD_TOOL_NAME = 'deployments_upload';
65
+ /**
66
+ * How long an anonymous deployment lives, in the words an agent reads.
67
+ *
68
+ * **Derived, since `@shipstatic/types@2.5.0-beta.19`.** It was a restatement
69
+ * until then, and deliberately the only one — the duration had appeared in
70
+ * eight places across the two servers and the widget, so a TTL change had to
71
+ * find all eight. Both halves of the fix landed together: types declares the
72
+ * number and `cloudflare/api` imports it back, because exporting without the
73
+ * import-back would have given the fact two owners instead of ending the
74
+ * duplication.
75
+ *
76
+ * A phrase rather than a number because every consumer is prose: the value has
77
+ * to carry its own unit, and dividing by 86400 at eight sites would restate the
78
+ * unit eight times instead of the number.
79
+ *
80
+ * The unit stays literal, and that is the one assumption here: this reads
81
+ * correctly while the TTL is a whole number of days, which it has always been.
82
+ * A TTL of hours would need the prose reviewed anyway — the widget's own
83
+ * `formatExpires` speaks in days and hours too — so the honest failure is a
84
+ * sentence someone must rewrite, not a number that silently rounds.
85
+ */
86
+ export const PUBLIC_EXPIRY = `${PUBLIC_DEPLOYMENT_TTL_SECONDS / 86_400} days`;
87
+ const OPEN_WORLD = { openWorldHint: true };
88
+ /**
89
+ * MCP tool annotations by kind of operation. An agent reads these to decide
90
+ * whether it may call speculatively (`readOnlyHint`), whether a retry is free
91
+ * (`idempotentHint`), and whether it must confirm with the user first
92
+ * (`destructiveHint`).
93
+ *
94
+ * **`CREATE` carries no `idempotentHint`, deliberately.** A deploy creates a
95
+ * new deployment on every call. `idempotencyKey` makes a retry replay the
96
+ * original instead — but that property is conditional on an argument the
97
+ * caller may not pass, while the annotation is static per tool. Advertising it
98
+ * would promise every agent that any retry is free, which is exactly false for
99
+ * the keyless caller, and an annotation an agent trusts wrongly is worse than
100
+ * one it never reads.
101
+ */
102
+ export const ANNOTATIONS = {
103
+ READ: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, ...OPEN_WORLD },
104
+ CREATE: { readOnlyHint: false, destructiveHint: false, ...OPEN_WORLD },
105
+ WRITE: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, ...OPEN_WORLD },
106
+ DESTRUCTIVE: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...OPEN_WORLD },
107
+ };
108
+ /**
109
+ * INSTRUCTIONS sentences both transports say.
110
+ *
111
+ * `initialize`'s instructions are the other half of what an agent reads
112
+ * before acting (the catalogue is the first). Each transport composes its own
113
+ * from these blocks plus the two things that are genuinely its own: how files
114
+ * arrive, and how a caller authenticates.
115
+ *
116
+ * Three of these are duplicated prose TODAY, kept equal by review. The last
117
+ * two are stdio-only only because the hosted transport has no domain tools
118
+ * yet — when it gains them with OAuth they become shared too, which is
119
+ * precisely when someone would otherwise copy them across.
120
+ */
121
+ export const INSTRUCTION_BLOCKS = {
122
+ opening: 'ShipStatic deploys static websites instantly. Free, no account required.',
123
+ 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.',
124
+ 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.',
125
+ conceptsHeader: 'Concepts:',
126
+ deploymentConcept: '- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.',
127
+ domainConcept: '- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.',
128
+ domainWorkflow: 'To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.',
129
+ };
130
+ /**
131
+ * The fragments of the upload tool's description that both transports say.
132
+ *
133
+ * The surrounding descriptions diverge on purpose — hosted opens for an
134
+ * Apps-SDK caller and spends a paragraph on plain-text-vs-base64, stdio takes
135
+ * a filesystem path and has no such hazard — so what is shared is smaller than
136
+ * a sentence in one case and exactly a sentence in the other. Both were pinned
137
+ * by a `toContain` on each side, which meant three copies of each fragment
138
+ * (two sources and a test literal) held equal by nobody.
139
+ *
140
+ * Same reasoning as `INSTRUCTION_BLOCKS`, one altitude down: blocks are shared,
141
+ * composition is per transport.
142
+ */
143
+ export const DESCRIPTION_BLOCKS = {
144
+ /** The no-account promise, mid-sentence in both openings. */
145
+ free: 'free, no account or API key required',
146
+ /** The password read-back rule — a password the user never sees locks them out. */
147
+ password: 'To make the site private, pass `password`; always show the password to the user if you set one.',
148
+ };
149
+ /**
150
+ * Deploy-parameter descriptions shared by both transports.
151
+ *
152
+ * The numbers interpolate from `@shipstatic/types` rather than being written
153
+ * out, so a platform constraint change reaches every agent-facing string
154
+ * without anyone editing prose — the same reason the API and the SDK import
155
+ * them instead of restating them.
156
+ */
157
+ export const PARAM_DESCRIPTIONS = {
158
+ 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}.`,
159
+ 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.`,
160
+ /**
161
+ * Shared even though only stdio offers the option today: the hosted door
162
+ * gains it with OAuth (it can scope a replay per user once callers have an
163
+ * identity), and the law this teaches — key the ATTEMPT, never the try — is
164
+ * the same one on both. The window is derived, never typed out.
165
+ */
166
+ 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.`,
167
+ };
package/package.json CHANGED
@@ -1,13 +1,23 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.10",
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",
7
7
  "main": "./dist/index.js",
8
+ "sideEffects": [
9
+ "./dist/bin.js"
10
+ ],
8
11
  "types": "./dist/index.d.ts",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
9
19
  "bin": {
10
- "shipstatic-mcp": "./dist/index.js"
20
+ "shipstatic-mcp": "./dist/bin.js"
11
21
  },
12
22
  "scripts": {
13
23
  "build": "tsc",
@@ -17,6 +27,7 @@
17
27
  "typecheck": "tsc -p tsconfig.check.json --noEmit",
18
28
  "lint": "biome check .",
19
29
  "format": "biome format --write .",
30
+ "smoke": "node smoke.mjs",
20
31
  "coverage": "vitest --run --coverage"
21
32
  },
22
33
  "files": [
@@ -55,14 +66,15 @@
55
66
  "license": "MIT",
56
67
  "dependencies": {
57
68
  "@modelcontextprotocol/sdk": "^1.30.0",
58
- "@shipstatic/ship": "2.0.0-beta.15",
69
+ "@shipstatic/ship": "2.0.0-beta.17",
70
+ "@shipstatic/types": "2.5.0-beta.21",
59
71
  "zod": "^4.4.3"
60
72
  },
61
73
  "devDependencies": {
62
74
  "@biomejs/biome": "2.5.5",
63
- "@shipstatic/types": "2.5.0-beta.18",
64
75
  "@types/node": "^25.9.5",
65
76
  "@vitest/coverage-v8": "4.1.10",
77
+ "esbuild": "^0.25.12",
66
78
  "typescript": "^6.0.3",
67
79
  "vitest": "4.1.10"
68
80
  },