@warlock.js/ai-tools 5.32.0 → 5.34.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.
@@ -2,5 +2,5 @@ import { FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult, Search
2
2
  import { HttpMethod, HttpRequestInput, HttpRequestOptions, HttpRequestResult } from "./http.type.mjs";
3
3
  import { CalculatorInput, CalculatorOptions, CalculatorResult, DateTimeInput, DateTimeOp, DateTimeOptions, DateTimeResult } from "./utility.type.mjs";
4
4
  import { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpToolCallResult, McpToolDescriptor, McpTransport } from "./mcp.type.mjs";
5
- import { McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpServeHandle, McpHttpServeTransport, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeTransport, McpServer, McpToolSource } from "./mcp-serve.type.mjs";
6
- export type { CalculatorInput, CalculatorOptions, CalculatorResult, DateTimeInput, DateTimeOp, DateTimeOptions, DateTimeResult, FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult, HttpMethod, HttpRequestInput, HttpRequestOptions, HttpRequestResult, JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpServeHandle, McpHttpServeTransport, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeTransport, McpServer, McpToolCallResult, McpToolDescriptor, McpToolSource, McpTransport, SearchProvider, WebSearchInput, WebSearchOptions, WebSearchResult, WebSearchResultItem };
5
+ import { McpBeforeCall, McpCallRefusal, McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpRoute, McpHttpRouteContext, McpHttpRouteResponse, McpHttpServeHandle, McpHttpServeTransport, McpListFilter, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeToolContext, McpServeTransport, McpServer, McpStdioServeTransport, McpToolSource, McpValidateCallParams } from "./mcp-serve.type.mjs";
6
+ export type { CalculatorInput, CalculatorOptions, CalculatorResult, DateTimeInput, DateTimeOp, DateTimeOptions, DateTimeResult, FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult, HttpMethod, HttpRequestInput, HttpRequestOptions, HttpRequestResult, JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpBeforeCall, McpCallRefusal, McpClient, McpClientOptions, McpContentBlock, McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpRoute, McpHttpRouteContext, McpHttpRouteResponse, McpHttpServeHandle, McpHttpServeTransport, McpListFilter, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeToolContext, McpServeTransport, McpServer, McpStdioServeTransport, McpToolCallResult, McpToolDescriptor, McpToolSource, McpTransport, McpValidateCallParams, SearchProvider, WebSearchInput, WebSearchOptions, WebSearchResult, WebSearchResultItem };
@@ -1,4 +1,4 @@
1
- import { McpContentBlock } from "./mcp.type.mjs";
1
+ import { JsonRpcId, McpContentBlock, McpToolCallResult, McpToolDescriptor } from "./mcp.type.mjs";
2
2
  import { JsonSchemaTarget, ToolContract, ToolRequestContext } from "@warlock.js/ai";
3
3
  //#region ../../packages/ai-tools/src/contracts/mcp-serve.type.d.ts
4
4
  /**
@@ -46,8 +46,8 @@ export type McpHttpRequestInfo = ToolRequestContext & {
46
46
  };
47
47
  /**
48
48
  * Authentication for an HTTP-served MCP endpoint. At least one of
49
- * `bearer` / `authorize` is required; when both are set, a request must
50
- * pass both. A request that fails gets HTTP 401 before its body is read.
49
+ * `bearer` / `authorize` / `authenticate` is required; all configured checks
50
+ * must pass, in that order. A request that fails gets HTTP 401 before its body is read.
51
51
  */
52
52
  export type McpHttpAuth = {
53
53
  /**
@@ -64,6 +64,40 @@ export type McpHttpAuth = {
64
64
  * request (fails closed).
65
65
  */
66
66
  authorize?: (request: McpHttpRequestInfo) => boolean | Promise<boolean>;
67
+ /**
68
+ * Resolve the caller after Host/Origin, bearer and authorize checks.
69
+ * Undefined or a thrown/rejected error denies with 401 and a Bearer challenge.
70
+ * Any other value becomes context.principal (including null or false).
71
+ */
72
+ authenticate?: (request: McpHttpRequestInfo) => unknown | Promise<unknown>;
73
+ };
74
+ /** Authenticated context for an extra HTTP route; there is no JSON-RPC id. */
75
+ export type McpHttpRouteContext = McpServeCallContext & {
76
+ request: McpHttpRequestInfo;
77
+ /** Parsed request URL; use searchParams to read query parameters. */
78
+ url: URL;
79
+ /** UTF-8 request body, subject to the transport's maxBodyBytes limit. */
80
+ body: string;
81
+ signal: AbortSignal;
82
+ };
83
+ /** JSON response from an extra HTTP route. Omit body for an empty response. */
84
+ export type McpHttpRouteResponse = {
85
+ /** @default 200 */
86
+ status?: number;
87
+ headers?: Record<string, string>;
88
+ body?: unknown;
89
+ };
90
+ /** An exact method/path route beneath the MCP endpoint, sharing its auth. */
91
+ export type McpHttpRoute = {
92
+ method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
93
+ /**
94
+ * Suffix such as /tool-policy, appended to the MCP path without its trailing
95
+ * slash. Nonempty segments use letters, digits, _, -, . or ~; . and .. are
96
+ * forbidden. No query, fragment, escapes, wildcard or trailing slash.
97
+ * The MCP endpoint itself is reserved for every method. Duplicates fail start().
98
+ */
99
+ path: string;
100
+ handler(context: McpHttpRouteContext): McpHttpRouteResponse | Promise<McpHttpRouteResponse>;
67
101
  };
68
102
  /**
69
103
  * Serve over the MCP Streamable HTTP transport.
@@ -89,13 +123,15 @@ export type McpHttpServeTransport = {
89
123
  */
90
124
  port?: number;
91
125
  /**
92
- * The single MCP endpoint path. Other paths answer 404.
126
+ * The MCP endpoint path. Other paths answer 404 unless registered in routes.
93
127
  *
94
128
  * @default "/mcp"
95
129
  */
96
130
  path?: string;
97
- /** Bearer token(s) and/or a custom authorize hook. Required off loopback. */
131
+ /** Bearer, authorize and/or authenticate checks. Required off loopback. */
98
132
  auth?: McpHttpAuth;
133
+ /** Extra HTTP routes with the same Host/Origin and authentication checks. */
134
+ routes?: readonly McpHttpRoute[];
99
135
  /**
100
136
  * Extra browser origins allowed to call the endpoint, as full origins
101
137
  * (`"https://app.example.com"`). Loopback origins (`http://localhost:*`,
@@ -132,9 +168,15 @@ export type McpHttpServeTransport = {
132
168
  * - `"stdio"` — serve JSON-RPC over this process's stdin/stdout.
133
169
  * - `"http"` — serve MCP Streamable HTTP (see {@link McpHttpServeTransport}).
134
170
  */
135
- export type McpServeTransport = {
171
+ export type McpServeTransport = McpStdioServeTransport | McpHttpServeTransport;
172
+ /** Local caller identity for the stdio connection. */
173
+ export type McpStdioServeTransport = {
136
174
  type: "stdio";
137
- } | McpHttpServeTransport;
175
+ /** Static trusted identity; never read from tool arguments. */
176
+ principal?: unknown;
177
+ /** Per-request identity provider; takes precedence over principal when set. */
178
+ principalProvider?: (context: McpServeCallContext) => unknown | Promise<unknown>;
179
+ };
138
180
  /**
139
181
  * Per-call context handed to resource readers and prompt builders (tools
140
182
  * receive the same data on their `ToolContext`).
@@ -142,9 +184,30 @@ export type McpServeTransport = {
142
184
  export type McpServeCallContext = {
143
185
  /** The inbound HTTP request; absent over stdio. */
144
186
  request?: McpHttpRequestInfo;
145
- /** Aborted when an HTTP server's `close()` gives up draining. */
187
+ /** JSON-RPC request id; populated by the serving boundary. */
188
+ requestId?: JsonRpcId;
189
+ /** Caller identity from HTTP authenticate or the stdio transport options. */
190
+ principal?: unknown;
191
+ /** Aborted on cancellation, client disconnect or server shutdown. */
146
192
  signal?: AbortSignal;
147
193
  };
