@shipstatic/mcp 1.7.0 → 1.8.0

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
@@ -64,7 +64,7 @@ Everything above works with no account at all. Connect one when your client offe
64
64
 
65
65
  Install this package when your agent needs to deploy **a folder on your own machine** — the hosted endpoint takes files inline, so it has no path to read from — when you'd rather configure a token once than sign in, or when your MCP client doesn't speak OAuth yet.
66
66
 
67
- The config asks for one thing: `SHIP_TOKEN`. **Its value is your API key** — one credential, two names. The console mints it as an *API key* (it starts with `ship-`), and the setting that carries it is called the *token*. Get yours free at [my.shipstatic.com/api-key](https://my.shipstatic.com/api-key), paste it where the snippets below show `ship-your-api-key`, and every site you publish lands in your account, never expires, and gets bigger limits.
67
+ The config asks for one thing: `SHIP_TOKEN`. **Its value is your API key** — one credential, two names. The console mints it as an *API key* (it starts with `ship-`), and the setting that carries it is called the *token*. Get yours free at [my.shipstatic.com/api-key](https://my.shipstatic.com/api-key), paste it where the snippets below show `ship-your-api-key`, and every site you publish lands in your account and never expires, with a rate limit of your own instead of the shared anonymous pools.
68
68
 
69
69
  The key is optional: leave it out and the local server behaves exactly like the hosted endpoint with no account connected — public sites, claim links, live for 3 days.
70
70
 
package/dist/index.d.ts CHANGED
@@ -63,4 +63,4 @@
63
63
  export { type AuthFailure, type CallFn, type CallOptions, createCall, type ErrorHints, } from './call.js';
64
64
  export { createServer } from './server.js';
65
65
  export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
66
- export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
66
+ export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, titled, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
package/dist/index.js CHANGED
@@ -63,4 +63,4 @@
63
63
  export { createCall, } from './call.js';
64
64
  export { createServer } from './server.js';
65
65
  export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
66
- export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
66
+ export { ANNOTATIONS, 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
@@ -3,7 +3,7 @@ import { DeploymentVia, MY_API_KEY_URL } from '@shipstatic/types';
3
3
  import { z } from 'zod';
4
4
  import { call } from './call.js';
5
5
  import { registerAccountTools } from './tools.js';
6
- import { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.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
7
  // Destructured so the fifteen registrations below read as they always have.
8
8
  // The definitions live in `vocabulary.ts` because the hosted transport speaks
9
9
  // the same ones — that file records what is shared, what is not, and why.
@@ -39,7 +39,7 @@ export function createServer(ship, options) {
39
39
  instructions: INSTRUCTIONS,
40
40
  });
41
41
  // Deployments
42
- server.registerTool(UPLOAD_TOOL_NAME, {
42
+ server.registerTool(UPLOAD_TOOL_NAME, titled({
43
43
  title: UPLOAD_TOOL_TITLE,
44
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
45
  annotations: CREATE,
@@ -58,7 +58,7 @@ export function createServer(ship, options) {
58
58
  // disagree with the first, silently; its absence fails loudly instead.
59
59
  ttl: z.number().optional().describe(PARAM_DESCRIPTIONS.ttl),
60
60
  },
61
- }, ({ path, labels, password, idempotencyKey, ttl }) => call(() => ship.deployments.upload(path, { labels, password, idempotencyKey, ttl, via })));
61
+ }), ({ path, labels, password, idempotencyKey, ttl }) => call(() => ship.deployments.upload(path, { labels, password, idempotencyKey, ttl, via })));
62
62
  // The other fourteen. Identical on every transport, so they live in the
63
63
  // shared package rather than here — see tools.ts for why upload is not
64
64
  // among them.
package/dist/tools.js CHANGED
@@ -37,7 +37,7 @@
37
37
  * every tool, so the next one cannot be added without one.
38
38
  */
39
39
  import { z } from 'zod';
40
- import { ANNOTATIONS } from './vocabulary.js';
40
+ import { ANNOTATIONS, titled } from './vocabulary.js';
41
41
  const { READ, WRITE, DESTRUCTIVE } = ANNOTATIONS;
42
42
  /**
43
43
  * The fourteen, by name, in registration order.
@@ -103,13 +103,13 @@ const PAGING_NOTE = " The response's `cursor` is null on the last page; pass it
103
103
  const DEPLOYMENT_EXAMPLE = 'happy-cat-abc1234.shipstatic.com';
104
104
  export function registerAccountTools(server, ship, call) {
105
105
  // Deployments
106
- server.registerTool('deployments_list', {
106
+ server.registerTool('deployments_list', titled({
107
107
  title: 'List Deployments',
108
108
  description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
109
109
  annotations: READ,
110
110
  inputSchema: PAGINATION_INPUT,
111
- }, ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
112
- server.registerTool('deployments_get', {
111
+ }), ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
112
+ server.registerTool('deployments_get', titled({
113
113
  title: 'Get Deployment',
114
114
  description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
115
115
  annotations: READ,
@@ -118,8 +118,8 @@ export function registerAccountTools(server, ship, call) {
118
118
  .string()
119
119
  .describe(`Deployment hostname (e.g. "${DEPLOYMENT_EXAMPLE}"). Returned by deployments_upload or deployments_list.`),
120
120
  },
121
- }, ({ deployment }) => call(() => ship.deployments.get(deployment)));
122
- server.registerTool('deployments_set', {
121
+ }), ({ deployment }) => call(() => ship.deployments.get(deployment)));
122
+ server.registerTool('deployments_set', titled({
123
123
  title: 'Update Deployment Labels',
124
124
  description: 'Update deployment labels. Replaces all existing labels.',
125
125
  annotations: WRITE,
@@ -131,8 +131,8 @@ export function registerAccountTools(server, ship, call) {
131
131
  .array(z.string())
132
132
  .describe('Labels to set. Replaces all existing labels. Pass empty array to clear.'),
133
133
  },
134
- }, ({ deployment, labels }) => call(() => ship.deployments.set(deployment, { labels })));
135
- server.registerTool('deployments_delete', {
134
+ }), ({ deployment, labels }) => call(() => ship.deployments.set(deployment, { labels })));
135
+ server.registerTool('deployments_delete', titled({
136
136
  title: 'Delete Deployment',
137
137
  description: 'Permanently delete a deployment and its files. You MUST confirm with the user before calling this tool, referencing the deployment.',
138
138
  annotations: DESTRUCTIVE,
@@ -141,9 +141,9 @@ export function registerAccountTools(server, ship, call) {
141
141
  .string()
142
142
  .describe(`Deployment hostname to delete (e.g. "${DEPLOYMENT_EXAMPLE}")`),
143
143
  },
144
- }, ({ deployment }) => call(() => ship.deployments.delete(deployment)));
144
+ }), ({ deployment }) => call(() => ship.deployments.delete(deployment)));
145
145
  // Domains
146
- server.registerTool('domains_set', {
146
+ server.registerTool('domains_set', titled({
147
147
  title: 'Connect Custom Domain',
148
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
149
  annotations: WRITE,
@@ -158,14 +158,14 @@ export function registerAccountTools(server, ship, call) {
158
158
  .optional()
159
159
  .describe('Labels for organizing domains (e.g. ["production"]).'),
160
160
  },
161
- }, ({ domain, deployment, labels }) => call(() => ship.domains.set(domain, { deployment, labels })));
162
- server.registerTool('domains_list', {
161
+ }), ({ domain, deployment, labels }) => call(() => ship.domains.set(domain, { deployment, labels })));
162
+ server.registerTool('domains_list', titled({
163
163
  title: 'List Domains',
164
164
  description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
165
165
  annotations: READ,
166
166
  inputSchema: PAGINATION_INPUT,
167
- }, ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
168
- server.registerTool('domains_get', {
167
+ }), ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
168
+ server.registerTool('domains_get', titled({
169
169
  title: 'Get Domain',
170
170
  description: 'Get domain details including URL, linked deployment, verification status, and labels.',
171
171
  annotations: READ,
@@ -174,8 +174,8 @@ export function registerAccountTools(server, ship, call) {
174
174
  .string()
175
175
  .describe('Domain name (e.g. "www.example.com"). Use domains_list to find names.'),
176
176
  },
177
- }, ({ domain }) => call(() => ship.domains.get(domain)));
178
- server.registerTool('domains_records', {
177
+ }), ({ domain }) => call(() => ship.domains.get(domain)));
178
+ server.registerTool('domains_records', titled({
179
179
  title: 'Get DNS Records',
180
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
181
  annotations: READ,
@@ -184,8 +184,8 @@ export function registerAccountTools(server, ship, call) {
184
184
  .string()
185
185
  .describe('Domain name. Must be a domain previously created with domains_set.'),
186
186
  },
187
- }, ({ domain }) => call(() => ship.domains.records(domain)));
188
- server.registerTool('domains_dns', {
187
+ }), ({ domain }) => call(() => ship.domains.records(domain)));
188
+ server.registerTool('domains_dns', titled({
189
189
  title: 'Look Up DNS Provider',
190
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
191
  annotations: READ,
@@ -194,8 +194,8 @@ export function registerAccountTools(server, ship, call) {
194
194
  .string()
195
195
  .describe('Domain name to look up DNS provider for (e.g. "www.example.com")'),
196
196
  },
197
- }, ({ domain }) => call(() => ship.domains.dns(domain)));
198
- server.registerTool('domains_share', {
197
+ }), ({ domain }) => call(() => ship.domains.dns(domain)));
198
+ server.registerTool('domains_share', titled({
199
199
  title: 'Share DNS Setup',
200
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
201
  annotations: READ,
@@ -204,8 +204,8 @@ export function registerAccountTools(server, ship, call) {
204
204
  .string()
205
205
  .describe('Domain name to generate a share link for. Must be a domain previously created with domains_set.'),
206
206
  },
207
- }, ({ domain }) => call(() => ship.domains.share(domain)));
208
- server.registerTool('domains_validate', {
207
+ }), ({ domain }) => call(() => ship.domains.share(domain)));
208
+ server.registerTool('domains_validate', titled({
209
209
  title: 'Check Domain Availability',
210
210
  description: 'Check if a domain name is valid and available before creating it. Returns the normalized form and availability.',
211
211
  annotations: READ,
@@ -214,8 +214,8 @@ export function registerAccountTools(server, ship, call) {
214
214
  .string()
215
215
  .describe('Domain name to check (e.g. "www.example.com"). Call before domains_set to check availability.'),
216
216
  },
217
- }, ({ domain }) => call(() => ship.domains.validate(domain)));
218
- server.registerTool('domains_verify', {
217
+ }), ({ domain }) => call(() => ship.domains.validate(domain)));
218
+ server.registerTool('domains_verify', titled({
219
219
  title: 'Verify Domain DNS',
220
220
  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
221
  annotations: WRITE,
@@ -224,19 +224,19 @@ export function registerAccountTools(server, ship, call) {
224
224
  .string()
225
225
  .describe('Domain name to verify DNS for. Must be a domain previously created with domains_set.'),
226
226
  },
227
- }, ({ domain }) => call(() => ship.domains.verify(domain)));
228
- server.registerTool('domains_delete', {
227
+ }), ({ domain }) => call(() => ship.domains.verify(domain)));
228
+ server.registerTool('domains_delete', titled({
229
229
  title: 'Delete Domain',
230
230
  description: 'Permanently delete a domain. You MUST confirm with the user before calling this tool, referencing the domain name.',
231
231
  annotations: DESTRUCTIVE,
232
232
  inputSchema: {
233
233
  domain: z.string().describe('Domain name to delete (e.g. "www.example.com")'),
234
234
  },
235
- }, ({ domain }) => call(() => ship.domains.delete(domain)));
235
+ }), ({ domain }) => call(() => ship.domains.delete(domain)));
236
236
  // Account
237
- server.registerTool('whoami', {
237
+ server.registerTool('whoami', titled({
238
238
  title: 'Show Account',
239
239
  description: 'Show authenticated account details including email, plan, and usage.',
240
240
  annotations: READ,
241
- }, () => call(() => ship.whoami()));
241
+ }), () => call(() => ship.whoami()));
242
242
  }
@@ -141,6 +141,28 @@ export declare const ANNOTATIONS: {
141
141
  readonly idempotentHint: true;
142
142
  };
143
143
  };
144
+ /**
145
+ * One title, two wire slots: project a tool config's top-level `title` into
146
+ * `annotations.title` at registration.
147
+ *
148
+ * The MCP spec carries a tool title in two places. The 2025-06-18 revision
149
+ * put it top-level on the Tool object (where every registration here states
150
+ * it), and `ToolAnnotations.title` is the older 2025-03-26 slot that some
151
+ * readers still key on. The Claude connectors portal is one of them, measured
152
+ * on submission day 2026-08-31: its Tools step rendered every top-level title
153
+ * as the heading and simultaneously flagged all fifteen tools with "Missing
154
+ * annotations: title". So both slots must be populated, and they must agree.
155
+ *
156
+ * The top-level `title` stays the single owner. This projection is the only
157
+ * writer of the annotations copy, applied at every registration on both
158
+ * transports, and the catalogue fences hold the two slots equal off a real
159
+ * `tools/list` so a registration that bypasses it goes red rather than
160
+ * shipping a bare slot back to the portal.
161
+ */
162
+ export declare function titled<C extends {
163
+ title: string;
164
+ annotations?: object;
165
+ }>(config: C): C;
144
166
  /**
145
167
  * INSTRUCTIONS sentences both transports say.
146
168
  *
@@ -125,6 +125,27 @@ export const ANNOTATIONS = {
125
125
  WRITE: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, ...OPEN_WORLD },
126
126
  DESTRUCTIVE: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, ...OPEN_WORLD },
127
127
  };
128
+ /**
129
+ * One title, two wire slots: project a tool config's top-level `title` into
130
+ * `annotations.title` at registration.
131
+ *
132
+ * The MCP spec carries a tool title in two places. The 2025-06-18 revision
133
+ * put it top-level on the Tool object (where every registration here states
134
+ * it), and `ToolAnnotations.title` is the older 2025-03-26 slot that some
135
+ * readers still key on. The Claude connectors portal is one of them, measured
136
+ * on submission day 2026-08-31: its Tools step rendered every top-level title
137
+ * as the heading and simultaneously flagged all fifteen tools with "Missing
138
+ * annotations: title". So both slots must be populated, and they must agree.
139
+ *
140
+ * The top-level `title` stays the single owner. This projection is the only
141
+ * writer of the annotations copy, applied at every registration on both
142
+ * transports, and the catalogue fences hold the two slots equal off a real
143
+ * `tools/list` so a registration that bypasses it goes red rather than
144
+ * shipping a bare slot back to the portal.
145
+ */
146
+ export function titled(config) {
147
+ return { ...config, annotations: { ...config.annotations, title: config.title } };
148
+ }
128
149
  /**
129
150
  * INSTRUCTIONS sentences both transports say.
130
151
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "mcpName": "com.shipstatic/mcp",
5
5
  "description": "Deploy static websites from AI agents. Start with the hosted endpoint at mcp.shipstatic.com \u2014 no install, no signup, no API key \u2014 or install this package to deploy folders from your own machine. The same fifteen tools either way.",
6
6
  "type": "module",