@shipstatic/mcp 1.0.0-beta.12 → 1.0.0-beta.14

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
@@ -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
@@ -60,7 +60,7 @@
60
60
  * extra — because adding an export is the quiet failure: everything published
61
61
  * becomes a breaking change to remove.
62
62
  */
63
- 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';
64
64
  export { createServer } from './server.js';
65
65
  export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
66
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
@@ -60,7 +60,7 @@
60
60
  * extra — because adding an export is the quiet failure: everything published
61
61
  * becomes a breaking change to remove.
62
62
  */
63
- export { createCall } from './call.js';
63
+ export { createCall, } from './call.js';
64
64
  export { createServer } from './server.js';
65
65
  export { ACCOUNT_TOOL_NAMES, registerAccountTools } from './tools.js';
66
66
  export { ANNOTATIONS, DESCRIPTION_BLOCKS, INSTRUCTION_BLOCKS, PARAM_DESCRIPTIONS, PUBLIC_EXPIRY, SERVER_NAME, UPLOAD_TOOL_NAME, UPLOAD_TOOL_TITLE, } from './vocabulary.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipstatic/mcp",
3
- "version": "1.0.0-beta.12",
3
+ "version": "1.0.0-beta.14",
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",
@@ -66,8 +66,8 @@
66
66
  "license": "MIT",
67
67
  "dependencies": {
68
68
  "@modelcontextprotocol/sdk": "^1.30.0",
69
- "@shipstatic/ship": "2.0.0-beta.18",
70
- "@shipstatic/types": "2.5.0-beta.22",
69
+ "@shipstatic/ship": "2.0.0-beta.20",
70
+ "@shipstatic/types": "2.5.0-beta.23",
71
71
  "zod": "^4.4.3"
72
72
  },
73
73
  "devDependencies": {