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

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/call.d.ts CHANGED
@@ -16,6 +16,12 @@ export interface ErrorHints {
16
16
  /** Appended after `Hint: ` when the platform refuses an authenticated call. */
17
17
  forbidden: string;
18
18
  }
19
+ /**
20
+ * The wrapper every tool handler delegates to. Named because the shared
21
+ * toolset takes it as an argument: the tools are identical across transports,
22
+ * the hints inside `call` are not.
23
+ */
24
+ export type CallFn = <T>(fn: () => Promise<T>) => Promise<CallToolResult>;
19
25
  export interface CallOptions {
20
26
  hints: ErrorHints;
21
27
  /**
@@ -37,5 +43,5 @@ export interface CallOptions {
37
43
  * facts an agent observes — so they live here once, rather than in two files
38
44
  * kept equal by review.
39
45
  */
40
- export declare function createCall(options: CallOptions): <T>(fn: () => Promise<T>) => Promise<CallToolResult>;
41
- export declare const call: <T>(fn: () => Promise<T>) => Promise<CallToolResult>;
46
+ export declare function createCall(options: CallOptions): CallFn;
47
+ export declare const call: CallFn;
package/dist/index.d.ts CHANGED
@@ -1,20 +1,25 @@
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
+ * `tests/index.test.ts` fences both directions — nothing missing, nothing
20
+ * extra — because adding an export is the quiet failure: everything published
21
+ * becomes a breaking change to remove.
17
22
  */
18
- export { type CallOptions, call, createCall, type ErrorHints } from './call.js';
19
- export { createServer } from './server.js';
20
- export { ANNOTATIONS, PARAM_DESCRIPTIONS } from './vocabulary.js';
23
+ 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';
package/dist/index.js CHANGED
@@ -1,20 +1,25 @@
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
+ * `tests/index.test.ts` fences both directions — nothing missing, nothing
20
+ * extra — because adding an export is the quiet failure: everything published
21
+ * becomes a breaking change to remove.
17
22
  */
18
- export { call, createCall } from './call.js';
19
- export { createServer } from './server.js';
20
- export { ANNOTATIONS, PARAM_DESCRIPTIONS } from './vocabulary.js';
23
+ export { createCall } from './call.js';
24
+ export { registerAccountTools } from './tools.js';
25
+ export { ANNOTATIONS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS } from './vocabulary.js';
package/dist/server.js CHANGED
@@ -2,49 +2,30 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { IDEMPOTENCY_KEY_CONSTRAINTS } from '@shipstatic/ship';
3
3
  import { z } from 'zod';
4
4
  import { call } from './call.js';
5
- import { ANNOTATIONS, PARAM_DESCRIPTIONS } from './vocabulary.js';
5
+ import { registerAccountTools } from './tools.js';
6
+ import { ANNOTATIONS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS } from './vocabulary.js';
6
7
  // Destructured so the fifteen registrations below read as they always have.
7
8
  // The definitions live in `vocabulary.ts` because the hosted transport speaks
8
9
  // the same ones — that file records what is shared, what is not, and why.
9
- const { READ, CREATE, WRITE, DESTRUCTIVE } = ANNOTATIONS;
10
- /**
11
- * The pagination surface, shared by every list tool because it is one
12
- * contract, not two. A list answers `{<collection>, cursor}` and nothing
13
- * else — `cursor` carries the whole has-more signal and is null on the last
14
- * page, so there is no `total` to ask for and no has-more boolean.
15
- *
16
- * No upper bound is stated here on purpose. The API clamps an unusable
17
- * `limit` server-side and owns that number; restating a cap in the tool
18
- * schema would give one fact two owners and let them drift. `min(1)` is not
19
- * a cap — it rejects a value that could never mean anything.
20
- */
21
- const PAGINATION_INPUT = {
22
- limit: z
23
- .number()
24
- .int()
25
- .min(1)
26
- .optional()
27
- .describe('Maximum number of items to return in one page. Omit for the server default.'),
28
- cursor: z
29
- .string()
30
- .optional()
31
- .describe("Opaque position from the previous response's `cursor` field; omit for the first page."),
32
- };
33
- /** Appended to every list tool's description — the paging contract, stated once. */
34
- const PAGING_NOTE = " The response's `cursor` is null on the last page; pass it back as `cursor` to fetch the next.";
35
- const INSTRUCTIONS = `ShipStatic deploys static websites instantly. Free, no account required.
10
+ const { CREATE } = ANNOTATIONS;
11
+ const B = INSTRUCTION_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}
36
17
 
37
- 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}
38
19
 
39
- 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 3 days. ${B.claim}
40
21
 
41
22
  With SHIP_TOKEN configured, deployments go to the user's account and never expire. Listing, managing, and domain operations also require SHIP_TOKEN.
42
23
 
43
- Concepts:
44
- - Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.
45
- - 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}
46
27
 
47
- To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.`;
28
+ ${B.domainWorkflow}`;
48
29
  /**
49
30
  * Builds the stdio server's full 15-tool surface over an injected client.
50
31
  *
@@ -76,127 +57,9 @@ export function createServer(ship, version) {
76
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.`),
77
58
  },