194
+ /** Context supplied to beforeCall after successful tool input validation. */
195
+ export type McpServeToolContext = McpServeCallContext & {
196
+ requestId: JsonRpcId;
197
+ };
198
+ /** Native MCP error result returned by a beforeCall policy hook. */
199
+ export type McpCallRefusal = McpToolCallResult & {
200
+ isError: true;
201
+ };
202
+ /** Validate policy before execute; a refusal bypasses result serialization. */
203
+ export type McpBeforeCall = (context: McpServeToolContext, tool: ToolContract, input: unknown) => void | McpCallRefusal | Promise<void | McpCallRefusal>;
204
+ /** Filter fresh wire descriptors for discovery only; hidden tools stay callable. */
205
+ export type McpListFilter = (context: McpServeToolContext, tools: readonly McpToolDescriptor[]) => readonly McpToolDescriptor[] | Promise<readonly McpToolDescriptor[]>;
206
+ /**
207
+ * Inspect the complete tools/call params before lookup, schema validation or
208
+ * beforeCall. Treat params as read-only; throw McpError for an app-defined error.
209
+ */
210
+ export type McpValidateCallParams = (context: McpServeToolContext, params: Readonly<Record<string, unknown>>) => void | Promise<void>;
148
211
  /**
149
212
  * A static MCP resource served by `resources/list` + `resources/read`.
150
213
  */
