@shipstatic/mcp 1.10.2 → 1.11.0-beta.1

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
@@ -139,7 +139,7 @@ All fifteen tools are on both doors. What changes is not which tools exist, but
139
139
  | `domains_list` | List all domains with their linked deployment and verification status. Pages with `limit` and `cursor` |
140
140
  | `domains_get` | Get domain details including linked deployment, verification status, and labels |
141
141
  | `domains_records` | Get the DNS records you need to configure at your DNS provider |
142
- | `domains_dns` | Look up which DNS provider hosts a domain (e.g. Cloudflare, Namecheap) |
142
+ | `domains_dns` | The DNS provider recorded for a domain, if known (e.g. Cloudflare, Namecheap) |
143
143
  | `domains_share` | Get a shareable link so someone else can see the required DNS records |
144
144
  | `domains_validate` | Check if a domain name is valid and available before connecting it |
145
145
  | `domains_verify` | Check if DNS is configured correctly after you set up the records |
@@ -149,7 +149,7 @@ All fifteen tools are on both doors. What changes is not which tools exist, but
149
149
 
150
150
  | Tool | Description |
151
151
  |------|-------------|
152
- | `whoami` | Get your account details including email, plan, and usage |
152
+ | `whoami` | Your account's email, name, plan, current usage and plan caps |
153
153
 
154
154
  ### Paging long lists
155
155
 
package/dist/index.d.ts CHANGED
@@ -26,8 +26,10 @@
26
26
  * was the same duration written out eight times, `DESCRIPTION_BLOCKS` the
27
27
  * fragments two tool descriptions genuinely share, `ACCOUNT_TOOL_NAMES` is
28
28
  * what lets the hosted catalogue fence name the fourteen without counting them
