@shipstatic/mcp 1.11.0-beta.2 → 1.12.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/dist/call.d.ts CHANGED
@@ -13,19 +13,6 @@ export interface ErrorHints {
13
13
  export type CallFn = <T>(fn: () => Promise<T>) => Promise<CallToolResult>;
14
14
  export interface CallOptions {
15
15
  hints: ErrorHints;
16
- /**
17
- * Attach a plain-object SUCCESS result as `structuredContent` beside the
18
- * text. Hosted-only: it is what feeds the Apps-SDK widget, and the MCP spec
19
- * pairs it with an `outputSchema`, which is a hand-maintained zod twin of a
20
- * published type. One such twin is worth it for a widget; fifteen would be a
21
- * drift surface with no consumer asking for it. See
22
- * `cloudflare/mcp/CLAUDE.md`, "What deliberately differs".
23
- *
24
- * It does NOT gate the ERROR envelope, which every transport carries — see
25
- * `toErrorResult`. The objection above is about fifteen success shapes; a
26
- * failure has exactly one published shape, and no schema to keep in step.
27
- */
28
- structuredContent?: boolean;
29
16
  /**
30
17
  * Called when the API refuses on CREDENTIAL grounds — and only then.
31
18
  *
package/dist/call.js CHANGED
@@ -39,7 +39,7 @@ const MAINTENANCE_HINT = 'The platform is temporarily closed for scheduled maint
39
39
  * kept equal by review.
40
40
  */
41
41
  export function createCall(options) {
42
- const { hints, structuredContent = false, onAuthFailure } = options;
42
+ const { hints, onAuthFailure } = options;
43
43
  return async function call(fn) {
44
44
  try {
45
45
  const result = await fn();
@@ -48,13 +48,15 @@ export function createCall(options) {
48
48
  if (result === undefined) {
49
49
  return { content: [{ type: 'text', text: 'Done.' }] };
50
50
  }
51
+ // The wire shape rides twice: as the text every client reads, and as
52
+ // `structuredContent` for the tool's `outputSchema` (every tool declares
53
+ // one, imported from the constitution; the SDK validates the result
54
+ // against it and refuses a result that carries none). A non-object has
55
+ // no schema to satisfy and rides the text channel alone.
51
56
  const text = JSON.stringify(result, null, 2);
52
- const structured = structuredContent && isPlainObject(result)
53
- ? result
54
- : undefined;
55
57
  return {
56
58
  content: [{ type: 'text', text }],
57
- ...(structured ? { structuredContent: structured } : {}),
59
+ ...(isPlainObject(result) ? { structuredContent: result } : {}),
58
60
  };
59
61
  }
60
62
  catch (error) {
@@ -98,13 +100,9 @@ function isPlainObject(value) {
98
100
  * need of a precise backoff to parse "try again in 9 minutes" out of English.
99
101
  *
100
102
  * Safe on every arm, and checked rather than assumed: the MCP SDK validates
101
- * `structuredContent` only when a tool declares an `outputSchema`, and returns
102
- * early again when `isError` is set. No tool here declares one. So this is
103
- * additive for every client and invisible to any that does not look.
104
- *
105
- * It is deliberately NOT behind `CallOptions.structuredContent` — that flag
106
- * governs success shapes, where the schema-twin objection lives. A failure has
107
- * one published shape on every transport.
103
+ * `structuredContent` against a declared `outputSchema` only when `isError`
104
+ * is not set, so an error envelope never meets a success schema. Every tool
105
+ * declares one (imported from the constitution), and this arm is unaffected.
108
106
  */
109
107
  function toErrorResult(error, hints, onAuthFailure) {
110
108
  if (isShipError(error)) {
package/dist/server.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { DeploymentVia, MY_API_KEY_URL } from '@shipstatic/types';
3
+ import { DeploymentCreateResponseSchema } from '@shipstatic/types/schemas';
3
4
  import { z } from 'zod';
4
5
  import { call } from './call.js';
5
6
  import { registerAccountTools, TOOLS } from './tools.js';
@@ -43,6 +44,7 @@ export function createServer(ship, options) {
43
44
  // shares with the hosted one; only the input schema is this door's own.
44
45
  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
46
  annotations: annotate(TOOLS[UPLOAD_TOOL_NAME]),
47
+ outputSchema: DeploymentCreateResponseSchema,
46
48
  inputSchema: {
47
49
  path: z
48
50
  .string()
package/dist/tools.d.ts CHANGED
@@ -35,9 +35,18 @@
35
35
  * the agent, the title reads as English for the human, and the description
36
36
  * carries every precision neither can. Both catalogue pins assert a title on
37
37
  * every tool, so the next one cannot be added without one.
38
+ *
39
+ * **Every tool publishes an `outputSchema`, imported from the constitution**
40
+ * (`@shipstatic/types/schemas`), never written here: a schema written beside
41
+ * a tool is a twin of a published type, and the SDK enforces it on every
42
+ * success, so a twin that drifts fails a call that succeeded. The constitution
43
+ * fences its schemas to its interfaces at compile time; this file only names
44
+ * which one each tool answers with, and `call` attaches the result as
45
+ * `structuredContent` so the schema has something to validate.
38
46
  */
39
47
  import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
40
48
  import type Ship from '@shipstatic/ship';
49
+ import { z } from 'zod';
41
50
  import type { CallFn } from './call.js';
42
51
  /**
43
52
  * THE REGISTRY: one row per tool, every fact about it that a host reads.
@@ -154,4 +163,32 @@ export type AccountToolName = {
154
163
  * place a tool's account requirement is stated.
155
164
  */
156
165
  export declare const ACCOUNT_TOOL_NAMES: AccountToolName[];
166
+ /**
167
+ * What `whoami` answers: exactly the keys its description names, and the
168
+ * schema IS the projection.
169
+ *
170
+ * `Account` also carries billing state, the API-key hint, the picture and
171
+ * timestamps, none of which the description mentions and none of which an
172
+ * agent acts on. A tool's result is the shape its description states, so the
173
+ * result is the constitution's `AccountSchema` narrowed to five keys, and
174
+ * parsing the wire through it is what drops the rest: one declaration owns
175
+ * both the published `outputSchema` and the projection. `suspended` is
176
+ * deliberately out: it means every write is refused, and the refusal says so
177
+ * itself at the moment it matters.
178
+ */
179
+ export declare const ACCOUNT_SUMMARY: z.ZodObject<{
180
+ name: z.ZodNullable<z.ZodString>;
181
+ email: z.ZodString;
182
+ plan: z.ZodString;
183
+ usage: z.ZodObject<{
184
+ deployments: z.ZodInt;
185
+ platformDomains: z.ZodInt;
186
+ customDomains: z.ZodInt;
187
+ }, z.core.$strip>;
188
+ caps: z.ZodObject<{
189
+ deployments: z.ZodInt;
190
+ platformDomains: z.ZodInt;
191
+ customDomains: z.ZodInt;
192
+ }, z.core.$strip>;
193
+ }, z.core.$strip>;
157
194
  export declare function registerAccountTools(server: McpServer, ship: Ship, call: CallFn): void;
package/dist/tools.js CHANGED
@@ -35,7 +35,16 @@
35
35
  * the agent, the title reads as English for the human, and the description
36
36
  * carries every precision neither can. Both catalogue pins assert a title on
37
37
  * every tool, so the next one cannot be added without one.
38
+ *
39
+ * **Every tool publishes an `outputSchema`, imported from the constitution**
40
+ * (`@shipstatic/types/schemas`), never written here: a schema written beside
41
+ * a tool is a twin of a published type, and the SDK enforces it on every
42
+ * success, so a twin that drifts fails a call that succeeded. The constitution
43
+ * fences its schemas to its interfaces at compile time; this file only names
44
+ * which one each tool answers with, and `call` attaches the result as
45
+ * `structuredContent` so the schema has something to validate.
38
46
  */
47
+ import { AccountSchema, DeploymentDeleteResponseSchema, DeploymentListResponseSchema, DeploymentSchema, DomainDeleteResponseSchema, DomainDnsResponseSchema, DomainListResponseSchema, DomainRecordsResponseSchema, DomainSchema, DomainSetResultSchema, DomainShareResponseSchema, DomainValidateResponseSchema, DomainVerifyResponseSchema, } from '@shipstatic/types/schemas';
39
48
  import { z } from 'zod';
40
49
  import { annotate, titled, UPLOAD_TOOL_NAME } from './vocabulary.js';
41
50
  /**
@@ -89,18 +98,25 @@ export const TOOLS = {
89
98
  */
90
99
  export const ACCOUNT_TOOL_NAMES = Object.keys(TOOLS).filter((name) => TOOLS[name].auth === 'required');
91
100
  /**
92
- * What `whoami` answers: exactly the keys its description names.
101
+ * What `whoami` answers: exactly the keys its description names, and the
102
+ * schema IS the projection.
93
103
  *
94
104
  * `Account` also carries billing state, the API-key hint, the picture and
95
105
  * 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.
106
+ * agent acts on. A tool's result is the shape its description states, so the
107
+ * result is the constitution's `AccountSchema` narrowed to five keys, and
108
+ * parsing the wire through it is what drops the rest: one declaration owns
109
+ * both the published `outputSchema` and the projection. `suspended` is
110
+ * deliberately out: it means every write is refused, and the refusal says so
111
+ * itself at the moment it matters.
100
112
  */
101
- function accountSummary({ email, name, plan, usage, caps }) {
102
- return { email, name, plan, usage, caps };
103
- }
113
+ export const ACCOUNT_SUMMARY = AccountSchema.pick({
114
+ email: true,
115
+ name: true,
116
+ plan: true,
117
+ usage: true,
118
+ caps: true,
119
+ });
104
120
  /**
105
121
  * The pagination surface, shared by every list tool because it is one
106
122
  * contract, not two. A list answers `{<collection>, cursor}` and nothing
@@ -134,12 +150,14 @@ export function registerAccountTools(server, ship, call) {
134
150
  title: 'List Deployments',
135
151
  description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
136
152
  annotations: annotate(TOOLS.deployments_list),
153
+ outputSchema: DeploymentListResponseSchema,
137
154
  inputSchema: PAGINATION_INPUT,
138
155
  }), ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
139
156
  server.registerTool('deployments_get', titled({
140
157
  title: 'Get Deployment',
141
158
  description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
142
159
  annotations: annotate(TOOLS.deployments_get),
160
+ outputSchema: DeploymentSchema,
143
161
  inputSchema: {
144
162
  deployment: z
145
163
  .string()
@@ -150,6 +168,7 @@ export function registerAccountTools(server, ship, call) {
150
168
  title: 'Update Deployment Labels',
151
169
  description: 'Update deployment labels. Replaces all existing labels.',
152
170
  annotations: annotate(TOOLS.deployments_set),
171
+ outputSchema: DeploymentSchema,
153
172
  inputSchema: {
154
173
  deployment: z
155
174
  .string()
@@ -163,6 +182,7 @@ export function registerAccountTools(server, ship, call) {
163
182
  title: 'Delete Deployment',
164
183
  description: 'Permanently deletes a deployment and its files.',
165
184
  annotations: annotate(TOOLS.deployments_delete),
185
+ outputSchema: DeploymentDeleteResponseSchema,
166
186
  inputSchema: {
167
187
  deployment: z
168
188
  .string()
@@ -174,6 +194,7 @@ export function registerAccountTools(server, ship, call) {
174
194
  title: 'Connect Custom Domain',
175
195
  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
196
  annotations: annotate(TOOLS.domains_set),
197
+ outputSchema: DomainSetResultSchema,
177
198
  inputSchema: {
178
199
  domain: z.string().describe('Domain name (e.g. "www.example.com" or "blog.example.com")'),
179
200
  deployment: z
@@ -190,12 +211,14 @@ export function registerAccountTools(server, ship, call) {
190
211
  title: 'List Domains',
191
212
  description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
192
213
  annotations: annotate(TOOLS.domains_list),
214
+ outputSchema: DomainListResponseSchema,
193
215
  inputSchema: PAGINATION_INPUT,
194
216
  }), ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
195
217
  server.registerTool('domains_get', titled({
196
218
  title: 'Get Domain',
197
219
  description: 'Get domain details including URL, linked deployment, verification status, and labels.',
198
220
  annotations: annotate(TOOLS.domains_get),
221
+ outputSchema: DomainSchema,
199
222
  inputSchema: {
200
223
  domain: z
201
224
  .string()
@@ -206,6 +229,7 @@ export function registerAccountTools(server, ship, call) {
206
229
  title: 'Get DNS Records',
207
230
  description: "Returns the DNS records to configure at the domain's DNS provider. Call after domains_set.",
208
231
  annotations: annotate(TOOLS.domains_records),
232
+ outputSchema: DomainRecordsResponseSchema,
209
233
  inputSchema: {
210
234
  domain: z
211
235
  .string()
@@ -216,6 +240,7 @@ export function registerAccountTools(server, ship, call) {
216
240
  title: 'Get DNS Provider',
217
241
  description: 'Returns the DNS provider recorded for the domain, if known (e.g. Cloudflare, Namecheap): where its DNS records are configured.',
218
242
  annotations: annotate(TOOLS.domains_dns),
243
+ outputSchema: DomainDnsResponseSchema,
219
244
  inputSchema: {
220
245
  domain: z
221
246
  .string()
@@ -226,6 +251,7 @@ export function registerAccountTools(server, ship, call) {
226
251
  title: 'Share DNS Setup',
227
252
  description: "Returns a shareable DNS setup URL that needs no API key, for whoever manages the domain's DNS.",
228
253
  annotations: annotate(TOOLS.domains_share),
254
+ outputSchema: DomainShareResponseSchema,
229
255
  inputSchema: {
230
256
  domain: z
231
257
  .string()
@@ -236,6 +262,7 @@ export function registerAccountTools(server, ship, call) {
236
262
  title: 'Check Domain Availability',
237
263
  description: 'Check if a domain name is valid and available before creating it. Returns the normalized form and availability.',
238
264
  annotations: annotate(TOOLS.domains_validate),
265
+ outputSchema: DomainValidateResponseSchema,
239
266
  inputSchema: {
240
267
  domain: z
241
268
  .string()
@@ -246,6 +273,7 @@ export function registerAccountTools(server, ship, call) {
246
273
  title: 'Verify Domain DNS',
247
274
  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.',
248
275
  annotations: annotate(TOOLS.domains_verify),
276
+ outputSchema: DomainVerifyResponseSchema,
249
277
  inputSchema: {
250
278
  domain: z
251
279
  .string()
@@ -256,6 +284,7 @@ export function registerAccountTools(server, ship, call) {
256
284
  title: 'Delete Domain',
257
285
  description: 'Permanently deletes a domain.',
258
286
  annotations: annotate(TOOLS.domains_delete),
287
+ outputSchema: DomainDeleteResponseSchema,
259
288
  inputSchema: {
260
289
  domain: z.string().describe('Domain name to delete (e.g. "www.example.com")'),
261
290
  },
@@ -265,5 +294,6 @@ export function registerAccountTools(server, ship, call) {
265
294
  title: 'Show Account',
266
295
  description: "Returns the account's email, name, plan, current usage and plan caps.",
267
296
  annotations: annotate(TOOLS.whoami),
268
- }), () => call(() => ship.whoami().then(accountSummary)));
297
+ outputSchema: ACCOUNT_SUMMARY,
298
+ }), () => call(() => ship.whoami().then((account) => ACCOUNT_SUMMARY.parse(account))));
269
299
  }
@@ -190,9 +190,8 @@ export declare const INSTRUCTION_BLOCKS: {
190
190
  readonly opening: "ShipStatic deploys static websites instantly. Free, no account required.";
191
191
  readonly live: "The site is live immediately.";
192
192
  /**
193
- * Two sentences in one block, and a block of its own rather than a clause
194
- * of `live`: a door that collects no visitor password (the hosted `/gpt`
195
- * door, by OpenAI's data rule) composes `live` without this.
193
+ * A block of its own rather than a clause of `live`: two sentences, two
194
+ * facts, and a door may compose them independently.
196
195
  *
197
196
  * The read-back rule lives HERE and not in the upload description: a
198
197
  * password the user never sees locks them out, and `instructions` is the
@@ -169,9 +169,8 @@ export const INSTRUCTION_BLOCKS = {
169
169
  opening: 'ShipStatic deploys static websites instantly. Free, no account required.',
170
170
  live: 'The site is live immediately.',
171
171
  /**
172
- * Two sentences in one block, and a block of its own rather than a clause
173
- * of `live`: a door that collects no visitor password (the hosted `/gpt`
174
- * door, by OpenAI's data rule) composes `live` without this.
172
+ * A block of its own rather than a clause of `live`: two sentences, two
173
+ * facts, and a door may compose them independently.
175
174
  *
176
175
  * The read-back rule lives HERE and not in the upload description: a
177
176
  * password the user never sees locks them out, and `instructions` is the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.11.0-beta.2",
3
+ "version": "1.12.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",
@@ -69,7 +69,7 @@
69
69
  "dependencies": {
70
70
  "@modelcontextprotocol/sdk": "^1.30.0",
71
71
  "@shipstatic/ship": "2.9.2",
72
- "@shipstatic/types": "2.24.0",
72
+ "@shipstatic/types": "2.25.0-beta.1",
73
73
  "zod": "^4.4.3"
74
74
  },
75
75
  "devDependencies": {