@@ -236,6 +299,20 @@ export interface McpServeOptions {
236
299
  * @default "text"
237
300
  */
238
301
  results?: "text" | "native";
302
+ /**
303
+ * Runs after schema validation and before invocation. Input is the validated
304
+ * value and should be treated as read-only. Return an isError:true native
305
+ * result to refuse. McpError preserves its code/message/data; other throws
306
+ * answer with a JSON-RPC invalid-params error.
307
+ */
308
+ beforeCall?: McpBeforeCall;
309
+ /** Per-request discovery filter; never changes tools/call lookup or policy. */
310
+ listFilter?: McpListFilter;
311
+ /**
312
+ * Optional envelope policy. Receives all params, including unknown fields,
313
+ * task and _meta. By default extra fields are ignored (no task capability).
314
+ */
315
+ validateCallParams?: McpValidateCallParams;
239
316
  /**
240
317
  * Prefix each tool of a grouped source (`{ groups: { … } }`) with its
241
318
  * group name. `true` exposes `<group>_<tool>`; a function builds the
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-serve.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/contracts/mcp-serve.type.ts"],"mappings":";;;;;;;;;;;;;YAcY;EAAkB,SAAS;IAAmB;;;;;;;;;YAU9C;;EAEV,QAAQ,eAAe;;;;;;;;YASb,iBAAiB,gBAAgB;;;;;;YAOjC,qBAAqB;;EAE/B;;EAEA;;;;;;;YAQU;;;;;;;;EAQV;;;;;;EAMA,aAAa,SAAS,iCAAiC;;;;;;;;YAS7C;;EAEV;;;;;;;EAOA;;;;;;;EAOA;;;;;;EAMA;;EAEA,OAAO;;;;;;;;;EASP;;;;;;;EAOA;;;;;;EAMA;;;;;;;EAOA;;;;;;;;YASU;EAAsB;IAAkB;;;;;YAMxC;;EAEV,UAAU;;EAEV,SAAS;;;;;YAMC;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,KAAK,SAAS,+BAA+B,aAAa,iBAAiB;;;YAIjE;;EAEV;;EAEA;;EAEA;;;YAIU;;EAEV;;EAEA,SAAS;;;;;YAMC;;EAEV;;EAEA;;EAEA;;EAEA,YAAY;;;;;;EAMZ,IACE,MAAM,wBACN,SAAS,+BACC,qBAAqB,iBAAiB;;;;;;iBAOnC;;EAEf;;;;;;EAMA;;;;;;EAMA,YAAY;;;;;;;;EAQZ,eAAe;;;;;;;;;EASf;;;;;;;;EAQA,8BAA8B,eAAe;;EAE7C,YAAY;;EAEZ,UAAU;;;;;;iBAOK;;EAEf;;;;;;EAMA,SAAS;;;iBAIM,2BAA2B;;EAE1C;;;;;iBAMe,UAAU,gBAAgB,iBAAiB;;;;;;EAM1D,SAAS,QAAQ;;EAEjB,QAAQ"}
1
+ {"version":3,"file":"mcp-serve.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/contracts/mcp-serve.type.ts"],"mappings":";;;;;;;;;;;;;YAcY;EAAkB,SAAS;IAAmB;;;;;;;;;YAU9C;;EAEV,QAAQ,eAAe;;;;;;;;YASb,iBAAiB,gBAAgB;;;;;;YAOjC,qBAAqB;;EAE/B;;EAEA;;;;;;;YAQU;;;;;;;;EAQV;;;;;;EAMA,aAAa,SAAS,iCAAiC;;;;;;EAMvD,gBAAgB,SAAS,iCAAiC;;;YAIhD,sBAAsB;EAChC,SAAS;;EAET,KAAK;;EAEL;EACA,QAAQ;;;YAIE;;EAEV;EACA,UAAU;EACV;;;YAIU;EACV;;;;;;;EAOA;EACA,QAAQ,SAAS,sBAAsB,uBAAuB,QAAQ;;;;;;;;YAS5D;;EAEV;;;;;;;EAOA;;;;;;;EAOA;;;;;;EAMA;;EAEA,OAAO;;EAEP,kBAAkB;;;;;;;;;EASlB;;;;;;;EAOA;;;;;;EAMA;;;;;;;EAOA;;;;;;;;YASU,oBAAoB,yBAAyB;;YAG7C;EACV;;EAEA;;EAEA,qBAAqB,SAAS,kCAAkC;;;;;;YAOtD;;EAEV,UAAU;;EAEV,YAAY;;EAEZ;;EAEA,SAAS;;;YAIC,sBAAsB;EAAwB,WAAW;;;YAGzD,iBAAiB;EAAsB;;;YAGvC,iBACV,SAAS,qBACT,MAAM,cACN,0BACU,iBAAiB,eAAe;;YAGhC,iBACV,SAAS,qBACT,gBAAgB,iCACJ,sBAAsB,iBAAiB;;;;;YAMzC,yBACV,SAAS,qBACT,QAAQ,SAAS,oCACP;;;;YAKA;;EAEV;;EAEA;;EAEA;;EAEA;;EAEA;;;;;EAKA,KAAK,SAAS,+BAA+B,aAAa,iBAAiB;;;YAIjE;;EAEV;;EAEA;;EAEA;;;YAIU;;EAEV;;EAEA,SAAS;;;;;YAMC;;EAEV;;EAEA;;EAEA;;EAEA,YAAY;;;;;;EAMZ,IACE,MAAM,wBACN,SAAS,+BACC,qBAAqB,iBAAiB;;;;;;iBAOnC;;EAEf;;;;;;EAMA;;;;;;EAMA,YAAY;;;;;;;;EAQZ,eAAe;;;;;;;;;EASf;;;;;;;EAOA,aAAa;;EAEb,aAAa;;;;;EAKb,qBAAqB;;;;;;;;EAQrB,8BAA8B,eAAe;;EAE7C,YAAY;;EAEZ,UAAU;;;;;;iBAOK;;EAEf;;;;;;EAMA,SAAS;;;iBAIM,2BAA2B;;EAE1C;;;;;iBAMe,UAAU,gBAAgB,iBAAiB;;;;;;EAM1D,SAAS,QAAQ;;EAEjB,QAAQ"}
package/esm/index.d.mts CHANGED
@@ -2,10 +2,11 @@ import { FetchUrlExtract, FetchUrlInput, FetchUrlOptions, FetchUrlResult, Search
2
2
  import { HttpMethod, HttpRequestInput, HttpRequestOptions, HttpRequestResult } from "./contracts/http.type.mjs";
3
3
  import { CalculatorInput, CalculatorOptions, CalculatorResult, DateTimeInput, DateTimeOp, DateTimeOptions, DateTimeResult } from "./contracts/utility.type.mjs";
4
4
  import { JsonRpcError, JsonRpcId, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, McpClient, McpClientOptions, McpContentBlock, McpToolCallResult, McpToolDescriptor, McpTransport } from "./contracts/mcp.type.mjs";
5
- import { McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpServeHandle, McpHttpServeTransport, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeTransport, McpServer, McpToolSource } from "./contracts/mcp-serve.type.mjs";
5
+ import { McpBeforeCall, McpCallRefusal, McpGroupedToolSource, McpHttpAuth, McpHttpRequestInfo, McpHttpRoute, McpHttpRouteContext, McpHttpRouteResponse, McpHttpServeHandle, McpHttpServeTransport, McpListFilter, McpPromptArgument, McpPromptMessage, McpServeCallContext, McpServeHandle, McpServeOptions, McpServePrompt, McpServeResource, McpServeSource, McpServeToolContext, McpServeTransport, McpServer, McpStdioServeTransport, McpToolSource, McpValidateCallParams } from "./contracts/mcp-serve.type.mjs";
6
6
  import "./contracts/index.mjs";
7
7
  import { McpServeRequestHandler, createServeHandler, serve } from "./mcp/serve.mjs";
8
8
  import { SUPPORTED_PROTOCOL_VERSIONS } from "./mcp/protocol.mjs";
9
+ import { McpError } from "./mcp/error.mjs";
9
10
  import { McpNativeResult, mcpResult } from "./mcp/result.mjs";
10
11
  import { jsonSchemaToStandard } from "./mcp/json-schema-to-standard.mjs";
11
12
  import { McpTransportClient } from "./mcp/transport.type.mjs";
@@ -20,6 +21,6 @@ import { calculatorTool } from "./utility/calculator.mjs";
20
21
  import { dateTimeTool } from "./utility/date-time.mjs";
21
22
  import "./utility/index.mjs";
22
23
  import { CalculatorError, CalculatorErrorOptions, CalculatorFailure, DateTimeError, DateTimeErrorOptions, DateTimeFailure, HttpPolicyError, HttpPolicyErrorOptions, HttpPolicyFailure, McpServeError, McpServeErrorOptions, McpServeFailure, McpTransportError, McpTransportErrorOptions, McpTransportFailure, WebToolError, WebToolErrorOptions, WebToolFailure } from "./errors.mjs";
23
- export { type AiToolsNamespace, CalculatorError, type CalculatorErrorOptions, type CalculatorFailure, type CalculatorInput, type CalculatorOptions, type CalculatorResult, DateTimeError, type DateTimeErrorOptions, type DateTimeFailure, type DateTimeInput, type DateTimeOp, type DateTimeOptions, type DateTimeResult, type FetchUrlExtract, type FetchUrlInput, type FetchUrlOptions, type FetchUrlResult, type HttpMethod, HttpPolicyError, type HttpPolicyErrorOptions, type HttpPolicyFailure, type HttpRequestInput, type HttpRequestOptions, type HttpRequestResult, type JsonRpcClientHandle, type JsonRpcError, type JsonRpcId, type JsonRpcMessage, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type JsonRpcVersion, type McpClient, type McpClientOptions, type McpContentBlock, type McpFactory, type McpGroupedToolSource, type McpHttpAuth, type McpHttpRequestInfo, type McpHttpServeHandle, type McpHttpServeTransport, type McpNativeResult, type McpPromptArgument, type McpPromptMessage, type McpServeCallContext, McpServeError, type McpServeErrorOptions, type McpServeFailure, type McpServeHandle, type McpServeOptions, type McpServePrompt, type McpServeRequestHandler, type McpServeResource, type McpServeSource, type McpServeTransport, type McpServer, type McpToolCallResult, type McpToolDescriptor, type McpToolSource, type McpTransport, type McpTransportClient, McpTransportError, type McpTransportErrorOptions, type McpTransportFailure, SUPPORTED_PROTOCOL_VERSIONS, type SearchProvider, type WebSearchInput, type WebSearchOptions, type WebSearchResult, type WebSearchResultItem, WebToolError, type WebToolErrorOptions, type WebToolFailure, calculatorTool, createJsonRpcClient, createServeHandler, createTransport, dateTimeTool, fetchUrlTool, httpRequestTool, jsonSchemaToStandard, mcp, mcpResult, serve, webSearchTool };
24
+ export { type AiToolsNamespace, CalculatorError, type CalculatorErrorOptions, type CalculatorFailure, type CalculatorInput, type CalculatorOptions, type CalculatorResult, DateTimeError, type DateTimeErrorOptions, type DateTimeFailure, type DateTimeInput, type DateTimeOp, type DateTimeOptions, type DateTimeResult, type FetchUrlExtract, type FetchUrlInput, type FetchUrlOptions, type FetchUrlResult, type HttpMethod, HttpPolicyError, type HttpPolicyErrorOptions, type HttpPolicyFailure, type HttpRequestInput, type HttpRequestOptions, type HttpRequestResult, type JsonRpcClientHandle, type JsonRpcError, type JsonRpcId, type JsonRpcMessage, type JsonRpcNotification, type JsonRpcRequest, type JsonRpcResponse, type JsonRpcVersion, type McpBeforeCall, type McpCallRefusal, type McpClient, type McpClientOptions, type McpContentBlock, McpError, type McpFactory, type McpGroupedToolSource, type McpHttpAuth, type McpHttpRequestInfo, type McpHttpRoute, type McpHttpRouteContext, type McpHttpRouteResponse, type McpHttpServeHandle, type McpHttpServeTransport, type McpListFilter, type McpNativeResult, type McpPromptArgument, type McpPromptMessage, type McpServeCallContext, McpServeError, type McpServeErrorOptions, type McpServeFailure, type McpServeHandle, type McpServeOptions, type McpServePrompt, type McpServeRequestHandler, type McpServeResource, type McpServeSource, type McpServeToolContext, type McpServeTransport, type McpServer, type McpStdioServeTransport, type McpToolCallResult, type McpToolDescriptor, type McpToolSource, type McpTransport, type McpTransportClient, McpTransportError, type McpTransportErrorOptions, type McpTransportFailure, type McpValidateCallParams, SUPPORTED_PROTOCOL_VERSIONS, type SearchProvider, type WebSearchInput, type WebSearchOptions, type WebSearchResult, type WebSearchResultItem, WebToolError, type WebToolErrorOptions, type WebToolFailure, calculatorTool, createJsonRpcClient, createServeHandler, createTransport, dateTimeTool, fetchUrlTool, httpRequestTool, jsonSchemaToStandard, mcp, mcpResult, serve, webSearchTool };
24
25
  import "./mcp/index.mjs";
25
26
  import "./register.mjs";
package/esm/index.mjs CHANGED
@@ -2,6 +2,7 @@ import { CalculatorError, DateTimeError, HttpPolicyError, McpServeError, McpTran
2
2
  import { httpRequestTool } from "./http/http-request.mjs";
3
3
  import { jsonSchemaToStandard } from "./mcp/json-schema-to-standard.mjs";
4
4
  import { createJsonRpcClient, createTransport } from "./mcp/transport.mjs";
5
+ import { McpError } from "./mcp/error.mjs";
5
6
  import { SUPPORTED_PROTOCOL_VERSIONS } from "./mcp/protocol.mjs";
6
7
  import { mcpResult } from "./mcp/result.mjs";
7
8
  import { createServeHandler, serve } from "./mcp/serve.mjs";
@@ -14,4 +15,4 @@ import "./register.mjs";
14
15
  import "./web/index.mjs";
15
16
  import "./utility/index.mjs";
16
17
 
17
- export { CalculatorError, DateTimeError, HttpPolicyError, McpServeError, McpTransportError, SUPPORTED_PROTOCOL_VERSIONS, WebToolError, calculatorTool, createJsonRpcClient, createServeHandler, createTransport, dateTimeTool, fetchUrlTool, httpRequestTool, jsonSchemaToStandard, mcp, mcpResult, serve, webSearchTool };
18
+ export { CalculatorError, DateTimeError, HttpPolicyError, McpError, McpServeError, McpTransportError, SUPPORTED_PROTOCOL_VERSIONS, WebToolError, calculatorTool, createJsonRpcClient, createServeHandler, createTransport, dateTimeTool, fetchUrlTool, httpRequestTool, jsonSchemaToStandard, mcp, mcpResult, serve, webSearchTool };
@@ -0,0 +1,9 @@
1
+ //#region ../../packages/ai-tools/src/mcp/error.d.ts
2
+ /** An application-defined JSON-RPC error thrown by MCP serving policy hooks. */
3
+ export declare class McpError extends Error {
4
+ readonly code: number;
5
+ readonly data: unknown;
6
+ constructor(code: number, message: string, data?: unknown);
7
+ }
8
+ //#endregion
9
+ //# sourceMappingURL=error.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/error.ts"],"mappings":";;qBACa,iBAAiB;WACZ;WAEA;EAEhB,YAAmB,cAAc,iBAAiB"}
@@ -0,0 +1,24 @@
1
+ //#region ../../packages/ai-tools/src/mcp/error.ts
2
+ /** An application-defined JSON-RPC error thrown by MCP serving policy hooks. */
3
+ var McpError = class extends Error {
4
+ constructor(code, message, data) {
5
+ super(message);
6
+ this.name = "McpError";
7
+ this.code = code;
8
+ this.data = data;
9
+ }
10
+ };
11
+ /**
12
+ * Recognize our error and the SDK's McpError without a runtime SDK dependency.
13
+ * SDK source (checked 2026-10-19):
14
+ * https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/types.ts
15
+ * Only explicitly named errors with an integer code opt in; ordinary throws
16
+ * retain the existing mapping. Preserve the error's message verbatim.
17
+ */
18
+ function isMcpError(cause) {
19
+ return cause instanceof Error && cause.name === "McpError" && "code" in cause && typeof cause.code === "number" && Number.isInteger(cause.code);
20
+ }
21
+
22
+ //#endregion
23
+ export { McpError, isMcpError };
24
+ //# sourceMappingURL=error.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/error.ts"],"sourcesContent":["/** An application-defined JSON-RPC error thrown by MCP serving policy hooks. */\nexport class McpError extends Error {\n public readonly code: number;\n\n public readonly data: unknown;\n\n public constructor(code: number, message: string, data?: unknown) {\n super(message);\n this.name = \"McpError\";\n this.code = code;\n this.data = data;\n }\n}\n\n/**\n * Recognize our error and the SDK's McpError without a runtime SDK dependency.\n * SDK source (checked 2026-10-19):\n * https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.x/src/types.ts\n * Only explicitly named errors with an integer code opt in; ordinary throws\n * retain the existing mapping. Preserve the error's message verbatim.\n */\nexport function isMcpError(cause: unknown): cause is Error & { code: number; data?: unknown } {\n return (\n cause instanceof Error &&\n cause.name === \"McpError\" &&\n \"code\" in cause &&\n typeof cause.code === \"number\" &&\n Number.isInteger(cause.code)\n );\n}\n"],"mappings":";;AACA,IAAa,WAAb,cAA8B,MAAM;CAKlC,AAAO,YAAY,MAAc,SAAiB,MAAgB;EAChE,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,SAAgB,WAAW,OAAmE;CAC5F,OACE,iBAAiB,SACjB,MAAM,SAAS,cACf,UAAU,SACV,OAAO,MAAM,SAAS,YACtB,OAAO,UAAU,MAAM,IAAI;AAE/B"}
@@ -3,6 +3,7 @@ import { McpHttpServeHandle, McpHttpServeTransport, McpServeOptions, McpServeSou
3
3
  import "../contracts/index.mjs";
4
4
  import { McpServeRequestHandler, createServeHandler, serve } from "./serve.mjs";
5
5
  import { SUPPORTED_PROTOCOL_VERSIONS } from "./protocol.mjs";
6
+ import { McpError } from "./error.mjs";
6
7
  import { McpNativeResult, mcpResult } from "./result.mjs";
7
8
  import { jsonSchemaToStandard } from "./json-schema-to-standard.mjs";
8
9
  import { McpTransportClient } from "./transport.type.mjs";
@@ -47,5 +48,5 @@ export interface McpFactory {
47
48
  */
48
49
  export declare const mcp: McpFactory;
49
50
  //#endregion
50
- export { type McpNativeResult, type McpServeRequestHandler, type McpTransportClient, SUPPORTED_PROTOCOL_VERSIONS, createServeHandler, mcpResult, serve };
51
+ export { McpError, type McpNativeResult, type McpServeRequestHandler, type McpTransportClient, SUPPORTED_PROTOCOL_VERSIONS, createServeHandler, mcpResult, serve };
51
52
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/index.ts"],"mappings":";;;;;;;;;;;;;;;;;iBA8BiB;;;;;;;;GAQd,QAAQ,cAAc,UAAU,mBAAmB;;;;;;;;;EASpD,MACE,QAAQ,gBACR,SAAS;IAAoB,WAAW;MACvC,UAAU;EACb,MAAM,QAAQ,gBAAgB,SAAS,kBAAkB;;;;;;;;;;qBAW9C,KAAK"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/index.ts"],"mappings":";;;;;;;;;;;;;;;;;;iBA+BiB;;;;;;;;GAQd,QAAQ,cAAc,UAAU,mBAAmB;;;;;;;;;EASpD,MACE,QAAQ,gBACR,SAAS;IAAoB,WAAW;MACvC,UAAU;EACb,MAAM,QAAQ,gBAAgB,SAAS,kBAAkB;;;;;;;;;;qBAW9C,KAAK"}
package/esm/mcp/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { jsonSchemaToStandard } from "./json-schema-to-standard.mjs";
2
2
  import { createJsonRpcClient, createTransport } from "./transport.mjs";
3
3
  import { mcp as mcp$1 } from "./client.mjs";
4
+ import { McpError } from "./error.mjs";
4
5
  import { SUPPORTED_PROTOCOL_VERSIONS } from "./protocol.mjs";
5
6
  import { mcpResult } from "./result.mjs";
6
7
  import { createServeHandler, serve } from "./serve.mjs";
@@ -17,5 +18,5 @@ import { createServeHandler, serve } from "./serve.mjs";
17
18
  const mcp = Object.assign((server, options) => mcp$1(server, options), { serve });
18
19
 
19
20
  //#endregion
20
- export { SUPPORTED_PROTOCOL_VERSIONS, createServeHandler, mcp, mcpResult, serve };
21
+ export { McpError, SUPPORTED_PROTOCOL_VERSIONS, createServeHandler, mcp, mcpResult, serve };
21
22
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["mcpClient"],"sources":["../../../../../../../../packages/ai-tools/src/mcp/index.ts"],"sourcesContent":["import type {\n McpClient,\n McpClientOptions,\n McpHttpServeHandle,\n McpHttpServeTransport,\n McpServeOptions,\n McpServeSource,\n McpServer,\n McpTransport,\n} from \"../contracts\";\nimport { mcp as mcpClient } from \"./client\";\nimport { serve } from \"./serve\";\n\nexport { serve, createServeHandler } from \"./serve\";\nexport type { McpServeRequestHandler } from \"./serve\";\nexport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\nexport { mcpResult } from \"./result\";\nexport type { McpNativeResult } from \"./result\";\nexport { jsonSchemaToStandard } from \"./json-schema-to-standard\";\nexport { createJsonRpcClient, createTransport } from \"./transport\";\nexport type { JsonRpcClientHandle } from \"./transport\";\nexport type { McpTransportClient } from \"./transport.type\";\n\n/**\n * The callable `ai.mcp` surface — a factory that connects to an external\n * MCP server (Direction A) and also carries `.serve` to expose a local\n * primitive AS an MCP server (Direction B). Modeled as a function with an\n * attached `serve` property, mirroring how `ai.mcp(server).tools()` and\n * `ai.mcp.serve(source, options).start()` read in the design.\n */\nexport interface McpFactory {\n /**\n * Connect to an external MCP server and adapt its tools as agent tools.\n *\n * @param server - The transport config (`{ type: \"stdio\" }` / `{ type: \"http\" }`).\n * @param options - Prefix / filter / per-call timeout, and a `signal`\n * that closes the client (and fails its calls) when it aborts.\n */\n (server: McpTransport, options?: McpClientOptions): McpClient;\n /**\n * Expose a built agent / supervisor / orchestrator (or a literal\n * `ToolContract[]`, or named `{ groups }`) AS an MCP server. Over\n * `{ type: \"http\" }`, `start()` resolves `{ url, close }`.\n *\n * @param source - The tools to expose.\n * @param options - Server name, version, transport, schema dialect, resources, prompts.\n */\n serve(\n source: McpServeSource,\n options: McpServeOptions & { transport: McpHttpServeTransport },\n ): McpServer<McpHttpServeHandle>;\n serve(source: McpServeSource, options: McpServeOptions): McpServer;\n}\n\n/**\n * The `ai.mcp` factory value: the client factory with `.serve` attached.\n * `Object.assign` keeps `mcp` callable (Direction A) while widening it with\n * the `serve` member (Direction B) — one object, both directions. The\n * `declare module \"@warlock.js/ai\"` augmentation and the runtime\n * registration (`ai.mcp = mcp`) live in `../register`, so this barrel is a\n * pure value/type module the registrar consumes.\n */\nexport const mcp: McpFactory = Object.assign(\n (server: McpTransport, options?: McpClientOptions): McpClient => mcpClient(server, options),\n { serve },\n);\n"],"mappings":";;;;;;;;;;;;;;;;AA8DA,MAAa,MAAkB,OAAO,QACnC,QAAsB,YAA0CA,MAAU,QAAQ,OAAO,GAC1F,EAAE,MAAM,CACV"}
1
+ {"version":3,"file":"index.mjs","names":["mcpClient"],"sources":["../../../../../../../../packages/ai-tools/src/mcp/index.ts"],"sourcesContent":["import type {\n McpClient,\n McpClientOptions,\n McpHttpServeHandle,\n McpHttpServeTransport,\n McpServeOptions,\n McpServeSource,\n McpServer,\n McpTransport,\n} from \"../contracts\";\nimport { mcp as mcpClient } from \"./client\";\nimport { serve } from \"./serve\";\n\nexport { serve, createServeHandler } from \"./serve\";\nexport type { McpServeRequestHandler } from \"./serve\";\nexport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\nexport { McpError } from \"./error\";\nexport { mcpResult } from \"./result\";\nexport type { McpNativeResult } from \"./result\";\nexport { jsonSchemaToStandard } from \"./json-schema-to-standard\";\nexport { createJsonRpcClient, createTransport } from \"./transport\";\nexport type { JsonRpcClientHandle } from \"./transport\";\nexport type { McpTransportClient } from \"./transport.type\";\n\n/**\n * The callable `ai.mcp` surface — a factory that connects to an external\n * MCP server (Direction A) and also carries `.serve` to expose a local\n * primitive AS an MCP server (Direction B). Modeled as a function with an\n * attached `serve` property, mirroring how `ai.mcp(server).tools()` and\n * `ai.mcp.serve(source, options).start()` read in the design.\n */\nexport interface McpFactory {\n /**\n * Connect to an external MCP server and adapt its tools as agent tools.\n *\n * @param server - The transport config (`{ type: \"stdio\" }` / `{ type: \"http\" }`).\n * @param options - Prefix / filter / per-call timeout, and a `signal`\n * that closes the client (and fails its calls) when it aborts.\n */\n (server: McpTransport, options?: McpClientOptions): McpClient;\n /**\n * Expose a built agent / supervisor / orchestrator (or a literal\n * `ToolContract[]`, or named `{ groups }`) AS an MCP server. Over\n * `{ type: \"http\" }`, `start()` resolves `{ url, close }`.\n *\n * @param source - The tools to expose.\n * @param options - Server name, version, transport, schema dialect, resources, prompts.\n */\n serve(\n source: McpServeSource,\n options: McpServeOptions & { transport: McpHttpServeTransport },\n ): McpServer<McpHttpServeHandle>;\n serve(source: McpServeSource, options: McpServeOptions): McpServer;\n}\n\n/**\n * The `ai.mcp` factory value: the client factory with `.serve` attached.\n * `Object.assign` keeps `mcp` callable (Direction A) while widening it with\n * the `serve` member (Direction B) — one object, both directions. The\n * `declare module \"@warlock.js/ai\"` augmentation and the runtime\n * registration (`ai.mcp = mcp`) live in `../register`, so this barrel is a\n * pure value/type module the registrar consumes.\n */\nexport const mcp: McpFactory = Object.assign(\n (server: McpTransport, options?: McpClientOptions): McpClient => mcpClient(server, options),\n { serve },\n);\n"],"mappings":";;;;;;;;;;;;;;;;;AA+DA,MAAa,MAAkB,OAAO,QACnC,QAAsB,YAA0CA,MAAU,QAAQ,OAAO,GAC1F,EAAE,MAAM,CACV"}
@@ -1,5 +1,6 @@
1
1
  import { McpServeError } from "../errors.mjs";