78
59
  }, ({ path, labels, password, idempotencyKey }) => call(() => ship.deployments.upload(path, { labels, password, idempotencyKey, via: 'mcp' })));
79
- server.registerTool('deployments_list', {
80
- description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
81
- annotations: READ,
82
- inputSchema: PAGINATION_INPUT,
83
- }, ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
84
- server.registerTool('deployments_get', {
85
- description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
86
- annotations: READ,
87
- inputSchema: {
88
- deployment: z
89
- .string()
90
- .describe('Deployment hostname (e.g. "happy-cat-abc1234.shipstatic.com"). Returned by deployments_upload or deployments_list.'),
91
- },
92
- }, ({ deployment }) => call(() => ship.deployments.get(deployment)));
93
- server.registerTool('deployments_set', {
94
- description: 'Update deployment labels. Replaces all existing labels.',
95
- annotations: WRITE,
96
- inputSchema: {
97
- deployment: z
98
- .string()
99
- .describe('Deployment hostname (e.g. "happy-cat-abc1234.shipstatic.com"). Use deployments_list to find deployments.'),
100
- labels: z
101
- .array(z.string())
102
- .describe('Labels to set. Replaces all existing labels. Pass empty array to clear.'),
103
- },
104
- }, ({ deployment, labels }) => call(() => ship.deployments.set(deployment, { labels })));
105
- server.registerTool('deployments_delete', {
106
- description: 'Permanently delete a deployment and its files. You MUST confirm with the user before calling this tool, referencing the deployment.',
107
- annotations: DESTRUCTIVE,
108
- inputSchema: {
109
- deployment: z
110
- .string()
111
- .describe('Deployment hostname to delete (e.g. "happy-cat-abc1234.shipstatic.com")'),
112
- },
113
- }, ({ deployment }) => call(() => ship.deployments.delete(deployment)));
114
- // Domains
115
- server.registerTool('domains_set', {
116
- description: 'Create or update a custom domain. Can reserve a name (omit deployment), link it to a deployment, switch deployments, or update labels. After creating, call domains_records and show the DNS records to the user.',
117
- annotations: WRITE,
118
- inputSchema: {
119
- domain: z.string().describe('Domain name (e.g. "www.example.com" or "blog.example.com")'),
120
- deployment: z
121
- .string()
122
- .optional()
123
- .describe('Deployment to serve on this domain (e.g. "happy-cat-abc1234.shipstatic.com"). Omit to reserve the domain without linking.'),
124
- labels: z
125
- .array(z.string())
126
- .optional()
127
- .describe('Labels for organizing domains (e.g. ["production"]).'),
128
- },
129
- }, ({ domain, deployment, labels }) => call(() => ship.domains.set(domain, { deployment, labels })));
130
- server.registerTool('domains_list', {
131
- description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
132
- annotations: READ,
133
- inputSchema: PAGINATION_INPUT,
134
- }, ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
135
- server.registerTool('domains_get', {
136
- description: 'Get domain details including URL, linked deployment, verification status, and labels.',
137
- annotations: READ,
138
- inputSchema: {
139
- domain: z
140
- .string()
141
- .describe('Domain name (e.g. "www.example.com"). Use domains_list to find names.'),
142
- },
143
- }, ({ domain }) => call(() => ship.domains.get(domain)));
144
- server.registerTool('domains_records', {
145
- description: 'Get the DNS records the user needs to configure at their DNS provider. Call after domains_set. You MUST show the returned records to the user.',
146
- annotations: READ,
147
- inputSchema: {
148
- domain: z
149
- .string()
150
- .describe('Domain name. Must be a domain previously created with domains_set.'),
151
- },
152
- }, ({ domain }) => call(() => ship.domains.records(domain)));
153
- server.registerTool('domains_dns', {
154
- description: 'Look up the DNS provider for a domain (e.g. Cloudflare, Namecheap). Helps the user know where to configure their DNS records.',
155
- annotations: READ,
156
- inputSchema: {
157
- domain: z
158
- .string()
159
- .describe('Domain name to look up DNS provider for (e.g. "www.example.com")'),
160
- },
161
- }, ({ domain }) => call(() => ship.domains.dns(domain)));
162
- server.registerTool('domains_share', {
163
- description: 'Get a shareable DNS setup hash for a domain. The hash can be shared with the user so they can view the required DNS records without needing an API key.',
164
- annotations: READ,
165
- inputSchema: {
166
- domain: z
167
- .string()
168
- .describe('Domain name to generate a share link for. Must be a domain previously created with domains_set.'),
169
- },
170
- }, ({ domain }) => call(() => ship.domains.share(domain)));
171
- server.registerTool('domains_validate', {
172
- description: 'Check if a domain name is valid and available before creating it. Returns the normalized form and availability.',
173
- annotations: READ,
174
- inputSchema: {
175
- domain: z
176
- .string()
177
- .describe('Domain name to check (e.g. "www.example.com"). Call before domains_set to check availability.'),
178
- },
179
- }, ({ domain }) => call(() => ship.domains.validate(domain)));
180
- server.registerTool('domains_verify', {
181
- description: 'Trigger DNS verification for a custom domain. Call after the user has configured DNS records from domains_records. Verification is asynchronous — the domain status updates once DNS propagates.',
182
- annotations: WRITE,
183
- inputSchema: {
184
- domain: z
185
- .string()
186
- .describe('Domain name to verify DNS for. Must be a domain previously created with domains_set.'),
187
- },
188
- }, ({ domain }) => call(() => ship.domains.verify(domain)));
189
- server.registerTool('domains_delete', {
190
- description: 'Permanently delete a domain. You MUST confirm with the user before calling this tool, referencing the domain name.',
191
- annotations: DESTRUCTIVE,
192
- inputSchema: {
193
- domain: z.string().describe('Domain name to delete (e.g. "www.example.com")'),
194
- },
195
- }, ({ domain }) => call(() => ship.domains.delete(domain)));
196
- // Debugging
197
- server.registerTool('whoami', {
198
- description: 'Show authenticated account details including email, plan, and usage.',
199
- annotations: READ,
200
- }, () => call(() => ship.whoami()));
60
+ // The other fourteen. Identical on every transport, so they live in the
61
+ // shared package rather than here see tools.ts for why upload is not
62
+ // among them.
63
+ registerAccountTools(server, ship, call);
201
64
  return server;
202
65
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The account-tied toolset — fourteen tools, identical on every transport.
3
+ *
4
+ * `deployments_upload` is not here, and the split is exactly the product's
5
+ * own shape rather than a convenience:
6
+ *
7
+ * - **Upload is the anonymous door.** It is the one operation that works
8
+ * with no account, and it is the one whose INPUT differs by transport —
9
+ * a filesystem path over stdio, inline bytes over HTTP, because a Worker
10
+ * has no filesystem. It also carries the Apps-SDK widget hosted-side.
11
+ * So it is authored per transport, in each `server.ts`.
12
+ * - **Everything else needs an identity**, and once a transport has one,
13
+ * nothing about these fourteen depends on how the bytes arrived. Same
14
+ * names, same schemas, same prose, same 1:1 SDK calls.
15
+ *
16
+ * That is why they live in the shared package: when the hosted transport
17
+ * gains OAuth it registers this function and has the complete toolset, rather
18
+ * than someone copying fourteen definitions into a second repo — which is the
19
+ * moment the two surfaces would begin to drift. The cost of doing it after
20
+ * the copy is a de-duplication under deadline; the cost of doing it before is
21
+ * this file.
22
+ *
23
+ * **The catalogue is static; identity decides what SUCCEEDS.** These are
24
+ * registered whether or not a credential is present — an anonymous caller
25
+ * sees them and gets a typed authentication error naming how to authenticate
26
+ * on *this* transport (the hint is `createCall`'s one per-transport argument).
27
+ * A tool list that changes shape under the caller would be a second, dynamic
28
+ * contract for an agent to track, and MCP clients cache the catalogue.
29
+ */
30
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
31
+ import type Ship from '@shipstatic/ship';
32
+ import type { CallFn } from './call.js';
33
+ export declare function registerAccountTools(server: McpServer, ship: Ship, call: CallFn): void;
package/dist/tools.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * The account-tied toolset — fourteen tools, identical on every transport.
3
+ *
4
+ * `deployments_upload` is not here, and the split is exactly the product's
5
+ * own shape rather than a convenience:
6
+ *
7
+ * - **Upload is the anonymous door.** It is the one operation that works
8
+ * with no account, and it is the one whose INPUT differs by transport —
9
+ * a filesystem path over stdio, inline bytes over HTTP, because a Worker
10
+ * has no filesystem. It also carries the Apps-SDK widget hosted-side.
11
+ * So it is authored per transport, in each `server.ts`.
12
+ * - **Everything else needs an identity**, and once a transport has one,
13
+ * nothing about these fourteen depends on how the bytes arrived. Same
14
+ * names, same schemas, same prose, same 1:1 SDK calls.
15
+ *
16
+ * That is why they live in the shared package: when the hosted transport
17
+ * gains OAuth it registers this function and has the complete toolset, rather
18
+ * than someone copying fourteen definitions into a second repo — which is the
19
+ * moment the two surfaces would begin to drift. The cost of doing it after
20
+ * the copy is a de-duplication under deadline; the cost of doing it before is
21
+ * this file.
22
+ *
23
+ * **The catalogue is static; identity decides what SUCCEEDS.** These are
24
+ * registered whether or not a credential is present — an anonymous caller
25
+ * sees them and gets a typed authentication error naming how to authenticate
26
+ * on *this* transport (the hint is `createCall`'s one per-transport argument).
27
+ * A tool list that changes shape under the caller would be a second, dynamic
28
+ * contract for an agent to track, and MCP clients cache the catalogue.
29
+ */
30
+ import { z } from 'zod';
31
+ import { ANNOTATIONS } from './vocabulary.js';
32
+ const { READ, WRITE, DESTRUCTIVE } = ANNOTATIONS;
33
+ /**
34
+ * The pagination surface, shared by every list tool because it is one
35
+ * contract, not two. A list answers `{<collection>, cursor}` and nothing
36
+ * else — `cursor` carries the whole has-more signal and is null on the last
37
+ * page, so there is no `total` to ask for and no has-more boolean.
38
+ *
39
+ * No upper bound is stated here on purpose. The API clamps an unusable
40
+ * `limit` server-side and owns that number; restating a cap in the tool
41
+ * schema would give one fact two owners and let them drift. `min(1)` is not
42
+ * a cap — it rejects a value that could never mean anything.
43
+ */
44
+ const PAGINATION_INPUT = {
45
+ limit: z
46
+ .number()
47
+ .int()
48
+ .min(1)
49
+ .optional()
50
+ .describe('Maximum number of items to return in one page. Omit for the server default.'),
51
+ cursor: z
52
+ .string()
53
+ .optional()
54
+ .describe("Opaque position from the previous response's `cursor` field; omit for the first page."),
55
+ };
56
+ /** Appended to every list tool's description — the paging contract, stated once. */
57
+ const PAGING_NOTE = " The response's `cursor` is null on the last page; pass it back as `cursor` to fetch the next.";
58
+ /** The deployment argument, described identically wherever it is accepted. */
59
+ const DEPLOYMENT_EXAMPLE = 'happy-cat-abc1234.shipstatic.com';
60
+ export function registerAccountTools(server, ship, call) {
61
+ // Deployments
62
+ server.registerTool('deployments_list', {
63
+ description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
64
+ annotations: READ,
65
+ inputSchema: PAGINATION_INPUT,
66
+ }, ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
67
+ server.registerTool('deployments_get', {
68
+ description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
69
+ annotations: READ,
70
+ inputSchema: {
71
+ deployment: z
72
+ .string()
73
+ .describe(`Deployment hostname (e.g. "${DEPLOYMENT_EXAMPLE}"). Returned by deployments_upload or deployments_list.`),
74
+ },
75
+ }, ({ deployment }) => call(() => ship.deployments.get(deployment)));
76
+ server.registerTool('deployments_set', {
77
+ description: 'Update deployment labels. Replaces all existing labels.',
78
+ annotations: WRITE,
79
+ inputSchema: {
80
+ deployment: z
81
+ .string()
82
+ .describe(`Deployment hostname (e.g. "${DEPLOYMENT_EXAMPLE}"). Use deployments_list to find deployments.`),
83
+ labels: z
84
+ .array(z.string())
85
+ .describe('Labels to set. Replaces all existing labels. Pass empty array to clear.'),
86
+ },
87
+ }, ({ deployment, labels }) => call(() => ship.deployments.set(deployment, { labels })));
88
+ server.registerTool('deployments_delete', {
89
+ description: 'Permanently delete a deployment and its files. You MUST confirm with the user before calling this tool, referencing the deployment.',
90
+ annotations: DESTRUCTIVE,
91
+ inputSchema: {
92
+ deployment: z
93
+ .string()
94
+ .describe(`Deployment hostname to delete (e.g. "${DEPLOYMENT_EXAMPLE}")`),
95
+ },
96
+ }, ({ deployment }) => call(() => ship.deployments.delete(deployment)));
97
+ // Domains
98
+ server.registerTool('domains_set', {
99
+ description: 'Create or update a custom domain. Can reserve a name (omit deployment), link it to a deployment, switch deployments, or update labels. After creating, call domains_records and show the DNS records to the user.',
100
+ annotations: WRITE,
101
+ inputSchema: {
102
+ domain: z.string().describe('Domain name (e.g. "www.example.com" or "blog.example.com")'),
103
+ deployment: z
104
+ .string()
105
+ .optional()
106
+ .describe(`Deployment to serve on this domain (e.g. "${DEPLOYMENT_EXAMPLE}"). Omit to reserve the domain without linking.`),
107
+ labels: z
108
+ .array(z.string())
109
+ .optional()
110
+ .describe('Labels for organizing domains (e.g. ["production"]).'),
111
+ },
112
+ }, ({ domain, deployment, labels }) => call(() => ship.domains.set(domain, { deployment, labels })));
113
+ server.registerTool('domains_list', {
114
+ description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
115
+ annotations: READ,
116
+ inputSchema: PAGINATION_INPUT,
117
+ }, ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
118
+ server.registerTool('domains_get', {
119
+ description: 'Get domain details including URL, linked deployment, verification status, and labels.',
120
+ annotations: READ,
121
+ inputSchema: {
122
+ domain: z
123
+ .string()
124
+ .describe('Domain name (e.g. "www.example.com"). Use domains_list to find names.'),
125
+ },
126
+ }, ({ domain }) => call(() => ship.domains.get(domain)));
127
+ server.registerTool('domains_records', {
128
+ description: 'Get the DNS records the user needs to configure at their DNS provider. Call after domains_set. You MUST show the returned records to the user.',
129
+ annotations: READ,
130
+ inputSchema: {
131
+ domain: z
132
+ .string()
133
+ .describe('Domain name. Must be a domain previously created with domains_set.'),
134
+ },
135
+ }, ({ domain }) => call(() => ship.domains.records(domain)));
136
+ server.registerTool('domains_dns', {
137
+ description: 'Look up the DNS provider for a domain (e.g. Cloudflare, Namecheap). Helps the user know where to configure their DNS records.',
138
+ annotations: READ,
139
+ inputSchema: {
140
+ domain: z
141
+ .string()
142
+ .describe('Domain name to look up DNS provider for (e.g. "www.example.com")'),
143
+ },
144
+ }, ({ domain }) => call(() => ship.domains.dns(domain)));
145
+ server.registerTool('domains_share', {
146
+ description: 'Get a shareable DNS setup hash for a domain. The hash can be shared with the user so they can view the required DNS records without needing an API key.',
147
+ annotations: READ,
148
+ inputSchema: {
149
+ domain: z
150
+ .string()
151
+ .describe('Domain name to generate a share link for. Must be a domain previously created with domains_set.'),
152
+ },
153
+ }, ({ domain }) => call(() => ship.domains.share(domain)));
154
+ server.registerTool('domains_validate', {
155
+ description: 'Check if a domain name is valid and available before creating it. Returns the normalized form and availability.',
156
+ annotations: READ,
157
+ inputSchema: {
158
+ domain: z
159
+ .string()
160
+ .describe('Domain name to check (e.g. "www.example.com"). Call before domains_set to check availability.'),
161
+ },
162
+ }, ({ domain }) => call(() => ship.domains.validate(domain)));
163
+ server.registerTool('domains_verify', {
164
+ description: 'Trigger DNS verification for a custom domain. Call after the user has configured DNS records from domains_records. Verification is asynchronous — the domain status updates once DNS propagates.',
165
+ annotations: WRITE,
166
+ inputSchema: {
167
+ domain: z
168
+ .string()
169
+ .describe('Domain name to verify DNS for. Must be a domain previously created with domains_set.'),
170
+ },
171
+ }, ({ domain }) => call(() => ship.domains.verify(domain)));
172
+ server.registerTool('domains_delete', {
173
+ description: 'Permanently delete a domain. You MUST confirm with the user before calling this tool, referencing the domain name.',
174
+ annotations: DESTRUCTIVE,
175
+ inputSchema: {
176
+ domain: z.string().describe('Domain name to delete (e.g. "www.example.com")'),
177
+ },
178
+ }, ({ domain }) => call(() => ship.domains.delete(domain)));
179
+ // Account
180
+ server.registerTool('whoami', {
181
+ description: 'Show authenticated account details including email, plan, and usage.',
182
+ annotations: READ,
183
+ }, () => call(() => ship.whoami()));
184
+ }
@@ -73,6 +73,28 @@ export declare const ANNOTATIONS: {
73
73
  * without anyone editing prose — the same reason the API and the SDK import
74
74
  * them instead of restating them.
75
75
  */
76
+ /**
77
+ * INSTRUCTIONS sentences both transports say.
78
+ *
79
+ * `initialize`'s instructions are the other half of what an agent reads
80
+ * before acting (the catalogue is the first). Each transport composes its own
81
+ * from these blocks plus the two things that are genuinely its own: how files
82
+ * arrive, and how a caller authenticates.
83
+ *
84
+ * Three of these are duplicated prose TODAY, kept equal by review. The last
85
+ * two are stdio-only only because the hosted transport has no domain tools
86
+ * yet — when it gains them with OAuth they become shared too, which is
87
+ * precisely when someone would otherwise copy them across.
88
+ */
89
+ export declare const INSTRUCTION_BLOCKS: {
90
+ readonly opening: "ShipStatic deploys static websites instantly. Free, no account required.";
91
+ 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.";
92
+ 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.";
93
+ readonly conceptsHeader: "Concepts:";
94
+ readonly deploymentConcept: "- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.";
95
+ readonly domainConcept: "- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.";
96
+ readonly domainWorkflow: "To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.";
97
+ };
76
98
  export declare const PARAM_DESCRIPTIONS: {
77
99
  readonly labels: "Labels for organizing deployments (e.g. [\"production\", \"v1.2\"]). Lowercase, 3-25 chars, allows . _ - separators. Up to 10.";
78
100
  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.";
@@ -56,6 +56,28 @@ export const ANNOTATIONS = {
56
56
  * without anyone editing prose — the same reason the API and the SDK import
57
57
  * them instead of restating them.
58
58
  */
59
+ /**
60
+ * INSTRUCTIONS sentences both transports say.
61
+ *
62
+ * `initialize`'s instructions are the other half of what an agent reads
63
+ * before acting (the catalogue is the first). Each transport composes its own
64
+ * from these blocks plus the two things that are genuinely its own: how files
65
+ * arrive, and how a caller authenticates.
66
+ *
67
+ * Three of these are duplicated prose TODAY, kept equal by review. The last
68
+ * two are stdio-only only because the hosted transport has no domain tools
69
+ * yet — when it gains them with OAuth they become shared too, which is
70
+ * precisely when someone would otherwise copy them across.
71
+ */
72
+ export const INSTRUCTION_BLOCKS = {
73
+ opening: 'ShipStatic deploys static websites instantly. Free, no account required.',
74
+ 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.',
75
+ 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.',
76
+ conceptsHeader: 'Concepts:',
77
+ deploymentConcept: '- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.',
78
+ domainConcept: '- Domain: a custom domain (e.g. www.example.com) pointing to a deployment. Optional. Subdomains only — not apex domains.',
79
+ domainWorkflow: 'To add a custom domain: domains_validate → domains_set → domains_records (show DNS records to user) → user configures DNS → domains_verify.',
80
+ };
59
81
  export const PARAM_DESCRIPTIONS = {
60
82
  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
83
  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.`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.4",
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",