@shipstatic/mcp 1.0.0-beta.0 → 1.0.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
@@ -102,7 +102,7 @@ The hosted endpoint exposes `deployments_upload` only. The local install exposes
102
102
  | Tool | Description | Hosted |
103
103
  |------|-------------|:---:|
104
104
  | `deployments_upload` | Publish files and get a live URL instantly, optionally protected by a password | ✓ |
105
- | `deployments_list` | List all deployments with their URLs, status, labels, and password protection state | |
105
+ | `deployments_list` | List all deployments with their URLs, status, labels, and password protection state. Pages with `limit` and `cursor` | |
106
106
  | `deployments_get` | Get deployment details including URL, status, file count, size, labels, and password protection state | |
107
107
  | `deployments_set` | Update the labels on a deployment for organization and filtering | |
108
108
  | `deployments_delete` | Permanently delete a deployment and all its files | |
@@ -112,7 +112,7 @@ The hosted endpoint exposes `deployments_upload` only. The local install exposes
112
112
  | Tool | Description |
113
113
  |------|-------------|
114
114
  | `domains_set` | Connect a custom domain to your site, switch deployments, or update labels |
115
- | `domains_list` | List all domains with their linked deployment and verification status |
115
+ | `domains_list` | List all domains with their linked deployment and verification status. Pages with `limit` and `cursor` |
116
116
  | `domains_get` | Get domain details including linked deployment, verification status, and labels |
117
117
  | `domains_records` | Get the DNS records you need to configure at your DNS provider |
118
118
  | `domains_dns` | Look up which DNS provider hosts a domain (e.g. Cloudflare, Namecheap) |
@@ -127,6 +127,16 @@ The hosted endpoint exposes `deployments_upload` only. The local install exposes
127
127
  |------|-------------|
128
128
  | `whoami` | Get your account details including email, plan, and usage |
129
129
 
130
+ ### Paging
131
+
132
+ `deployments_list` and `domains_list` accept `limit` and `cursor`. Each response carries a `cursor` — pass it back to fetch the next page; `null` means you are on the last one.
133
+
134
+ ### Retrying a deploy safely
135
+
136
+ `deployments_upload` accepts an `idempotencyKey`. If a deploy times out you cannot tell "it never landed" from "it landed and the response was lost", and retrying without a key creates a second site. Send the same key on the retry and the original deployment is returned instead.
137
+
138
+ Key the *attempt*, not the try — a run id, a commit sha, or a uuid generated before the first call. A key that changes on every retry does nothing.
139
+
130
140
  ## Registry
131
141
 
132
142
  Published to the [MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=com.shipstatic/mcp) as `com.shipstatic/mcp`. Registry-aware clients see both the hosted endpoint and the local install and pick the right transport for their environment.
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
- import { LABEL_CONSTRAINTS, PASSWORD_CONSTRAINTS } from '@shipstatic/ship';
3
+ import { IDEMPOTENCY_KEY_CONSTRAINTS, LABEL_CONSTRAINTS, PASSWORD_CONSTRAINTS, } from '@shipstatic/ship';
4
4
  import { z } from 'zod';
5
5
  import { call } from './call.js';
6
6
  const { version } = createRequire(import.meta.url)('../package.json');
@@ -11,6 +11,15 @@ const READ = {
11
11
  idempotentHint: true,
12
12
  ...OPEN_WORLD,
13
13
  };
14
+ /**
15
+ * Deploys carry no `idempotentHint`, and that stays true now that
16
+ * `idempotencyKey` exists. The annotation is a STATIC per-tool claim; the
17
+ * property it would assert is per-CALL — true only when the caller supplies a
18
+ * key, false for the keyless caller, who is the common one. Advertising it
19
+ * would tell every agent that any retry is free, which is exactly wrong for
20
+ * the majority. An annotation an agent trusts wrongly is worse than one it
21
+ * never reads.
22
+ */
14
23
  const CREATE = { readOnlyHint: false, destructiveHint: false, ...OPEN_WORLD };
15
24
  const WRITE = {
16
25
  readOnlyHint: false,
@@ -24,6 +33,31 @@ const DESTRUCTIVE = {
24
33
  idempotentHint: true,
25
34
  ...OPEN_WORLD,
26
35
  };
36
+ /**
37
+ * The pagination surface, shared by every list tool because it is one
38
+ * contract, not two. A list answers `{<collection>, cursor}` and nothing
39
+ * else — `cursor` carries the whole has-more signal and is null on the last
40
+ * page, so there is no `total` to ask for and no has-more boolean.
41
+ *
42
+ * No upper bound is stated here on purpose. The API clamps an unusable
43
+ * `limit` server-side and owns that number; restating a cap in the tool
44
+ * schema would give one fact two owners and let them drift. `min(1)` is not
45
+ * a cap — it rejects a value that could never mean anything.
46
+ */
47
+ const PAGINATION_INPUT = {
48
+ limit: z
49
+ .number()
50
+ .int()
51
+ .min(1)
52
+ .optional()
53
+ .describe('Maximum number of items to return in one page. Omit for the server default.'),
54
+ cursor: z
55
+ .string()
56
+ .optional()
57
+ .describe("Opaque position from the previous response's `cursor` field; omit for the first page."),
58
+ };
59
+ /** Appended to every list tool's description — the paging contract, stated once. */
60
+ const PAGING_NOTE = " The response's `cursor` is null on the last page; pass it back as `cursor` to fetch the next.";
27
61
  const INSTRUCTIONS = `ShipStatic deploys static websites instantly. Free, no account required.
28
62
 
29
63
  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.
@@ -55,17 +89,22 @@ export function createServer(ship) {
55
89
  labels: z
56
90
  .array(z.string())
57
91
  .optional()
58
- .describe(`Labels for organizing deployments (e.g. ["production", "v1.2"]). Lowercase, ${LABEL_CONSTRAINTS.MIN_LENGTH}-${LABEL_CONSTRAINTS.MAX_LENGTH} chars, allows . _ - separators.`),
92
+ .describe(`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}.`),
59
93
  password: z
60
94
  .string()
61
95
  .optional()
62
96
  .describe(`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.`),
97
+ idempotencyKey: z
98
+ .string()
99
+ .optional()
100
+ .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.`),
63
101
  },
64
- }, ({ path, labels, password }) => call(() => ship.deployments.upload(path, { labels, password, via: 'mcp' })));
102
+ }, ({ path, labels, password, idempotencyKey }) => call(() => ship.deployments.upload(path, { labels, password, idempotencyKey, via: 'mcp' })));
65
103
  server.registerTool('deployments_list', {
66
- description: 'List all deployments with their URLs, status, labels, and password protection state.',
104
+ description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
67
105
  annotations: READ,
68
- }, () => call(() => ship.deployments.list()));
106
+ inputSchema: PAGINATION_INPUT,
107
+ }, ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
69
108
  server.registerTool('deployments_get', {
70
109
  description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
71
110
  annotations: READ,
@@ -113,9 +152,10 @@ export function createServer(ship) {
113
152
  },
114
153
  }, ({ domain, deployment, labels }) => call(() => ship.domains.set(domain, { deployment, labels })));
115
154
  server.registerTool('domains_list', {
116
- description: 'List all domains with their URLs, linked deployment, and verification status.',
155
+ description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
117
156
  annotations: READ,
118
- }, () => call(() => ship.domains.list()));
157
+ inputSchema: PAGINATION_INPUT,
158
+ }, ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
119
159
  server.registerTool('domains_get', {
120
160
  description: 'Get domain details including URL, linked deployment, verification status, and labels.',
121
161
  annotations: READ,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.1",
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",