2
2
  import { SUPPORTED_PROTOCOL_VERSIONS } from "./protocol.mjs";
3
+ import { McpServeRequests } from "./serve-requests.mjs";
3
4
  import { createHash, timingSafeEqual } from "node:crypto";
4
5
  import { createServer } from "node:http";
5
6
  import { isIPv4, isIPv6 } from "node:net";
@@ -12,7 +13,7 @@ import { isIPv4, isIPv6 } from "node:net";
12
13
  * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
13
14
  * checked 2026-10-17) on `node:http`, with no SDK dependency:
14
15
  *
15
- * - one endpoint path; each POST carries exactly one JSON-RPC message
16
+ * - one MCP endpoint path (plus optional authenticated app routes); each MCP POST carries exactly one JSON-RPC message
16
17
  * (batches were removed from the protocol in 2025-06-18 and get 400);
17
18
  * - a request is answered with `Content-Type: application/json` (the spec
18
19
  * lets a server pick JSON or SSE per request); a notification or a
@@ -70,6 +71,35 @@ function hostnameOf(hostHeader) {
70
71
  function digest(value) {
71
72
  return createHash("sha256").update(value, "utf8").digest();
72
73
  }
74
+ /** Resolve exact descendant routes; no route may replace the MCP endpoint. */
75
+ function resolveRoutes(path, routes) {
76
+ const resolved = /* @__PURE__ */ new Map();
77
+ const methods = /* @__PURE__ */ new Set([
78
+ "GET",
79
+ "POST",
80
+ "PUT",
81
+ "PATCH",
82
+ "DELETE",
83
+ "HEAD",
84
+ "OPTIONS"
85
+ ]);
86
+ if (routes.length > 0) {
87
+ let canonical = false;
88
+ try {
89
+ canonical = !path.startsWith("//") && new URL(path, "http://localhost").pathname === path;
90
+ } catch {}
91
+ if (!canonical) throw new McpServeError("MCP http routes require a canonical endpoint pathname.", { type: "config" });
92
+ }
93
+ for (const route of routes) {
94
+ if (!methods.has(route.method) || !/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(route.path) || route.path.split("/").some((segment) => segment === "." || segment === "..") || typeof route.handler !== "function") throw new McpServeError("Invalid MCP http route method, path or handler.", { type: "config" });
95
+ const fullPath = `${path.replace(/\/+$/, "")}${route.path}`;
96
+ const byMethod = resolved.get(fullPath) ?? /* @__PURE__ */ new Map();
97
+ if (fullPath === path || byMethod.has(route.method)) throw new McpServeError(`MCP http route "${route.method} ${fullPath}" conflicts.`, { type: "collision" });
98
+ byMethod.set(route.method, { ...route });
99
+ resolved.set(fullPath, byMethod);
100
+ }
101
+ return resolved;
102
+ }
73
103
  /** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
74
104
  function normalizeOrigin(origin) {
75
105
  try {
@@ -101,9 +131,9 @@ function resolveSettings(transport) {
101
131
  const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
102
132
  if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
103
133
  if (auth && auth.bearer !== void 0 && tokens.length === 0) throw new McpServeError("MCP http auth.bearer needs at least one token.", { type: "config" });
104
- if (auth && tokens.length === 0 && !auth.authorize) throw new McpServeError("MCP http auth needs `bearer` and/or `authorize`.", { type: "config" });
105
- const authRequired = tokens.length > 0 || auth?.authorize !== void 0;
106
- if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer } or { authorize }), or bind 127.0.0.1.`, {
134
+ if (auth && tokens.length === 0 && !auth.authorize && !auth.authenticate) throw new McpServeError("MCP http auth needs `bearer`, `authorize` and/or `authenticate`.", { type: "config" });
135
+ const authRequired = tokens.length > 0 || auth?.authorize !== void 0 || auth?.authenticate !== void 0;
136
+ if (!loopback && !authRequired) throw new McpServeError(`Refusing to serve MCP over http on non-loopback host "${host}" without auth. Set transport.auth ({ bearer }, { authorize } or { authenticate }), or bind 127.0.0.1.`, {
107
137
  type: "config",
108
138
  context: { host }
109
139
  });
@@ -116,9 +146,11 @@ function resolveSettings(transport) {
116
146
  drainTimeoutMs,
117
147
  tokenDigests: tokens.map(digest),
118
148
  authorize: auth?.authorize,
149
+ authenticate: auth?.authenticate,
119
150
  authRequired,
120
151
  allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
121
- allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
152
+ allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),
153
+ routes: resolveRoutes(path, transport.routes ?? [])
122
154
  };
123
155
  }
124
156
  /** Flatten Node's header bag into lower-cased `name → value` strings. */
@@ -174,6 +206,7 @@ function sendEmpty(response, status, headers = {}) {
174
206
  async function startHttpServer(transport, dispatch) {
175
207
  const settings = resolveSettings(transport);
176
208
  const shutdown = new AbortController();
209
+ const requests = new McpServeRequests();
177
210
  const inFlight = /* @__PURE__ */ new Set();
178
211
  let closing;
179
212
  const server = createServer((request, response) => {
@@ -181,7 +214,7 @@ async function startHttpServer(transport, dispatch) {
181
214
  sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
182
215
  return;
183
216
  }
184
- const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
217
+ const work = handleRequest(request, response, settings, dispatch, shutdown.signal, requests).catch(() => {
185
218
  sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
186
219
  });
187
220
  inFlight.add(work);
@@ -240,13 +273,15 @@ async function startHttpServer(transport, dispatch) {
240
273
  };
241
274
  }
242
275
  /**
243
- * Validate and answer one HTTP request: path → Host / Origin → auth →
244
- * method → headers → body → JSON-RPC dispatch.
276
+ * Validate and answer one HTTP request: Host / Origin → path → auth →
277
+ * method → route handler or MCP headers/body/JSON-RPC dispatch.
245
278
  */
246
- async function handleRequest(request, response, settings, dispatch, signal) {
279
+ async function handleRequest(request, response, settings, dispatch, signal, requests) {
247
280
  const headers = flattenHeaders(request.headers);
248
281
  const method = (request.method ?? "GET").toUpperCase();
249
- const path = new URL(request.url ?? "/", "http://localhost").pathname;
282
+ const url = new URL(request.url ?? "/", "http://localhost");
283
+ const path = url.pathname;
284
+ const routes = settings.routes.get(path);
250
285
  if (!hostAllowed(headers.host, settings)) {
251
286
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
252
287
  return;
@@ -255,7 +290,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
255
290
  sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
256
291
  return;
257
292
  }
258
- if (path !== settings.path) {
293
+ if (path !== settings.path && !routes) {
259
294
  sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
260
295
  return;
261
296
  }
@@ -265,11 +300,21 @@ async function handleRequest(request, response, settings, dispatch, signal) {
265
300
  headers
266
301
  };
267
302
  if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
268
- if (!await authorized(info, settings)) {
269
- const challenge = settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
303
+ const authentication = await authorized(info, settings);
304
+ if (!authentication.allowed) {
305
+ const challenge = settings.tokenDigests.length > 0 || settings.authenticate ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {};
270
306
  sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", challenge);
271
307
  return;
272
308
  }
309
+ if (routes) {
310
+ const route = routes.get(method);
311
+ if (!route) {
312
+ sendEmpty(response, 405, { allow: [...routes.keys()].sort().join(", ") });
313
+ return;
314
+ }
315
+ await handleRoute(request, response, route, info, url, authentication.principal, signal, settings);
316
+ return;
317
+ }
273
318
  if (method !== "POST") {
274
319
  sendEmpty(response, 405, { allow: "POST" });
275
320
  return;
@@ -311,6 +356,7 @@ async function handleRequest(request, response, settings, dispatch, signal) {
311
356
  const id = record.id;
312
357
  if (typeof record.method === "string") {
313
358
  if (typeof id !== "string" && typeof id !== "number") {
359
+ requests.cancel(record, cancellationScope(info));
314
360
  sendEmpty(response, 202);
315
361
  return;
316
362
  }
@@ -320,10 +366,20 @@ async function handleRequest(request, response, settings, dispatch, signal) {
320
366
  method: record.method
321
367
  };
322
368
  if (record.params !== void 0) rpcRequest.params = record.params;
323
- sendJson(response, 200, await dispatch(rpcRequest, {
324
- request: info,
325
- signal
326
- }));
369
+ const disconnected = new AbortController();
370
+ const onClose = () => disconnected.abort();
371
+ response.once("close", onClose);
372
+ try {
373
+ const rpcResponse = await requests.run(rpcRequest, {
374
+ request: info,
375
+ principal: authentication.principal,
376
+ signal: AbortSignal.any([signal, disconnected.signal])
377
+ }, (context) => dispatch(rpcRequest, context), cancellationScope(info));
378
+ if (rpcResponse === void 0) sendEmpty(response, 202);
379
+ else sendJson(response, 200, rpcResponse);
380
+ } finally {
381
+ response.off("close", onClose);
382
+ }
327
383
  return;
328
384
  }
329
385
  if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
@@ -332,6 +388,32 @@ async function handleRequest(request, response, settings, dispatch, signal) {
332
388
  }
333
389
  sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, "Invalid request: not a JSON-RPC message.");
334
390
  }
391
+ /** Run a side route after the shared guards, tracking disconnect and shutdown. */
392
+ async function handleRoute(request, response, route, info, url, principal, shutdown, settings) {
393
+ const disconnected = new AbortController();
394
+ const onClose = () => disconnected.abort();
395
+ response.once("close", onClose);
396
+ try {
397
+ const body = await readBody(request, settings.maxBodyBytes);
398
+ if (body.type === "too-large") {
399
+ sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
400
+ return;
401
+ }
402
+ const signal = AbortSignal.any([shutdown, disconnected.signal]);
403
+ signal.throwIfAborted();
404
+ const result = await route.handler({
405
+ request: info,
406
+ url,
407
+ principal,
408
+ body: body.text,
409
+ signal
410
+ });
411
+ if (result.body === void 0 || info.method === "HEAD") sendEmpty(response, result.status ?? 200, result.headers);
412
+ else sendJson(response, result.status ?? 200, result.body, result.headers);
413
+ } finally {
414
+ response.off("close", onClose);
415
+ }
416
+ }
335
417
  /**
336
418
  * Host-header check. While bound to loopback only loopback names (plus
337
419
  * `allowedHosts`) are accepted, so a DNS-rebound name pointing at
@@ -362,27 +444,50 @@ function originAllowed(origin, settings) {
362
444
  if (settings.allowedOrigins.has(parsed.origin)) return true;
363
445
  return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
364
446
  }
365
- /**
366
- * Run the configured auth. Bearer tokens are compared in constant time
367
- * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
368
- * every configured token is compared so the match position does not leak.
369
- * An `authorize` hook that throws denies the request.
370
- */
371
447
  async function authorized(info, settings) {
372
- if (!settings.authRequired) return true;
448
+ if (!settings.authRequired) return { allowed: true };
373
449
  if (settings.tokenDigests.length > 0) {
374
450
  const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
375
451
  const presented = digest(match?.[1]?.trim() ?? "");
376
452
  let matched = false;
377
453
  for (const expected of settings.tokenDigests) matched = timingSafeEqual(presented, expected) || matched;
378
- if (!match || !matched) return false;
454
+ if (!match || !matched) return { allowed: false };
379
455
  }
380
- if (settings.authorize) try {
381
- return await settings.authorize(info) === true;
456
+ try {
457
+ if (settings.authorize && await settings.authorize(info) !== true) return { allowed: false };
458
+ if (settings.authenticate) {
459
+ const principal = await settings.authenticate(info);
460
+ return principal === void 0 ? { allowed: false } : {
461
+ allowed: true,
462
+ principal
463
+ };
464
+ }
465
+ return { allowed: true };
382
466
  } catch {
383
- return false;
467
+ return { allowed: false };
384
468
  }
385
- return true;
469
+ }
470
+ /**
471
+ * Stateless HTTP has no connection/session id. Repeat the caller's credential
472
+ * and application headers on notifications; per-message transport headers do
473
+ * not identify a caller. Hash the material rather than retaining secrets as keys.
474
+ */
475
+ function cancellationScope(info) {
476
+ const transportHeaders = /* @__PURE__ */ new Set([
477
+ "host",
478
+ "origin",
479
+ "accept",
480
+ "accept-encoding",
481
+ "accept-language",
482
+ "connection",
483
+ "content-type",
484
+ "content-length",
485
+ "transfer-encoding",
486
+ "mcp-protocol-version",
487
+ "user-agent"
488
+ ]);
489
+ const credentials = Object.entries(info.headers).filter(([name]) => !transportHeaders.has(name)).sort(([left], [right]) => left.localeCompare(right));
490
+ return digest(JSON.stringify([info.remoteAddress, credentials])).toString("hex");
386
491
  }
387
492
  /** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
388
493
  function isJsonContentType(contentType) {