@shipstatic/mcp 1.0.0-beta.9 → 1.0.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/dist/bin.js +2 -1
- package/dist/call.d.ts +50 -11
- package/dist/call.js +58 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -3
- package/dist/server.js +2 -1
- package/dist/tools.d.ts +9 -0
- package/dist/tools.js +23 -0
- package/dist/vocabulary.d.ts +20 -0
- package/dist/vocabulary.js +20 -0
- package/package.json +10 -5
package/dist/bin.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
17
|
import Ship from '@shipstatic/ship';
|
|
18
|
+
import { SHIP_ENV } from '@shipstatic/types';
|
|
18
19
|
import { createServer } from './server.js';
|
|
19
20
|
// The executable knows its own manifest; the library it drives does not have
|
|
20
21
|
// to. `createServer` takes the version as an argument precisely so no module
|
|
@@ -28,7 +29,7 @@ async function main() {
|
|
|
28
29
|
// is (`ship-` API key, `deploy-` deploy token, anything else an opaque
|
|
29
30
|
// bearer) and the server classifies it. MCP never has to know which kind it
|
|
30
31
|
// holds.
|
|
31
|
-
const ship = new Ship({ token: process.env.
|
|
32
|
+
const ship = new Ship({ token: process.env[SHIP_ENV.TOKEN] });
|
|
32
33
|
// No `via` — this executable IS the `mcp` origin, which is the default.
|
|
33
34
|
const server = createServer(ship, { version });
|
|
34
35
|
const transport = new StdioServerTransport();
|
package/dist/call.d.ts
CHANGED
|
@@ -1,15 +1,4 @@
|
|
|
1
1
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
-
/**
|
|
3
|
-
* The two error arms that earn a hint. Everything else relays verbatim — a
|
|
4
|
-
* hint on any other type sends the agent chasing a credential that is not the
|
|
5
|
-
* problem.
|
|
6
|
-
*
|
|
7
|
-
* They are ARGUMENTS rather than constants because they are the one part of
|
|
8
|
-
* the mapping that legitimately differs per transport: stdio can name the
|
|
9
|
-
* environment variable it owns, and the hosted endpoint deliberately cannot
|
|
10
|
-
* (it has no configuration of its own, and naming another package's variable
|
|
11
|
-
* is how this pair silently desynchronised once already).
|
|
12
|
-
*/
|
|
13
2
|
export interface ErrorHints {
|
|
14
3
|
/** Appended after `Hint: ` when the SDK rejects the credential. */
|
|
15
4
|
authentication: string;
|
|
@@ -37,6 +26,56 @@ export interface CallOptions {
|
|
|
37
26
|
* failure has exactly one published shape, and no schema to keep in step.
|
|
38
27
|
*/
|
|
39
28
|
structuredContent?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Called when the API refuses on CREDENTIAL grounds — and only then.
|
|
31
|
+
*
|
|
32
|
+
* An HTTP transport has an obligation stdio does not: a client learns it
|
|
33
|
+
* must authenticate from a real `401` with a `WWW-Authenticate` header, and
|
|
34
|
+
* ignores that header entirely on a `200`. So a tool error saying "please
|
|
35
|
+
* authenticate" is, on that transport, a dead end — the caller is told
|
|
36
|
+
* something it has no way to act on.
|
|
37
|
+
*
|
|
38
|
+
* This is the seam that lets the transport answer properly, and the
|
|
39
|
+
* decision of WHAT counts as a credential failure stays here, beside the
|
|
40
|
+
* hints, rather than being made a second time by each consumer:
|
|
41
|
+
*
|
|
42
|
+
* - **Authentication** always reports. The credential was absent, malformed,
|
|
43
|
+
* unknown, or expired — the transport cannot tell which, and does not
|
|
44
|
+
* need to.
|
|
45
|
+
* - **Forbidden reports only when it carries `requiredScope`.** That field
|
|
46
|
+
* is the API's own signal that a valid grant simply lacks a permission,
|
|
47
|
+
* which re-consent can fix. Every other refusal on that arm — plan limits,
|
|
48
|
+
* a terminated account, an action no scope can authorize — is a genuine
|
|
49
|
+
* answer to the question asked, and stays an ordinary in-band tool error.
|
|
50
|
+
*
|
|
51
|
+
* The result is unchanged either way: the agent still receives the full
|
|
52
|
+
* text envelope, hints included. This is a notification, not a substitution.
|
|
53
|
+
*
|
|
54
|
+
* Why an observer at all, rather than the transport inspecting the request:
|
|
55
|
+
* peeking at a JSON-RPC body to guess whether a call needs a credential
|
|
56
|
+
* means parsing it twice — on the deploy path, that is tens of megabytes of
|
|
57
|
+
* base64 re-parsed before the size caps run — and it can only ever guess at
|
|
58
|
+
* PRESENCE, so an EXPIRED token would answer in-band and a connected client
|
|
59
|
+
* would never refresh. Reporting what the API actually answered costs
|
|
60
|
+
* nothing and is correct for both.
|
|
61
|
+
*/
|
|
62
|
+
onAuthFailure?: (failure: AuthFailure) => void;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* What a credential refusal was, in the only two shapes a transport acts on
|
|
66
|
+
* differently.
|
|
67
|
+
*
|
|
68
|
+
* Deliberately not an HTTP status or an RFC 6750 error code: those are the
|
|
69
|
+
* consuming transport's vocabulary, and stdio — which also builds a `call` —
|
|
70
|
+
* has neither. The presence of `requiredScope` is the whole discriminator, so
|
|
71
|
+
* there is no second field restating it.
|
|
72
|
+
*/
|
|
73
|
+
export interface AuthFailure {
|
|
74
|
+
/**
|
|
75
|
+
* The scope the grant is missing, from the API's `details.requiredScope`.
|
|
76
|
+
* Absent when the credential itself was refused rather than its permissions.
|
|
77
|
+
*/
|
|
78
|
+
requiredScope?: string;
|
|
40
79
|
}
|
|
41
80
|
/**
|
|
42
81
|
* Builds the `call()` wrapper both transports use: SDK promise in, MCP
|
package/dist/call.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
import { ErrorType, isShipError } from '@shipstatic/ship';
|
|
2
|
+
/**
|
|
3
|
+
* The two CREDENTIAL arms that earn a per-transport hint. Everything else
|
|
4
|
+
* relays verbatim — a credential hint on any other type sends the agent
|
|
5
|
+
* chasing a problem it does not have.
|
|
6
|
+
*
|
|
7
|
+
* They are ARGUMENTS rather than constants because they are the one part of
|
|
8
|
+
* the mapping that legitimately differs per transport: stdio can name the
|
|
9
|
+
* environment variable it owns, and the hosted endpoint deliberately cannot
|
|
10
|
+
* (it has no configuration of its own, and naming another package's variable
|
|
11
|
+
* is how this pair silently desynchronised once already).
|
|
12
|
+
*
|
|
13
|
+
* A third arm — maintenance — is hinted too, but it is NOT a member here: a
|
|
14
|
+
* closed platform is closed identically on every transport, so its text is a
|
|
15
|
+
* module constant (`MAINTENANCE_HINT`) rather than a per-transport argument.
|
|
16
|
+
* The membership test for this interface is "does the text differ per
|
|
17
|
+
* transport?", not "does the arm get a hint?".
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The maintenance hint — a CONSTANT, for the reason stated on `ErrorHints`:
|
|
21
|
+
* this text does not differ per transport, so making it an argument would ask
|
|
22
|
+
* two callers to agree on one sentence forever.
|
|
23
|
+
*
|
|
24
|
+
* The instruction to an agent matters more here than on any other arm. A tool
|
|
25
|
+
* failure normally invites a retry, and retrying is exactly wrong against a
|
|
26
|
+
* platform that is closed on purpose: the loop cannot succeed, and it spends
|
|
27
|
+
* the caller's budget discovering that. So the hint leads with the refusal to
|
|
28
|
+
* retry and closes with the reassurance the agent should relay to its user.
|
|
29
|
+
*/
|
|
30
|
+
const MAINTENANCE_HINT = 'The platform is temporarily closed for scheduled maintenance. Do not retry in a loop — wait and try again later. Deployed sites are unaffected and stay online.';
|
|
2
31
|
/**
|
|
3
32
|
* Builds the `call()` wrapper both transports use: SDK promise in, MCP
|
|
4
33
|
* `CallToolResult` out.
|
|
@@ -9,7 +38,7 @@ import { ErrorType, isShipError } from '@shipstatic/ship';
|
|
|
9
38
|
* kept equal by review.
|
|
10
39
|
*/
|
|
11
40
|
export function createCall(options) {
|
|
12
|
-
const { hints, structuredContent = false } = options;
|
|
41
|
+
const { hints, structuredContent = false, onAuthFailure } = options;
|
|
13
42
|
return async function call(fn) {
|
|
14
43
|
try {
|
|
15
44
|
const result = await fn();
|
|
@@ -28,10 +57,23 @@ export function createCall(options) {
|
|
|
28
57
|
};
|
|
29
58
|
}
|
|
30
59
|
catch (error) {
|
|
31
|
-
return toErrorResult(error, hints);
|
|
60
|
+
return toErrorResult(error, hints, onAuthFailure);
|
|
32
61
|
}
|
|
33
62
|
};
|
|
34
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the API's `details.requiredScope`, if this refusal carries one.
|
|
66
|
+
*
|
|
67
|
+
* `details` is `unknown` on the wire by design — every arm shapes it
|
|
68
|
+
* differently — so the read is a narrowing rather than a cast, and anything
|
|
69
|
+
* that is not a non-empty string means "no scope was named".
|
|
70
|
+
*/
|
|
71
|
+
function requiredScopeOf(details) {
|
|
72
|
+
if (!details || typeof details !== 'object')
|
|
73
|
+
return undefined;
|
|
74
|
+
const scope = details.requiredScope;
|
|
75
|
+
return typeof scope === 'string' && scope ? scope : undefined;
|
|
76
|
+
}
|
|
35
77
|
function isPlainObject(value) {
|
|
36
78
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
79
|
}
|
|
@@ -63,14 +105,27 @@ function isPlainObject(value) {
|
|
|
63
105
|
* governs success shapes, where the schema-twin objection lives. A failure has
|
|
64
106
|
* one published shape on every transport.
|
|
65
107
|
*/
|
|
66
|
-
function toErrorResult(error, hints) {
|
|
108
|
+
function toErrorResult(error, hints, onAuthFailure) {
|
|
67
109
|
if (isShipError(error)) {
|
|
68
110
|
let message = error.message;
|
|
111
|
+
// First, because it is the one arm that is not about this caller at all:
|
|
112
|
+
// the platform is closed, nothing the agent sends can succeed, and the
|
|
113
|
+
// useful instruction is to stop rather than to fix something.
|
|
114
|
+
if (error.isType(ErrorType.Maintenance)) {
|
|
115
|
+
message += `\n\nHint: ${MAINTENANCE_HINT}`;
|
|
116
|
+
}
|
|
69
117
|
if (error.isType(ErrorType.Authentication)) {
|
|
70
118
|
message += `\n\nHint: ${hints.authentication}`;
|
|
119
|
+
onAuthFailure?.({});
|
|
71
120
|
}
|
|
72
121
|
if (error.isType(ErrorType.Forbidden)) {
|
|
73
122
|
message += `\n\nHint: ${hints.forbidden}`;
|
|
123
|
+
// Only a MISSING SCOPE is a credential problem. The same arm carries
|
|
124
|
+
// plan limits and terminated accounts, which re-consenting cannot fix
|
|
125
|
+
// and which the caller should read as the answer it is.
|
|
126
|
+
const requiredScope = requiredScopeOf(error.details);
|
|
127
|
+
if (requiredScope)
|
|
128
|
+
onAuthFailure?.({ requiredScope });
|
|
74
129
|
}
|
|
75
130
|
if (error.isType(ErrorType.Validation) && error.details) {
|
|
76
131
|
message += `\n\nDetails: ${safeStringify(error.details)}`;
|
package/dist/index.d.ts
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* Every name below answers a question a consumer must otherwise answer for
|
|
21
21
|
* itself, and each admission was a restatement deleted, not a convenience
|
|
22
22
|
* added: `SERVER_NAME` and `UPLOAD_TOOL_NAME` were literals in two repos (the
|
|
23
|
-
* first also correlates the Apps-SDK widget to the connector
|
|
23
|
+
* first also correlates the Apps-SDK widget to the connector; the third,
|
|
24
|
+
* `UPLOAD_TOOL_TITLE`, is the same shape — one operation, one English name for
|
|
25
|
+
* it, authored per transport only because upload itself is), `PUBLIC_EXPIRY`
|
|
24
26
|
* was the same duration written out eight times, `DESCRIPTION_BLOCKS` the
|
|
25
27
|
* fragments two tool descriptions genuinely share, `ACCOUNT_TOOL_NAMES` is
|
|
26
28
|
* what lets the hosted catalogue fence name the fourteen without counting them
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
* extra — because adding an export is the quiet failure: everything published
|
|
59
61
|
* becomes a breaking change to remove.
|
|
60
62
|
*/
|
|
61
|
-
export { type CallFn, type CallOptions, createCall, type ErrorHints } from './call.js';
|
|
63
|
+
export { type AuthFailure, type CallFn, type CallOptions, createCall, type ErrorHints, } from './call.js';
|
|
62
64
|
export { createServer } from './server.js';
|
|
63
65
|
export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
|
|
64
|
-
export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, } from './vocabulary.js';
|
|
66
|
+
export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,9 @@
|
|
|
20
20
|
* Every name below answers a question a consumer must otherwise answer for
|
|
21
21
|
* itself, and each admission was a restatement deleted, not a convenience
|
|
22
22
|
* added: `SERVER_NAME` and `UPLOAD_TOOL_NAME` were literals in two repos (the
|
|
23
|
-
* first also correlates the Apps-SDK widget to the connector
|
|
23
|
+
* first also correlates the Apps-SDK widget to the connector; the third,
|
|
24
|
+
* `UPLOAD_TOOL_TITLE`, is the same shape — one operation, one English name for
|
|
25
|
+
* it, authored per transport only because upload itself is), `PUBLIC_EXPIRY`
|
|
24
26
|
* was the same duration written out eight times, `DESCRIPTION_BLOCKS` the
|
|
25
27
|
* fragments two tool descriptions genuinely share, `ACCOUNT_TOOL_NAMES` is
|
|
26
28
|
* what lets the hosted catalogue fence name the fourteen without counting them
|
|
@@ -58,7 +60,7 @@
|
|
|
58
60
|
* extra — because adding an export is the quiet failure: everything published
|
|
59
61
|
* becomes a breaking change to remove.
|
|
60
62
|
*/
|
|
61
|
-
export { createCall } from './call.js';
|
|
63
|
+
export { createCall, } from './call.js';
|
|
62
64
|
export { createServer } from './server.js';
|
|
63
65
|
export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
|
|
64
|
-
export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, } from './vocabulary.js';
|
|
66
|
+
export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
|
package/dist/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { DeploymentVia } 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, } from './vocabulary.js';
|
|
6
|
+
import { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, 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.
|
|
@@ -40,6 +40,7 @@ export function createServer(ship, options) {
|
|
|
40
40
|
});
|
|
41
41
|
// Deployments
|
|
42
42
|
server.registerTool(UPLOAD_TOOL_NAME, {
|
|
43
|
+
title: UPLOAD_TOOL_TITLE,
|
|
43
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}`,
|
|
44
45
|
annotations: CREATE,
|
|
45
46
|
inputSchema: {
|
package/dist/tools.d.ts
CHANGED
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
* on *this* transport (the hint is `createCall`'s one per-transport argument).
|
|
27
27
|
* A tool list that changes shape under the caller would be a second, dynamic
|
|
28
28
|
* contract for an agent to track, and MCP clients cache the catalogue.
|
|
29
|
+
*
|
|
30
|
+
* **Every tool carries a `title`, and it is a gate rather than a nicety.** The
|
|
31
|
+
* Claude connectors directory refuses submission for a tool that lacks one, so
|
|
32
|
+
* a titleless tool is not a shabby tool — it is an unlistable product. The
|
|
33
|
+
* style is short Title Case verb phrases naming what the USER gets ("List
|
|
34
|
+
* Deployments", "Connect Custom Domain"); the name obeys `resource_action` for
|
|
35
|
+
* the agent, the title reads as English for the human, and the description
|
|
36
|
+
* carries every precision neither can. Both catalogue pins assert a title on
|
|
37
|
+
* every tool, so the next one cannot be added without one.
|
|
29
38
|
*/
|
|
30
39
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
31
40
|
import type Ship from '@shipstatic/ship';
|
package/dist/tools.js
CHANGED
|
@@ -26,6 +26,15 @@
|
|
|
26
26
|
* on *this* transport (the hint is `createCall`'s one per-transport argument).
|
|
27
27
|
* A tool list that changes shape under the caller would be a second, dynamic
|
|
28
28
|
* contract for an agent to track, and MCP clients cache the catalogue.
|
|
29
|
+
*
|
|
30
|
+
* **Every tool carries a `title`, and it is a gate rather than a nicety.** The
|
|
31
|
+
* Claude connectors directory refuses submission for a tool that lacks one, so
|
|
32
|
+
* a titleless tool is not a shabby tool — it is an unlistable product. The
|
|
33
|
+
* style is short Title Case verb phrases naming what the USER gets ("List
|
|
34
|
+
* Deployments", "Connect Custom Domain"); the name obeys `resource_action` for
|
|
35
|
+
* the agent, the title reads as English for the human, and the description
|
|
36
|
+
* carries every precision neither can. Both catalogue pins assert a title on
|
|
37
|
+
* every tool, so the next one cannot be added without one.
|
|
29
38
|
*/
|
|
30
39
|
import { z } from 'zod';
|
|
31
40
|
import { ANNOTATIONS } from './vocabulary.js';
|
|
@@ -95,11 +104,13 @@ const DEPLOYMENT_EXAMPLE = 'happy-cat-abc1234.shipstatic.com';
|
|
|
95
104
|
export function registerAccountTools(server, ship, call) {
|
|
96
105
|
// Deployments
|
|
97
106
|
server.registerTool('deployments_list', {
|
|
107
|
+
title: 'List Deployments',
|
|
98
108
|
description: `List all deployments with their URLs, status, labels, and password protection state.${PAGING_NOTE}`,
|
|
99
109
|
annotations: READ,
|
|
100
110
|
inputSchema: PAGINATION_INPUT,
|
|
101
111
|
}, ({ limit, cursor }) => call(() => ship.deployments.list({ limit, cursor })));
|
|
102
112
|
server.registerTool('deployments_get', {
|
|
113
|
+
title: 'Get Deployment',
|
|
103
114
|
description: 'Get deployment details including URL, status, file count, size, labels, and password protection state.',
|
|
104
115
|
annotations: READ,
|
|
105
116
|
inputSchema: {
|
|
@@ -109,6 +120,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
109
120
|
},
|
|
110
121
|
}, ({ deployment }) => call(() => ship.deployments.get(deployment)));
|
|
111
122
|
server.registerTool('deployments_set', {
|
|
123
|
+
title: 'Update Deployment Labels',
|
|
112
124
|
description: 'Update deployment labels. Replaces all existing labels.',
|
|
113
125
|
annotations: WRITE,
|
|
114
126
|
inputSchema: {
|
|
@@ -121,6 +133,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
121
133
|
},
|
|
122
134
|
}, ({ deployment, labels }) => call(() => ship.deployments.set(deployment, { labels })));
|
|
123
135
|
server.registerTool('deployments_delete', {
|
|
136
|
+
title: 'Delete Deployment',
|
|
124
137
|
description: 'Permanently delete a deployment and its files. You MUST confirm with the user before calling this tool, referencing the deployment.',
|
|
125
138
|
annotations: DESTRUCTIVE,
|
|
126
139
|
inputSchema: {
|
|
@@ -131,6 +144,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
131
144
|
}, ({ deployment }) => call(() => ship.deployments.delete(deployment)));
|
|
132
145
|
// Domains
|
|
133
146
|
server.registerTool('domains_set', {
|
|
147
|
+
title: 'Connect Custom Domain',
|
|
134
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.',
|
|
135
149
|
annotations: WRITE,
|
|
136
150
|
inputSchema: {
|
|
@@ -146,11 +160,13 @@ export function registerAccountTools(server, ship, call) {
|
|
|
146
160
|
},
|
|
147
161
|
}, ({ domain, deployment, labels }) => call(() => ship.domains.set(domain, { deployment, labels })));
|
|
148
162
|
server.registerTool('domains_list', {
|
|
163
|
+
title: 'List Domains',
|
|
149
164
|
description: `List all domains with their URLs, linked deployment, and verification status.${PAGING_NOTE}`,
|
|
150
165
|
annotations: READ,
|
|
151
166
|
inputSchema: PAGINATION_INPUT,
|
|
152
167
|
}, ({ limit, cursor }) => call(() => ship.domains.list({ limit, cursor })));
|
|
153
168
|
server.registerTool('domains_get', {
|
|
169
|
+
title: 'Get Domain',
|
|
154
170
|
description: 'Get domain details including URL, linked deployment, verification status, and labels.',
|
|
155
171
|
annotations: READ,
|
|
156
172
|
inputSchema: {
|
|
@@ -160,6 +176,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
160
176
|
},
|
|
161
177
|
}, ({ domain }) => call(() => ship.domains.get(domain)));
|
|
162
178
|
server.registerTool('domains_records', {
|
|
179
|
+
title: 'Get DNS Records',
|
|
163
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.',
|
|
164
181
|
annotations: READ,
|
|
165
182
|
inputSchema: {
|
|
@@ -169,6 +186,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
169
186
|
},
|
|
170
187
|
}, ({ domain }) => call(() => ship.domains.records(domain)));
|
|
171
188
|
server.registerTool('domains_dns', {
|
|
189
|
+
title: 'Look Up DNS Provider',
|
|
172
190
|
description: 'Look up the DNS provider for a domain (e.g. Cloudflare, Namecheap). Helps the user know where to configure their DNS records.',
|
|
173
191
|
annotations: READ,
|
|
174
192
|
inputSchema: {
|
|
@@ -178,6 +196,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
178
196
|
},
|
|
179
197
|
}, ({ domain }) => call(() => ship.domains.dns(domain)));
|
|
180
198
|
server.registerTool('domains_share', {
|
|
199
|
+
title: 'Share DNS Setup',
|
|
181
200
|
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.',
|
|
182
201
|
annotations: READ,
|
|
183
202
|
inputSchema: {
|
|
@@ -187,6 +206,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
187
206
|
},
|
|
188
207
|
}, ({ domain }) => call(() => ship.domains.share(domain)));
|
|
189
208
|
server.registerTool('domains_validate', {
|
|
209
|
+
title: 'Check Domain Availability',
|
|
190
210
|
description: 'Check if a domain name is valid and available before creating it. Returns the normalized form and availability.',
|
|
191
211
|
annotations: READ,
|
|
192
212
|
inputSchema: {
|
|
@@ -196,6 +216,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
196
216
|
},
|
|
197
217
|
}, ({ domain }) => call(() => ship.domains.validate(domain)));
|
|
198
218
|
server.registerTool('domains_verify', {
|
|
219
|
+
title: 'Verify Domain DNS',
|
|
199
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.',
|
|
200
221
|
annotations: WRITE,
|
|
201
222
|
inputSchema: {
|
|
@@ -205,6 +226,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
205
226
|
},
|
|
206
227
|
}, ({ domain }) => call(() => ship.domains.verify(domain)));
|
|
207
228
|
server.registerTool('domains_delete', {
|
|
229
|
+
title: 'Delete Domain',
|
|
208
230
|
description: 'Permanently delete a domain. You MUST confirm with the user before calling this tool, referencing the domain name.',
|
|
209
231
|
annotations: DESTRUCTIVE,
|
|
210
232
|
inputSchema: {
|
|
@@ -213,6 +235,7 @@ export function registerAccountTools(server, ship, call) {
|
|
|
213
235
|
}, ({ domain }) => call(() => ship.domains.delete(domain)));
|
|
214
236
|
// Account
|
|
215
237
|
server.registerTool('whoami', {
|
|
238
|
+
title: 'Show Account',
|
|
216
239
|
description: 'Show authenticated account details including email, plan, and usage.',
|
|
217
240
|
annotations: READ,
|
|
218
241
|
}, () => call(() => ship.whoami()));
|
package/dist/vocabulary.d.ts
CHANGED
|
@@ -60,6 +60,26 @@ export declare const SERVER_NAME = "shipstatic";
|
|
|
60
60
|
* `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen.
|
|
61
61
|
*/
|
|
62
62
|
export declare const UPLOAD_TOOL_NAME = "deployments_upload";
|
|
63
|
+
/**
|
|
64
|
+
* The upload tool's human-readable `title`, shared for the same reason the
|
|
65
|
+
* name is — and NOT for the reason the description is not.
|
|
66
|
+
*
|
|
67
|
+
* A title names the OPERATION, and the operation is identical on both doors:
|
|
68
|
+
* a user reading "Deploy Static Site" in a client's tool list learns nothing
|
|
69
|
+
* about how the bytes got there. The description is the opposite — hosted
|
|
70
|
+
* spends a paragraph telling an Apps-SDK caller not to base64-encode text, a
|
|
71
|
+
* hazard a filesystem path cannot have — which is why one is exported whole
|
|
72
|
+
* and the other only in fragments.
|
|
73
|
+
*
|
|
74
|
+
* The other fourteen titles live inline in `tools.ts`: one definition, both
|
|
75
|
+
* transports, nothing to keep in agreement. This one is authored per transport
|
|
76
|
+
* (upload is the tool each door writes for itself), so without an owner it
|
|
77
|
+
* would be two literals in two repos with nothing comparing them.
|
|
78
|
+
*
|
|
79
|
+
* Titles are not decoration here: the Claude connectors directory refuses
|
|
80
|
+
* submission for a tool that lacks one.
|
|
81
|
+
*/
|
|
82
|
+
export declare const UPLOAD_TOOL_TITLE = "Deploy Static Site";
|
|
63
83
|
/**
|
|
64
84
|
* How long an anonymous deployment lives, in the words an agent reads.
|
|
65
85
|
*
|
package/dist/vocabulary.js
CHANGED
|
@@ -62,6 +62,26 @@ export const SERVER_NAME = 'shipstatic';
|
|
|
62
62
|
* `[UPLOAD_TOOL_NAME, ...ACCOUNT_TOOL_NAMES]` rather than counting to fifteen.
|
|
63
63
|
*/
|
|
64
64
|
export const UPLOAD_TOOL_NAME = 'deployments_upload';
|
|
65
|
+
/**
|
|
66
|
+
* The upload tool's human-readable `title`, shared for the same reason the
|
|
67
|
+
* name is — and NOT for the reason the description is not.
|
|
68
|
+
*
|
|
69
|
+
* A title names the OPERATION, and the operation is identical on both doors:
|
|
70
|
+
* a user reading "Deploy Static Site" in a client's tool list learns nothing
|
|
71
|
+
* about how the bytes got there. The description is the opposite — hosted
|
|
72
|
+
* spends a paragraph telling an Apps-SDK caller not to base64-encode text, a
|
|
73
|
+
* hazard a filesystem path cannot have — which is why one is exported whole
|
|
74
|
+
* and the other only in fragments.
|
|
75
|
+
*
|
|
76
|
+
* The other fourteen titles live inline in `tools.ts`: one definition, both
|
|
77
|
+
* transports, nothing to keep in agreement. This one is authored per transport
|
|
78
|
+
* (upload is the tool each door writes for itself), so without an owner it
|
|
79
|
+
* would be two literals in two repos with nothing comparing them.
|
|
80
|
+
*
|
|
81
|
+
* Titles are not decoration here: the Claude connectors directory refuses
|
|
82
|
+
* submission for a tool that lacks one.
|
|
83
|
+
*/
|
|
84
|
+
export const UPLOAD_TOOL_TITLE = 'Deploy Static Site';
|
|
65
85
|
/**
|
|
66
86
|
* How long an anonymous deployment lives, in the words an agent reads.
|
|
67
87
|
*
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipstatic/mcp",
|
|
3
|
-
"version": "1.0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"mcpName": "com.shipstatic/mcp",
|
|
5
|
-
"description": "ShipStatic MCP
|
|
5
|
+
"description": "ShipStatic MCP \u2014 deploy static websites from AI agents. Full toolset incl. custom domains. Free hosted endpoint at mcp.shipstatic.com \u2014 no install.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/index.js",
|
|
8
8
|
"sideEffects": [
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc",
|
|
24
|
+
"prepack": "pnpm run build",
|
|
24
25
|
"clean": "rm -rf dist",
|
|
25
26
|
"prepare": "git config core.hooksPath scripts/githooks",
|
|
26
27
|
"test": "vitest",
|
|
@@ -66,8 +67,8 @@
|
|
|
66
67
|
"license": "MIT",
|
|
67
68
|
"dependencies": {
|
|
68
69
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
69
|
-
"@shipstatic/ship": "2.0.0
|
|
70
|
-
"@shipstatic/types": "2.5.0
|
|
70
|
+
"@shipstatic/ship": "2.0.0",
|
|
71
|
+
"@shipstatic/types": "2.5.0",
|
|
71
72
|
"zod": "^4.4.3"
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
|
@@ -78,5 +79,9 @@
|
|
|
78
79
|
"typescript": "^6.0.3",
|
|
79
80
|
"vitest": "4.1.10"
|
|
80
81
|
},
|
|
81
|
-
"packageManager": "pnpm@10.12.4"
|
|
82
|
+
"packageManager": "pnpm@10.12.4",
|
|
83
|
+
"publishConfig": {
|
|
84
|
+
"access": "public",
|
|
85
|
+
"provenance": true
|
|
86
|
+
}
|
|
82
87
|
}
|