@warlock.js/ai-tools 5.30.0 → 5.31.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/CHANGELOG.md +37 -1
- package/README.md +25 -13
- package/cjs/index.cjs +843 -101
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/mcp-serve.type.d.mts +257 -0
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
- package/esm/contracts/mcp.type.d.mts +16 -59
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/errors.d.mts +36 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +20 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +6 -4
- package/esm/index.mjs +3 -2
- package/esm/mcp/client.mjs +35 -5
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +12 -5
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +1 -0
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/protocol.d.mts +16 -0
- package/esm/mcp/protocol.d.mts.map +1 -0
- package/esm/mcp/protocol.mjs +23 -0
- package/esm/mcp/protocol.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +443 -0
- package/esm/mcp/serve-http.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +45 -26
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +325 -92
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/register.d.mts +12 -1
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +3 -2
- package/esm/register.mjs.map +1 -1
- package/esm/schema.mjs.map +1 -1
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +194 -57
- package/llms.txt +3 -3
- package/package.json +2 -6
- package/skills/connect-mcp-server/SKILL.md +28 -6
- package/skills/expose-as-mcp-server/SKILL.md +117 -36
- package/skills/index.md +5 -2
- package/skills/use-web-and-http-tools/SKILL.md +49 -15
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { McpServeError } from "../errors.mjs";
|
|
2
|
+
import { SUPPORTED_PROTOCOL_VERSIONS } from "./protocol.mjs";
|
|
3
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { isIPv4, isIPv6 } from "node:net";
|
|
6
|
+
|
|
7
|
+
//#region ../../packages/ai-tools/src/mcp/serve-http.ts
|
|
8
|
+
/**
|
|
9
|
+
* MCP Streamable HTTP server transport for `ai.mcp.serve`.
|
|
10
|
+
*
|
|
11
|
+
* Implements the server side of the Streamable HTTP transport from the MCP
|
|
12
|
+
* specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,
|
|
13
|
+
* checked 2026-10-17) on `node:http`, with no SDK dependency:
|
|
14
|
+
*
|
|
15
|
+
* - one endpoint path; each POST carries exactly one JSON-RPC message
|
|
16
|
+
* (batches were removed from the protocol in 2025-06-18 and get 400);
|
|
17
|
+
* - a request is answered with `Content-Type: application/json` (the spec
|
|
18
|
+
* lets a server pick JSON or SSE per request); a notification or a
|
|
19
|
+
* response is acknowledged with `202 Accepted` and no body;
|
|
20
|
+
* - stateless: no `MCP-Session-Id` is issued (optional in the spec), so
|
|
21
|
+
* GET (server-initiated SSE stream) and DELETE (session end) answer
|
|
22
|
+
* `405 Method Not Allowed`, as the spec allows;
|
|
23
|
+
* - an unsupported `MCP-Protocol-Version` header gets 400;
|
|
24
|
+
* - security warning items: `Origin` is validated on every request (an
|
|
25
|
+
* invalid one gets 403), the default bind is `127.0.0.1`, and
|
|
26
|
+
* authentication is required for any non-loopback bind.
|
|
27
|
+
*/
|
|
28
|
+
/** Default bind interface — loopback only. */
|
|
29
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
30
|
+
/** Default endpoint path. */
|
|
31
|
+
const DEFAULT_PATH = "/mcp";
|
|
32
|
+
/** Default request body cap: 1 MiB. */
|
|
33
|
+
const DEFAULT_MAX_BODY_BYTES = 1048576;
|
|
34
|
+
/** Default time `close()` waits for in-flight requests. */
|
|
35
|
+
const DEFAULT_DRAIN_TIMEOUT_MS = 5e3;
|
|
36
|
+
/** JSON-RPC codes used for transport-level error bodies. */
|
|
37
|
+
const JSON_RPC_PARSE_ERROR = -32700;
|
|
38
|
+
const JSON_RPC_INVALID_REQUEST = -32600;
|
|
39
|
+
/** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */
|
|
40
|
+
const JSON_RPC_SERVER_ERROR = -32e3;
|
|
41
|
+
/** Host names that always mean "this machine". */
|
|
42
|
+
const LOOPBACK_NAMES = new Set([
|
|
43
|
+
"localhost",
|
|
44
|
+
"127.0.0.1",
|
|
45
|
+
"::1",
|
|
46
|
+
"[::1]"
|
|
47
|
+
]);
|
|
48
|
+
/**
|
|
49
|
+
* True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,
|
|
50
|
+
* `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).
|
|
51
|
+
*/
|
|
52
|
+
function isLoopbackHost(host) {
|
|
53
|
+
const normalized = host.trim().toLowerCase();
|
|
54
|
+
if (LOOPBACK_NAMES.has(normalized)) return true;
|
|
55
|
+
const unbracketed = normalized.replace(/^\[|\]$/g, "");
|
|
56
|
+
const mapped = unbracketed.startsWith("::ffff:") ? unbracketed.slice(7) : unbracketed;
|
|
57
|
+
return isIPv4(mapped) && mapped.startsWith("127.");
|
|
58
|
+
}
|
|
59
|
+
/** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */
|
|
60
|
+
function hostnameOf(hostHeader) {
|
|
61
|
+
const value = hostHeader.trim().toLowerCase();
|
|
62
|
+
if (value.startsWith("[")) {
|
|
63
|
+
const end = value.indexOf("]");
|
|
64
|
+
return end === -1 ? value : value.slice(0, end + 1);
|
|
65
|
+
}
|
|
66
|
+
const colon = value.lastIndexOf(":");
|
|
67
|
+
return colon === -1 ? value : value.slice(0, colon);
|
|
68
|
+
}
|
|
69
|
+
/** SHA-256 digest — equal-length input for `timingSafeEqual`. */
|
|
70
|
+
function digest(value) {
|
|
71
|
+
return createHash("sha256").update(value, "utf8").digest();
|
|
72
|
+
}
|
|
73
|
+
/** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */
|
|
74
|
+
function normalizeOrigin(origin) {
|
|
75
|
+
try {
|
|
76
|
+
return new URL(origin).origin;
|
|
77
|
+
} catch (cause) {
|
|
78
|
+
throw new McpServeError(`allowedOrigins entry "${origin}" is not a valid origin URL.`, {
|
|
79
|
+
type: "config",
|
|
80
|
+
cause
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Validate and default the transport options. Throws {@link McpServeError}
|
|
86
|
+
* (`"config"`) for a non-loopback bind without auth, an empty bearer
|
|
87
|
+
* token, an `auth` with neither check, a bad path / port / limit.
|
|
88
|
+
*/
|
|
89
|
+
function resolveSettings(transport) {
|
|
90
|
+
const host = transport.host ?? DEFAULT_HOST;
|
|
91
|
+
const port = transport.port ?? 0;
|
|
92
|
+
const path = transport.path ?? DEFAULT_PATH;
|
|
93
|
+
const loopback = isLoopbackHost(host);
|
|
94
|
+
const auth = transport.auth;
|
|
95
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) throw new McpServeError(`MCP http port must be an integer in 0–65535 (got ${port}).`, { type: "config" });
|
|
96
|
+
if (!path.startsWith("/")) throw new McpServeError(`MCP http path must start with "/" (got "${path}").`, { type: "config" });
|
|
97
|
+
const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
98
|
+
if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) throw new McpServeError("MCP http maxBodyBytes must be a positive integer.", { type: "config" });
|
|
99
|
+
const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;
|
|
100
|
+
if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) throw new McpServeError("MCP http drainTimeoutMs must be zero or a positive number.", { type: "config" });
|
|
101
|
+
const tokens = auth?.bearer === void 0 ? [] : typeof auth.bearer === "string" ? [auth.bearer] : [...auth.bearer];
|
|
102
|
+
if (tokens.some((token) => token.length === 0)) throw new McpServeError("MCP http auth.bearer tokens must be non-empty strings.", { type: "config" });
|
|
103
|
+
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.`, {
|
|
107
|
+
type: "config",
|
|
108
|
+
context: { host }
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
host,
|
|
112
|
+
port,
|
|
113
|
+
path,
|
|
114
|
+
loopback,
|
|
115
|
+
maxBodyBytes,
|
|
116
|
+
drainTimeoutMs,
|
|
117
|
+
tokenDigests: tokens.map(digest),
|
|
118
|
+
authorize: auth?.authorize,
|
|
119
|
+
authRequired,
|
|
120
|
+
allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),
|
|
121
|
+
allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name)))
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Flatten Node's header bag into lower-cased `name → value` strings. */
|
|
125
|
+
function flattenHeaders(headers) {
|
|
126
|
+
const flat = {};
|
|
127
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
128
|
+
if (value === void 0) continue;
|
|
129
|
+
flat[name.toLowerCase()] = Array.isArray(value) ? value.join(", ") : value;
|
|
130
|
+
}
|
|
131
|
+
return flat;
|
|
132
|
+
}
|
|
133
|
+
/** Write a JSON body with a status code. */
|
|
134
|
+
function sendJson(response, status, body, headers = {}) {
|
|
135
|
+
if (response.headersSent || response.destroyed) return;
|
|
136
|
+
const payload = JSON.stringify(body);
|
|
137
|
+
response.writeHead(status, {
|
|
138
|
+
...headers,
|
|
139
|
+
"content-type": "application/json",
|
|
140
|
+
"content-length": Buffer.byteLength(payload).toString()
|
|
141
|
+
});
|
|
142
|
+
response.end(payload);
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Write an HTTP-level refusal: the status plus a JSON-RPC error body with
|
|
146
|
+
* no request id (the spec allows such a body on 4xx transport errors).
|
|
147
|
+
*/
|
|
148
|
+
function sendRefusal(response, status, code, message, headers = {}) {
|
|
149
|
+
sendJson(response, status, {
|
|
150
|
+
jsonrpc: "2.0",
|
|
151
|
+
id: null,
|
|
152
|
+
error: {
|
|
153
|
+
code,
|
|
154
|
+
message
|
|
155
|
+
}
|
|
156
|
+
}, headers);
|
|
157
|
+
}
|
|
158
|
+
/** Write an empty-bodied status (202 / 405). */
|
|
159
|
+
function sendEmpty(response, status, headers = {}) {
|
|
160
|
+
if (response.headersSent || response.destroyed) return;
|
|
161
|
+
response.writeHead(status, headers);
|
|
162
|
+
response.end();
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request
|
|
166
|
+
* through `dispatch`. Resolves once the port is bound.
|
|
167
|
+
*
|
|
168
|
+
* Throws {@link McpServeError}: `"config"` for invalid / unsafe options
|
|
169
|
+
* (checked before anything binds), `"listen"` when binding fails.
|
|
170
|
+
*
|
|
171
|
+
* @param transport - The http transport options.
|
|
172
|
+
* @param dispatch - Maps one JSON-RPC request (plus call context) to its response.
|
|
173
|
+
*/
|
|
174
|
+
async function startHttpServer(transport, dispatch) {
|
|
175
|
+
const settings = resolveSettings(transport);
|
|
176
|
+
const shutdown = new AbortController();
|
|
177
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
178
|
+
let closing;
|
|
179
|
+
const server = createServer((request, response) => {
|
|
180
|
+
if (closing) {
|
|
181
|
+
sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, "Server is shutting down.", { connection: "close" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {
|
|
185
|
+
sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, "Internal server error.");
|
|
186
|
+
});
|
|
187
|
+
inFlight.add(work);
|
|
188
|
+
work.finally(() => {
|
|
189
|
+
inFlight.delete(work);
|
|
190
|
+
if (closing) server.closeIdleConnections();
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
await new Promise((resolve, reject) => {
|
|
194
|
+
const onError = (cause) => {
|
|
195
|
+
reject(new McpServeError(`MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`, {
|
|
196
|
+
type: "listen",
|
|
197
|
+
cause,
|
|
198
|
+
context: {
|
|
199
|
+
host: settings.host,
|
|
200
|
+
port: settings.port
|
|
201
|
+
}
|
|
202
|
+
}));
|
|
203
|
+
};
|
|
204
|
+
server.once("error", onError);
|
|
205
|
+
server.listen(settings.port, settings.host, () => {
|
|
206
|
+
server.off("error", onError);
|
|
207
|
+
resolve();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
const address = server.address();
|
|
211
|
+
const boundPort = typeof address === "object" && address !== null ? address.port : settings.port;
|
|
212
|
+
const url = `http://${isIPv6(settings.host) ? `[${settings.host}]` : settings.host}:${boundPort}${settings.path}`;
|
|
213
|
+
const close = () => {
|
|
214
|
+
if (closing) return closing;
|
|
215
|
+
closing = (async () => {
|
|
216
|
+
const closed = new Promise((resolve) => {
|
|
217
|
+
server.close(() => resolve());
|
|
218
|
+
});
|
|
219
|
+
server.closeIdleConnections();
|
|
220
|
+
if (inFlight.size > 0) {
|
|
221
|
+
let timer;
|
|
222
|
+
const timedOut = new Promise((resolve) => {
|
|
223
|
+
timer = setTimeout(() => resolve("timeout"), settings.drainTimeoutMs);
|
|
224
|
+
});
|
|
225
|
+
const outcome = await Promise.race([Promise.allSettled([...inFlight]).then(() => "drained"), timedOut]);
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
if (outcome === "timeout") {
|
|
228
|
+
shutdown.abort();
|
|
229
|
+
server.closeAllConnections();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
server.closeIdleConnections();
|
|
233
|
+
await closed;
|
|
234
|
+
})();
|
|
235
|
+
return closing;
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
url,
|
|
239
|
+
close
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Validate and answer one HTTP request: path → Host / Origin → auth →
|
|
244
|
+
* method → headers → body → JSON-RPC dispatch.
|
|
245
|
+
*/
|
|
246
|
+
async function handleRequest(request, response, settings, dispatch, signal) {
|
|
247
|
+
const headers = flattenHeaders(request.headers);
|
|
248
|
+
const method = (request.method ?? "GET").toUpperCase();
|
|
249
|
+
const path = new URL(request.url ?? "/", "http://localhost").pathname;
|
|
250
|
+
if (!hostAllowed(headers.host, settings)) {
|
|
251
|
+
sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Host header not allowed.");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!originAllowed(headers.origin, settings)) {
|
|
255
|
+
sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, "Forbidden: Origin not allowed.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (path !== settings.path) {
|
|
259
|
+
sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, "Not found.");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const info = {
|
|
263
|
+
method,
|
|
264
|
+
path,
|
|
265
|
+
headers
|
|
266
|
+
};
|
|
267
|
+
if (request.socket.remoteAddress !== void 0) info.remoteAddress = request.socket.remoteAddress;
|
|
268
|
+
if (!await authorized(info, settings)) {
|
|
269
|
+
sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, "Unauthorized.", settings.tokenDigests.length > 0 ? { "www-authenticate": "Bearer realm=\"mcp\"" } : {});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (method !== "POST") {
|
|
273
|
+
sendEmpty(response, 405, { allow: "POST" });
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const protocolVersion = headers["mcp-protocol-version"];
|
|
277
|
+
if (protocolVersion !== void 0 && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {
|
|
278
|
+
sendRefusal(response, 400, JSON_RPC_SERVER_ERROR, `Unsupported MCP-Protocol-Version "${protocolVersion}" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}).`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (!isJsonContentType(headers["content-type"])) {
|
|
282
|
+
sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, "Content-Type must be application/json.");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!acceptsJson(headers.accept)) {
|
|
286
|
+
sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, "Accept must allow application/json.");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const body = await readBody(request, settings.maxBodyBytes);
|
|
290
|
+
if (body.type === "too-large") {
|
|
291
|
+
sendRefusal(response, 413, JSON_RPC_SERVER_ERROR, `Request body exceeds ${settings.maxBodyBytes} bytes.`, { connection: "close" });
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
let message;
|
|
295
|
+
try {
|
|
296
|
+
message = JSON.parse(body.text);
|
|
297
|
+
} catch {
|
|
298
|
+
sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, "Parse error: body is not valid JSON.");
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) {
|
|
302
|
+
sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, "Invalid request: the body must be one JSON-RPC message (batches are not supported).");
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const record = message;
|
|
306
|
+
if (record.jsonrpc !== "2.0") {
|
|
307
|
+
sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, "Invalid request: jsonrpc must be \"2.0\".");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const id = record.id;
|
|
311
|
+
if (typeof record.method === "string") {
|
|
312
|
+
if (typeof id !== "string" && typeof id !== "number") {
|
|
313
|
+
sendEmpty(response, 202);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
const rpcRequest = {
|
|
317
|
+
jsonrpc: "2.0",
|
|
318
|
+
id,
|
|
319
|
+
method: record.method
|
|
320
|
+
};
|
|
321
|
+
if (record.params !== void 0) rpcRequest.params = record.params;
|
|
322
|
+
sendJson(response, 200, await dispatch(rpcRequest, {
|
|
323
|
+
request: info,
|
|
324
|
+
signal
|
|
325
|
+
}));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if ((typeof id === "string" || typeof id === "number") && ("result" in record || "error" in record)) {
|
|
329
|
+
sendEmpty(response, 202);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, "Invalid request: not a JSON-RPC message.");
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Host-header check. While bound to loopback only loopback names (plus
|
|
336
|
+
* `allowedHosts`) are accepted, so a DNS-rebound name pointing at
|
|
337
|
+
* 127.0.0.1 is refused. Off loopback the check applies only when
|
|
338
|
+
* `allowedHosts` is set.
|
|
339
|
+
*/
|
|
340
|
+
function hostAllowed(hostHeader, settings) {
|
|
341
|
+
if (!settings.loopback && settings.allowedHosts.size === 0) return true;
|
|
342
|
+
if (hostHeader === void 0) return false;
|
|
343
|
+
const name = hostnameOf(hostHeader);
|
|
344
|
+
if (settings.allowedHosts.has(name)) return true;
|
|
345
|
+
return settings.loopback && isLoopbackHost(name);
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Origin check (MCP Streamable HTTP security warning). No `Origin` header —
|
|
349
|
+
* a non-browser client — passes; otherwise the origin must be a loopback
|
|
350
|
+
* `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable
|
|
351
|
+
* origins are refused.
|
|
352
|
+
*/
|
|
353
|
+
function originAllowed(origin, settings) {
|
|
354
|
+
if (origin === void 0) return true;
|
|
355
|
+
let parsed;
|
|
356
|
+
try {
|
|
357
|
+
parsed = new URL(origin);
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
if (settings.allowedOrigins.has(parsed.origin)) return true;
|
|
362
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && isLoopbackHost(parsed.hostname);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Run the configured auth. Bearer tokens are compared in constant time
|
|
366
|
+
* over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and
|
|
367
|
+
* every configured token is compared so the match position does not leak.
|
|
368
|
+
* An `authorize` hook that throws denies the request.
|
|
369
|
+
*/
|
|
370
|
+
async function authorized(info, settings) {
|
|
371
|
+
if (!settings.authRequired) return true;
|
|
372
|
+
if (settings.tokenDigests.length > 0) {
|
|
373
|
+
const match = /^Bearer[ \t]+(.+)$/i.exec(info.headers.authorization ?? "");
|
|
374
|
+
const presented = digest(match?.[1]?.trim() ?? "");
|
|
375
|
+
let matched = false;
|
|
376
|
+
for (const expected of settings.tokenDigests) matched = timingSafeEqual(presented, expected) || matched;
|
|
377
|
+
if (!match || !matched) return false;
|
|
378
|
+
}
|
|
379
|
+
if (settings.authorize) try {
|
|
380
|
+
return await settings.authorize(info) === true;
|
|
381
|
+
} catch {
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */
|
|
387
|
+
function isJsonContentType(contentType) {
|
|
388
|
+
return contentType?.split(";")[0]?.trim().toLowerCase() === "application/json";
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* True when the `Accept` header allows a JSON reply. Clients are required
|
|
392
|
+
* to list both `application/json` and `text/event-stream`; this server
|
|
393
|
+
* always answers JSON, so it only insists on that half. A missing header
|
|
394
|
+
* is accepted.
|
|
395
|
+
*/
|
|
396
|
+
function acceptsJson(accept) {
|
|
397
|
+
if (accept === void 0 || accept.trim() === "") return true;
|
|
398
|
+
return accept.split(",").map((part) => part.split(";")[0]?.trim().toLowerCase()).some((type) => type === "application/json" || type === "application/*" || type === "*/*");
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Read the request body as UTF-8, refusing anything over `limit` bytes —
|
|
402
|
+
* up front from `Content-Length`, and while streaming for chunked bodies.
|
|
403
|
+
* A refused body is left unread; the 413 response closes the connection.
|
|
404
|
+
*/
|
|
405
|
+
function readBody(request, limit) {
|
|
406
|
+
const declared = Number(request.headers["content-length"]);
|
|
407
|
+
if (Number.isFinite(declared) && declared > limit) return Promise.resolve({ type: "too-large" });
|
|
408
|
+
return new Promise((resolve, reject) => {
|
|
409
|
+
const chunks = [];
|
|
410
|
+
let size = 0;
|
|
411
|
+
let done = false;
|
|
412
|
+
const finish = (result) => {
|
|
413
|
+
if (!done) {
|
|
414
|
+
done = true;
|
|
415
|
+
resolve(result);
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
request.on("data", (chunk) => {
|
|
419
|
+
if (done) return;
|
|
420
|
+
size += chunk.length;
|
|
421
|
+
if (size > limit) {
|
|
422
|
+
request.pause();
|
|
423
|
+
finish({ type: "too-large" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
chunks.push(chunk);
|
|
427
|
+
});
|
|
428
|
+
request.on("end", () => finish({
|
|
429
|
+
type: "ok",
|
|
430
|
+
text: Buffer.concat(chunks).toString("utf8")
|
|
431
|
+
}));
|
|
432
|
+
request.on("error", (error) => {
|
|
433
|
+
if (!done) {
|
|
434
|
+
done = true;
|
|
435
|
+
reject(error);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
//#endregion
|
|
442
|
+
export { startHttpServer };
|
|
443
|
+
//# sourceMappingURL=serve-http.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serve-http.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve-http.ts"],"sourcesContent":["import { createHash, timingSafeEqual } from \"node:crypto\";\nimport {\n createServer,\n type IncomingHttpHeaders,\n type IncomingMessage,\n type Server,\n type ServerResponse,\n} from \"node:http\";\nimport { isIPv4, isIPv6 } from \"node:net\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpHttpAuth,\n McpHttpRequestInfo,\n McpHttpServeTransport,\n McpServeCallContext,\n} from \"../contracts\";\nimport { McpServeError } from \"../errors\";\nimport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\n\n/**\n * MCP Streamable HTTP server transport for `ai.mcp.serve`.\n *\n * Implements the server side of the Streamable HTTP transport from the MCP\n * specification (https://modelcontextprotocol.io/specification/2025-11-25/basic/transports,\n * checked 2026-10-17) on `node:http`, with no SDK dependency:\n *\n * - one endpoint path; each POST carries exactly one JSON-RPC message\n * (batches were removed from the protocol in 2025-06-18 and get 400);\n * - a request is answered with `Content-Type: application/json` (the spec\n * lets a server pick JSON or SSE per request); a notification or a\n * response is acknowledged with `202 Accepted` and no body;\n * - stateless: no `MCP-Session-Id` is issued (optional in the spec), so\n * GET (server-initiated SSE stream) and DELETE (session end) answer\n * `405 Method Not Allowed`, as the spec allows;\n * - an unsupported `MCP-Protocol-Version` header gets 400;\n * - security warning items: `Origin` is validated on every request (an\n * invalid one gets 403), the default bind is `127.0.0.1`, and\n * authentication is required for any non-loopback bind.\n */\n\n/** Default bind interface — loopback only. */\nconst DEFAULT_HOST = \"127.0.0.1\";\n\n/** Default endpoint path. */\nconst DEFAULT_PATH = \"/mcp\";\n\n/** Default request body cap: 1 MiB. */\nconst DEFAULT_MAX_BODY_BYTES = 1_048_576;\n\n/** Default time `close()` waits for in-flight requests. */\nconst DEFAULT_DRAIN_TIMEOUT_MS = 5_000;\n\n/** JSON-RPC codes used for transport-level error bodies. */\nconst JSON_RPC_PARSE_ERROR = -32700;\nconst JSON_RPC_INVALID_REQUEST = -32600;\n\n/** Generic server-defined JSON-RPC code for HTTP-level refusals (auth, origin, size). */\nconst JSON_RPC_SERVER_ERROR = -32000;\n\n/** Host names that always mean \"this machine\". */\nconst LOOPBACK_NAMES = new Set([\"localhost\", \"127.0.0.1\", \"::1\", \"[::1]\"]);\n\n/** The function the transport pumps each JSON-RPC request through. */\nexport type McpHttpDispatch = (\n request: JsonRpcRequest,\n context: McpServeCallContext,\n) => Promise<JsonRpcResponse>;\n\n/** A bound HTTP endpoint. */\nexport type RunningMcpHttpServer = {\n /** Full endpoint URL. */\n url: string;\n /** Drain in-flight requests (up to the drain timeout) and free the port. Idempotent. */\n close(): Promise<void>;\n};\n\n/**\n * True when `host` names a loopback interface (`localhost`, `127.0.0.0/8`,\n * `::1`, or an IPv4-mapped `::ffff:127.x.x.x`).\n */\nexport function isLoopbackHost(host: string): boolean {\n const normalized = host.trim().toLowerCase();\n\n if (LOOPBACK_NAMES.has(normalized)) {\n return true;\n }\n\n const unbracketed = normalized.replace(/^\\[|\\]$/g, \"\");\n const mapped = unbracketed.startsWith(\"::ffff:\")\n ? unbracketed.slice(\"::ffff:\".length)\n : unbracketed;\n\n return isIPv4(mapped) && mapped.startsWith(\"127.\");\n}\n\n/** Strip the port from a `Host` header value (`[::1]:3000` → `[::1]`, `a.b:80` → `a.b`). */\nfunction hostnameOf(hostHeader: string): string {\n const value = hostHeader.trim().toLowerCase();\n\n if (value.startsWith(\"[\")) {\n const end = value.indexOf(\"]\");\n\n return end === -1 ? value : value.slice(0, end + 1);\n }\n\n const colon = value.lastIndexOf(\":\");\n\n return colon === -1 ? value : value.slice(0, colon);\n}\n\n/** SHA-256 digest — equal-length input for `timingSafeEqual`. */\nfunction digest(value: string): Buffer {\n return createHash(\"sha256\").update(value, \"utf8\").digest();\n}\n\n/**\n * Validated, defaulted transport settings plus the precomputed security\n * material (token digests, origin / host allowlists).\n */\ntype ResolvedSettings = {\n host: string;\n port: number;\n path: string;\n loopback: boolean;\n maxBodyBytes: number;\n drainTimeoutMs: number;\n tokenDigests: Buffer[];\n authorize: McpHttpAuth[\"authorize\"];\n authRequired: boolean;\n allowedOrigins: Set<string>;\n allowedHosts: Set<string>;\n};\n\n/** Normalise a configured origin to `scheme://host[:port]`, or throw a config error. */\nfunction normalizeOrigin(origin: string): string {\n try {\n return new URL(origin).origin;\n } catch (cause) {\n throw new McpServeError(`allowedOrigins entry \"${origin}\" is not a valid origin URL.`, {\n type: \"config\",\n cause,\n });\n }\n}\n\n/**\n * Validate and default the transport options. Throws {@link McpServeError}\n * (`\"config\"`) for a non-loopback bind without auth, an empty bearer\n * token, an `auth` with neither check, a bad path / port / limit.\n */\nfunction resolveSettings(transport: McpHttpServeTransport): ResolvedSettings {\n const host = transport.host ?? DEFAULT_HOST;\n const port = transport.port ?? 0;\n const path = transport.path ?? DEFAULT_PATH;\n const loopback = isLoopbackHost(host);\n const auth = transport.auth;\n\n if (!Number.isInteger(port) || port < 0 || port > 65_535) {\n throw new McpServeError(`MCP http port must be an integer in 0–65535 (got ${port}).`, {\n type: \"config\",\n });\n }\n\n if (!path.startsWith(\"/\")) {\n throw new McpServeError(`MCP http path must start with \"/\" (got \"${path}\").`, {\n type: \"config\",\n });\n }\n\n const maxBodyBytes = transport.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;\n\n if (!Number.isInteger(maxBodyBytes) || maxBodyBytes <= 0) {\n throw new McpServeError(\"MCP http maxBodyBytes must be a positive integer.\", {\n type: \"config\",\n });\n }\n\n const drainTimeoutMs = transport.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS;\n\n if (!Number.isFinite(drainTimeoutMs) || drainTimeoutMs < 0) {\n throw new McpServeError(\"MCP http drainTimeoutMs must be zero or a positive number.\", {\n type: \"config\",\n });\n }\n\n const tokens =\n auth?.bearer === undefined\n ? []\n : typeof auth.bearer === \"string\"\n ? [auth.bearer]\n : [...auth.bearer];\n\n if (tokens.some((token) => token.length === 0)) {\n throw new McpServeError(\"MCP http auth.bearer tokens must be non-empty strings.\", {\n type: \"config\",\n });\n }\n\n if (auth && auth.bearer !== undefined && tokens.length === 0) {\n throw new McpServeError(\"MCP http auth.bearer needs at least one token.\", { type: \"config\" });\n }\n\n if (auth && tokens.length === 0 && !auth.authorize) {\n throw new McpServeError(\"MCP http auth needs `bearer` and/or `authorize`.\", {\n type: \"config\",\n });\n }\n\n const authRequired = tokens.length > 0 || auth?.authorize !== undefined;\n\n if (!loopback && !authRequired) {\n throw new McpServeError(\n `Refusing to serve MCP over http on non-loopback host \"${host}\" without auth. ` +\n \"Set transport.auth ({ bearer } or { authorize }), or bind 127.0.0.1.\",\n { type: \"config\", context: { host } },\n );\n }\n\n return {\n host,\n port,\n path,\n loopback,\n maxBodyBytes,\n drainTimeoutMs,\n tokenDigests: tokens.map(digest),\n authorize: auth?.authorize,\n authRequired,\n allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),\n allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),\n };\n}\n\n/** Flatten Node's header bag into lower-cased `name → value` strings. */\nfunction flattenHeaders(headers: IncomingHttpHeaders): Record<string, string> {\n const flat: Record<string, string> = {};\n\n for (const [name, value] of Object.entries(headers)) {\n if (value === undefined) {\n continue;\n }\n\n flat[name.toLowerCase()] = Array.isArray(value) ? value.join(\", \") : value;\n }\n\n return flat;\n}\n\n/** Write a JSON body with a status code. */\nfunction sendJson(\n response: ServerResponse,\n status: number,\n body: unknown,\n headers: Record<string, string> = {},\n): void {\n if (response.headersSent || response.destroyed) {\n return;\n }\n\n const payload = JSON.stringify(body);\n\n response.writeHead(status, {\n ...headers,\n \"content-type\": \"application/json\",\n \"content-length\": Buffer.byteLength(payload).toString(),\n });\n response.end(payload);\n}\n\n/**\n * Write an HTTP-level refusal: the status plus a JSON-RPC error body with\n * no request id (the spec allows such a body on 4xx transport errors).\n */\nfunction sendRefusal(\n response: ServerResponse,\n status: number,\n code: number,\n message: string,\n headers: Record<string, string> = {},\n): void {\n sendJson(response, status, { jsonrpc: \"2.0\", id: null, error: { code, message } }, headers);\n}\n\n/** Write an empty-bodied status (202 / 405). */\nfunction sendEmpty(\n response: ServerResponse,\n status: number,\n headers: Record<string, string> = {},\n): void {\n if (response.headersSent || response.destroyed) {\n return;\n }\n\n response.writeHead(status, headers);\n response.end();\n}\n\n/**\n * Bind an MCP Streamable HTTP endpoint and pump each JSON-RPC request\n * through `dispatch`. Resolves once the port is bound.\n *\n * Throws {@link McpServeError}: `\"config\"` for invalid / unsafe options\n * (checked before anything binds), `\"listen\"` when binding fails.\n *\n * @param transport - The http transport options.\n * @param dispatch - Maps one JSON-RPC request (plus call context) to its response.\n */\nexport async function startHttpServer(\n transport: McpHttpServeTransport,\n dispatch: McpHttpDispatch,\n): Promise<RunningMcpHttpServer> {\n const settings = resolveSettings(transport);\n const shutdown = new AbortController();\n const inFlight = new Set<Promise<void>>();\n let closing: Promise<void> | undefined;\n\n const server: Server = createServer((request, response) => {\n if (closing) {\n sendRefusal(response, 503, JSON_RPC_SERVER_ERROR, \"Server is shutting down.\", {\n connection: \"close\",\n });\n\n return;\n }\n\n const work = handleRequest(request, response, settings, dispatch, shutdown.signal).catch(() => {\n sendRefusal(response, 500, JSON_RPC_SERVER_ERROR, \"Internal server error.\");\n });\n\n inFlight.add(work);\n void work.finally(() => {\n inFlight.delete(work);\n\n if (closing) {\n server.closeIdleConnections();\n }\n });\n });\n\n await new Promise<void>((resolve, reject) => {\n const onError = (cause: Error) => {\n reject(\n new McpServeError(\n `MCP http server could not listen on ${settings.host}:${settings.port}: ${cause.message}`,\n { type: \"listen\", cause, context: { host: settings.host, port: settings.port } },\n ),\n );\n };\n\n server.once(\"error\", onError);\n server.listen(settings.port, settings.host, () => {\n server.off(\"error\", onError);\n resolve();\n });\n });\n\n const address = server.address();\n const boundPort = typeof address === \"object\" && address !== null ? address.port : settings.port;\n const urlHost = isIPv6(settings.host) ? `[${settings.host}]` : settings.host;\n const url = `http://${urlHost}:${boundPort}${settings.path}`;\n\n const close = (): Promise<void> => {\n if (closing) {\n return closing;\n }\n\n closing = (async () => {\n const closed = new Promise<void>((resolve) => {\n server.close(() => resolve());\n });\n\n server.closeIdleConnections();\n\n if (inFlight.size > 0) {\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timedOut = new Promise<\"timeout\">((resolve) => {\n timer = setTimeout(() => resolve(\"timeout\"), settings.drainTimeoutMs);\n });\n\n const outcome = await Promise.race([\n Promise.allSettled([...inFlight]).then(() => \"drained\" as const),\n timedOut,\n ]);\n\n clearTimeout(timer);\n\n if (outcome === \"timeout\") {\n shutdown.abort();\n server.closeAllConnections();\n }\n }\n\n server.closeIdleConnections();\n await closed;\n })();\n\n return closing;\n };\n\n return { url, close };\n}\n\n/**\n * Validate and answer one HTTP request: path → Host / Origin → auth →\n * method → headers → body → JSON-RPC dispatch.\n */\nasync function handleRequest(\n request: IncomingMessage,\n response: ServerResponse,\n settings: ResolvedSettings,\n dispatch: McpHttpDispatch,\n signal: AbortSignal,\n): Promise<void> {\n const headers = flattenHeaders(request.headers);\n const method = (request.method ?? \"GET\").toUpperCase();\n const path = new URL(request.url ?? \"/\", \"http://localhost\").pathname;\n\n // DNS-rebinding guards apply to every request on every path.\n if (!hostAllowed(headers.host, settings)) {\n sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, \"Forbidden: Host header not allowed.\");\n\n return;\n }\n\n if (!originAllowed(headers.origin, settings)) {\n sendRefusal(response, 403, JSON_RPC_SERVER_ERROR, \"Forbidden: Origin not allowed.\");\n\n return;\n }\n\n if (path !== settings.path) {\n sendRefusal(response, 404, JSON_RPC_SERVER_ERROR, \"Not found.\");\n\n return;\n }\n\n const info: McpHttpRequestInfo = { method, path, headers };\n\n if (request.socket.remoteAddress !== undefined) {\n info.remoteAddress = request.socket.remoteAddress;\n }\n\n if (!(await authorized(info, settings))) {\n const challenge: Record<string, string> =\n settings.tokenDigests.length > 0 ? { \"www-authenticate\": 'Bearer realm=\"mcp\"' } : {};\n\n sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, \"Unauthorized.\", challenge);\n\n return;\n }\n\n if (method !== \"POST\") {\n // Stateless server: no server-initiated SSE stream (GET) and no\n // session to terminate (DELETE). The spec allows 405 for both.\n sendEmpty(response, 405, { allow: \"POST\" });\n\n return;\n }\n\n const protocolVersion = headers[\"mcp-protocol-version\"];\n\n if (protocolVersion !== undefined && !SUPPORTED_PROTOCOL_VERSIONS.includes(protocolVersion)) {\n sendRefusal(\n response,\n 400,\n JSON_RPC_SERVER_ERROR,\n `Unsupported MCP-Protocol-Version \"${protocolVersion}\" (supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(\", \")}).`,\n );\n\n return;\n }\n\n if (!isJsonContentType(headers[\"content-type\"])) {\n sendRefusal(response, 415, JSON_RPC_SERVER_ERROR, \"Content-Type must be application/json.\");\n\n return;\n }\n\n if (!acceptsJson(headers.accept)) {\n sendRefusal(response, 406, JSON_RPC_SERVER_ERROR, \"Accept must allow application/json.\");\n\n return;\n }\n\n const body = await readBody(request, settings.maxBodyBytes);\n\n if (body.type === \"too-large\") {\n sendRefusal(\n response,\n 413,\n JSON_RPC_SERVER_ERROR,\n `Request body exceeds ${settings.maxBodyBytes} bytes.`,\n { connection: \"close\" },\n );\n\n return;\n }\n\n let message: unknown;\n\n try {\n message = JSON.parse(body.text);\n } catch {\n sendRefusal(response, 400, JSON_RPC_PARSE_ERROR, \"Parse error: body is not valid JSON.\");\n\n return;\n }\n\n if (typeof message !== \"object\" || message === null || Array.isArray(message)) {\n sendRefusal(\n response,\n 400,\n JSON_RPC_INVALID_REQUEST,\n \"Invalid request: the body must be one JSON-RPC message (batches are not supported).\",\n );\n\n return;\n }\n\n const record = message as Record<string, unknown>;\n\n if (record.jsonrpc !== \"2.0\") {\n sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, 'Invalid request: jsonrpc must be \"2.0\".');\n\n return;\n }\n\n const id = record.id;\n\n if (typeof record.method === \"string\") {\n if (typeof id !== \"string\" && typeof id !== \"number\") {\n // A notification (notifications/initialized, notifications/cancelled…).\n sendEmpty(response, 202);\n\n return;\n }\n\n const rpcRequest: JsonRpcRequest = { jsonrpc: \"2.0\", id, method: record.method };\n\n if (record.params !== undefined) {\n rpcRequest.params = record.params;\n }\n\n const rpcResponse = await dispatch(rpcRequest, { request: info, signal });\n\n sendJson(response, 200, rpcResponse);\n\n return;\n }\n\n if (\n (typeof id === \"string\" || typeof id === \"number\") &&\n (\"result\" in record || \"error\" in record)\n ) {\n // A response to a server-initiated request; this server sends none, so acknowledge.\n sendEmpty(response, 202);\n\n return;\n }\n\n sendRefusal(response, 400, JSON_RPC_INVALID_REQUEST, \"Invalid request: not a JSON-RPC message.\");\n}\n\n/**\n * Host-header check. While bound to loopback only loopback names (plus\n * `allowedHosts`) are accepted, so a DNS-rebound name pointing at\n * 127.0.0.1 is refused. Off loopback the check applies only when\n * `allowedHosts` is set.\n */\nfunction hostAllowed(hostHeader: string | undefined, settings: ResolvedSettings): boolean {\n if (!settings.loopback && settings.allowedHosts.size === 0) {\n return true;\n }\n\n if (hostHeader === undefined) {\n return false;\n }\n\n const name = hostnameOf(hostHeader);\n\n if (settings.allowedHosts.has(name)) {\n return true;\n }\n\n return settings.loopback && isLoopbackHost(name);\n}\n\n/**\n * Origin check (MCP Streamable HTTP security warning). No `Origin` header —\n * a non-browser client — passes; otherwise the origin must be a loopback\n * `http(s)` origin or listed in `allowedOrigins`. `null` and unparsable\n * origins are refused.\n */\nfunction originAllowed(origin: string | undefined, settings: ResolvedSettings): boolean {\n if (origin === undefined) {\n return true;\n }\n\n let parsed: URL;\n\n try {\n parsed = new URL(origin);\n } catch {\n return false;\n }\n\n if (settings.allowedOrigins.has(parsed.origin)) {\n return true;\n }\n\n return (\n (parsed.protocol === \"http:\" || parsed.protocol === \"https:\") && isLoopbackHost(parsed.hostname)\n );\n}\n\n/**\n * Run the configured auth. Bearer tokens are compared in constant time\n * over SHA-256 digests (equal-length buffers for `timingSafeEqual`), and\n * every configured token is compared so the match position does not leak.\n * An `authorize` hook that throws denies the request.\n */\nasync function authorized(info: McpHttpRequestInfo, settings: ResolvedSettings): Promise<boolean> {\n if (!settings.authRequired) {\n return true;\n }\n\n if (settings.tokenDigests.length > 0) {\n const match = /^Bearer[ \\t]+(.+)$/i.exec(info.headers.authorization ?? \"\");\n const presented = digest(match?.[1]?.trim() ?? \"\");\n let matched = false;\n\n for (const expected of settings.tokenDigests) {\n matched = timingSafeEqual(presented, expected) || matched;\n }\n\n if (!match || !matched) {\n return false;\n }\n }\n\n if (settings.authorize) {\n try {\n return (await settings.authorize(info)) === true;\n } catch {\n return false;\n }\n }\n\n return true;\n}\n\n/** True for `application/json` (any parameters, e.g. `; charset=utf-8`). */\nfunction isJsonContentType(contentType: string | undefined): boolean {\n return contentType?.split(\";\")[0]?.trim().toLowerCase() === \"application/json\";\n}\n\n/**\n * True when the `Accept` header allows a JSON reply. Clients are required\n * to list both `application/json` and `text/event-stream`; this server\n * always answers JSON, so it only insists on that half. A missing header\n * is accepted.\n */\nfunction acceptsJson(accept: string | undefined): boolean {\n if (accept === undefined || accept.trim() === \"\") {\n return true;\n }\n\n return accept\n .split(\",\")\n .map((part) => part.split(\";\")[0]?.trim().toLowerCase())\n .some((type) => type === \"application/json\" || type === \"application/*\" || type === \"*/*\");\n}\n\n/** The outcome of reading a request body. */\ntype BodyResult = { type: \"ok\"; text: string } | { type: \"too-large\" };\n\n/**\n * Read the request body as UTF-8, refusing anything over `limit` bytes —\n * up front from `Content-Length`, and while streaming for chunked bodies.\n * A refused body is left unread; the 413 response closes the connection.\n */\nfunction readBody(request: IncomingMessage, limit: number): Promise<BodyResult> {\n const declared = Number(request.headers[\"content-length\"]);\n\n if (Number.isFinite(declared) && declared > limit) {\n return Promise.resolve({ type: \"too-large\" });\n }\n\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n let done = false;\n\n const finish = (result: BodyResult) => {\n if (!done) {\n done = true;\n resolve(result);\n }\n };\n\n request.on(\"data\", (chunk: Buffer) => {\n if (done) {\n return;\n }\n\n size += chunk.length;\n\n if (size > limit) {\n request.pause();\n finish({ type: \"too-large\" });\n\n return;\n }\n\n chunks.push(chunk);\n });\n request.on(\"end\", () => finish({ type: \"ok\", text: Buffer.concat(chunks).toString(\"utf8\") }));\n request.on(\"error\", (error) => {\n if (!done) {\n done = true;\n reject(error);\n }\n });\n });\n}\n"],"mappings":"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"}
|
package/esm/mcp/serve.d.mts
CHANGED
|
@@ -1,46 +1,65 @@
|
|
|
1
|
-
import { JsonRpcRequest, JsonRpcResponse
|
|
1
|
+
import { JsonRpcRequest, JsonRpcResponse } from "../contracts/mcp.type.mjs";
|
|
2
|
+
import { McpHttpServeHandle, McpHttpServeTransport, McpServeCallContext, McpServeOptions, McpServeSource, McpServer } from "../contracts/mcp-serve.type.mjs";
|
|
2
3
|
//#region ../../packages/ai-tools/src/mcp/serve.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* The pure protocol handler: one JSON-RPC request in, one response out.
|
|
6
|
+
*/
|
|
7
|
+
interface McpServeRequestHandler {
|
|
8
|
+
/**
|
|
9
|
+
* Map one request to its response.
|
|
10
|
+
*
|
|
11
|
+
* @param request - The JSON-RPC request.
|
|
12
|
+
* @param context - Optional per-call context (inbound HTTP request, abort signal).
|
|
13
|
+
*/
|
|
14
|
+
handle(request: JsonRpcRequest, context?: McpServeCallContext): Promise<JsonRpcResponse>;
|
|
15
|
+
}
|
|
3
16
|
/**
|
|
4
17
|
* Build the pure protocol handler for a serve source. Exposed (alongside
|
|
5
18
|
* {@link serve}) so callers and tests can drive the MCP protocol without an
|
|
6
19
|
* actual transport — feed it a JSON-RPC request, get the response.
|
|
7
20
|
*
|
|
8
|
-
* @
|
|
9
|
-
*
|
|
10
|
-
*
|
|
21
|
+
* Throws {@link McpServeError} on duplicate tool names / resource URIs /
|
|
22
|
+
* prompt names, or `prefixToolNames` on an ungrouped source.
|
|
23
|
+
*
|
|
24
|
+
* @param source - The tools to expose (one tool set or `{ groups }`).
|
|
25
|
+
* @param options - Serve options (name / version / schema target / resources / prompts).
|
|
26
|
+
* @returns An object whose `handle(request, context?)` maps a request to a response.
|
|
11
27
|
*/
|
|
12
|
-
declare function createServeHandler(source: McpServeSource, options: McpServeOptions):
|
|
13
|
-
handle(request: JsonRpcRequest): Promise<JsonRpcResponse>;
|
|
14
|
-
};
|
|
28
|
+
declare function createServeHandler(source: McpServeSource, options: McpServeOptions): McpServeRequestHandler;
|
|
15
29
|
/**
|
|
16
30
|
* Expose a built agent / supervisor / orchestrator (or a raw
|
|
17
|
-
* `ToolContract[]
|
|
18
|
-
* server other clients consume).
|
|
31
|
+
* `ToolContract[]`, or several named groups) AS an MCP server (Direction B:
|
|
32
|
+
* local primitive → MCP server other clients consume).
|
|
19
33
|
*
|
|
20
|
-
* Enumerates
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
* `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
34
|
+
* Enumerates the tools once, here. Every exposed name must be unique — a
|
|
35
|
+
* duplicate throws {@link McpServeError} (`type: "collision"`) right away;
|
|
36
|
+
* serve `{ groups: { … } }` with `prefixToolNames: true` to namespace
|
|
37
|
+
* same-named tools from different sets. `tools/list` answers with each
|
|
38
|
+
* tool's `inputSchema` extracted via `extractJsonSchema` at the configured
|
|
39
|
+
* `schemaTarget` (default `draft-2020-12`). `tools/call` routes to the
|
|
40
|
+
* contract's `invoke()` and maps the never-throwing result — `data`
|
|
41
|
+
* becomes a text content block, `error` an `isError: true` result.
|
|
27
42
|
*
|
|
28
|
-
*
|
|
29
|
-
* `
|
|
30
|
-
*
|
|
31
|
-
*
|
|
43
|
+
* Transports: `stdio` (default) pumps `process.stdin` / `process.stdout`;
|
|
44
|
+
* `http` binds an MCP Streamable HTTP endpoint on `127.0.0.1` by default,
|
|
45
|
+
* with bearer / custom auth (required off loopback), `Origin` and `Host`
|
|
46
|
+
* validation against DNS rebinding, a body-size cap, and a draining
|
|
47
|
+
* `close()`. `start()` resolves `{ url, close }`.
|
|
32
48
|
*
|
|
33
49
|
* @param source - The tools to expose.
|
|
34
|
-
* @param options - Server name, version, transport,
|
|
50
|
+
* @param options - Server name, version, transport, schema dialect, resources, prompts.
|
|
35
51
|
* @returns An {@link McpServer} with `start()` / `stop()`.
|
|
36
52
|
*
|
|
37
53
|
* @example
|
|
38
|
-
* serve(
|
|
39
|
-
*
|
|
40
|
-
* {
|
|
41
|
-
* ).start();
|
|
54
|
+
* const { url, close } = await serve([calc, clock], {
|
|
55
|
+
* name: "session-tools",
|
|
56
|
+
* transport: { type: "http", auth: { bearer: sessionToken } },
|
|
57
|
+
* }).start();
|
|
42
58
|
*/
|
|
59
|
+
declare function serve(source: McpServeSource, options: McpServeOptions & {
|
|
60
|
+
transport: McpHttpServeTransport;
|
|
61
|
+
}): McpServer<McpHttpServeHandle>;
|
|
43
62
|
declare function serve(source: McpServeSource, options: McpServeOptions): McpServer;
|
|
44
63
|
//#endregion
|
|
45
|
-
export { createServeHandler, serve };
|
|
64
|
+
export { McpServeRequestHandler, createServeHandler, serve };
|
|
46
65
|
//# sourceMappingURL=serve.d.mts.map
|
package/esm/mcp/serve.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"serve.d.mts","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"mappings":";;;;;;UAmnBiB,sBAAA;EAOwD;;;;;;EAAvE,MAAA,CAAO,OAAA,EAAS,cAAA,EAAgB,OAAA,GAAU,mBAAA,GAAsB,OAAA,CAAQ,eAAA;AAAA;;AAAe;AAezF;;;;;;;;;;iBAAgB,kBAAA,CACd,MAAA,EAAQ,cAAA,EACR,OAAA,EAAS,eAAA,GACR,sBAAA;;;;AAAsB;AAuIzB;;;;;;;;;;;;;;;;;;;AAG+B;AAC/B;;;;;;iBAJgB,KAAA,CACd,MAAA,EAAQ,cAAA,EACR,OAAA,EAAS,eAAA;EAAoB,SAAA,EAAW,qBAAA;AAAA,IACvC,SAAA,CAAU,kBAAA;AAAA,iBACG,KAAA,CAAM,MAAA,EAAQ,cAAA,EAAgB,OAAA,EAAS,eAAA,GAAkB,SAAA"}
|