29
- * again, and `createServer` deleted three regexes in another repo's build (see
30
- * below).
29
+ * again, `TOOLS` and `annotate` replaced four annotation classes plus a
30
+ * hand-annotated upload on each transport plus a separate list for the auth
31
+ * need (three owners for one tool's facts, 1.11.0), and `createServer`
32
+ * deleted three regexes in another repo's build (see below).
31
33
  *
32
34
  * **`createServer` is exported; the configured `call` is not.** The extension
33
35
  * previously reached stdio's composition by REGEX-PATCHING this package's
@@ -62,5 +64,5 @@
62
64
  */
63
65
  export { type AuthFailure, type CallFn, type CallOptions, createCall, type ErrorHints, } from './call.js';
64
66
  export { createServer } from './server.js';
65
- export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
66
- export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
67
+ export { ACCOUNT_TOOL_NAMES, type AccountToolName, registerAccountTools, TOOLS, type ToolName, } from './tools.js';
68
+ export { annotate, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, type ToolContract, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
package/dist/index.js CHANGED
@@ -26,8 +26,10 @@
26
26
  * was the same duration written out eight times, `DESCRIPTION_BLOCKS` the
27
27
  * fragments two tool descriptions genuinely share, `ACCOUNT_TOOL_NAMES` is
28
28
  * what lets the hosted catalogue fence name the fourteen without counting them
29
- * again, and `createServer` deleted three regexes in another repo's build (see
30
- * below).
29
+ * again, `TOOLS` and `annotate` replaced four annotation classes plus a
30
+ * hand-annotated upload on each transport plus a separate list for the auth
31
+ * need (three owners for one tool's facts, 1.11.0), and `createServer`
32
+ * deleted three regexes in another repo's build (see below).
31
33
  *
32
34
  * **`createServer` is exported; the configured `call` is not.** The extension
33
35
  * previously reached stdio's composition by REGEX-PATCHING this package's
@@ -62,5 +64,5 @@
62
64
  */
63
65
  export { createCall, } from './call.js';
64
66
  export { createServer } from './server.js';
65
- export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
66
- export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
67
+ export { ACCOUNT_TOOL_NAMES, registerAccountTools, TOOLS, } from './tools.js';
68
+ export { annotate, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
package/dist/server.js CHANGED
@@ -2,12 +2,8 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { DeploymentVia, MY_API_KEY_URL } from '@shipstatic/types';
3
3
  import { z } from 'zod';
4
4
  import { call } from './call.js';
5
- import { registerAccountTools } from './tools.js';
6
- import { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
7
- // Destructured so the fifteen registrations below read as they always have.
8
- // The definitions live in `vocabulary.ts` because the hosted transport speaks
9
- // the same ones — that file records what is shared, what is not, and why.
10
- const { CREATE } = ANNOTATIONS;
5
+ import { registerAccountTools, TOOLS } from './tools.js';
6
+ import { annotate, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
11
7
  const B = INSTRUCTION_BLOCKS;
12
8
  const D = DESCRIPTION_BLOCKS;
13
9
  // Composed from the shared blocks plus the two sentences that are genuinely
@@ -41,8 +37,12 @@ export function createServer(ship, options) {
41
37
  // Deployments
42
38
  server.registerTool(UPLOAD_TOOL_NAME, titled({
43
39
  title: UPLOAD_TOOL_TITLE,
44
- 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}`,
45
- annotations: CREATE,
40
+ // Describes the tool and instructs nobody: what an agent should DO with
41
+ // the claim URL and a password is the INSTRUCTIONS' job, stated once
42
+ // above. The annotations come from the registry row this transport
43
+ // shares with the hosted one; only the input schema is this door's own.
44
+ description: `Deploy a static site instantly: ${D.free}. Returns the live URL, file count, and size. Without SHIP_TOKEN, the response also includes a one-time claim URL, and the site expires in ${PUBLIC_EXPIRY} unless claimed. ${D.password}`,
45
+ annotations: annotate(TOOLS[UPLOAD_TOOL_NAME]),
46
46
  inputSchema: {
47
47
  path: z
48
48
  .string()
package/dist/tools.d.ts CHANGED
@@ -40,23 +40,118 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
40
40
  import type Ship from '@shipstatic/ship';
41
41
  import type { CallFn } from './call.js';
42
42
  /**
43
- * The fourteen, by name, in registration order.
43
+ * THE REGISTRY: one row per tool, every fact about it that a host reads.
44
+ *
45
+ * Fifteen rows, in registration order, `deployments_upload` included even
46
+ * though each transport authors that registration itself: its INPUT differs
47
+ * per transport, its contract does not. Everything else is derived from
48
+ * here. `annotate(row)` is every registration's `annotations`;
49
+ * `ACCOUNT_TOOL_NAMES` is the rows whose `auth` is `required`, and the
50
+ * hosted door reads the same column for `securitySchemes`; the listing
51
+ * repo's justifications are held to the hints these rows produce.
52
+ *
53
+ * The rows are DATA the registrations read, not a table they are generated
54
+ * from, and that is deliberate: a `Record<name, factory>` would cost the
55
+ * zod→handler inference every one-liner below relies on (`({ deployment })
56
+ * => …` is typed from the `inputSchema` literal in the same call). A row
57
+ * without a registration, a registration without a row, and a typo in
58
+ * either all turn `tests/server.test.ts` red, through a real `tools/list`.
59
+ *
60
+ * Two rows are worth a second look, because their names suggest otherwise:
61
+ * `domains_dns` READS the provider the platform recorded when the domain
62
+ * was created (nothing is looked up on call), and `domains_validate` and
63
+ * `domains_share` persist nothing.
64
+ */
65
+ export declare const TOOLS: {
66
+ readonly deployments_upload: {
67
+ readonly auth: "optional";
68
+ readonly mutation: "add";
69
+ readonly reach: "public";
70
+ };
71
+ readonly deployments_list: {
72
+ readonly auth: "required";
73
+ readonly mutation: "none";
74
+ readonly reach: "account";
75
+ };
76
+ readonly deployments_get: {
77
+ readonly auth: "required";
78
+ readonly mutation: "none";
79
+ readonly reach: "account";
80
+ };
81
+ readonly deployments_set: {
82
+ readonly auth: "required";
83
+ readonly mutation: "replace";
84
+ readonly reach: "account";
85
+ };
86
+ readonly deployments_delete: {
87
+ readonly auth: "required";
88
+ readonly mutation: "remove";
89
+ readonly reach: "public";
90
+ };
91
+ readonly domains_set: {
92
+ readonly auth: "required";
93
+ readonly mutation: "replace";
94
+ readonly reach: "public";
95
+ };
96
+ readonly domains_list: {
97
+ readonly auth: "required";
98
+ readonly mutation: "none";
99
+ readonly reach: "account";
100
+ };
101
+ readonly domains_get: {
102
+ readonly auth: "required";
103
+ readonly mutation: "none";
104
+ readonly reach: "account";
105
+ };
106
+ readonly domains_records: {
107
+ readonly auth: "required";
108
+ readonly mutation: "none";
109
+ readonly reach: "account";
110
+ };
111
+ readonly domains_dns: {
112
+ readonly auth: "required";
113
+ readonly mutation: "none";
114
+ readonly reach: "account";
115
+ };
116
+ readonly domains_share: {
117
+ readonly auth: "required";
118
+ readonly mutation: "none";
119
+ readonly reach: "account";
120
+ };
121
+ readonly domains_validate: {
122
+ readonly auth: "required";
123
+ readonly mutation: "none";
124
+ readonly reach: "account";
125
+ };
126
+ readonly domains_verify: {
127
+ readonly auth: "required";
128
+ readonly mutation: "add";
129
+ readonly reach: "public";
130
+ };
131
+ readonly domains_delete: {
132
+ readonly auth: "required";
133
+ readonly mutation: "remove";
134
+ readonly reach: "public";
135
+ };
136
+ readonly whoami: {
137
+ readonly auth: "required";
138
+ readonly mutation: "none";
139
+ readonly reach: "account";
140
+ };
141
+ };
142
+ export type ToolName = keyof typeof TOOLS;
143
+ /** The names whose row says an account is required. */
144
+ export type AccountToolName = {
145
+ [N in ToolName]: (typeof TOOLS)[N]['auth'] extends 'required' ? N : never;
146
+ }[ToolName];
147
+ /**
148
+ * The fourteen, derived from the registry rather than listed beside it.
44
149
  *
45
150
  * Exported so a second transport can state its expected catalogue as
46
- * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` instead of listing fifteen
47
- * strings it would then have to keep in agreement with this file — the hosted
48
- * parity fence is the consumer, and "fifteen" is otherwise a number two repos
49
- * count separately.
50
- *
51
- * **Deliberately a list beside the registrations rather than a table they are
52
- * generated from.** A `Record<name, factory>` would make the pairing
53
- * structural, and it would cost the zod→handler inference every one of the
54
- * fourteen one-liners below relies on: `({ deployment }) => …` is typed today
55
- * from the `inputSchema` literal in the same call, and a loop over a
56
- * heterogeneous table cannot correlate the two. The same guarantee costs
57
- * nothing as a set comparison, and `tests/server.test.ts` makes it — through a
58
- * real `tools/list`, so a registration without a name, a name without a
59
- * registration, and a typo in either all turn it red.
151
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` instead of counting to fifteen
152
+ * in a second repo. It was a hand-written list until 1.11.0, which made the
153
+ * auth need a second owner beside the annotations; a row is now the only
154
+ * place a tool's account requirement is stated.
60
155
  */
61
- 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"];
156
+ export declare const ACCOUNT_TOOL_NAMES: AccountToolName[];
62
157
  export declare function registerAccountTools(server: McpServer, ship: Ship, call: CallFn): void;
package/dist/tools.js CHANGED
@@ -37,43 +37,70 @@
37
37
  * every tool, so the next one cannot be added without one.
38
38
  */
39
39
  import { z } from 'zod';
40
- import { ANNOTATIONS, titled } from './vocabulary.js';
41
- const { READ, WRITE, DESTRUCTIVE } = ANNOTATIONS;
40
+ import { annotate, titled, UPLOAD_TOOL_NAME } from './vocabulary.js';
42
41
  /**
43
- * The fourteen, by name, in registration order.
42
+ * THE REGISTRY: one row per tool, every fact about it that a host reads.
43
+ *
44
+ * Fifteen rows, in registration order, `deployments_upload` included even
45
+ * though each transport authors that registration itself: its INPUT differs
46
+ * per transport, its contract does not. Everything else is derived from
47
+ * here. `annotate(row)` is every registration's `annotations`;
48
+ * `ACCOUNT_TOOL_NAMES` is the rows whose `auth` is `required`, and the
49
+ * hosted door reads the same column for `securitySchemes`; the listing
50
+ * repo's justifications are held to the hints these rows produce.
51
+ *
52
+ * The rows are DATA the registrations read, not a table they are generated
53
+ * from, and that is deliberate: a `Record<name, factory>` would cost the
54
+ * zod→handler inference every one-liner below relies on (`({ deployment })
55
+ * => …` is typed from the `inputSchema` literal in the same call). A row
56
+ * without a registration, a registration without a row, and a typo in
57
+ * either all turn `tests/server.test.ts` red, through a real `tools/list`.
58
+ *
59
+ * Two rows are worth a second look, because their names suggest otherwise:
60
+ * `domains_dns` READS the provider the platform recorded when the domain
61
+ * was created (nothing is looked up on call), and `domains_validate` and
62
+ * `domains_share` persist nothing.
63
+ */
64
+ export const TOOLS = {
65
+ [UPLOAD_TOOL_NAME]: { auth: 'optional', mutation: 'add', reach: 'public' },
66
+ deployments_list: { auth: 'required', mutation: 'none', reach: 'account' },
67
+ deployments_get: { auth: 'required', mutation: 'none', reach: 'account' },
68
+ deployments_set: { auth: 'required', mutation: 'replace', reach: 'account' },
69
+ deployments_delete: { auth: 'required', mutation: 'remove', reach: 'public' },
70
+ domains_set: { auth: 'required', mutation: 'replace', reach: 'public' },
71
+ domains_list: { auth: 'required', mutation: 'none', reach: 'account' },
72
+ domains_get: { auth: 'required', mutation: 'none', reach: 'account' },
73
+ domains_records: { auth: 'required', mutation: 'none', reach: 'account' },
74
+ domains_dns: { auth: 'required', mutation: 'none', reach: 'account' },
75
+ domains_share: { auth: 'required', mutation: 'none', reach: 'account' },
76
+ domains_validate: { auth: 'required', mutation: 'none', reach: 'account' },
77
+ domains_verify: { auth: 'required', mutation: 'add', reach: 'public' },
78
+ domains_delete: { auth: 'required', mutation: 'remove', reach: 'public' },
79
+ whoami: { auth: 'required', mutation: 'none', reach: 'account' },
80
+ };
81
+ /**
82
+ * The fourteen, derived from the registry rather than listed beside it.
44
83
  *
45
84
  * Exported so a second transport can state its expected catalogue as
46
- * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` instead of listing fifteen
47
- * strings it would then have to keep in agreement with this file — the hosted
48
- * parity fence is the consumer, and "fifteen" is otherwise a number two repos
49
- * count separately.
85
+ * `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` instead of counting to fifteen
86
+ * in a second repo. It was a hand-written list until 1.11.0, which made the
87
+ * auth need a second owner beside the annotations; a row is now the only
88
+ * place a tool's account requirement is stated.
89
+ */
90
+ export const ACCOUNT_TOOL_NAMES = Object.keys(TOOLS).filter((name) => TOOLS[name].auth === 'required');
91
+ /**
92
+ * What `whoami` answers: exactly the keys its description names.
50
93
  *
51
- * **Deliberately a list beside the registrations rather than a table they are
52
- * generated from.** A `Record<name, factory>` would make the pairing
53
- * structural, and it would cost the zod→handler inference every one of the
54
- * fourteen one-liners below relies on: `({ deployment }) => …` is typed today
55
- * from the `inputSchema` literal in the same call, and a loop over a
56
- * heterogeneous table cannot correlate the two. The same guarantee costs
57
- * nothing as a set comparison, and `tests/server.test.ts` makes it — through a
58
- * real `tools/list`, so a registration without a name, a name without a
59
- * registration, and a typo in either all turn it red.
94
+ * `Account` also carries billing state, the API-key hint, the picture and
95
+ * timestamps, none of which the description mentions and none of which an
96
+ * agent acts on. A tool's result is the shape its description states; this
97
+ * is the one tool where the wire's own entity said more than the sentence.
98
+ * `suspended` is deliberately out: it means every write is refused, and the
99
+ * refusal says so itself at the moment it matters.
60
100
  */
61
- export const ACCOUNT_TOOL_NAMES = [
62
- 'deployments_list',
63
- 'deployments_get',
64
- 'deployments_set',
65
- 'deployments_delete',
66
- 'domains_set',
67
- 'domains_list',
68
- 'domains_get',
69
- 'domains_records',
70
- 'domains_dns',
71
- 'domains_share',
72
- 'domains_validate',
73
- 'domains_verify',
74
- 'domains_delete',
75
- 'whoami',
76
- ];
101
+ function accountSummary({ email, name, plan, usage, caps }) {
102
+ return { email, name, plan, usage, caps };
103
+ }
77
104
  /**
78
105
  * The pagination surface, shared by every list tool because it is one
79
106
  * contract, not two. A list answers `{<collection>, cursor}` and nothing
@@ -106,13 +133,13 @@ export function registerAccountTools(server, ship, call) {
106
133
  server.registerTool('deployments_list', titled({
107
134
  title: 'List Deployments',
108
135
  description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
109
- annotations: READ,
136
+ annotations: annotate(TOOLS.deployments_list),
110
137
  inputSchema: PAGINATION_INPUT,
111
138
  }), ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
112
139
  server.registerTool('deployments_get', titled({
113
140
  title: 'Get Deployment',
114
141
  description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
115
- annotations: READ,
142
+ annotations: annotate(TOOLS.deployments_get),
116
143
  inputSchema: {
117
144
  deployment: z
118
145
  .string()
@@ -122,7 +149,7 @@ export function registerAccountTools(server, ship, call) {
122
149
  server.registerTool('deployments_set', titled({
123
150
  title: 'Update Deployment Labels',
124
151
  description: 'Update deployment labels. Replaces all existing labels.',
125
- annotations: WRITE,
152
+ annotations: annotate(TOOLS.deployments_set),
126
153
  inputSchema: {
127
154
  deployment: z
128
155
  .string()
@@ -134,8 +161,8 @@ export function registerAccountTools(server, ship, call) {
134
161
  }), ({ deployment, labels }) => call(() => ship.deployments.set(deployment, { labels })));
135
162
  server.registerTool('deployments_delete', titled({
136
163
  title: 'Delete Deployment',
137
- description: 'Permanently delete a deployment and its files. You MUST confirm with the user before calling this tool, referencing the deployment.',
138
- annotations: DESTRUCTIVE,
164
+ description: 'Permanently deletes a deployment and its files.',
165
+ annotations: annotate(TOOLS.deployments_delete),
139
166
  inputSchema: {
140
167
  deployment: z
141
168
  .string()
@@ -145,8 +172,8 @@ export function registerAccountTools(server, ship, call) {
145
172
  // Domains
146
173
  server.registerTool('domains_set', titled({
147
174
  title: 'Connect Custom Domain',
148
- 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.',
149
- annotations: WRITE,
175
+ description: 'Create or update a custom domain. Can reserve a name (omit deployment), link it to a deployment, switch deployments, or update labels. domains_records then returns the DNS records to configure.',
176
+ annotations: annotate(TOOLS.domains_set),
150
177
  inputSchema: {
151
178
  domain: z.string().describe('Domain name (e.g. "www.example.com" or "blog.example.com")'),
152
179
  deployment: z
@@ -162,13 +189,13 @@ export function registerAccountTools(server, ship, call) {
162
189
  server.registerTool('domains_list', titled({
163
190
  title: 'List Domains',
164
191
  description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
165
- annotations: READ,
192
+ annotations: annotate(TOOLS.domains_list),
166
193
  inputSchema: PAGINATION_INPUT,
167
194
  }), ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
168
195
  server.registerTool('domains_get', titled({
169
196
  title: 'Get Domain',
170
197
  description: 'Get domain details including URL, linked deployment, verification status, and labels.',
171
- annotations: READ,
198
+ annotations: annotate(TOOLS.domains_get),
172
199
  inputSchema: {
173
200
  domain: z
174
201
  .string()
@@ -177,8 +204,8 @@ export function registerAccountTools(server, ship, call) {
177
204
  }), ({ domain }) => call(() => ship.domains.get(domain)));
178
205
  server.registerTool('domains_records', titled({
179
206
  title: 'Get DNS Records',
180
- 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.',
181
- annotations: READ,
207
+ description: "Returns the DNS records to configure at the domain's DNS provider. Call after domains_set.",
208
+ annotations: annotate(TOOLS.domains_records),
182
209
  inputSchema: {
183
210
  domain: z
184
211
  .string()
@@ -186,19 +213,19 @@ export function registerAccountTools(server, ship, call) {
186
213
  },
187
214
  }), ({ domain }) => call(() => ship.domains.records(domain)));
188
215
  server.registerTool('domains_dns', titled({
189
- title: 'Look Up DNS Provider',
190
- description: 'Look up the DNS provider for a domain (e.g. Cloudflare, Namecheap). Helps the user know where to configure their DNS records.',
191
- annotations: READ,
216
+ title: 'Get DNS Provider',
217
+ description: 'Returns the DNS provider recorded for the domain, if known (e.g. Cloudflare, Namecheap): where its DNS records are configured.',
218
+ annotations: annotate(TOOLS.domains_dns),
192
219
  inputSchema: {
193
220
  domain: z
194
221
  .string()
195
- .describe('Domain name to look up DNS provider for (e.g. "www.example.com")'),
222
+ .describe('Domain name (e.g. "www.example.com"). Must be a domain previously created with domains_set.'),
196
223
  },
197
224
  }), ({ domain }) => call(() => ship.domains.dns(domain)));
198
225
  server.registerTool('domains_share', titled({
199
226
  title: 'Share DNS Setup',
200
- description: 'Get a shareable DNS setup link for a domain. Share the link with the user so they, or whoever manages their DNS, can view the required records without needing an API key.',
201
- annotations: READ,
227
+ description: "Returns a shareable DNS setup URL that needs no API key, for whoever manages the domain's DNS.",
228
+ annotations: annotate(TOOLS.domains_share),
202
229
  inputSchema: {
203
230
  domain: z
204
231
  .string()
@@ -208,7 +235,7 @@ export function registerAccountTools(server, ship, call) {
208
235
  server.registerTool('domains_validate', titled({
209
236
  title: 'Check Domain Availability',
210
237
  description: 'Check if a domain name is valid and available before creating it. Returns the normalized form and availability.',
211
- annotations: READ,
238
+ annotations: annotate(TOOLS.domains_validate),
212
239
  inputSchema: {
213
240
  domain: z
214
241
  .string()
@@ -218,7 +245,7 @@ export function registerAccountTools(server, ship, call) {
218
245
  server.registerTool('domains_verify', titled({
219
246
  title: 'Verify Domain DNS',
220
247
  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.',
221
- annotations: WRITE,
248
+ annotations: annotate(TOOLS.domains_verify),
222
249
  inputSchema: {
223
250
  domain: z
224
251
  .string()
@@ -227,8 +254,8 @@ export function registerAccountTools(server, ship, call) {
227
254
  }), ({ domain }) => call(() => ship.domains.verify(domain)));
228
255
  server.registerTool('domains_delete', titled({
229
256
  title: 'Delete Domain',
230
- description: 'Permanently delete a domain. You MUST confirm with the user before calling this tool, referencing the domain name.',
231
- annotations: DESTRUCTIVE,
257
+ description: 'Permanently deletes a domain.',
258
+ annotations: annotate(TOOLS.domains_delete),
232
259
  inputSchema: {
233
260
  domain: z.string().describe('Domain name to delete (e.g. "www.example.com")'),
234
261
  },
@@ -236,7 +263,7 @@ export function registerAccountTools(server, ship, call) {
236
263
  // Account
237
264
  server.registerTool('whoami', titled({
238
265
  title: 'Show Account',
239
- description: 'Show authenticated account details including email, plan, and usage.',
240
- annotations: READ,
241
- }), () => call(() => ship.whoami()));
266
+ description: "Returns the account's email, name, plan, current usage and plan caps.",
267
+ annotations: annotate(TOOLS.whoami),
268
+ }), () => call(() => ship.whoami().then(accountSummary)));
242
269
  }
@@ -103,43 +103,53 @@ export declare const UPLOAD_TOOL_TITLE = "Deploy Static Site";
103
103
  */
104
104
  export declare const PUBLIC_EXPIRY: string;
105
105
  /**
106
- * MCP tool annotations by kind of operation. An agent reads these to decide
107
- * whether it may call speculatively (`readOnlyHint`), whether a retry is free
108
- * (`idempotentHint`), and whether it must confirm with the user first
109
- * (`destructiveHint`).
110
- *
111
- * **`CREATE` carries no `idempotentHint`, deliberately.** A deploy creates a
112
- * new deployment on every call. `idempotencyKey` makes a retry replay the
113
- * original instead — but that property is conditional on an argument the
114
- * caller may not pass, while the annotation is static per tool. Advertising it
115
- * would promise every agent that any retry is free, which is exactly false for
116
- * the keyless caller, and an annotation an agent trusts wrongly is worse than
117
- * one it never reads.
106
+ * What is true of one tool, on the three axes a host reads.
107
+ *
108
+ * One row per tool lives in the registry (`tools.ts`, `TOOLS`), and every
109
+ * other statement about the tool is derived from it: the MCP annotations
110
+ * (`annotate`), the account-tool set (and so the hosted door's
111
+ * `securitySchemes` and challenge scope), the listing justifications in
112
+ * `integrations/gpt`, and the catalogue tests. Until 1.11.0 these facts had
113
+ * three owners: four annotation classes here, a hand-written name list for
114
+ * the auth need, and the upload tool annotated by hand on both transports.
115
+ * Three owners for facts about one tool is how the classes came to spread
116
+ * `openWorldHint: true` over ten reads that touch nothing public.
117
+ */
118
+ export interface ToolContract {
119
+ /** `optional`: works without an account, does more with one. */
120
+ auth: 'optional' | 'required';
121
+ /**
122
+ * What a call does to the platform's state. `add` creates something new
123
+ * on every call; `replace` overwrites what is there; `remove` deletes it.
124
+ */
125
+ mutation: 'none' | 'add' | 'replace' | 'remove';
126
+ /** Whether the call changes public internet state, or only the account's. */
127
+ reach: 'account' | 'public';
128
+ }
129
+ /**
130
+ * The MCP annotations a tool's contract implies. An agent reads them to
131
+ * decide whether it may call speculatively (`readOnlyHint`), whether it must
132
+ * confirm with the user first (`destructiveHint`), whether a retry is free
133
+ * (`idempotentHint`), and whether the call reaches beyond the account
134
+ * (`openWorldHint`). Both hosts read the same four: Anthropic's review asks
135
+ * for an accurate `readOnlyHint` and `destructiveHint`, OpenAI additionally
136
+ * reads `openWorldHint` as "can change public state".
137
+ *
138
+ * **`idempotentHint` is promised only for a remove**, the one mutation where
139
+ * a repeat is MEASURED to have no further effect (both deletes are fenced in
140
+ * the API: a repeat neither flips state nor writes an audit row). A replace
141
+ * writes an activity row per call, and `domains_set` notifies and may enqueue
142
+ * DNS work besides, so it is not claimed. An add creates on every call, and
143
+ * `idempotencyKey` cannot rescue the claim: that property is conditional on
144
+ * an argument the caller may not pass, while the annotation is static per
145
+ * tool. Reads carry none because the spec defines the hint only for tools
146
+ * that mutate.
118
147
  */
119
- export declare const ANNOTATIONS: {
120
- readonly READ: {
121
- readonly openWorldHint: true;
122
- readonly readOnlyHint: true;
123
- readonly destructiveHint: false;
124
- readonly idempotentHint: true;
125
- };
126
- readonly CREATE: {
127
- readonly openWorldHint: true;
128
- readonly readOnlyHint: false;
129
- readonly destructiveHint: false;
130
- };
131
- readonly WRITE: {
132
- readonly openWorldHint: true;
133
- readonly readOnlyHint: false;
134
- readonly destructiveHint: false;
135
- readonly idempotentHint: true;
136
- };
137
- readonly DESTRUCTIVE: {
138
- readonly openWorldHint: true;
139
- readonly readOnlyHint: false;
140
- readonly destructiveHint: true;
141
- readonly idempotentHint: true;
142
- };
148
+ export declare function annotate({ mutation, reach }: ToolContract): {
149
+ readonly openWorldHint: boolean;
150
+ readonly idempotentHint?: boolean | undefined;
151
+ readonly readOnlyHint: boolean;
152
+ readonly destructiveHint: boolean;
143
153
  };
144
154
  /**
145
155
  * One title, two wire slots: project a tool config's top-level `title` into
@@ -178,7 +188,14 @@ export declare function titled<C extends {
178
188
  */
179
189
  export declare const INSTRUCTION_BLOCKS: {
180
190
  readonly opening: "ShipStatic deploys static websites instantly. Free, no account required.";
181
- 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.";
191
+ /**
192
+ * The password read-back rule lives HERE and not in the upload
193
+ * description: a password the user never sees locks them out, and
194
+ * `instructions` is the one place a server may say what an agent should
195
+ * do. A tool description describes the tool; both listing reviews reject
196
+ * one that instructs the model, and the catalogue tests grep for it.
197
+ */
198
+ 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. Tell the user any password you set, since one they never see locks them out.";
182
199
  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.";
183
200
  readonly conceptsHeader: "Concepts:";
184
201
  readonly deploymentConcept: "- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.";
@@ -201,8 +218,12 @@ export declare const INSTRUCTION_BLOCKS: {
201
218
  export declare const DESCRIPTION_BLOCKS: {
202
219
  /** The no-account promise, mid-sentence in both openings. */
203
220
  readonly free: "free, no account or API key required";
204
- /** The password read-back rule — a password the user never sees locks them out. */
205
- readonly password: "To make the site private, pass `password`; always show the password to the user if you set one.";
221
+ /**
222
+ * The option, stated as a capability. The read-back rule that used to
223
+ * follow it ("always show the password to the user") is behavioural
224
+ * guidance, which belongs to `INSTRUCTION_BLOCKS.liveAndPassword`.
225
+ */
226
+ readonly password: "Pass `password` to make the site private.";
206
227
  };
207
228
  /**
208
229
  * Deploy-parameter descriptions shared by both transports.
@@ -104,27 +104,33 @@ export const UPLOAD_TOOL_TITLE = 'Deploy Static Site';
104
104
  * sentence someone must rewrite, not a number that silently rounds.
105
105
  */
106
106
  export const PUBLIC_EXPIRY = `${PUBLIC_DEPLOYMENT_TTL_SECONDS / 86_400} days`;
107
- const OPEN_WORLD = { openWorldHint: true };
108
107
  /**
109
- * MCP tool annotations by kind of operation. An agent reads these to decide
110
- * whether it may call speculatively (`readOnlyHint`), whether a retry is free
111
- * (`idempotentHint`), and whether it must confirm with the user first
112
- * (`destructiveHint`).
113
- *
114
- * **`CREATE` carries no `idempotentHint`, deliberately.** A deploy creates a
115
- * new deployment on every call. `idempotencyKey` makes a retry replay the
116
- * original instead — but that property is conditional on an argument the
117
- * caller may not pass, while the annotation is static per tool. Advertising it
118
- * would promise every agent that any retry is free, which is exactly false for
119
- * the keyless caller, and an annotation an agent trusts wrongly is worse than
120
- * one it never reads.
108
+ * The MCP annotations a tool's contract implies. An agent reads them to
109
+ * decide whether it may call speculatively (`readOnlyHint`), whether it must
110
+ * confirm with the user first (`destructiveHint`), whether a retry is free
111
+ * (`idempotentHint`), and whether the call reaches beyond the account
112
+ * (`openWorldHint`). Both hosts read the same four: Anthropic's review asks
113
+ * for an accurate `readOnlyHint` and `destructiveHint`, OpenAI additionally
114
+ * reads `openWorldHint` as "can change public state".
115
+ *
116
+ * **`idempotentHint` is promised only for a remove**, the one mutation where
117
+ * a repeat is MEASURED to have no further effect (both deletes are fenced in
118
+ * the API: a repeat neither flips state nor writes an audit row). A replace
119
+ * writes an activity row per call, and `domains_set` notifies and may enqueue
120
+ * DNS work besides, so it is not claimed. An add creates on every call, and
121
+ * `idempotencyKey` cannot rescue the claim: that property is conditional on
122
+ * an argument the caller may not pass, while the annotation is static per
123
+ * tool. Reads carry none because the spec defines the hint only for tools
124
+ * that mutate.
121
125
  */
122
- export const ANNOTATIONS = {
123
- READ: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, ...OPEN_WORLD },
124
- CREATE: { readOnlyHint: false, destructiveHint: false, ...OPEN_WORLD },
125
- WRITE: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, ...OPEN_WORLD },
126
- DESTRUCTIVE: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...OPEN_WORLD },
127
- };
126
+ export function annotate({ mutation, reach }) {
127
+ return {
128
+ readOnlyHint: mutation === 'none',
129
+ destructiveHint: mutation === 'replace' || mutation === 'remove',
130
+ ...(mutation === 'remove' ? { idempotentHint: true } : {}),
131
+ openWorldHint: reach === 'public',
132
+ };
133
+ }
128
134
  /**
129
135
  * One title, two wire slots: project a tool config's top-level `title` into
130
136
  * `annotations.title` at registration.
@@ -161,7 +167,14 @@ export function titled(config) {
161
167
  */
162
168
  export const INSTRUCTION_BLOCKS = {
163
169
  opening: 'ShipStatic deploys static websites instantly. Free, no account required.',
164
- 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.',
170
+ /**
171
+ * The password read-back rule lives HERE and not in the upload
172
+ * description: a password the user never sees locks them out, and
173
+ * `instructions` is the one place a server may say what an agent should
174
+ * do. A tool description describes the tool; both listing reviews reject
175
+ * one that instructs the model, and the catalogue tests grep for it.
176
+ */
177
+ 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. Tell the user any password you set, since one they never see locks them out.',
165
178
  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.',
166
179
  conceptsHeader: 'Concepts:',
167
180
  deploymentConcept: '- Deployment: an immutable set of files with an instant URL (e.g. happy-cat-abc1234.shipstatic.com). No setup needed.',
@@ -184,8 +197,12 @@ export const INSTRUCTION_BLOCKS = {
184
197
  export const DESCRIPTION_BLOCKS = {
185
198
  /** The no-account promise, mid-sentence in both openings. */
186
199
  free: 'free, no account or API key required',
187
- /** The password read-back rule — a password the user never sees locks them out. */
188
- password: 'To make the site private, pass `password`; always show the password to the user if you set one.',
200
+ /**
201
+ * The option, stated as a capability. The read-back rule that used to
202
+ * follow it ("always show the password to the user") is behavioural
203
+ * guidance, which belongs to `INSTRUCTION_BLOCKS.liveAndPassword`.
204
+ */
205
+ password: 'Pass `password` to make the site private.',
189
206
  };
190
207
  /**
191
208
  * Deploy-parameter descriptions shared by both transports.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.10.2",
3
+ "version": "1.11.0-beta.1",
4
4
  "mcpName": "com.shipstatic/mcp",
5
5
  "description": "Deploy static websites, landing pages, and prototypes instantly from AI agents. Start with the hosted endpoint at mcp.shipstatic.com (no install, no signup, no API key), or install this package to deploy folders from your own machine. The same fifteen tools either way.",
6
6
  "type": "module",