@nifrajs/mcp 2.4.0 → 2.6.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/http.d.ts +9 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +116 -13
- package/dist/http.js.map +1 -1
- package/dist/protocol.d.ts +22 -2
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +106 -22
- package/dist/protocol.js.map +1 -1
- package/dist/server.d.ts +5 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/tool.d.ts +4 -1
- package/dist/tool.d.ts.map +1 -1
- package/dist/tool.js +1 -0
- package/dist/tool.js.map +1 -1
- package/package.json +1 -1
- package/src/http.ts +133 -12
- package/src/protocol.ts +166 -42
- package/src/server.ts +7 -0
- package/src/tool.ts +11 -1
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EAEb,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;IACnC,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAA;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAA;IACvC,4BAA4B;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,sDAAsD;IACtD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,OAAO,EAEb,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,OAAO,EAAE,CAAA;IACnC,6FAA6F;IAC7F,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAA;IACvC,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,WAAW,EAAE,CAAA;IAC3C,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,SAAS,EAAE,CAAA;IACvC,4BAA4B;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,sDAAsD;IACtD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,wGAAwG;IACxG,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B;uEACmE;IACnE,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC5C;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,CAAA;IACpC,QAAQ,CAAC,UAAU,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAA;IACtD,qGAAqG;IACrG,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1C,mGAAmG;IACnG,MAAM,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAA;CACjE;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,SAAS,CA0BvE"}
|
package/dist/server.js
CHANGED
|
@@ -32,6 +32,7 @@ export function createMcpServer(opts) {
|
|
|
32
32
|
...(resources.length > 0 ? { resources } : {}),
|
|
33
33
|
...(opts.prompts !== undefined ? { prompts: opts.prompts } : {}),
|
|
34
34
|
...(widgets.length > 0 ? { ui: { mimeTypes: [UI_MIME] } } : {}),
|
|
35
|
+
...(opts.instructions !== undefined ? { instructions: opts.instructions } : {}),
|
|
35
36
|
};
|
|
36
37
|
return {
|
|
37
38
|
tools,
|
|
@@ -41,6 +42,7 @@ export function createMcpServer(opts) {
|
|
|
41
42
|
features,
|
|
42
43
|
...(opts.health !== undefined ? { health: opts.health } : {}),
|
|
43
44
|
...(opts.maxBodyBytes !== undefined ? { maxBodyBytes: opts.maxBodyBytes } : {}),
|
|
45
|
+
...(opts.allowedOrigins !== undefined ? { allowedOrigins: opts.allowedOrigins } : {}),
|
|
44
46
|
}),
|
|
45
47
|
handle: (message) => handleRpc(message, tools, serverInfo, features),
|
|
46
48
|
};
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EACL,SAAS,EAOT,OAAO,GACR,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1C,OAAO,EACL,SAAS,EAOT,OAAO,GACR,MAAM,eAAe,CAAA;AAiCtB,MAAM,UAAU,eAAe,CAAC,IAA4B;IAC1D,MAAM,KAAK,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,CAAA;IACrC,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAA;IAClC,sGAAsG;IACtG,gDAAgD;IAChD,MAAM,SAAS,GAAkB,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAA;IAC/F,MAAM,UAAU,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAA;IAC7D,MAAM,QAAQ,GAAsB;QAClC,GAAG,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,GAAG,CAAC,IAAI,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,SAAS,EAAE,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/D,GAAG,CAAC,IAAI,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAChF,CAAA;IACD,OAAO;QACL,KAAK;QACL,QAAQ;QACR,UAAU;QACV,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CACjB,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE;YACzC,QAAQ;YACR,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,GAAG,CAAC,IAAI,CAAC,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC/E,GAAG,CAAC,IAAI,CAAC,cAAc,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACtF,CAAC;QACJ,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,QAAQ,CAAC;KACrE,CAAA;AACH,CAAC"}
|
package/dist/tool.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `widget` is passed, the tool advertises it via `_meta.ui.resourceUri` on both `tools/list` and each
|
|
7
7
|
* `tools/call` result, so the host knows to render the widget - see {@link ../protocol.ts}.
|
|
8
8
|
*/
|
|
9
|
-
import type { McpContentBlock, McpTool, McpToolContext } from "./protocol.js";
|
|
9
|
+
import type { McpContentBlock, McpTool, McpToolAnnotations, McpToolContext } from "./protocol.js";
|
|
10
10
|
import type { InferOutput, StandardSchemaV1 } from "./standard.js";
|
|
11
11
|
import type { McpWidget } from "./widget.js";
|
|
12
12
|
/** The ergonomic result an MCP-tool handler may return (coerced to the protocol's {@link McpToolResult}). */
|
|
@@ -45,6 +45,9 @@ export interface DefineMcpToolOptions<S extends StandardSchemaV1 = UntypedArgs>
|
|
|
45
45
|
* whose schemas carry no JSON Schema, so the agent still sees real parameter docs.
|
|
46
46
|
*/
|
|
47
47
|
readonly inputSchema?: Record<string, unknown>;
|
|
48
|
+
/** MCP tool safety hints (`readOnlyHint`/`destructiveHint`/…) surfaced in `tools/list`. Hosts use
|
|
49
|
+
* them to decide confirmation UX, and connector directory reviews expect every tool to carry them. */
|
|
50
|
+
readonly annotations?: McpToolAnnotations;
|
|
48
51
|
/** Link a widget so the tool's result renders as interactive UI in MCP Apps hosts (iframe). */
|
|
49
52
|
readonly widget?: McpWidget;
|
|
50
53
|
/** Render-intent for generative hosts that build their own themed UI from `structuredContent`. Lands
|
package/dist/tool.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"tool.d.ts","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EACV,eAAe,EACf,OAAO,EACP,kBAAkB,EAClB,cAAc,EAEf,MAAM,eAAe,CAAA;AACtB,OAAO,KAAK,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAElE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAE5C,6GAA6G;AAC7G,MAAM,MAAM,oBAAoB,GAC5B,MAAM,GACN;IACE,yFAAyF;IACzF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,kDAAkD;IAClD,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS,eAAe,EAAE,CAAA;IAC7C,gFAAgF;IAChF,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACpD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACxC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAC3B,CAAA;AAEL;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,MAAM,GACN,OAAO,GACP,MAAM,GACN,QAAQ,GACR,QAAQ,GACR,OAAO,GACP,CAAC,MAAM,GAAG,EAAE,CAAC,CAAA;AAEjB,6FAA6F;AAC7F,KAAK,WAAW,GAAG,gBAAgB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAErE,MAAM,WAAW,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,GAAG,WAAW;IAC5E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B;;;;;OAKG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAA;IAClB;;;;OAIG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9C;0GACsG;IACtG,QAAQ,CAAC,WAAW,CAAC,EAAE,kBAAkB,CAAA;IACzC,+FAA+F;IAC/F,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAA;IAC3B;;0CAEsC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAA;IAC7B,QAAQ,CAAC,OAAO,EAAE,CAChB,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,EACpB,OAAO,EAAE,cAAc,KACpB,oBAAoB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAAA;CAC1D;AAgCD,wBAAgB,aAAa,CAAC,CAAC,SAAS,gBAAgB,GAAG,WAAW,EACpE,IAAI,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAC5B,OAAO,CA4BT"}
|
package/dist/tool.js
CHANGED
|
@@ -46,6 +46,7 @@ export function defineMcpTool(opts) {
|
|
|
46
46
|
inputSchema: opts.inputSchema ??
|
|
47
47
|
(input !== undefined ? jsonSchemaOf(input) : undefined) ??
|
|
48
48
|
EMPTY_OBJECT_SCHEMA,
|
|
49
|
+
...(opts.annotations !== undefined ? { annotations: opts.annotations } : {}),
|
|
49
50
|
...(meta !== undefined ? { _meta: meta } : {}),
|
|
50
51
|
handler: async (args, context) => {
|
|
51
52
|
let validated = args;
|
package/dist/tool.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool.js","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;
|
|
1
|
+
{"version":3,"file":"tool.js","sourceRoot":"","sources":["../src/tool.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nifrajs/mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Build MCP servers - and MCP Apps (interactive `ui://` widgets, SEP-1865) - for a nifra app. The transport-agnostic JSON-RPC core (also used by `@nifrajs/cli`'s dev MCP), a Streamable-HTTP handler you mount at `POST /mcp`, and `defineMcpWidget`/`defineMcpTool` for tool results that render real UI in MCP hosts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
package/src/http.ts
CHANGED
|
@@ -10,15 +10,45 @@
|
|
|
10
10
|
import {
|
|
11
11
|
handleRpc,
|
|
12
12
|
type JsonRpcRequest,
|
|
13
|
+
type JsonRpcResponse,
|
|
14
|
+
MCP_ERROR,
|
|
13
15
|
type McpServerFeatures,
|
|
14
16
|
type McpTool,
|
|
17
|
+
modernVersionOf,
|
|
15
18
|
rpcError,
|
|
16
19
|
} from "./protocol.ts"
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
// Headers on every response. `access-control-allow-headers` lists the request headers a browser MCP client
|
|
22
|
+
// (claude.ai, ChatGPT) actually sends: `mcp-protocol-version` (required per POST since 2025-06-18),
|
|
23
|
+
// `mcp-method`/`mcp-name` (the 2026-07-28 request-metadata mirror), `authorization` (OAuth-protected hosts),
|
|
24
|
+
// plus `accept`/`last-event-id`. Omit any and the CORS preflight fails, so the connector never loads.
|
|
25
|
+
const CORS_BASE: Record<string, string> = {
|
|
20
26
|
"access-control-allow-methods": "POST, GET, OPTIONS",
|
|
21
|
-
"access-control-allow-headers":
|
|
27
|
+
"access-control-allow-headers":
|
|
28
|
+
"content-type, authorization, mcp-protocol-version, mcp-session-id, mcp-method, mcp-name, accept, last-event-id",
|
|
29
|
+
"access-control-expose-headers": "mcp-protocol-version, mcp-session-id",
|
|
30
|
+
"access-control-max-age": "86400",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the CORS/Origin headers for one request against the host's `allowedOrigins` policy, or `null`
|
|
35
|
+
* when the request's `Origin` is present but not allowed - the caller then answers 403, per the
|
|
36
|
+
* Streamable-HTTP DNS-rebinding rule ("Servers MUST validate the `Origin` header ... respond with 403").
|
|
37
|
+
* With no policy (the default) every origin is allowed and reflected as `*` - correct for a public,
|
|
38
|
+
* secret-free, unauthenticated server, whose browser clients send arbitrary origins we can't enumerate.
|
|
39
|
+
*/
|
|
40
|
+
function corsFor(
|
|
41
|
+
request: Request,
|
|
42
|
+
allowedOrigins: readonly string[] | undefined,
|
|
43
|
+
): Record<string, string> | null {
|
|
44
|
+
if (allowedOrigins === undefined) return { ...CORS_BASE, "access-control-allow-origin": "*" }
|
|
45
|
+
const origin = request.headers.get("origin")
|
|
46
|
+
// A caller with no Origin (curl, server-to-server) can't mount a DNS-rebinding attack - allow it.
|
|
47
|
+
if (origin === null) return { ...CORS_BASE, vary: "Origin" }
|
|
48
|
+
if (allowedOrigins.includes(origin)) {
|
|
49
|
+
return { ...CORS_BASE, "access-control-allow-origin": origin, vary: "Origin" }
|
|
50
|
+
}
|
|
51
|
+
return null
|
|
22
52
|
}
|
|
23
53
|
|
|
24
54
|
const DEFAULT_MAX_BODY_BYTES = 1_000_000
|
|
@@ -31,6 +61,12 @@ export interface McpHttpOptions {
|
|
|
31
61
|
readonly features?: McpServerFeatures
|
|
32
62
|
/** Shown on the GET health page so each host can describe itself. */
|
|
33
63
|
readonly health?: string
|
|
64
|
+
/**
|
|
65
|
+
* Origin allowlist for the DNS-rebinding guard. Omit (the default) to allow any origin - the right
|
|
66
|
+
* choice for a public, unauthenticated docs server that can't enumerate its browser clients' origins.
|
|
67
|
+
* Set it (e.g. a localhost origin for a hardened local host) to reject any other browser origin with 403.
|
|
68
|
+
*/
|
|
69
|
+
readonly allowedOrigins?: readonly string[]
|
|
34
70
|
}
|
|
35
71
|
|
|
36
72
|
function parseContentLength(value: string): number | undefined {
|
|
@@ -86,10 +122,69 @@ async function readJsonBounded(
|
|
|
86
122
|
}
|
|
87
123
|
}
|
|
88
124
|
|
|
125
|
+
const SENTINEL_PREFIX = "=?base64?"
|
|
126
|
+
const SENTINEL_SUFFIX = "?="
|
|
127
|
+
|
|
128
|
+
/** Decode the Streamable-HTTP `=?base64?...?=` header sentinel (used when a name/uri isn't header-safe).
|
|
129
|
+
* A plain value passes through; a malformed sentinel is returned as-is so validation still fails closed. */
|
|
130
|
+
function decodeSentinel(value: string | null): string | null {
|
|
131
|
+
if (value === null) return null
|
|
132
|
+
if (!value.startsWith(SENTINEL_PREFIX) || !value.endsWith(SENTINEL_SUFFIX)) return value
|
|
133
|
+
const encoded = value.slice(SENTINEL_PREFIX.length, value.length - SENTINEL_SUFFIX.length)
|
|
134
|
+
try {
|
|
135
|
+
return TEXT_DECODER.decode(Uint8Array.from(atob(encoded), (ch) => ch.charCodeAt(0)))
|
|
136
|
+
} catch {
|
|
137
|
+
return value
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Validate a modern (2026-07-28) request's mirrored headers against its body: `MCP-Protocol-Version`,
|
|
142
|
+
* `Mcp-Method`, and - for name-bearing methods - `Mcp-Name` MUST match, else a `HeaderMismatch` the caller
|
|
143
|
+
* returns as 400. Stops an intermediary from routing on a header while the server acts on a different body. */
|
|
144
|
+
function modernHeaderMismatch(
|
|
145
|
+
request: Request,
|
|
146
|
+
message: JsonRpcRequest,
|
|
147
|
+
bodyVersion: string,
|
|
148
|
+
): JsonRpcResponse | null {
|
|
149
|
+
const rid = message.id ?? null
|
|
150
|
+
const headerVersion = request.headers.get("mcp-protocol-version")
|
|
151
|
+
if (headerVersion === null || headerVersion !== bodyVersion) {
|
|
152
|
+
return rpcError(
|
|
153
|
+
rid,
|
|
154
|
+
MCP_ERROR.HEADER_MISMATCH,
|
|
155
|
+
"MCP-Protocol-Version header missing or mismatched",
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
const method = typeof message.method === "string" ? message.method : ""
|
|
159
|
+
const headerMethod = request.headers.get("mcp-method")
|
|
160
|
+
if (headerMethod === null || headerMethod !== method) {
|
|
161
|
+
return rpcError(rid, MCP_ERROR.HEADER_MISMATCH, "Mcp-Method header missing or mismatched")
|
|
162
|
+
}
|
|
163
|
+
if (method === "tools/call" || method === "resources/read" || method === "prompts/get") {
|
|
164
|
+
const bodyName = method === "resources/read" ? message.params?.uri : message.params?.name
|
|
165
|
+
const headerName = decodeSentinel(request.headers.get("mcp-name"))
|
|
166
|
+
if (headerName === null || typeof bodyName !== "string" || headerName !== bodyName) {
|
|
167
|
+
return rpcError(rid, MCP_ERROR.HEADER_MISMATCH, "Mcp-Name header missing or mismatched")
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** HTTP status for a modern request's dispatch result: 400 for version/header errors, 404 for an unknown
|
|
174
|
+
* method, 200 otherwise (in-band tool errors are ordinary JSON-RPC results and stay 200). */
|
|
175
|
+
function modernErrorStatus(response: JsonRpcResponse): number {
|
|
176
|
+
if (!("error" in response)) return 200
|
|
177
|
+
const { code } = response.error
|
|
178
|
+
if (code === MCP_ERROR.UNSUPPORTED_VERSION || code === MCP_ERROR.HEADER_MISMATCH) return 400
|
|
179
|
+
return code === -32601 ? 404 : 200
|
|
180
|
+
}
|
|
181
|
+
|
|
89
182
|
/**
|
|
90
183
|
* Handle one MCP request over HTTP against the given `tools`/`features`. POST a JSON-RPC body → JSON-RPC
|
|
91
184
|
* response; GET → a plain-text health page; OPTIONS → CORS preflight. Never throws - a bad body becomes a
|
|
92
|
-
* JSON-RPC parse error.
|
|
185
|
+
* JSON-RPC parse error. Dual-era: a modern (2026-07-28) POST that mirrors its method/name/version into
|
|
186
|
+
* headers is validated against the body before dispatch and gets spec HTTP statuses; legacy requests are
|
|
187
|
+
* served unchanged. The dispatch is the shared, transport-agnostic {@link handleRpc}.
|
|
93
188
|
*/
|
|
94
189
|
export async function respondMcpHttp(
|
|
95
190
|
request: Request,
|
|
@@ -97,18 +192,33 @@ export async function respondMcpHttp(
|
|
|
97
192
|
serverInfo: { name: string; version: string },
|
|
98
193
|
options: McpHttpOptions = {},
|
|
99
194
|
): Promise<Response> {
|
|
100
|
-
|
|
195
|
+
const cors = corsFor(request, options.allowedOrigins)
|
|
196
|
+
if (cors === null) {
|
|
197
|
+
// Origin present but not allowlisted: reject before the body is ever read (DNS-rebinding guard). No
|
|
198
|
+
// `id` (no request parsed) and no CORS headers - a disallowed origin gets nothing to work with.
|
|
199
|
+
return Response.json(rpcError(null, -32600, "origin not allowed"), { status: 403 })
|
|
200
|
+
}
|
|
201
|
+
if (request.method === "OPTIONS") return new Response(null, { status: 204, headers: cors })
|
|
101
202
|
if (request.method === "GET") {
|
|
203
|
+
// The 2026-07-28 revision removed the GET SSE stream; a client probing for it (`Accept:
|
|
204
|
+
// text/event-stream`) gets 405 so its transport fallback fires. A plain browser/monitor GET still
|
|
205
|
+
// gets the human-readable health page.
|
|
206
|
+
if ((request.headers.get("accept") ?? "").includes("text/event-stream")) {
|
|
207
|
+
return new Response("event stream not supported", {
|
|
208
|
+
status: 405,
|
|
209
|
+
headers: { allow: "POST, GET", ...cors },
|
|
210
|
+
})
|
|
211
|
+
}
|
|
102
212
|
return new Response(
|
|
103
213
|
options.health ??
|
|
104
214
|
"MCP server - POST JSON-RPC 2.0 here (methods: initialize, tools/list, tools/call).",
|
|
105
|
-
{ headers: { "content-type": "text/plain; charset=utf-8", ...
|
|
215
|
+
{ headers: { "content-type": "text/plain; charset=utf-8", ...cors } },
|
|
106
216
|
)
|
|
107
217
|
}
|
|
108
218
|
if (request.method !== "POST") {
|
|
109
219
|
return new Response("method not allowed", {
|
|
110
220
|
status: 405,
|
|
111
|
-
headers: { allow: "POST, GET", ...
|
|
221
|
+
headers: { allow: "POST, GET", ...cors },
|
|
112
222
|
})
|
|
113
223
|
}
|
|
114
224
|
const parsed = await readJsonBounded(request, options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES)
|
|
@@ -116,16 +226,27 @@ export async function respondMcpHttp(
|
|
|
116
226
|
if (parsed.status === 413) {
|
|
117
227
|
return Response.json(rpcError(null, -32000, "payload too large"), {
|
|
118
228
|
status: 413,
|
|
119
|
-
headers:
|
|
229
|
+
headers: cors,
|
|
120
230
|
})
|
|
121
231
|
}
|
|
122
|
-
return Response.json(rpcError(null, -32700, "parse error"), { status: 400, headers:
|
|
232
|
+
return Response.json(rpcError(null, -32700, "parse error"), { status: 400, headers: cors })
|
|
123
233
|
}
|
|
124
234
|
const message = parsed.value as JsonRpcRequest
|
|
235
|
+
const bodyVersion = modernVersionOf(message.params)
|
|
236
|
+
// Mirror the protocol version back for intermediaries (SHOULD, 2025-06-18+); absent is fine.
|
|
237
|
+
const echoVersion = request.headers.get("mcp-protocol-version")
|
|
238
|
+
const headers = echoVersion !== null ? { ...cors, "mcp-protocol-version": echoVersion } : cors
|
|
239
|
+
if (bodyVersion !== undefined) {
|
|
240
|
+
// Modern request: header/body mirror MUST agree. Reject divergence with 400 before dispatch.
|
|
241
|
+
const headerError = modernHeaderMismatch(request, message, bodyVersion)
|
|
242
|
+
if (headerError !== null) return Response.json(headerError, { status: 400, headers })
|
|
243
|
+
}
|
|
125
244
|
const response = await handleRpc(message, tools, serverInfo, options.features ?? {}, {
|
|
126
245
|
signal: request.signal,
|
|
127
246
|
})
|
|
128
|
-
// A notification (no id) yields null - acknowledge with
|
|
129
|
-
if (response === null) return new Response(null, { status:
|
|
130
|
-
|
|
247
|
+
// A notification (no id) yields null - acknowledge with 202 Accepted and no body (Streamable-HTTP).
|
|
248
|
+
if (response === null) return new Response(null, { status: 202, headers })
|
|
249
|
+
// Modern requests carry spec HTTP statuses (400 version/header, 404 unknown method); legacy stays 200.
|
|
250
|
+
const status = bodyVersion !== undefined ? modernErrorStatus(response) : 200
|
|
251
|
+
return Response.json(response, { status, headers })
|
|
131
252
|
}
|
package/src/protocol.ts
CHANGED
|
@@ -10,7 +10,28 @@
|
|
|
10
10
|
* it is additive - a tool whose handler returns a plain `string` behaves exactly as before.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
/** The protocol revision this server advertises by default. A handshake-era version - the HTTP transport
|
|
14
|
+
* still uses `initialize`; current clients reach it through their backward-compatibility fallback.
|
|
15
|
+
* `initialize` echoes the client's own requested version instead when it's one this server also speaks
|
|
16
|
+
* ({@link SUPPORTED_PROTOCOL_VERSIONS}). */
|
|
17
|
+
export const PROTOCOL_VERSION = "2025-06-18"
|
|
18
|
+
/** The handshake-era revisions this server speaks. `initialize` echoes the client's requested
|
|
19
|
+
* `protocolVersion` when it's in this set (the smoothest negotiation), else answers {@link PROTOCOL_VERSION}. */
|
|
20
|
+
const SUPPORTED_PROTOCOL_VERSIONS = new Set([
|
|
21
|
+
"2024-11-05",
|
|
22
|
+
"2025-03-26",
|
|
23
|
+
"2025-06-18",
|
|
24
|
+
"2025-11-25",
|
|
25
|
+
])
|
|
26
|
+
|
|
27
|
+
/** The stateless per-request revision (2026-07-28+). A request carrying its version in `_meta`
|
|
28
|
+
* (`io.modelcontextprotocol/protocolVersion`) is served in modern mode; an `initialize` request stays
|
|
29
|
+
* legacy. This server is dual-era: `handleRpc` answers both on the same dispatch, per request. */
|
|
30
|
+
export const MODERN_PROTOCOL_VERSION = "2026-07-28"
|
|
31
|
+
/** MCP-reserved JSON-RPC error codes (2026-07-28 error-code policy carves out -32020..-32099 for the spec). */
|
|
32
|
+
export const MCP_ERROR = { HEADER_MISMATCH: -32020, UNSUPPORTED_VERSION: -32022 } as const
|
|
33
|
+
const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion"
|
|
34
|
+
const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo"
|
|
14
35
|
|
|
15
36
|
/** The MIME type a UI resource MUST use so a host recognizes it as an MCP App widget (SEP-1865). */
|
|
16
37
|
export const UI_MIME = "text/html;profile=mcp-app"
|
|
@@ -103,6 +124,9 @@ export interface McpServerFeatures {
|
|
|
103
124
|
/** Advertise the MCP Apps UI extension in `initialize`. Present when the server serves `ui://` widgets;
|
|
104
125
|
* `mimeTypes` defaults to `[UI_MIME]`. */
|
|
105
126
|
readonly ui?: { readonly mimeTypes?: readonly string[] }
|
|
127
|
+
/** Natural-language guidance for LLMs on how to use this server. Surfaced in the modern
|
|
128
|
+
* `server/discover` result (2026-07-28); legacy `initialize` does not carry it. */
|
|
129
|
+
readonly instructions?: string
|
|
106
130
|
}
|
|
107
131
|
|
|
108
132
|
export interface JsonRpcRequest {
|
|
@@ -120,17 +144,22 @@ export interface JsonRpcNotification {
|
|
|
120
144
|
|
|
121
145
|
export type JsonRpcResponse =
|
|
122
146
|
| { jsonrpc: "2.0"; id: JsonRpcId; result: unknown }
|
|
123
|
-
| { jsonrpc: "2.0"; id: JsonRpcId; error: { code: number; message: string } }
|
|
147
|
+
| { jsonrpc: "2.0"; id: JsonRpcId; error: { code: number; message: string; data?: unknown } }
|
|
124
148
|
|
|
125
149
|
export const rpcResult = (id: JsonRpcId, value: unknown): JsonRpcResponse => ({
|
|
126
150
|
jsonrpc: "2.0",
|
|
127
151
|
id,
|
|
128
152
|
result: value,
|
|
129
153
|
})
|
|
130
|
-
export const rpcError = (
|
|
154
|
+
export const rpcError = (
|
|
155
|
+
id: JsonRpcId,
|
|
156
|
+
code: number,
|
|
157
|
+
message: string,
|
|
158
|
+
data?: unknown,
|
|
159
|
+
): JsonRpcResponse => ({
|
|
131
160
|
jsonrpc: "2.0",
|
|
132
161
|
id,
|
|
133
|
-
error: { code, message },
|
|
162
|
+
error: data !== undefined ? { code, message, data } : { code, message },
|
|
134
163
|
})
|
|
135
164
|
|
|
136
165
|
export interface McpProtocolState {
|
|
@@ -152,10 +181,24 @@ function requestKey(id: unknown): string | undefined {
|
|
|
152
181
|
return typeof id === "number" && Number.isFinite(id) ? `n:${id}` : undefined
|
|
153
182
|
}
|
|
154
183
|
|
|
155
|
-
function
|
|
184
|
+
function metaObject(
|
|
185
|
+
params: Record<string, unknown> | undefined,
|
|
186
|
+
): Record<string, unknown> | undefined {
|
|
156
187
|
const meta = params?._meta
|
|
157
|
-
|
|
158
|
-
|
|
188
|
+
return typeof meta === "object" && meta !== null && !Array.isArray(meta)
|
|
189
|
+
? (meta as Record<string, unknown>)
|
|
190
|
+
: undefined
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The protocol version a modern (2026-07-28+) request declares in `_meta`, or `undefined` for a legacy
|
|
194
|
+
* request. Its presence is what puts the dispatch in modern mode for that one request. */
|
|
195
|
+
export function modernVersionOf(params: Record<string, unknown> | undefined): string | undefined {
|
|
196
|
+
const version = metaObject(params)?.[META_PROTOCOL_VERSION]
|
|
197
|
+
return typeof version === "string" ? version : undefined
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function progressTokenOf(params: Record<string, unknown> | undefined): ProgressToken | undefined {
|
|
201
|
+
const token = metaObject(params)?.progressToken
|
|
159
202
|
return typeof token === "string" || (typeof token === "number" && Number.isFinite(token))
|
|
160
203
|
? token
|
|
161
204
|
: undefined
|
|
@@ -191,6 +234,29 @@ function compactDescription(description: string): string {
|
|
|
191
234
|
return `${sentence.slice(0, 117).trimEnd()}...`
|
|
192
235
|
}
|
|
193
236
|
|
|
237
|
+
/** Freshness hint for the static docs corpus - a modern client MAY cache list/read results this long. */
|
|
238
|
+
const DEFAULT_CACHE_TTL_MS = 3_600_000
|
|
239
|
+
|
|
240
|
+
/** Wrap a result in the 2026-07-28 completion envelope: `resultType`, a self-identifying `serverInfo`
|
|
241
|
+
* under `_meta`, and - for the cacheable list/read methods - a public freshness hint. Legacy results skip
|
|
242
|
+
* this entirely and keep their exact pre-2026-07-28 shape. */
|
|
243
|
+
function withEnvelope(
|
|
244
|
+
value: Record<string, unknown>,
|
|
245
|
+
serverInfo: { name: string; version: string },
|
|
246
|
+
cacheable: boolean,
|
|
247
|
+
): Record<string, unknown> {
|
|
248
|
+
const existingMeta =
|
|
249
|
+
typeof value._meta === "object" && value._meta !== null && !Array.isArray(value._meta)
|
|
250
|
+
? (value._meta as Record<string, unknown>)
|
|
251
|
+
: undefined
|
|
252
|
+
return {
|
|
253
|
+
...value,
|
|
254
|
+
resultType: "complete",
|
|
255
|
+
_meta: { ...(existingMeta ?? {}), [META_SERVER_INFO]: serverInfo },
|
|
256
|
+
...(cacheable ? { ttlMs: DEFAULT_CACHE_TTL_MS, cacheScope: "public" } : {}),
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
194
260
|
/** Normalize a handler's return into the `tools/call` result object. A `string` becomes a single text
|
|
195
261
|
* block (base-MCP behavior, unchanged); a {@link McpToolResult} passes `content`/`structuredContent`
|
|
196
262
|
* through, and inherits the tool descriptor's `_meta` (the `ui.resourceUri` link) when it sets none. */
|
|
@@ -228,10 +294,32 @@ export async function handleRpc(
|
|
|
228
294
|
const prompts = features.prompts ?? []
|
|
229
295
|
const state = options.state ?? createMcpProtocolState()
|
|
230
296
|
|
|
297
|
+
// A request that declares its version in `_meta` is a modern (2026-07-28+) request. We speak exactly one
|
|
298
|
+
// modern revision; any other declared version is rejected so the client can retry with a supported one.
|
|
299
|
+
const requestedVersion = modernVersionOf(params)
|
|
300
|
+
const isModern = requestedVersion !== undefined
|
|
301
|
+
if (isModern && requestedVersion !== MODERN_PROTOCOL_VERSION) {
|
|
302
|
+
return rpcError(rid, MCP_ERROR.UNSUPPORTED_VERSION, "Unsupported protocol version", {
|
|
303
|
+
supported: [MODERN_PROTOCOL_VERSION],
|
|
304
|
+
requested: requestedVersion,
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
// Modern results carry the completion envelope; legacy results keep their exact prior shape. Every data
|
|
308
|
+
// case returns through `reply` so the one `isModern` decision drives the whole response shape.
|
|
309
|
+
const reply = (value: Record<string, unknown>, cacheable = false): JsonRpcResponse =>
|
|
310
|
+
rpcResult(rid, isModern ? withEnvelope(value, serverInfo, cacheable) : value)
|
|
311
|
+
|
|
231
312
|
switch (method) {
|
|
232
|
-
case "initialize":
|
|
313
|
+
case "initialize": {
|
|
314
|
+
// Handshake version negotiation: answer with the client's requested version when we speak it too,
|
|
315
|
+
// otherwise our default. The client then decides whether our answer is acceptable.
|
|
316
|
+
const requested = params?.protocolVersion
|
|
317
|
+
const protocolVersion =
|
|
318
|
+
typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.has(requested)
|
|
319
|
+
? requested
|
|
320
|
+
: PROTOCOL_VERSION
|
|
233
321
|
return rpcResult(rid, {
|
|
234
|
-
protocolVersion
|
|
322
|
+
protocolVersion,
|
|
235
323
|
capabilities: {
|
|
236
324
|
tools: {},
|
|
237
325
|
...(resources.length > 0 ? { resources: {} } : {}),
|
|
@@ -246,6 +334,33 @@ export async function handleRpc(
|
|
|
246
334
|
},
|
|
247
335
|
serverInfo,
|
|
248
336
|
})
|
|
337
|
+
}
|
|
338
|
+
case "server/discover":
|
|
339
|
+
// The modern capability/identity probe (replaces `initialize` for 2026-07-28 clients). Always
|
|
340
|
+
// enveloped and cacheable - it is a modern-only RPC.
|
|
341
|
+
return rpcResult(
|
|
342
|
+
rid,
|
|
343
|
+
withEnvelope(
|
|
344
|
+
{
|
|
345
|
+
supportedVersions: [MODERN_PROTOCOL_VERSION],
|
|
346
|
+
capabilities: {
|
|
347
|
+
tools: {},
|
|
348
|
+
...(resources.length > 0 ? { resources: {} } : {}),
|
|
349
|
+
...(prompts.length > 0 ? { prompts: {} } : {}),
|
|
350
|
+
...(features.ui !== undefined
|
|
351
|
+
? {
|
|
352
|
+
extensions: {
|
|
353
|
+
[UI_EXTENSION_KEY]: { mimeTypes: features.ui.mimeTypes ?? [UI_MIME] },
|
|
354
|
+
},
|
|
355
|
+
}
|
|
356
|
+
: {}),
|
|
357
|
+
},
|
|
358
|
+
...(features.instructions !== undefined ? { instructions: features.instructions } : {}),
|
|
359
|
+
},
|
|
360
|
+
serverInfo,
|
|
361
|
+
true,
|
|
362
|
+
),
|
|
363
|
+
)
|
|
249
364
|
case "notifications/initialized":
|
|
250
365
|
return null
|
|
251
366
|
case "notifications/cancelled": {
|
|
@@ -256,8 +371,7 @@ export async function handleRpc(
|
|
|
256
371
|
case "ping":
|
|
257
372
|
return rpcResult(rid, {})
|
|
258
373
|
case "tools/list":
|
|
259
|
-
return
|
|
260
|
-
rid,
|
|
374
|
+
return reply(
|
|
261
375
|
params?.compact === true
|
|
262
376
|
? {
|
|
263
377
|
tools: tools.map((t) => ({
|
|
@@ -274,12 +388,13 @@ export async function handleRpc(
|
|
|
274
388
|
...(t._meta !== undefined ? { _meta: t._meta } : {}),
|
|
275
389
|
})),
|
|
276
390
|
},
|
|
391
|
+
true,
|
|
277
392
|
)
|
|
278
393
|
case "tools/describe": {
|
|
279
394
|
const name = params?.name
|
|
280
395
|
const tool = tools.find((t) => t.name === name)
|
|
281
396
|
if (!tool) return rpcError(rid, -32602, `unknown tool: ${String(name)}`)
|
|
282
|
-
return
|
|
397
|
+
return reply({
|
|
283
398
|
tool: {
|
|
284
399
|
name: tool.name,
|
|
285
400
|
description: tool.description,
|
|
@@ -331,16 +446,16 @@ export async function handleRpc(
|
|
|
331
446
|
})
|
|
332
447
|
if (controller.signal.aborted) throw new DOMException("cancelled", "AbortError")
|
|
333
448
|
reportProgress(1, 1)
|
|
334
|
-
return
|
|
449
|
+
return reply(toolCallResult(result, tool))
|
|
335
450
|
} catch (err) {
|
|
336
451
|
if (controller.signal.aborted) {
|
|
337
|
-
return
|
|
452
|
+
return reply({
|
|
338
453
|
content: [{ type: "text", text: abortMessage(controller.signal) }],
|
|
339
454
|
isError: true,
|
|
340
455
|
})
|
|
341
456
|
}
|
|
342
457
|
const msg = err instanceof Error ? err.message : String(err)
|
|
343
|
-
return
|
|
458
|
+
return reply({ content: [{ type: "text", text: `Error: ${msg}` }], isError: true })
|
|
344
459
|
} finally {
|
|
345
460
|
cleanupAbort()
|
|
346
461
|
if (key !== undefined && state.activeRequests.get(key) === controller) {
|
|
@@ -349,51 +464,60 @@ export async function handleRpc(
|
|
|
349
464
|
}
|
|
350
465
|
}
|
|
351
466
|
case "resources/list":
|
|
352
|
-
return
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
467
|
+
return reply(
|
|
468
|
+
{
|
|
469
|
+
resources: resources.map((r) => ({
|
|
470
|
+
uri: r.uri,
|
|
471
|
+
name: r.name,
|
|
472
|
+
...(r.description !== undefined ? { description: r.description } : {}),
|
|
473
|
+
...(r.mimeType !== undefined ? { mimeType: r.mimeType } : {}),
|
|
474
|
+
...(r._meta !== undefined ? { _meta: r._meta } : {}),
|
|
475
|
+
})),
|
|
476
|
+
},
|
|
477
|
+
true,
|
|
478
|
+
)
|
|
361
479
|
case "resources/read": {
|
|
362
480
|
const uri = params?.uri
|
|
363
481
|
const resource = resources.find((r) => r.uri === uri)
|
|
364
482
|
if (!resource) return rpcError(rid, -32602, `unknown resource: ${String(uri)}`)
|
|
365
483
|
try {
|
|
366
484
|
const content = await resource.read()
|
|
367
|
-
return
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
485
|
+
return reply(
|
|
486
|
+
{
|
|
487
|
+
contents: [
|
|
488
|
+
{
|
|
489
|
+
uri: resource.uri,
|
|
490
|
+
mimeType: content.mimeType ?? resource.mimeType ?? "text/plain",
|
|
491
|
+
text: content.text,
|
|
492
|
+
...(resource._meta !== undefined ? { _meta: resource._meta } : {}),
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
true,
|
|
497
|
+
)
|
|
377
498
|
} catch {
|
|
378
499
|
// Don't surface the raw read error - it can leak a filesystem path. Return a generic message.
|
|
379
500
|
return rpcError(rid, -32000, `failed to read resource: ${String(uri)}`)
|
|
380
501
|
}
|
|
381
502
|
}
|
|
382
503
|
case "prompts/list":
|
|
383
|
-
return
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
504
|
+
return reply(
|
|
505
|
+
{
|
|
506
|
+
prompts: prompts.map((p) => ({
|
|
507
|
+
name: p.name,
|
|
508
|
+
description: p.description,
|
|
509
|
+
...(p.arguments !== undefined ? { arguments: p.arguments } : {}),
|
|
510
|
+
})),
|
|
511
|
+
},
|
|
512
|
+
true,
|
|
513
|
+
)
|
|
390
514
|
case "prompts/get": {
|
|
391
515
|
const name = params?.name
|
|
392
516
|
const prompt = prompts.find((p) => p.name === name)
|
|
393
517
|
if (!prompt) return rpcError(rid, -32602, `unknown prompt: ${String(name)}`)
|
|
394
518
|
const args = (params?.arguments as Record<string, unknown>) ?? {}
|
|
395
519
|
try {
|
|
396
|
-
return
|
|
520
|
+
return reply({
|
|
397
521
|
description: prompt.description,
|
|
398
522
|
messages: await prompt.handler(args),
|
|
399
523
|
})
|
package/src/server.ts
CHANGED
|
@@ -46,6 +46,11 @@ export interface CreateMcpServerOptions {
|
|
|
46
46
|
readonly health?: string
|
|
47
47
|
/** Max JSON-RPC body size in bytes (default 1 MB). */
|
|
48
48
|
readonly maxBodyBytes?: number
|
|
49
|
+
/** Natural-language guidance for LLMs, surfaced in the modern `server/discover` result (2026-07-28). */
|
|
50
|
+
readonly instructions?: string
|
|
51
|
+
/** Origin allowlist for the DNS-rebinding guard. Omit to allow any origin; set it to reject other
|
|
52
|
+
* browser origins with 403 (e.g. a hardened, non-public mount). */
|
|
53
|
+
readonly allowedOrigins?: readonly string[]
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
export interface McpServer {
|
|
@@ -69,6 +74,7 @@ export function createMcpServer(opts: CreateMcpServerOptions): McpServer {
|
|
|
69
74
|
...(resources.length > 0 ? { resources } : {}),
|
|
70
75
|
...(opts.prompts !== undefined ? { prompts: opts.prompts } : {}),
|
|
71
76
|
...(widgets.length > 0 ? { ui: { mimeTypes: [UI_MIME] } } : {}),
|
|
77
|
+
...(opts.instructions !== undefined ? { instructions: opts.instructions } : {}),
|
|
72
78
|
}
|
|
73
79
|
return {
|
|
74
80
|
tools,
|
|
@@ -79,6 +85,7 @@ export function createMcpServer(opts: CreateMcpServerOptions): McpServer {
|
|
|
79
85
|
features,
|
|
80
86
|
...(opts.health !== undefined ? { health: opts.health } : {}),
|
|
81
87
|
...(opts.maxBodyBytes !== undefined ? { maxBodyBytes: opts.maxBodyBytes } : {}),
|
|
88
|
+
...(opts.allowedOrigins !== undefined ? { allowedOrigins: opts.allowedOrigins } : {}),
|
|
82
89
|
}),
|
|
83
90
|
handle: (message) => handleRpc(message, tools, serverInfo, features),
|
|
84
91
|
}
|