@warlock.js/ai-tools 5.34.0 → 5.34.2
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 +16 -0
- package/README.md +1 -1
- package/cjs/index.cjs +97 -40
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/mcp-serve.type.d.mts +26 -1
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -1
- package/esm/index.d.mts +3 -3
- package/esm/mcp/index.d.mts +1 -1
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts +6 -3
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
- package/esm/mcp/json-schema-to-standard.mjs +15 -26
- package/esm/mcp/json-schema-to-standard.mjs.map +1 -1
- package/esm/mcp/serve-http.mjs +84 -15
- package/esm/mcp/serve-http.mjs.map +1 -1
- package/llms-full.txt +76 -3
- package/llms.txt +2 -1
- package/package.json +2 -2
- package/skills/connect-mcp-server/SKILL.md +1 -1
- package/skills/expose-as-mcp-server/SKILL.md +50 -2
- package/skills/index.md +1 -1
- package/skills/README.md +0 -17
|
@@ -1 +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 McpHttpRoute,\n McpHttpServeTransport,\n McpServeCallContext,\n} from \"../contracts\";\nimport { McpServeError } from \"../errors\";\nimport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\nimport { McpServeRequests } from \"./serve-requests\";\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 MCP endpoint path (plus optional authenticated app routes); each MCP 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 authenticate: McpHttpAuth[\"authenticate\"];\n authRequired: boolean;\n allowedOrigins: Set<string>;\n allowedHosts: Set<string>;\n routes: Map<string, Map<string, McpHttpRoute>>;\n};\n\n/** Resolve exact descendant routes; no route may replace the MCP endpoint. */\nfunction resolveRoutes(\n path: string,\n routes: readonly McpHttpRoute[],\n): Map<string, Map<string, McpHttpRoute>> {\n const resolved = new Map<string, Map<string, McpHttpRoute>>();\n const methods = new Set([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"]);\n\n if (routes.length > 0) {\n let canonical = false;\n\n try {\n canonical = !path.startsWith(\"//\") && new URL(path, \"http://localhost\").pathname === path;\n } catch {\n // Invalid URLs are configuration errors too, before the server binds.\n }\n\n if (!canonical) {\n throw new McpServeError(\"MCP http routes require a canonical endpoint pathname.\", {\n type: \"config\",\n });\n }\n }\n\n for (const route of routes) {\n if (\n !methods.has(route.method) ||\n !/^\\/[A-Za-z0-9._~-]+(?:\\/[A-Za-z0-9._~-]+)*$/.test(route.path) ||\n route.path.split(\"/\").some((segment) => segment === \".\" || segment === \"..\") ||\n typeof route.handler !== \"function\"\n ) {\n throw new McpServeError(\"Invalid MCP http route method, path or handler.\", {\n type: \"config\",\n });\n }\n\n const fullPath = `${path.replace(/\\/+$/, \"\")}${route.path}`;\n const byMethod = resolved.get(fullPath) ?? new Map<string, McpHttpRoute>();\n\n if (fullPath === path || byMethod.has(route.method)) {\n throw new McpServeError(`MCP http route \"${route.method} ${fullPath}\" conflicts.`, {\n type: \"collision\",\n });\n }\n\n byMethod.set(route.method, { ...route });\n resolved.set(fullPath, byMethod);\n }\n\n return resolved;\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 && !auth.authenticate) {\n throw new McpServeError(\"MCP http auth needs `bearer`, `authorize` and/or `authenticate`.\", {\n type: \"config\",\n });\n }\n\n const authRequired =\n tokens.length > 0 || auth?.authorize !== undefined || auth?.authenticate !== 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 }, { authorize } or { authenticate }), 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 authenticate: auth?.authenticate,\n authRequired,\n allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),\n allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),\n routes: resolveRoutes(path, transport.routes ?? []),\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 requests = new McpServeRequests();\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(\n request,\n response,\n settings,\n dispatch,\n shutdown.signal,\n requests,\n ).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: Host / Origin → path → auth →\n * method → route handler or MCP 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 requests: McpServeRequests,\n): Promise<void> {\n const headers = flattenHeaders(request.headers);\n const method = (request.method ?? \"GET\").toUpperCase();\n const url = new URL(request.url ?? \"/\", \"http://localhost\");\n const path = url.pathname;\n const routes = settings.routes.get(path);\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 && !routes) {\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 const authentication = await authorized(info, settings);\n\n if (!authentication.allowed) {\n const challenge: Record<string, string> =\n settings.tokenDigests.length > 0 || settings.authenticate\n ? { \"www-authenticate\": 'Bearer realm=\"mcp\"' }\n : {};\n\n sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, \"Unauthorized.\", challenge);\n\n return;\n }\n\n if (routes) {\n const route = routes.get(method);\n\n if (!route) {\n sendEmpty(response, 405, { allow: [...routes.keys()].sort().join(\", \") });\n\n return;\n }\n\n await handleRoute(\n request,\n response,\n route,\n info,\n url,\n authentication.principal,\n signal,\n settings,\n );\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 // Only the same caller credentials can cancel an in-flight request.\n requests.cancel(record, cancellationScope(info));\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 disconnected = new AbortController();\n const onClose = () => disconnected.abort();\n response.once(\"close\", onClose);\n\n try {\n const rpcResponse = await requests.run(\n rpcRequest,\n {\n request: info,\n principal: authentication.principal,\n signal: AbortSignal.any([signal, disconnected.signal]),\n },\n (context) => dispatch(rpcRequest, context),\n cancellationScope(info),\n );\n\n if (rpcResponse === undefined) {\n // End the HTTP exchange without a JSON-RPC response for cancelled work.\n sendEmpty(response, 202);\n } else {\n sendJson(response, 200, rpcResponse);\n }\n } finally {\n response.off(\"close\", onClose);\n }\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/** Run a side route after the shared guards, tracking disconnect and shutdown. */\nasync function handleRoute(\n request: IncomingMessage,\n response: ServerResponse,\n route: McpHttpRoute,\n info: McpHttpRequestInfo,\n url: URL,\n principal: unknown,\n shutdown: AbortSignal,\n settings: ResolvedSettings,\n): Promise<void> {\n const disconnected = new AbortController();\n const onClose = () => disconnected.abort();\n response.once(\"close\", onClose);\n\n try {\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 {\n connection: \"close\",\n },\n );\n\n return;\n }\n\n const signal = AbortSignal.any([shutdown, disconnected.signal]);\n signal.throwIfAborted();\n const result = await route.handler({ request: info, url, principal, body: body.text, signal });\n\n if (result.body === undefined || info.method === \"HEAD\") {\n sendEmpty(response, result.status ?? 200, result.headers);\n } else {\n sendJson(response, result.status ?? 200, result.body, result.headers);\n }\n } finally {\n response.off(\"close\", onClose);\n }\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 */\ntype Authentication = { allowed: false } | { allowed: true; principal?: unknown };\n\nasync function authorized(\n info: McpHttpRequestInfo,\n settings: ResolvedSettings,\n): Promise<Authentication> {\n if (!settings.authRequired) {\n return { allowed: 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 { allowed: false };\n }\n }\n\n try {\n if (settings.authorize && (await settings.authorize(info)) !== true) {\n return { allowed: false };\n }\n\n if (settings.authenticate) {\n const principal = await settings.authenticate(info);\n\n return principal === undefined ? { allowed: false } : { allowed: true, principal };\n }\n\n return { allowed: true };\n } catch {\n return { allowed: false };\n }\n}\n\n/**\n * Stateless HTTP has no connection/session id. Repeat the caller's credential\n * and application headers on notifications; per-message transport headers do\n * not identify a caller. Hash the material rather than retaining secrets as keys.\n */\nfunction cancellationScope(info: McpHttpRequestInfo): string {\n const transportHeaders = new Set([\n \"host\",\n \"origin\",\n \"accept\",\n \"accept-encoding\",\n \"accept-language\",\n \"connection\",\n \"content-type\",\n \"content-length\",\n \"transfer-encoding\",\n \"mcp-protocol-version\",\n \"user-agent\",\n ]);\n const credentials = Object.entries(info.headers)\n .filter(([name]) => !transportHeaders.has(name))\n .sort(([left], [right]) => left.localeCompare(right));\n\n return digest(JSON.stringify([info.remoteAddress, credentials])).toString(\"hex\");\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA,MAAM,eAAe;;AAGrB,MAAM,eAAe;;AAGrB,MAAM,yBAAyB;;AAG/B,MAAM,2BAA2B;;AAGjC,MAAM,uBAAuB;AAC7B,MAAM,2BAA2B;;AAGjC,MAAM,wBAAwB;;AAG9B,MAAM,iCAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;AAAO,CAAC;;;;;AAoBzE,SAAgB,eAAe,MAAuB;CACpD,MAAM,aAAa,KAAK,KAAK,CAAC,CAAC,YAAY;CAE3C,IAAI,eAAe,IAAI,UAAU,GAC/B,OAAO;CAGT,MAAM,cAAc,WAAW,QAAQ,YAAY,EAAE;CACrD,MAAM,SAAS,YAAY,WAAW,SAAS,IAC3C,YAAY,MAAM,CAAgB,IAClC;CAEJ,OAAO,OAAO,MAAM,KAAK,OAAO,WAAW,MAAM;AACnD;;AAGA,SAAS,WAAW,YAA4B;CAC9C,MAAM,QAAQ,WAAW,KAAK,CAAC,CAAC,YAAY;CAE5C,IAAI,MAAM,WAAW,GAAG,GAAG;EACzB,MAAM,MAAM,MAAM,QAAQ,GAAG;EAE7B,OAAO,QAAQ,KAAK,QAAQ,MAAM,MAAM,GAAG,MAAM,CAAC;CACpD;CAEA,MAAM,QAAQ,MAAM,YAAY,GAAG;CAEnC,OAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,GAAG,KAAK;AACpD;;AAGA,SAAS,OAAO,OAAuB;CACrC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO;AAC3D;;AAuBA,SAAS,cACP,MACA,QACwC;CACxC,MAAM,2BAAW,IAAI,IAAuC;CAC5D,MAAM,0BAAU,IAAI,IAAI;EAAC;EAAO;EAAQ;EAAO;EAAS;EAAU;EAAQ;CAAS,CAAC;CAEpF,IAAI,OAAO,SAAS,GAAG;EACrB,IAAI,YAAY;EAEhB,IAAI;GACF,YAAY,CAAC,KAAK,WAAW,IAAI,KAAK,IAAI,IAAI,MAAM,kBAAkB,CAAC,CAAC,aAAa;EACvF,QAAQ,CAER;EAEA,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,0DAA0D,EAChF,MAAM,SACR,CAAC;CAEL;CAEA,KAAK,MAAM,SAAS,QAAQ;EAC1B,IACE,CAAC,QAAQ,IAAI,MAAM,MAAM,KACzB,CAAC,8CAA8C,KAAK,MAAM,IAAI,KAC9D,MAAM,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM,YAAY,YAAY,OAAO,YAAY,IAAI,KAC3E,OAAO,MAAM,YAAY,YAEzB,MAAM,IAAI,cAAc,mDAAmD,EACzE,MAAM,SACR,CAAC;EAGH,MAAM,WAAW,GAAG,KAAK,QAAQ,QAAQ,EAAE,IAAI,MAAM;EACrD,MAAM,WAAW,SAAS,IAAI,QAAQ,qBAAK,IAAI,IAA0B;EAEzE,IAAI,aAAa,QAAQ,SAAS,IAAI,MAAM,MAAM,GAChD,MAAM,IAAI,cAAc,mBAAmB,MAAM,OAAO,GAAG,SAAS,eAAe,EACjF,MAAM,YACR,CAAC;EAGH,SAAS,IAAI,MAAM,QAAQ,EAAE,GAAG,MAAM,CAAC;EACvC,SAAS,IAAI,UAAU,QAAQ;CACjC;CAEA,OAAO;AACT;;AAGA,SAAS,gBAAgB,QAAwB;CAC/C,IAAI;EACF,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,cAAc,yBAAyB,OAAO,+BAA+B;GACrF,MAAM;GACN;EACF,CAAC;CACH;AACF;;;;;;AAOA,SAAS,gBAAgB,WAAoD;CAC3E,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,WAAW,eAAe,IAAI;CACpC,MAAM,OAAO,UAAU;CAEvB,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAChD,MAAM,IAAI,cAAc,oDAAoD,KAAK,KAAK,EACpF,MAAM,SACR,CAAC;CAGH,IAAI,CAAC,KAAK,WAAW,GAAG,GACtB,MAAM,IAAI,cAAc,2CAA2C,KAAK,MAAM,EAC5E,MAAM,SACR,CAAC;CAGH,MAAM,eAAe,UAAU,gBAAgB;CAE/C,IAAI,CAAC,OAAO,UAAU,YAAY,KAAK,gBAAgB,GACrD,MAAM,IAAI,cAAc,qDAAqD,EAC3E,MAAM,SACR,CAAC;CAGH,MAAM,iBAAiB,UAAU,kBAAkB;CAEnD,IAAI,CAAC,OAAO,SAAS,cAAc,KAAK,iBAAiB,GACvD,MAAM,IAAI,cAAc,8DAA8D,EACpF,MAAM,SACR,CAAC;CAGH,MAAM,SACJ,MAAM,WAAW,SACb,CAAC,IACD,OAAO,KAAK,WAAW,WACrB,CAAC,KAAK,MAAM,IACZ,CAAC,GAAG,KAAK,MAAM;CAEvB,IAAI,OAAO,MAAM,UAAU,MAAM,WAAW,CAAC,GAC3C,MAAM,IAAI,cAAc,0DAA0D,EAChF,MAAM,SACR,CAAC;CAGH,IAAI,QAAQ,KAAK,WAAW,UAAa,OAAO,WAAW,GACzD,MAAM,IAAI,cAAc,kDAAkD,EAAE,MAAM,SAAS,CAAC;CAG9F,IAAI,QAAQ,OAAO,WAAW,KAAK,CAAC,KAAK,aAAa,CAAC,KAAK,cAC1D,MAAM,IAAI,cAAc,oEAAoE,EAC1F,MAAM,SACR,CAAC;CAGH,MAAM,eACJ,OAAO,SAAS,KAAK,MAAM,cAAc,UAAa,MAAM,iBAAiB;CAE/E,IAAI,CAAC,YAAY,CAAC,cAChB,MAAM,IAAI,cACR,yDAAyD,KAAK,yGAE9D;EAAE,MAAM;EAAU,SAAS,EAAE,KAAK;CAAE,CACtC;CAGF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,cAAc,OAAO,IAAI,MAAM;EAC/B,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB;EACA,gBAAgB,IAAI,KAAK,UAAU,kBAAkB,CAAC,EAAC,CAAE,IAAI,eAAe,CAAC;EAC7E,cAAc,IAAI,KAAK,UAAU,gBAAgB,CAAC,EAAC,CAAE,KAAK,SAAS,WAAW,IAAI,CAAC,CAAC;EACpF,QAAQ,cAAc,MAAM,UAAU,UAAU,CAAC,CAAC;CACpD;AACF;;AAGA,SAAS,eAAe,SAAsD;CAC5E,MAAM,OAA+B,CAAC;CAEtC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;EACnD,IAAI,UAAU,QACZ;EAGF,KAAK,KAAK,YAAY,KAAK,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;CACvE;CAEA,OAAO;AACT;;AAGA,SAAS,SACP,UACA,QACA,MACA,UAAkC,CAAC,GAC7B;CACN,IAAI,SAAS,eAAe,SAAS,WACnC;CAGF,MAAM,UAAU,KAAK,UAAU,IAAI;CAEnC,SAAS,UAAU,QAAQ;EACzB,GAAG;EACH,gBAAgB;EAChB,kBAAkB,OAAO,WAAW,OAAO,CAAC,CAAC,SAAS;CACxD,CAAC;CACD,SAAS,IAAI,OAAO;AACtB;;;;;AAMA,SAAS,YACP,UACA,QACA,MACA,SACA,UAAkC,CAAC,GAC7B;CACN,SAAS,UAAU,QAAQ;EAAE,SAAS;EAAO,IAAI;EAAM,OAAO;GAAE;GAAM;EAAQ;CAAE,GAAG,OAAO;AAC5F;;AAGA,SAAS,UACP,UACA,QACA,UAAkC,CAAC,GAC7B;CACN,IAAI,SAAS,eAAe,SAAS,WACnC;CAGF,SAAS,UAAU,QAAQ,OAAO;CAClC,SAAS,IAAI;AACf;;;;;;;;;;;AAYA,eAAsB,gBACpB,WACA,UAC+B;CAC/B,MAAM,WAAW,gBAAgB,SAAS;CAC1C,MAAM,WAAW,IAAI,gBAAgB;CACrC,MAAM,WAAW,IAAI,iBAAiB;CACtC,MAAM,2BAAW,IAAI,IAAmB;CACxC,IAAI;CAEJ,MAAM,SAAiB,cAAc,SAAS,aAAa;EACzD,IAAI,SAAS;GACX,YAAY,UAAU,KAAK,uBAAuB,4BAA4B,EAC5E,YAAY,QACd,CAAC;GAED;EACF;EAEA,MAAM,OAAO,cACX,SACA,UACA,UACA,UACA,SAAS,QACT,QACF,CAAC,CAAC,YAAY;GACZ,YAAY,UAAU,KAAK,uBAAuB,wBAAwB;EAC5E,CAAC;EAED,SAAS,IAAI,IAAI;EACjB,AAAK,KAAK,cAAc;GACtB,SAAS,OAAO,IAAI;GAEpB,IAAI,SACF,OAAO,qBAAqB;EAEhC,CAAC;CACH,CAAC;CAED,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,MAAM,WAAW,UAAiB;GAChC,OACE,IAAI,cACF,uCAAuC,SAAS,KAAK,GAAG,SAAS,KAAK,IAAI,MAAM,WAChF;IAAE,MAAM;IAAU;IAAO,SAAS;KAAE,MAAM,SAAS;KAAM,MAAM,SAAS;IAAK;GAAE,CACjF,CACF;EACF;EAEA,OAAO,KAAK,SAAS,OAAO;EAC5B,OAAO,OAAO,SAAS,MAAM,SAAS,YAAY;GAChD,OAAO,IAAI,SAAS,OAAO;GAC3B,QAAQ;EACV,CAAC;CACH,CAAC;CAED,MAAM,UAAU,OAAO,QAAQ;CAC/B,MAAM,YAAY,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,SAAS;CAE5F,MAAM,MAAM,UADI,OAAO,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,KAAK,SAAS,KAC1C,GAAG,YAAY,SAAS;CAEtD,MAAM,cAA6B;EACjC,IAAI,SACF,OAAO;EAGT,WAAW,YAAY;GACrB,MAAM,SAAS,IAAI,SAAe,YAAY;IAC5C,OAAO,YAAY,QAAQ,CAAC;GAC9B,CAAC;GAED,OAAO,qBAAqB;GAE5B,IAAI,SAAS,OAAO,GAAG;IACrB,IAAI;IACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;KACnD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,SAAS,cAAc;IACtE,CAAC;IAED,MAAM,UAAU,MAAM,QAAQ,KAAK,CACjC,QAAQ,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,WAAW,SAAkB,GAC/D,QACF,CAAC;IAED,aAAa,KAAK;IAElB,IAAI,YAAY,WAAW;KACzB,SAAS,MAAM;KACf,OAAO,oBAAoB;IAC7B;GACF;GAEA,OAAO,qBAAqB;GAC5B,MAAM;EACR,EAAC,CAAE;EAEH,OAAO;CACT;CAEA,OAAO;EAAE;EAAK;CAAM;AACtB;;;;;AAMA,eAAe,cACb,SACA,UACA,UACA,UACA,QACA,UACe;CACf,MAAM,UAAU,eAAe,QAAQ,OAAO;CAC9C,MAAM,UAAU,QAAQ,UAAU,MAAK,CAAE,YAAY;CACrD,MAAM,MAAM,IAAI,IAAI,QAAQ,OAAO,KAAK,kBAAkB;CAC1D,MAAM,OAAO,IAAI;CACjB,MAAM,SAAS,SAAS,OAAO,IAAI,IAAI;CAGvC,IAAI,CAAC,YAAY,QAAQ,MAAM,QAAQ,GAAG;EACxC,YAAY,UAAU,KAAK,uBAAuB,qCAAqC;EAEvF;CACF;CAEA,IAAI,CAAC,cAAc,QAAQ,QAAQ,QAAQ,GAAG;EAC5C,YAAY,UAAU,KAAK,uBAAuB,gCAAgC;EAElF;CACF;CAEA,IAAI,SAAS,SAAS,QAAQ,CAAC,QAAQ;EACrC,YAAY,UAAU,KAAK,uBAAuB,YAAY;EAE9D;CACF;CAEA,MAAM,OAA2B;EAAE;EAAQ;EAAM;CAAQ;CAEzD,IAAI,QAAQ,OAAO,kBAAkB,QACnC,KAAK,gBAAgB,QAAQ,OAAO;CAGtC,MAAM,iBAAiB,MAAM,WAAW,MAAM,QAAQ;CAEtD,IAAI,CAAC,eAAe,SAAS;EAC3B,MAAM,YACJ,SAAS,aAAa,SAAS,KAAK,SAAS,eACzC,EAAE,oBAAoB,uBAAqB,IAC3C,CAAC;EAEP,YAAY,UAAU,KAAK,uBAAuB,iBAAiB,SAAS;EAE5E;CACF;CAEA,IAAI,QAAQ;EACV,MAAM,QAAQ,OAAO,IAAI,MAAM;EAE/B,IAAI,CAAC,OAAO;GACV,UAAU,UAAU,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;GAExE;EACF;EAEA,MAAM,YACJ,SACA,UACA,OACA,MACA,KACA,eAAe,WACf,QACA,QACF;EAEA;CACF;CAEA,IAAI,WAAW,QAAQ;EAGrB,UAAU,UAAU,KAAK,EAAE,OAAO,OAAO,CAAC;EAE1C;CACF;CAEA,MAAM,kBAAkB,QAAQ;CAEhC,IAAI,oBAAoB,UAAa,CAAC,4BAA4B,SAAS,eAAe,GAAG;EAC3F,YACE,UACA,KACA,uBACA,qCAAqC,gBAAgB,gBAAgB,4BAA4B,KAAK,IAAI,EAAE,GAC9G;EAEA;CACF;CAEA,IAAI,CAAC,kBAAkB,QAAQ,eAAe,GAAG;EAC/C,YAAY,UAAU,KAAK,uBAAuB,wCAAwC;EAE1F;CACF;CAEA,IAAI,CAAC,YAAY,QAAQ,MAAM,GAAG;EAChC,YAAY,UAAU,KAAK,uBAAuB,qCAAqC;EAEvF;CACF;CAEA,MAAM,OAAO,MAAM,SAAS,SAAS,SAAS,YAAY;CAE1D,IAAI,KAAK,SAAS,aAAa;EAC7B,YACE,UACA,KACA,uBACA,wBAAwB,SAAS,aAAa,UAC9C,EAAE,YAAY,QAAQ,CACxB;EAEA;CACF;CAEA,IAAI;CAEJ,IAAI;EACF,UAAU,KAAK,MAAM,KAAK,IAAI;CAChC,QAAQ;EACN,YAAY,UAAU,KAAK,sBAAsB,sCAAsC;EAEvF;CACF;CAEA,IAAI,OAAO,YAAY,YAAY,YAAY,QAAQ,MAAM,QAAQ,OAAO,GAAG;EAC7E,YACE,UACA,KACA,0BACA,qFACF;EAEA;CACF;CAEA,MAAM,SAAS;CAEf,IAAI,OAAO,YAAY,OAAO;EAC5B,YAAY,UAAU,KAAK,0BAA0B,2CAAyC;EAE9F;CACF;CAEA,MAAM,KAAK,OAAO;CAElB,IAAI,OAAO,OAAO,WAAW,UAAU;EACrC,IAAI,OAAO,OAAO,YAAY,OAAO,OAAO,UAAU;GAEpD,SAAS,OAAO,QAAQ,kBAAkB,IAAI,CAAC;GAC/C,UAAU,UAAU,GAAG;GAEvB;EACF;EAEA,MAAM,aAA6B;GAAE,SAAS;GAAO;GAAI,QAAQ,OAAO;EAAO;EAE/E,IAAI,OAAO,WAAW,QACpB,WAAW,SAAS,OAAO;EAG7B,MAAM,eAAe,IAAI,gBAAgB;EACzC,MAAM,gBAAgB,aAAa,MAAM;EACzC,SAAS,KAAK,SAAS,OAAO;EAE9B,IAAI;GACF,MAAM,cAAc,MAAM,SAAS,IACjC,YACA;IACE,SAAS;IACT,WAAW,eAAe;IAC1B,QAAQ,YAAY,IAAI,CAAC,QAAQ,aAAa,MAAM,CAAC;GACvD,IACC,YAAY,SAAS,YAAY,OAAO,GACzC,kBAAkB,IAAI,CACxB;GAEA,IAAI,gBAAgB,QAElB,UAAU,UAAU,GAAG;QAEvB,SAAS,UAAU,KAAK,WAAW;EAEvC,UAAU;GACR,SAAS,IAAI,SAAS,OAAO;EAC/B;EAEA;CACF;CAEA,KACG,OAAO,OAAO,YAAY,OAAO,OAAO,cACxC,YAAY,UAAU,WAAW,SAClC;EAEA,UAAU,UAAU,GAAG;EAEvB;CACF;CAEA,YAAY,UAAU,KAAK,0BAA0B,0CAA0C;AACjG;;AAGA,eAAe,YACb,SACA,UACA,OACA,MACA,KACA,WACA,UACA,UACe;CACf,MAAM,eAAe,IAAI,gBAAgB;CACzC,MAAM,gBAAgB,aAAa,MAAM;CACzC,SAAS,KAAK,SAAS,OAAO;CAE9B,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,SAAS,SAAS,YAAY;EAE1D,IAAI,KAAK,SAAS,aAAa;GAC7B,YACE,UACA,KACA,uBACA,wBAAwB,SAAS,aAAa,UAC9C,EACE,YAAY,QACd,CACF;GAEA;EACF;EAEA,MAAM,SAAS,YAAY,IAAI,CAAC,UAAU,aAAa,MAAM,CAAC;EAC9D,OAAO,eAAe;EACtB,MAAM,SAAS,MAAM,MAAM,QAAQ;GAAE,SAAS;GAAM;GAAK;GAAW,MAAM,KAAK;GAAM;EAAO,CAAC;EAE7F,IAAI,OAAO,SAAS,UAAa,KAAK,WAAW,QAC/C,UAAU,UAAU,OAAO,UAAU,KAAK,OAAO,OAAO;OAExD,SAAS,UAAU,OAAO,UAAU,KAAK,OAAO,MAAM,OAAO,OAAO;CAExE,UAAU;EACR,SAAS,IAAI,SAAS,OAAO;CAC/B;AACF;;;;;;;AAQA,SAAS,YAAY,YAAgC,UAAqC;CACxF,IAAI,CAAC,SAAS,YAAY,SAAS,aAAa,SAAS,GACvD,OAAO;CAGT,IAAI,eAAe,QACjB,OAAO;CAGT,MAAM,OAAO,WAAW,UAAU;CAElC,IAAI,SAAS,aAAa,IAAI,IAAI,GAChC,OAAO;CAGT,OAAO,SAAS,YAAY,eAAe,IAAI;AACjD;;;;;;;AAQA,SAAS,cAAc,QAA4B,UAAqC;CACtF,IAAI,WAAW,QACb,OAAO;CAGT,IAAI;CAEJ,IAAI;EACF,SAAS,IAAI,IAAI,MAAM;CACzB,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,GAC3C,OAAO;CAGT,QACG,OAAO,aAAa,WAAW,OAAO,aAAa,aAAa,eAAe,OAAO,QAAQ;AAEnG;AAUA,eAAe,WACb,MACA,UACyB;CACzB,IAAI,CAAC,SAAS,cACZ,OAAO,EAAE,SAAS,KAAK;CAGzB,IAAI,SAAS,aAAa,SAAS,GAAG;EACpC,MAAM,QAAQ,sBAAsB,KAAK,KAAK,QAAQ,iBAAiB,EAAE;EACzE,MAAM,YAAY,OAAO,QAAQ,EAAE,EAAE,KAAK,KAAK,EAAE;EACjD,IAAI,UAAU;EAEd,KAAK,MAAM,YAAY,SAAS,cAC9B,UAAU,gBAAgB,WAAW,QAAQ,KAAK;EAGpD,IAAI,CAAC,SAAS,CAAC,SACb,OAAO,EAAE,SAAS,MAAM;CAE5B;CAEA,IAAI;EACF,IAAI,SAAS,aAAc,MAAM,SAAS,UAAU,IAAI,MAAO,MAC7D,OAAO,EAAE,SAAS,MAAM;EAG1B,IAAI,SAAS,cAAc;GACzB,MAAM,YAAY,MAAM,SAAS,aAAa,IAAI;GAElD,OAAO,cAAc,SAAY,EAAE,SAAS,MAAM,IAAI;IAAE,SAAS;IAAM;GAAU;EACnF;EAEA,OAAO,EAAE,SAAS,KAAK;CACzB,QAAQ;EACN,OAAO,EAAE,SAAS,MAAM;CAC1B;AACF;;;;;;AAOA,SAAS,kBAAkB,MAAkC;CAC3D,MAAM,mCAAmB,IAAI,IAAI;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CACD,MAAM,cAAc,OAAO,QAAQ,KAAK,OAAO,CAAC,CAC7C,QAAQ,CAAC,UAAU,CAAC,iBAAiB,IAAI,IAAI,CAAC,CAAC,CAC/C,MAAM,CAAC,OAAO,CAAC,WAAW,KAAK,cAAc,KAAK,CAAC;CAEtD,OAAO,OAAO,KAAK,UAAU,CAAC,KAAK,eAAe,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK;AACjF;;AAGA,SAAS,kBAAkB,aAA0C;CACnE,OAAO,aAAa,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,MAAM;AAC9D;;;;;;;AAQA,SAAS,YAAY,QAAqC;CACxD,IAAI,WAAW,UAAa,OAAO,KAAK,MAAM,IAC5C,OAAO;CAGT,OAAO,OACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CACvD,MAAM,SAAS,SAAS,sBAAsB,SAAS,mBAAmB,SAAS,KAAK;AAC7F;;;;;;AAUA,SAAS,SAAS,SAA0B,OAAoC;CAC9E,MAAM,WAAW,OAAO,QAAQ,QAAQ,iBAAiB;CAEzD,IAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,OAC1C,OAAO,QAAQ,QAAQ,EAAE,MAAM,YAAY,CAAC;CAG9C,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,OAAO;EAEX,MAAM,UAAU,WAAuB;GACrC,IAAI,CAAC,MAAM;IACT,OAAO;IACP,QAAQ,MAAM;GAChB;EACF;EAEA,QAAQ,GAAG,SAAS,UAAkB;GACpC,IAAI,MACF;GAGF,QAAQ,MAAM;GAEd,IAAI,OAAO,OAAO;IAChB,QAAQ,MAAM;IACd,OAAO,EAAE,MAAM,YAAY,CAAC;IAE5B;GACF;GAEA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,QAAQ,GAAG,aAAa,OAAO;GAAE,MAAM;GAAM,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;EAAE,CAAC,CAAC;EAC5F,QAAQ,GAAG,UAAU,UAAU;GAC7B,IAAI,CAAC,MAAM;IACT,OAAO;IACP,OAAO,KAAK;GACd;EACF,CAAC;CACH,CAAC;AACH"}
|
|
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 validateHeaderValue,\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 McpHttpAuthRefusalBody,\n McpHttpAuthRefusalContext,\n McpHttpRequestInfo,\n McpHttpRoute,\n McpHttpServeTransport,\n McpServeCallContext,\n} from \"../contracts\";\nimport { McpServeError } from \"../errors\";\nimport { SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\nimport { McpServeRequests } from \"./serve-requests\";\nimport { isRecord } from \"./result\";\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 MCP endpoint path (plus optional authenticated app routes); each MCP 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 authenticate: McpHttpAuth[\"authenticate\"];\n onAuthRefusal: McpHttpServeTransport[\"onAuthRefusal\"];\n authRequired: boolean;\n allowedOrigins: Set<string>;\n allowedHosts: Set<string>;\n routes: Map<string, Map<string, McpHttpRoute>>;\n};\n\n/** Resolve exact descendant routes; no route may replace the MCP endpoint. */\nfunction resolveRoutes(\n path: string,\n routes: readonly McpHttpRoute[],\n): Map<string, Map<string, McpHttpRoute>> {\n const resolved = new Map<string, Map<string, McpHttpRoute>>();\n const methods = new Set([\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\", \"HEAD\", \"OPTIONS\"]);\n\n if (routes.length > 0) {\n let canonical = false;\n\n try {\n canonical = !path.startsWith(\"//\") && new URL(path, \"http://localhost\").pathname === path;\n } catch {\n // Invalid URLs are configuration errors too, before the server binds.\n }\n\n if (!canonical) {\n throw new McpServeError(\"MCP http routes require a canonical endpoint pathname.\", {\n type: \"config\",\n });\n }\n }\n\n for (const route of routes) {\n if (\n !methods.has(route.method) ||\n !/^\\/[A-Za-z0-9._~-]+(?:\\/[A-Za-z0-9._~-]+)*$/.test(route.path) ||\n route.path.split(\"/\").some((segment) => segment === \".\" || segment === \"..\") ||\n typeof route.handler !== \"function\"\n ) {\n throw new McpServeError(\"Invalid MCP http route method, path or handler.\", {\n type: \"config\",\n });\n }\n\n const fullPath = `${path.replace(/\\/+$/, \"\")}${route.path}`;\n const byMethod = resolved.get(fullPath) ?? new Map<string, McpHttpRoute>();\n\n if (fullPath === path || byMethod.has(route.method)) {\n throw new McpServeError(`MCP http route \"${route.method} ${fullPath}\" conflicts.`, {\n type: \"collision\",\n });\n }\n\n byMethod.set(route.method, { ...route });\n resolved.set(fullPath, byMethod);\n }\n\n return resolved;\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 && !auth.authenticate) {\n throw new McpServeError(\"MCP http auth needs `bearer`, `authorize` and/or `authenticate`.\", {\n type: \"config\",\n });\n }\n\n const authRequired =\n tokens.length > 0 || auth?.authorize !== undefined || auth?.authenticate !== 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 }, { authorize } or { authenticate }), 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 authenticate: auth?.authenticate,\n onAuthRefusal: transport.onAuthRefusal,\n authRequired,\n allowedOrigins: new Set((transport.allowedOrigins ?? []).map(normalizeOrigin)),\n allowedHosts: new Set((transport.allowedHosts ?? []).map((name) => hostnameOf(name))),\n routes: resolveRoutes(path, transport.routes ?? []),\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\nconst AUTH_REFUSAL_HEADERS = new Set([\n \"www-authenticate\",\n \"cache-control\",\n \"retry-after\",\n \"content-language\",\n \"x-request-id\",\n]);\n\n/** Snapshot plain JSON, rejecting cycles and values JSON would silently coerce/drop. */\nfunction copyRefusalBody(value: unknown, ancestors = new Set<object>()): McpHttpAuthRefusalBody {\n if (value === null || typeof value === \"string\" || typeof value === \"boolean\") return value;\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value !== \"object\" || value === null || ancestors.has(value)) {\n throw new TypeError(\"Invalid auth refusal body.\");\n }\n\n const prototype: unknown = Object.getPrototypeOf(value);\n if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {\n throw new TypeError(\"Invalid auth refusal body.\");\n }\n\n ancestors.add(value);\n const result = Array.isArray(value)\n ? Array.from(value, (entry: unknown) => copyRefusalBody(entry, ancestors))\n : Object.fromEntries(\n Object.entries(value).map(([key, entry]) => [key, copyRefusalBody(entry, ancestors)]),\n );\n ancestors.delete(value);\n return result;\n}\n\n/** A custom response can only end a denied exchange; it cannot authorize dispatch. */\nasync function sendAuthRefusal(\n response: ServerResponse,\n settings: ResolvedSettings,\n context: McpHttpAuthRefusalContext,\n): Promise<void> {\n const challenge: Record<string, string> =\n settings.tokenDigests.length > 0 || settings.authenticate\n ? { \"www-authenticate\": 'Bearer realm=\"mcp\"' }\n : {};\n\n if (settings.onAuthRefusal) {\n try {\n const result: unknown = await settings.onAuthRefusal(Object.freeze(context));\n if (!isRecord(result)) throw new TypeError(\"Invalid auth refusal response.\");\n const customStatus = result.status;\n const status = customStatus === undefined ? 401 : customStatus;\n if (typeof status !== \"number\" || !Number.isInteger(status) || status < 400 || status > 599) {\n throw new TypeError(\"Invalid auth refusal status.\");\n }\n const headers = { ...challenge };\n const customHeaders = result.headers;\n if (customHeaders !== undefined) {\n if (!isRecord(customHeaders)) throw new TypeError(\"Invalid auth refusal headers.\");\n const prototype: unknown = Object.getPrototypeOf(customHeaders);\n if (prototype !== Object.prototype && prototype !== null) {\n throw new TypeError(\"Invalid auth refusal headers.\");\n }\n for (const [name, value] of Object.entries(customHeaders)) {\n const normalized = name.toLowerCase();\n if (!AUTH_REFUSAL_HEADERS.has(normalized) || typeof value !== \"string\") {\n throw new TypeError(\"Invalid auth refusal header.\");\n }\n // Node's own HTTP validation rejects CR/LF and invalid header characters.\n // https://nodejs.org/docs/latest-v22.x/api/http.html#httpvalidateheadervaluename-value\n validateHeaderValue(normalized, value);\n headers[normalized] = value;\n }\n }\n const body = copyRefusalBody(result.body);\n sendJson(response, status, body, headers);\n return;\n } catch {\n // Neither authentication nor refusal-hook errors become wire content.\n }\n }\n\n sendRefusal(response, 401, JSON_RPC_SERVER_ERROR, \"Unauthorized.\", challenge);\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 requests = new McpServeRequests();\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(\n request,\n response,\n settings,\n dispatch,\n shutdown.signal,\n requests,\n ).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: Host / Origin → path → auth →\n * method → route handler or MCP 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 requests: McpServeRequests,\n): Promise<void> {\n const headers = flattenHeaders(request.headers);\n const method = (request.method ?? \"GET\").toUpperCase();\n const url = new URL(request.url ?? \"/\", \"http://localhost\");\n const path = url.pathname;\n const routes = settings.routes.get(path);\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 && !routes) {\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 const authentication = await authorized(info, settings);\n\n if (!authentication.allowed) {\n await sendAuthRefusal(response, settings, {\n check: authentication.check,\n method,\n path,\n target: routes ? \"route\" : \"mcp\",\n });\n\n return;\n }\n\n if (routes) {\n const route = routes.get(method);\n\n if (!route) {\n sendEmpty(response, 405, { allow: [...routes.keys()].sort().join(\", \") });\n\n return;\n }\n\n await handleRoute(\n request,\n response,\n route,\n info,\n url,\n authentication.principal,\n signal,\n settings,\n );\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 // Only the same caller credentials can cancel an in-flight request.\n requests.cancel(record, cancellationScope(info));\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 disconnected = new AbortController();\n const onClose = () => disconnected.abort();\n response.once(\"close\", onClose);\n\n try {\n const rpcResponse = await requests.run(\n rpcRequest,\n {\n request: info,\n principal: authentication.principal,\n signal: AbortSignal.any([signal, disconnected.signal]),\n },\n (context) => dispatch(rpcRequest, context),\n cancellationScope(info),\n );\n\n if (rpcResponse === undefined) {\n // End the HTTP exchange without a JSON-RPC response for cancelled work.\n sendEmpty(response, 202);\n } else {\n sendJson(response, 200, rpcResponse);\n }\n } finally {\n response.off(\"close\", onClose);\n }\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/** Run a side route after the shared guards, tracking disconnect and shutdown. */\nasync function handleRoute(\n request: IncomingMessage,\n response: ServerResponse,\n route: McpHttpRoute,\n info: McpHttpRequestInfo,\n url: URL,\n principal: unknown,\n shutdown: AbortSignal,\n settings: ResolvedSettings,\n): Promise<void> {\n const disconnected = new AbortController();\n const onClose = () => disconnected.abort();\n response.once(\"close\", onClose);\n\n try {\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 {\n connection: \"close\",\n },\n );\n\n return;\n }\n\n const signal = AbortSignal.any([shutdown, disconnected.signal]);\n signal.throwIfAborted();\n const result = await route.handler({ request: info, url, principal, body: body.text, signal });\n\n if (result.body === undefined || info.method === \"HEAD\") {\n sendEmpty(response, result.status ?? 200, result.headers);\n } else {\n sendJson(response, result.status ?? 200, result.body, result.headers);\n }\n } finally {\n response.off(\"close\", onClose);\n }\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 */\ntype Authentication =\n | { allowed: false; check: McpHttpAuthRefusalContext[\"check\"] }\n | { allowed: true; principal?: unknown };\n\nasync function authorized(\n info: McpHttpRequestInfo,\n settings: ResolvedSettings,\n): Promise<Authentication> {\n if (!settings.authRequired) {\n return { allowed: 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 { allowed: false, check: \"bearer\" };\n }\n }\n\n if (settings.authorize) {\n try {\n if ((await settings.authorize(info)) !== true) {\n return { allowed: false, check: \"authorize\" };\n }\n } catch {\n return { allowed: false, check: \"authorize\" };\n }\n }\n\n if (settings.authenticate) {\n try {\n const principal = await settings.authenticate(info);\n\n return principal === undefined\n ? { allowed: false, check: \"authenticate\" }\n : { allowed: true, principal };\n } catch {\n return { allowed: false, check: \"authenticate\" };\n }\n }\n\n return { allowed: true };\n}\n\n/**\n * Stateless HTTP has no connection/session id. Repeat the caller's credential\n * and application headers on notifications; per-message transport headers do\n * not identify a caller. Hash the material rather than retaining secrets as keys.\n */\nfunction cancellationScope(info: McpHttpRequestInfo): string {\n const transportHeaders = new Set([\n \"host\",\n \"origin\",\n \"accept\",\n \"accept-encoding\",\n \"accept-language\",\n \"connection\",\n \"content-type\",\n \"content-length\",\n \"transfer-encoding\",\n \"mcp-protocol-version\",\n \"user-agent\",\n ]);\n const credentials = Object.entries(info.headers)\n .filter(([name]) => !transportHeaders.has(name))\n .sort(([left], [right]) => left.localeCompare(right));\n\n return digest(JSON.stringify([info.remoteAddress, credentials])).toString(\"hex\");\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,MAAM,eAAe;;AAGrB,MAAM,eAAe;;AAGrB,MAAM,yBAAyB;;AAG/B,MAAM,2BAA2B;;AAGjC,MAAM,uBAAuB;AAC7B,MAAM,2BAA2B;;AAGjC,MAAM,wBAAwB;;AAG9B,MAAM,iCAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAO;AAAO,CAAC;;;;;AAoBzE,SAAgB,eAAe,MAAuB;CACpD,MAAM,aAAa,KAAK,KAAK,CAAC,CAAC,YAAY;CAE3C,IAAI,eAAe,IAAI,UAAU,GAC/B,OAAO;CAGT,MAAM,cAAc,WAAW,QAAQ,YAAY,EAAE;CACrD,MAAM,SAAS,YAAY,WAAW,SAAS,IAC3C,YAAY,MAAM,CAAgB,IAClC;CAEJ,OAAO,OAAO,MAAM,KAAK,OAAO,WAAW,MAAM;AACnD;;AAGA,SAAS,WAAW,YAA4B;CAC9C,MAAM,QAAQ,WAAW,KAAK,CAAC,CAAC,YAAY;CAE5C,IAAI,MAAM,WAAW,GAAG,GAAG;EACzB,MAAM,MAAM,MAAM,QAAQ,GAAG;EAE7B,OAAO,QAAQ,KAAK,QAAQ,MAAM,MAAM,GAAG,MAAM,CAAC;CACpD;CAEA,MAAM,QAAQ,MAAM,YAAY,GAAG;CAEnC,OAAO,UAAU,KAAK,QAAQ,MAAM,MAAM,GAAG,KAAK;AACpD;;AAGA,SAAS,OAAO,OAAuB;CACrC,OAAO,WAAW,QAAQ,CAAC,CAAC,OAAO,OAAO,MAAM,CAAC,CAAC,OAAO;AAC3D;;AAwBA,SAAS,cACP,MACA,QACwC;CACxC,MAAM,2BAAW,IAAI,IAAuC;CAC5D,MAAM,0BAAU,IAAI,IAAI;EAAC;EAAO;EAAQ;EAAO;EAAS;EAAU;EAAQ;CAAS,CAAC;CAEpF,IAAI,OAAO,SAAS,GAAG;EACrB,IAAI,YAAY;EAEhB,IAAI;GACF,YAAY,CAAC,KAAK,WAAW,IAAI,KAAK,IAAI,IAAI,MAAM,kBAAkB,CAAC,CAAC,aAAa;EACvF,QAAQ,CAER;EAEA,IAAI,CAAC,WACH,MAAM,IAAI,cAAc,0DAA0D,EAChF,MAAM,SACR,CAAC;CAEL;CAEA,KAAK,MAAM,SAAS,QAAQ;EAC1B,IACE,CAAC,QAAQ,IAAI,MAAM,MAAM,KACzB,CAAC,8CAA8C,KAAK,MAAM,IAAI,KAC9D,MAAM,KAAK,MAAM,GAAG,CAAC,CAAC,MAAM,YAAY,YAAY,OAAO,YAAY,IAAI,KAC3E,OAAO,MAAM,YAAY,YAEzB,MAAM,IAAI,cAAc,mDAAmD,EACzE,MAAM,SACR,CAAC;EAGH,MAAM,WAAW,GAAG,KAAK,QAAQ,QAAQ,EAAE,IAAI,MAAM;EACrD,MAAM,WAAW,SAAS,IAAI,QAAQ,qBAAK,IAAI,IAA0B;EAEzE,IAAI,aAAa,QAAQ,SAAS,IAAI,MAAM,MAAM,GAChD,MAAM,IAAI,cAAc,mBAAmB,MAAM,OAAO,GAAG,SAAS,eAAe,EACjF,MAAM,YACR,CAAC;EAGH,SAAS,IAAI,MAAM,QAAQ,EAAE,GAAG,MAAM,CAAC;EACvC,SAAS,IAAI,UAAU,QAAQ;CACjC;CAEA,OAAO;AACT;;AAGA,SAAS,gBAAgB,QAAwB;CAC/C,IAAI;EACF,OAAO,IAAI,IAAI,MAAM,CAAC,CAAC;CACzB,SAAS,OAAO;EACd,MAAM,IAAI,cAAc,yBAAyB,OAAO,+BAA+B;GACrF,MAAM;GACN;EACF,CAAC;CACH;AACF;;;;;;AAOA,SAAS,gBAAgB,WAAoD;CAC3E,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,OAAO,UAAU,QAAQ;CAC/B,MAAM,WAAW,eAAe,IAAI;CACpC,MAAM,OAAO,UAAU;CAEvB,IAAI,CAAC,OAAO,UAAU,IAAI,KAAK,OAAO,KAAK,OAAO,OAChD,MAAM,IAAI,cAAc,oDAAoD,KAAK,KAAK,EACpF,MAAM,SACR,CAAC;CAGH,IAAI,CAAC,KAAK,WAAW,GAAG,GACtB,MAAM,IAAI,cAAc,2CAA2C,KAAK,MAAM,EAC5E,MAAM,SACR,CAAC;CAGH,MAAM,eAAe,UAAU,gBAAgB;CAE/C,IAAI,CAAC,OAAO,UAAU,YAAY,KAAK,gBAAgB,GACrD,MAAM,IAAI,cAAc,qDAAqD,EAC3E,MAAM,SACR,CAAC;CAGH,MAAM,iBAAiB,UAAU,kBAAkB;CAEnD,IAAI,CAAC,OAAO,SAAS,cAAc,KAAK,iBAAiB,GACvD,MAAM,IAAI,cAAc,8DAA8D,EACpF,MAAM,SACR,CAAC;CAGH,MAAM,SACJ,MAAM,WAAW,SACb,CAAC,IACD,OAAO,KAAK,WAAW,WACrB,CAAC,KAAK,MAAM,IACZ,CAAC,GAAG,KAAK,MAAM;CAEvB,IAAI,OAAO,MAAM,UAAU,MAAM,WAAW,CAAC,GAC3C,MAAM,IAAI,cAAc,0DAA0D,EAChF,MAAM,SACR,CAAC;CAGH,IAAI,QAAQ,KAAK,WAAW,UAAa,OAAO,WAAW,GACzD,MAAM,IAAI,cAAc,kDAAkD,EAAE,MAAM,SAAS,CAAC;CAG9F,IAAI,QAAQ,OAAO,WAAW,KAAK,CAAC,KAAK,aAAa,CAAC,KAAK,cAC1D,MAAM,IAAI,cAAc,oEAAoE,EAC1F,MAAM,SACR,CAAC;CAGH,MAAM,eACJ,OAAO,SAAS,KAAK,MAAM,cAAc,UAAa,MAAM,iBAAiB;CAE/E,IAAI,CAAC,YAAY,CAAC,cAChB,MAAM,IAAI,cACR,yDAAyD,KAAK,yGAE9D;EAAE,MAAM;EAAU,SAAS,EAAE,KAAK;CAAE,CACtC;CAGF,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,cAAc,OAAO,IAAI,MAAM;EAC/B,WAAW,MAAM;EACjB,cAAc,MAAM;EACpB,eAAe,UAAU;EACzB;EACA,gBAAgB,IAAI,KAAK,UAAU,kBAAkB,CAAC,EAAC,CAAE,IAAI,eAAe,CAAC;EAC7E,cAAc,IAAI,KAAK,UAAU,gBAAgB,CAAC,EAAC,CAAE,KAAK,SAAS,WAAW,IAAI,CAAC,CAAC;EACpF,QAAQ,cAAc,MAAM,UAAU,UAAU,CAAC,CAAC;CACpD;AACF;;AAGA,SAAS,eAAe,SAAsD;CAC5E,MAAM,OAA+B,CAAC;CAEtC,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,OAAO,GAAG;EACnD,IAAI,UAAU,QACZ;EAGF,KAAK,KAAK,YAAY,KAAK,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,IAAI,IAAI;CACvE;CAEA,OAAO;AACT;;AAGA,SAAS,SACP,UACA,QACA,MACA,UAAkC,CAAC,GAC7B;CACN,IAAI,SAAS,eAAe,SAAS,WACnC;CAGF,MAAM,UAAU,KAAK,UAAU,IAAI;CAEnC,SAAS,UAAU,QAAQ;EACzB,GAAG;EACH,gBAAgB;EAChB,kBAAkB,OAAO,WAAW,OAAO,CAAC,CAAC,SAAS;CACxD,CAAC;CACD,SAAS,IAAI,OAAO;AACtB;;;;;AAMA,SAAS,YACP,UACA,QACA,MACA,SACA,UAAkC,CAAC,GAC7B;CACN,SAAS,UAAU,QAAQ;EAAE,SAAS;EAAO,IAAI;EAAM,OAAO;GAAE;GAAM;EAAQ;CAAE,GAAG,OAAO;AAC5F;AAEA,MAAM,uCAAuB,IAAI,IAAI;CACnC;CACA;CACA;CACA;CACA;AACF,CAAC;;AAGD,SAAS,gBAAgB,OAAgB,4BAAY,IAAI,IAAY,GAA2B;CAC9F,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW,OAAO;CACtF,IAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,GAAG,OAAO;CAChE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,IAAI,KAAK,GACpE,MAAM,IAAI,UAAU,4BAA4B;CAGlD,MAAM,YAAqB,OAAO,eAAe,KAAK;CACtD,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,cAAc,OAAO,aAAa,cAAc,MAC3E,MAAM,IAAI,UAAU,4BAA4B;CAGlD,UAAU,IAAI,KAAK;CACnB,MAAM,SAAS,MAAM,QAAQ,KAAK,IAC9B,MAAM,KAAK,QAAQ,UAAmB,gBAAgB,OAAO,SAAS,CAAC,IACvE,OAAO,YACL,OAAO,QAAQ,KAAK,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CAAC,KAAK,gBAAgB,OAAO,SAAS,CAAC,CAAC,CACtF;CACJ,UAAU,OAAO,KAAK;CACtB,OAAO;AACT;;AAGA,eAAe,gBACb,UACA,UACA,SACe;CACf,MAAM,YACJ,SAAS,aAAa,SAAS,KAAK,SAAS,eACzC,EAAE,oBAAoB,uBAAqB,IAC3C,CAAC;CAEP,IAAI,SAAS,eACX,IAAI;EACF,MAAM,SAAkB,MAAM,SAAS,cAAc,OAAO,OAAO,OAAO,CAAC;EAC3E,IAAI,CAAC,SAAS,MAAM,GAAG,MAAM,IAAI,UAAU,gCAAgC;EAC3E,MAAM,eAAe,OAAO;EAC5B,MAAM,SAAS,iBAAiB,SAAY,MAAM;EAClD,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,UAAU,MAAM,KAAK,SAAS,OAAO,SAAS,KACtF,MAAM,IAAI,UAAU,8BAA8B;EAEpD,MAAM,UAAU,EAAE,GAAG,UAAU;EAC/B,MAAM,gBAAgB,OAAO;EAC7B,IAAI,kBAAkB,QAAW;GAC/B,IAAI,CAAC,SAAS,aAAa,GAAG,MAAM,IAAI,UAAU,+BAA+B;GACjF,MAAM,YAAqB,OAAO,eAAe,aAAa;GAC9D,IAAI,cAAc,OAAO,aAAa,cAAc,MAClD,MAAM,IAAI,UAAU,+BAA+B;GAErD,KAAK,MAAM,CAAC,MAAM,UAAU,OAAO,QAAQ,aAAa,GAAG;IACzD,MAAM,aAAa,KAAK,YAAY;IACpC,IAAI,CAAC,qBAAqB,IAAI,UAAU,KAAK,OAAO,UAAU,UAC5D,MAAM,IAAI,UAAU,8BAA8B;IAIpD,oBAAoB,YAAY,KAAK;IACrC,QAAQ,cAAc;GACxB;EACF;EAEA,SAAS,UAAU,QADN,gBAAgB,OAAO,IACN,GAAG,OAAO;EACxC;CACF,QAAQ,CAER;CAGF,YAAY,UAAU,KAAK,uBAAuB,iBAAiB,SAAS;AAC9E;;AAGA,SAAS,UACP,UACA,QACA,UAAkC,CAAC,GAC7B;CACN,IAAI,SAAS,eAAe,SAAS,WACnC;CAGF,SAAS,UAAU,QAAQ,OAAO;CAClC,SAAS,IAAI;AACf;;;;;;;;;;;AAYA,eAAsB,gBACpB,WACA,UAC+B;CAC/B,MAAM,WAAW,gBAAgB,SAAS;CAC1C,MAAM,WAAW,IAAI,gBAAgB;CACrC,MAAM,WAAW,IAAI,iBAAiB;CACtC,MAAM,2BAAW,IAAI,IAAmB;CACxC,IAAI;CAEJ,MAAM,SAAiB,cAAc,SAAS,aAAa;EACzD,IAAI,SAAS;GACX,YAAY,UAAU,KAAK,uBAAuB,4BAA4B,EAC5E,YAAY,QACd,CAAC;GAED;EACF;EAEA,MAAM,OAAO,cACX,SACA,UACA,UACA,UACA,SAAS,QACT,QACF,CAAC,CAAC,YAAY;GACZ,YAAY,UAAU,KAAK,uBAAuB,wBAAwB;EAC5E,CAAC;EAED,SAAS,IAAI,IAAI;EACjB,AAAK,KAAK,cAAc;GACtB,SAAS,OAAO,IAAI;GAEpB,IAAI,SACF,OAAO,qBAAqB;EAEhC,CAAC;CACH,CAAC;CAED,MAAM,IAAI,SAAe,SAAS,WAAW;EAC3C,MAAM,WAAW,UAAiB;GAChC,OACE,IAAI,cACF,uCAAuC,SAAS,KAAK,GAAG,SAAS,KAAK,IAAI,MAAM,WAChF;IAAE,MAAM;IAAU;IAAO,SAAS;KAAE,MAAM,SAAS;KAAM,MAAM,SAAS;IAAK;GAAE,CACjF,CACF;EACF;EAEA,OAAO,KAAK,SAAS,OAAO;EAC5B,OAAO,OAAO,SAAS,MAAM,SAAS,YAAY;GAChD,OAAO,IAAI,SAAS,OAAO;GAC3B,QAAQ;EACV,CAAC;CACH,CAAC;CAED,MAAM,UAAU,OAAO,QAAQ;CAC/B,MAAM,YAAY,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO,SAAS;CAE5F,MAAM,MAAM,UADI,OAAO,SAAS,IAAI,IAAI,IAAI,SAAS,KAAK,KAAK,SAAS,KAC1C,GAAG,YAAY,SAAS;CAEtD,MAAM,cAA6B;EACjC,IAAI,SACF,OAAO;EAGT,WAAW,YAAY;GACrB,MAAM,SAAS,IAAI,SAAe,YAAY;IAC5C,OAAO,YAAY,QAAQ,CAAC;GAC9B,CAAC;GAED,OAAO,qBAAqB;GAE5B,IAAI,SAAS,OAAO,GAAG;IACrB,IAAI;IACJ,MAAM,WAAW,IAAI,SAAoB,YAAY;KACnD,QAAQ,iBAAiB,QAAQ,SAAS,GAAG,SAAS,cAAc;IACtE,CAAC;IAED,MAAM,UAAU,MAAM,QAAQ,KAAK,CACjC,QAAQ,WAAW,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,WAAW,SAAkB,GAC/D,QACF,CAAC;IAED,aAAa,KAAK;IAElB,IAAI,YAAY,WAAW;KACzB,SAAS,MAAM;KACf,OAAO,oBAAoB;IAC7B;GACF;GAEA,OAAO,qBAAqB;GAC5B,MAAM;EACR,EAAC,CAAE;EAEH,OAAO;CACT;CAEA,OAAO;EAAE;EAAK;CAAM;AACtB;;;;;AAMA,eAAe,cACb,SACA,UACA,UACA,UACA,QACA,UACe;CACf,MAAM,UAAU,eAAe,QAAQ,OAAO;CAC9C,MAAM,UAAU,QAAQ,UAAU,MAAK,CAAE,YAAY;CACrD,MAAM,MAAM,IAAI,IAAI,QAAQ,OAAO,KAAK,kBAAkB;CAC1D,MAAM,OAAO,IAAI;CACjB,MAAM,SAAS,SAAS,OAAO,IAAI,IAAI;CAGvC,IAAI,CAAC,YAAY,QAAQ,MAAM,QAAQ,GAAG;EACxC,YAAY,UAAU,KAAK,uBAAuB,qCAAqC;EAEvF;CACF;CAEA,IAAI,CAAC,cAAc,QAAQ,QAAQ,QAAQ,GAAG;EAC5C,YAAY,UAAU,KAAK,uBAAuB,gCAAgC;EAElF;CACF;CAEA,IAAI,SAAS,SAAS,QAAQ,CAAC,QAAQ;EACrC,YAAY,UAAU,KAAK,uBAAuB,YAAY;EAE9D;CACF;CAEA,MAAM,OAA2B;EAAE;EAAQ;EAAM;CAAQ;CAEzD,IAAI,QAAQ,OAAO,kBAAkB,QACnC,KAAK,gBAAgB,QAAQ,OAAO;CAGtC,MAAM,iBAAiB,MAAM,WAAW,MAAM,QAAQ;CAEtD,IAAI,CAAC,eAAe,SAAS;EAC3B,MAAM,gBAAgB,UAAU,UAAU;GACxC,OAAO,eAAe;GACtB;GACA;GACA,QAAQ,SAAS,UAAU;EAC7B,CAAC;EAED;CACF;CAEA,IAAI,QAAQ;EACV,MAAM,QAAQ,OAAO,IAAI,MAAM;EAE/B,IAAI,CAAC,OAAO;GACV,UAAU,UAAU,KAAK,EAAE,OAAO,CAAC,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,IAAI,EAAE,CAAC;GAExE;EACF;EAEA,MAAM,YACJ,SACA,UACA,OACA,MACA,KACA,eAAe,WACf,QACA,QACF;EAEA;CACF;CAEA,IAAI,WAAW,QAAQ;EAGrB,UAAU,UAAU,KAAK,EAAE,OAAO,OAAO,CAAC;EAE1C;CACF;CAEA,MAAM,kBAAkB,QAAQ;CAEhC,IAAI,oBAAoB,UAAa,CAAC,4BAA4B,SAAS,eAAe,GAAG;EAC3F,YACE,UACA,KACA,uBACA,qCAAqC,gBAAgB,gBAAgB,4BAA4B,KAAK,IAAI,EAAE,GAC9G;EAEA;CACF;CAEA,IAAI,CAAC,kBAAkB,QAAQ,eAAe,GAAG;EAC/C,YAAY,UAAU,KAAK,uBAAuB,wCAAwC;EAE1F;CACF;CAEA,IAAI,CAAC,YAAY,QAAQ,MAAM,GAAG;EAChC,YAAY,UAAU,KAAK,uBAAuB,qCAAqC;EAEvF;CACF;CAEA,MAAM,OAAO,MAAM,SAAS,SAAS,SAAS,YAAY;CAE1D,IAAI,KAAK,SAAS,aAAa;EAC7B,YACE,UACA,KACA,uBACA,wBAAwB,SAAS,aAAa,UAC9C,EAAE,YAAY,QAAQ,CACxB;EAEA;CACF;CAEA,IAAI;CAEJ,IAAI;EACF,UAAU,KAAK,MAAM,KAAK,IAAI;CAChC,QAAQ;EACN,YAAY,UAAU,KAAK,sBAAsB,sCAAsC;EAEvF;CACF;CAEA,IAAI,OAAO,YAAY,YAAY,YAAY,QAAQ,MAAM,QAAQ,OAAO,GAAG;EAC7E,YACE,UACA,KACA,0BACA,qFACF;EAEA;CACF;CAEA,MAAM,SAAS;CAEf,IAAI,OAAO,YAAY,OAAO;EAC5B,YAAY,UAAU,KAAK,0BAA0B,2CAAyC;EAE9F;CACF;CAEA,MAAM,KAAK,OAAO;CAElB,IAAI,OAAO,OAAO,WAAW,UAAU;EACrC,IAAI,OAAO,OAAO,YAAY,OAAO,OAAO,UAAU;GAEpD,SAAS,OAAO,QAAQ,kBAAkB,IAAI,CAAC;GAC/C,UAAU,UAAU,GAAG;GAEvB;EACF;EAEA,MAAM,aAA6B;GAAE,SAAS;GAAO;GAAI,QAAQ,OAAO;EAAO;EAE/E,IAAI,OAAO,WAAW,QACpB,WAAW,SAAS,OAAO;EAG7B,MAAM,eAAe,IAAI,gBAAgB;EACzC,MAAM,gBAAgB,aAAa,MAAM;EACzC,SAAS,KAAK,SAAS,OAAO;EAE9B,IAAI;GACF,MAAM,cAAc,MAAM,SAAS,IACjC,YACA;IACE,SAAS;IACT,WAAW,eAAe;IAC1B,QAAQ,YAAY,IAAI,CAAC,QAAQ,aAAa,MAAM,CAAC;GACvD,IACC,YAAY,SAAS,YAAY,OAAO,GACzC,kBAAkB,IAAI,CACxB;GAEA,IAAI,gBAAgB,QAElB,UAAU,UAAU,GAAG;QAEvB,SAAS,UAAU,KAAK,WAAW;EAEvC,UAAU;GACR,SAAS,IAAI,SAAS,OAAO;EAC/B;EAEA;CACF;CAEA,KACG,OAAO,OAAO,YAAY,OAAO,OAAO,cACxC,YAAY,UAAU,WAAW,SAClC;EAEA,UAAU,UAAU,GAAG;EAEvB;CACF;CAEA,YAAY,UAAU,KAAK,0BAA0B,0CAA0C;AACjG;;AAGA,eAAe,YACb,SACA,UACA,OACA,MACA,KACA,WACA,UACA,UACe;CACf,MAAM,eAAe,IAAI,gBAAgB;CACzC,MAAM,gBAAgB,aAAa,MAAM;CACzC,SAAS,KAAK,SAAS,OAAO;CAE9B,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,SAAS,SAAS,YAAY;EAE1D,IAAI,KAAK,SAAS,aAAa;GAC7B,YACE,UACA,KACA,uBACA,wBAAwB,SAAS,aAAa,UAC9C,EACE,YAAY,QACd,CACF;GAEA;EACF;EAEA,MAAM,SAAS,YAAY,IAAI,CAAC,UAAU,aAAa,MAAM,CAAC;EAC9D,OAAO,eAAe;EACtB,MAAM,SAAS,MAAM,MAAM,QAAQ;GAAE,SAAS;GAAM;GAAK;GAAW,MAAM,KAAK;GAAM;EAAO,CAAC;EAE7F,IAAI,OAAO,SAAS,UAAa,KAAK,WAAW,QAC/C,UAAU,UAAU,OAAO,UAAU,KAAK,OAAO,OAAO;OAExD,SAAS,UAAU,OAAO,UAAU,KAAK,OAAO,MAAM,OAAO,OAAO;CAExE,UAAU;EACR,SAAS,IAAI,SAAS,OAAO;CAC/B;AACF;;;;;;;AAQA,SAAS,YAAY,YAAgC,UAAqC;CACxF,IAAI,CAAC,SAAS,YAAY,SAAS,aAAa,SAAS,GACvD,OAAO;CAGT,IAAI,eAAe,QACjB,OAAO;CAGT,MAAM,OAAO,WAAW,UAAU;CAElC,IAAI,SAAS,aAAa,IAAI,IAAI,GAChC,OAAO;CAGT,OAAO,SAAS,YAAY,eAAe,IAAI;AACjD;;;;;;;AAQA,SAAS,cAAc,QAA4B,UAAqC;CACtF,IAAI,WAAW,QACb,OAAO;CAGT,IAAI;CAEJ,IAAI;EACF,SAAS,IAAI,IAAI,MAAM;CACzB,QAAQ;EACN,OAAO;CACT;CAEA,IAAI,SAAS,eAAe,IAAI,OAAO,MAAM,GAC3C,OAAO;CAGT,QACG,OAAO,aAAa,WAAW,OAAO,aAAa,aAAa,eAAe,OAAO,QAAQ;AAEnG;AAYA,eAAe,WACb,MACA,UACyB;CACzB,IAAI,CAAC,SAAS,cACZ,OAAO,EAAE,SAAS,KAAK;CAGzB,IAAI,SAAS,aAAa,SAAS,GAAG;EACpC,MAAM,QAAQ,sBAAsB,KAAK,KAAK,QAAQ,iBAAiB,EAAE;EACzE,MAAM,YAAY,OAAO,QAAQ,EAAE,EAAE,KAAK,KAAK,EAAE;EACjD,IAAI,UAAU;EAEd,KAAK,MAAM,YAAY,SAAS,cAC9B,UAAU,gBAAgB,WAAW,QAAQ,KAAK;EAGpD,IAAI,CAAC,SAAS,CAAC,SACb,OAAO;GAAE,SAAS;GAAO,OAAO;EAAS;CAE7C;CAEA,IAAI,SAAS,WACX,IAAI;EACF,IAAK,MAAM,SAAS,UAAU,IAAI,MAAO,MACvC,OAAO;GAAE,SAAS;GAAO,OAAO;EAAY;CAEhD,QAAQ;EACN,OAAO;GAAE,SAAS;GAAO,OAAO;EAAY;CAC9C;CAGF,IAAI,SAAS,cACX,IAAI;EACF,MAAM,YAAY,MAAM,SAAS,aAAa,IAAI;EAElD,OAAO,cAAc,SACjB;GAAE,SAAS;GAAO,OAAO;EAAe,IACxC;GAAE,SAAS;GAAM;EAAU;CACjC,QAAQ;EACN,OAAO;GAAE,SAAS;GAAO,OAAO;EAAe;CACjD;CAGF,OAAO,EAAE,SAAS,KAAK;AACzB;;;;;;AAOA,SAAS,kBAAkB,MAAkC;CAC3D,MAAM,mCAAmB,IAAI,IAAI;EAC/B;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF,CAAC;CACD,MAAM,cAAc,OAAO,QAAQ,KAAK,OAAO,CAAC,CAC7C,QAAQ,CAAC,UAAU,CAAC,iBAAiB,IAAI,IAAI,CAAC,CAAC,CAC/C,MAAM,CAAC,OAAO,CAAC,WAAW,KAAK,cAAc,KAAK,CAAC;CAEtD,OAAO,OAAO,KAAK,UAAU,CAAC,KAAK,eAAe,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,KAAK;AACjF;;AAGA,SAAS,kBAAkB,aAA0C;CACnE,OAAO,aAAa,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,MAAM;AAC9D;;;;;;;AAQA,SAAS,YAAY,QAAqC;CACxD,IAAI,WAAW,UAAa,OAAO,KAAK,MAAM,IAC5C,OAAO;CAGT,OAAO,OACJ,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CACvD,MAAM,SAAS,SAAS,sBAAsB,SAAS,mBAAmB,SAAS,KAAK;AAC7F;;;;;;AAUA,SAAS,SAAS,SAA0B,OAAoC;CAC9E,MAAM,WAAW,OAAO,QAAQ,QAAQ,iBAAiB;CAEzD,IAAI,OAAO,SAAS,QAAQ,KAAK,WAAW,OAC1C,OAAO,QAAQ,QAAQ,EAAE,MAAM,YAAY,CAAC;CAG9C,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EACX,IAAI,OAAO;EAEX,MAAM,UAAU,WAAuB;GACrC,IAAI,CAAC,MAAM;IACT,OAAO;IACP,QAAQ,MAAM;GAChB;EACF;EAEA,QAAQ,GAAG,SAAS,UAAkB;GACpC,IAAI,MACF;GAGF,QAAQ,MAAM;GAEd,IAAI,OAAO,OAAO;IAChB,QAAQ,MAAM;IACd,OAAO,EAAE,MAAM,YAAY,CAAC;IAE5B;GACF;GAEA,OAAO,KAAK,KAAK;EACnB,CAAC;EACD,QAAQ,GAAG,aAAa,OAAO;GAAE,MAAM;GAAM,MAAM,OAAO,OAAO,MAAM,CAAC,CAAC,SAAS,MAAM;EAAE,CAAC,CAAC;EAC5F,QAAQ,GAAG,UAAU,UAAU;GAC7B,IAAI,CAAC,MAAM;IACT,OAAO;IACP,OAAO,KAAK;GACd;EACF,CAAC;CACH,CAAC;AACH"}
|
package/llms-full.txt
CHANGED
|
@@ -4,6 +4,31 @@
|
|
|
4
4
|
|
|
5
5
|
> Generated artifact. Concatenates every SKILL.md and reference file under `@warlock.js/ai-tools/skills/`. Re-run `node scripts/generate-llms.mjs` after any change.
|
|
6
6
|
|
|
7
|
+
## Package orientation
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
description: "Ready-made tools for @warlock.js/ai agents: web search, URL fetch, HTTP requests, calculator, date-time, and MCP client and server support. Exports `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `mcp`, `serve`, `createServeHandler`, `McpTransportError`, `McpServeError`. Use for: let an agent search the web, fetch a page, call an HTTP API, do math, get the current date, connect to an MCP server, expose my tools as an MCP server over stdio or authenticated HTTP. Not this package: defining your own tool or the agent itself → @warlock.js/ai; file and shell tools → @warlock.js/ai-workspace."
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# @warlock.js/ai-tools
|
|
14
|
+
|
|
15
|
+
A belt of prebuilt tools for `@warlock.js/ai` agents plus the Model Context Protocol bridge in both directions. Importing the package attaches `ai.tools.*` and `ai.mcp` onto the shared `ai` object; the factories are also exported directly.
|
|
16
|
+
|
|
17
|
+
## The 80% path
|
|
18
|
+
|
|
19
|
+
1. Give an agent web or API access with the factories (`use-web-and-http-tools.md`); pass the results in the agent's `tools` array.
|
|
20
|
+
2. Consume tools from an external MCP server (`connect-mcp-server.md`); they arrive as normal agent tools.
|
|
21
|
+
3. Publish your own tools to MCP clients such as editors or agent CLIs, over stdio or a loopback HTTP endpoint with a bearer token (`expose-as-mcp-server.md`).
|
|
22
|
+
|
|
23
|
+
## Conventions and pitfalls
|
|
24
|
+
|
|
25
|
+
- Direction A (`connect-mcp-server.md`) consumes; Direction B (`expose-as-mcp-server.md`) serves. Do not mix them up.
|
|
26
|
+
- `fetchUrlTool` and `httpRequestTool` are policy-gated: set host allowlists and byte caps rather than opening them fully.
|
|
27
|
+
- `webSearchTool` needs a search provider choice and its API key (tavily, brave, or serpapi).
|
|
28
|
+
- Every error class extends the `AIError` of `@warlock.js/ai`; catch by subclass.
|
|
29
|
+
- Writing a custom tool with `ai.tool()` is covered by the `define-ai-tool` topic of the `@warlock.js/ai` package.
|
|
30
|
+
|
|
31
|
+
|
|
7
32
|
## connect-mcp-server `@warlock.js/ai-tools/connect-mcp-server/SKILL.md`
|
|
8
33
|
|
|
9
34
|
---
|
|
@@ -91,7 +116,7 @@ export async function triage(model: ModelContract): Promise<void> {
|
|
|
91
116
|
|
|
92
117
|
For each remote tool:
|
|
93
118
|
|
|
94
|
-
1. its JSON-Schema `inputSchema` is wrapped as a Standard Schema via `jsonSchemaToStandard` (Ajv-backed — `ajv` is an optional peer needed only for the client);
|
|
119
|
+
1. its JSON-Schema `inputSchema` is wrapped as a Standard Schema via `jsonSchemaToStandard` (Ajv-backed — `ajv` is an optional peer needed only for the client), which also exposes that schema (`~standard.jsonSchema`) so the model sees the server's real argument names;
|
|
95
120
|
2. `execute` issues `tools/call` honoring `ctx.signal` (cooperative abort) and the configured `timeoutMs`, then unwraps the result content (a single JSON text block is parsed back into an object);
|
|
96
121
|
3. an MCP `isError` result is thrown so the surrounding `tool()` wraps it as a `ToolExecutionError` — the agent reads it as `{ error }` data and self-corrects.
|
|
97
122
|
|
|
@@ -147,7 +172,7 @@ The MCP client does not use `@modelcontextprotocol/sdk`: it speaks MCP's JSON-RP
|
|
|
147
172
|
|
|
148
173
|
---
|
|
149
174
|
name: expose-as-mcp-server
|
|
150
|
-
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
175
|
+
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `onAuthRefusal`, `McpHttpAuthRefusal`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
151
176
|
---
|
|
152
177
|
|
|
153
178
|
# Expose tools AS an MCP server (Direction B)
|
|
@@ -211,12 +236,60 @@ transport: {
|
|
|
211
236
|
}
|
|
212
237
|
```
|
|
213
238
|
|
|
214
|
-
- Checks run in order: `bearer`, `authorize`, `authenticate`; every configured check must pass. A failed check skips later hooks. `authenticate` alone satisfies the non-loopback auth requirement. Host/Origin checks run first. A throwing authentication hook denies.
|
|
239
|
+
- Checks run in order: `bearer`, `authorize`, `authenticate`; every configured check must pass. A failed check skips later hooks. `authenticate` alone satisfies the non-loopback auth requirement. Host/Origin checks run first. A throwing authentication hook denies. By default, failures → `401`; bearer or authenticate failures include a Bearer `WWW-Authenticate` challenge.
|
|
215
240
|
- A foreign or `null` `Origin` → `403` (DNS-rebinding guard from the MCP spec). Requests without `Origin` pass.
|
|
216
241
|
- On loopback, a `Host` other than `localhost` / `127.0.0.1` / `[::1]` (or `allowedHosts`) → `403`.
|
|
217
242
|
- Stateless: no session id; `GET` / `DELETE` → `405`; batches → `400`; unsupported `MCP-Protocol-Version` → `400`.
|
|
218
243
|
- Plain HTTP: put TLS in front before exposing beyond the machine.
|
|
219
244
|
|
|
245
|
+
## Customize HTTP authentication refusals
|
|
246
|
+
|
|
247
|
+
Use `transport.onAuthRefusal` for a JSON error contract shared by the MCP endpoint
|
|
248
|
+
and `transport.routes`. It runs only after a failed bearer/authorize/authenticate
|
|
249
|
+
check. Host/Origin refusals always take precedence and never call it.
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
import { ai } from "@warlock.js/ai";
|
|
253
|
+
import type { McpHttpAuthRefusal } from "@warlock.js/ai-tools";
|
|
254
|
+
import "@warlock.js/ai-tools";
|
|
255
|
+
|
|
256
|
+
const onAuthRefusal: McpHttpAuthRefusal = async ({ check, target }) => ({
|
|
257
|
+
status: 401,
|
|
258
|
+
headers: { "cache-control": "no-store" },
|
|
259
|
+
body: { error: "MEMBER_UNAUTHORIZED", check, target },
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const server = ai.mcp.serve([], {
|
|
263
|
+
name: "member-tools",
|
|
264
|
+
transport: {
|
|
265
|
+
type: "http",
|
|
266
|
+
auth: { authorize: () => false },
|
|
267
|
+
onAuthRefusal,
|
|
268
|
+
routes: [{ method: "GET", path: "/policy", handler: () => ({ body: {} }) }],
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
const { url, close } = await server.start();
|
|
272
|
+
// url and `${url}/policy` share the custom refusal; no handler runs.
|
|
273
|
+
await close();
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
- Context has only `check: "bearer" | "authorize" | "authenticate"`, uppercase
|
|
277
|
+
`method`, query-free `path`, and `target: "mcp" | "route"`. No error, credentials,
|
|
278
|
+
headers, principal or request body is passed. Thrown auth details stay private.
|
|
279
|
+
- Return `{ status?, headers?, body }` or a promise. Status defaults to 401 and
|
|
280
|
+
must be an integer 400–599. Required body is plain JSON, including primitives,
|
|
281
|
+
arrays and null; no cycles, class instances, non-finite numbers or undefined entries.
|
|
282
|
+
- Header allowlist: `www-authenticate`, `cache-control`, `retry-after`,
|
|
283
|
+
`content-language`, `x-request-id`. Public types use lowercase keys; runtime
|
|
284
|
+
matches names case-insensitively and validates HTTP string values. Headers merge
|
|
285
|
+
over the default challenge. Framing, cookies, CORS and redirects cannot be set.
|
|
286
|
+
- Void, throws/rejections and any invalid result fall back to the original 401
|
|
287
|
+
JSON-RPC `Unauthorized.` response and default headers. A custom denial cannot
|
|
288
|
+
grant authority: it ends the request without later auth, route, policy or tool
|
|
289
|
+
handlers. Successful auth skips the refusal hook.
|
|
290
|
+
- Exported types: `McpHttpAuthRefusal`, `McpHttpAuthRefusalContext`,
|
|
291
|
+
`McpHttpAuthRefusalResponse`, `McpHttpAuthRefusalBody`, `McpHttpAuthRefusalHeader`.
|
|
292
|
+
|
|
220
293
|
## Tools read request headers from `ctx.request`
|
|
221
294
|
|
|
222
295
|
```ts
|
package/llms.txt
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
## Skills
|
|
8
8
|
|
|
9
|
+
- [Package orientation](@warlock.js/ai-tools/skills/index.md): Ready-made tools for @warlock.js/ai agents: web search, URL fetch, HTTP requests, calculator, date-time, and MCP client and server support. Exports `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `mcp`, `serve`, `createServeHandler`, `McpTransportError`, `McpServeError`. Use for: let an agent search the web, fetch a page, call an HTTP API, do math, get the current date, connect to an MCP server, expose my tools as an MCP server over stdio or authenticated HTTP. Not this package: defining your own tool or the agent itself → @warlock.js/ai; file and shell tools → @warlock.js/ai-workspace.
|
|
9
10
|
- [connect-mcp-server](@warlock.js/ai-tools/connect-mcp-server/SKILL.md): Connect a @warlock.js/ai agent to an external Model Context Protocol (MCP) server and adapt its tools as native agent tools (Direction A) with `ai.mcp(server)`. Triggers: `signal`, `ai.mcp(`, `ai.mcp.serve` (the other direction), `mcp(`, `McpClient`, `McpClientOptions`, `McpTransport`, `client.tools()`, `client.close()`, `namePrefix`, `filter`, `McpTransportError`, `{ type: "stdio" }`, `{ type: "http" }`, `tools/list`, `tools/call`, `jsonSchemaToStandard`, `@modelcontextprotocol/sdk`, `ajv`; 'consume an MCP server', 'use an external MCP server's tools in my agent', 'connect to the GitHub/Slack/Filesystem MCP server', 'adapt MCP tools as agent tools', 'bridge my agent to the MCP ecosystem', 'run an MCP server over stdio/http'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: exposing YOUR agent/tools AS an MCP server (the reverse direction) — the `expose-as-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
10
|
-
- [expose-as-mcp-server](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md): Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; 'expose my agent as an MCP server', 'serve my tools over HTTP', 'give each agent session its own MCP endpoint', 'protect my MCP server with a token', 'publish my tools to Cursor/Claude Desktop', 'serve MCP resources or prompts', 'tool name collision'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
11
|
+
- [expose-as-mcp-server](@warlock.js/ai-tools/expose-as-mcp-server/SKILL.md): Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `onAuthRefusal`, `McpHttpAuthRefusal`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; 'expose my agent as an MCP server', 'serve my tools over HTTP', 'give each agent session its own MCP endpoint', 'protect my MCP server with a token', 'publish my tools to Cursor/Claude Desktop', 'serve MCP resources or prompts', 'tool name collision'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.
|
|
11
12
|
- [use-web-and-http-tools](@warlock.js/ai-tools/use-web-and-http-tools/SKILL.md): Wire the @warlock.js/ai-tools belt — web search, fetch/scrape, HTTP/REST, calculator, date-time — into an agent under their guardrails. Triggers: `ai.tools.measure`, `measureTools`, `ai.tools.webSearch`, `ai.tools.fetchUrl`, `ai.tools.http`, `ai.tools.calculator`, `ai.tools.dateTime`, `webSearchTool`, `fetchUrlTool`, `httpRequestTool`, `calculatorTool`, `dateTimeTool`, `web_search`, `fetch_url`, `http_request`, `calculator`, `date_time`, `WebToolError`, `HttpPolicyError`, `CalculatorError`, `DateTimeError`, `allowHosts`, `allowMethods`, `allowPrivateNetwork`, `baseUrl`, `maxBytes`, `guardedFetch`, `SearchProvider`, `TAVILY_API_KEY`; 'give an agent web search', 'let an agent fetch/scrape a page', 'let an agent call a REST API', 'restrict which hosts/methods an agent can hit', 'add a calculator/date tool to an agent', 'SSRF guardrail for an agent tool', 'let a tool reach an internal/dev service'; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: connecting to an external MCP server — the `connect-mcp-server` topic; exposing tools AS an MCP server — the `expose-as-mcp-server` topic; filesystem/shell tools — the `use-a-workspace` topic of the `warlock-js-ai-workspace` skill; defining your own custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.
|
package/package.json
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"peerDependencies": {
|
|
23
23
|
"@mozilla/readability": "*",
|
|
24
24
|
"@tavily/core": "*",
|
|
25
|
-
"@warlock.js/ai": "5.34.
|
|
25
|
+
"@warlock.js/ai": "5.34.2",
|
|
26
26
|
"ajv": "*",
|
|
27
27
|
"jsdom": "*"
|
|
28
28
|
},
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"dependencies": {
|
|
44
44
|
"@standard-schema/spec": "^1.0.0"
|
|
45
45
|
},
|
|
46
|
-
"version": "5.34.
|
|
46
|
+
"version": "5.34.2",
|
|
47
47
|
"main": "./cjs/index.cjs",
|
|
48
48
|
"module": "./esm/index.mjs",
|
|
49
49
|
"types": "./esm/index.d.mts",
|
|
@@ -83,7 +83,7 @@ export async function triage(model: ModelContract): Promise<void> {
|
|
|
83
83
|
|
|
84
84
|
For each remote tool:
|
|
85
85
|
|
|
86
|
-
1. its JSON-Schema `inputSchema` is wrapped as a Standard Schema via `jsonSchemaToStandard` (Ajv-backed — `ajv` is an optional peer needed only for the client);
|
|
86
|
+
1. its JSON-Schema `inputSchema` is wrapped as a Standard Schema via `jsonSchemaToStandard` (Ajv-backed — `ajv` is an optional peer needed only for the client), which also exposes that schema (`~standard.jsonSchema`) so the model sees the server's real argument names;
|
|
87
87
|
2. `execute` issues `tools/call` honoring `ctx.signal` (cooperative abort) and the configured `timeoutMs`, then unwraps the result content (a single JSON text block is parsed back into an object);
|
|
88
88
|
3. an MCP `isError` result is thrown so the surrounding `tool()` wraps it as a `ToolExecutionError` — the agent reads it as `{ error }` data and self-corrects.
|
|
89
89
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: expose-as-mcp-server
|
|
3
|
-
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
3
|
+
description: 'Expose a @warlock.js/ai agent / supervisor / orchestrator (or a raw ToolContract[], or named tool groups) AS a Model Context Protocol (MCP) server other clients consume (Direction B) with `ai.mcp.serve(source, options)` — over stdio or authenticated Streamable HTTP. Triggers: `ai.mcp.serve(`, `serve(`, `createServeHandler`, `McpServer`, `McpServeOptions`, `McpServeSource`, `McpServeTransport`, `McpHttpServeTransport`, `McpHttpAuth`, `onAuthRefusal`, `McpHttpAuthRefusal`, `McpServeError`, `server.start()`, `server.stop()`, `{ url, close }`, `{ type: "http", host, port, path, auth }`, `bearer`, `authorize`, `authenticate`, `beforeCall`, `listFilter`, `validateCallParams`, `McpError`, `routes`, `McpHttpRouteContext`, `requestId`, `principal`, `notifications/cancelled`, `allowedOrigins`, `maxBodyBytes`, `prefixToolNames`, `groups`, `resources`, `prompts`, `ctx.request.headers`, `schemaTarget`, `tools/list`, `tools/call`, `{ type: "stdio" }`; ''expose my agent as an MCP server'', ''serve my tools over HTTP'', ''give each agent session its own MCP endpoint'', ''protect my MCP server with a token'', ''publish my tools to Cursor/Claude Desktop'', ''serve MCP resources or prompts'', ''tool name collision''; typical import `import "@warlock.js/ai-tools"; import { ai } from "@warlock.js/ai"`. Skip: CONSUMING an external MCP server (the reverse direction) — the `connect-mcp-server` topic; the built-in web/HTTP/calc/date tools — the `use-web-and-http-tools` topic.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Expose tools AS an MCP server (Direction B)
|
|
@@ -64,12 +64,60 @@ transport: {
|
|
|
64
64
|
}
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
-
- Checks run in order: `bearer`, `authorize`, `authenticate`; every configured check must pass. A failed check skips later hooks. `authenticate` alone satisfies the non-loopback auth requirement. Host/Origin checks run first. A throwing authentication hook denies.
|
|
67
|
+
- Checks run in order: `bearer`, `authorize`, `authenticate`; every configured check must pass. A failed check skips later hooks. `authenticate` alone satisfies the non-loopback auth requirement. Host/Origin checks run first. A throwing authentication hook denies. By default, failures → `401`; bearer or authenticate failures include a Bearer `WWW-Authenticate` challenge.
|
|
68
68
|
- A foreign or `null` `Origin` → `403` (DNS-rebinding guard from the MCP spec). Requests without `Origin` pass.
|
|
69
69
|
- On loopback, a `Host` other than `localhost` / `127.0.0.1` / `[::1]` (or `allowedHosts`) → `403`.
|
|
70
70
|
- Stateless: no session id; `GET` / `DELETE` → `405`; batches → `400`; unsupported `MCP-Protocol-Version` → `400`.
|
|
71
71
|
- Plain HTTP: put TLS in front before exposing beyond the machine.
|
|
72
72
|
|
|
73
|
+
## Customize HTTP authentication refusals
|
|
74
|
+
|
|
75
|
+
Use `transport.onAuthRefusal` for a JSON error contract shared by the MCP endpoint
|
|
76
|
+
and `transport.routes`. It runs only after a failed bearer/authorize/authenticate
|
|
77
|
+
check. Host/Origin refusals always take precedence and never call it.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { ai } from "@warlock.js/ai";
|
|
81
|
+
import type { McpHttpAuthRefusal } from "@warlock.js/ai-tools";
|
|
82
|
+
import "@warlock.js/ai-tools";
|
|
83
|
+
|
|
84
|
+
const onAuthRefusal: McpHttpAuthRefusal = async ({ check, target }) => ({
|
|
85
|
+
status: 401,
|
|
86
|
+
headers: { "cache-control": "no-store" },
|
|
87
|
+
body: { error: "MEMBER_UNAUTHORIZED", check, target },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const server = ai.mcp.serve([], {
|
|
91
|
+
name: "member-tools",
|
|
92
|
+
transport: {
|
|
93
|
+
type: "http",
|
|
94
|
+
auth: { authorize: () => false },
|
|
95
|
+
onAuthRefusal,
|
|
96
|
+
routes: [{ method: "GET", path: "/policy", handler: () => ({ body: {} }) }],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
const { url, close } = await server.start();
|
|
100
|
+
// url and `${url}/policy` share the custom refusal; no handler runs.
|
|
101
|
+
await close();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- Context has only `check: "bearer" | "authorize" | "authenticate"`, uppercase
|
|
105
|
+
`method`, query-free `path`, and `target: "mcp" | "route"`. No error, credentials,
|
|
106
|
+
headers, principal or request body is passed. Thrown auth details stay private.
|
|
107
|
+
- Return `{ status?, headers?, body }` or a promise. Status defaults to 401 and
|
|
108
|
+
must be an integer 400–599. Required body is plain JSON, including primitives,
|
|
109
|
+
arrays and null; no cycles, class instances, non-finite numbers or undefined entries.
|
|
110
|
+
- Header allowlist: `www-authenticate`, `cache-control`, `retry-after`,
|
|
111
|
+
`content-language`, `x-request-id`. Public types use lowercase keys; runtime
|
|
112
|
+
matches names case-insensitively and validates HTTP string values. Headers merge
|
|
113
|
+
over the default challenge. Framing, cookies, CORS and redirects cannot be set.
|
|
114
|
+
- Void, throws/rejections and any invalid result fall back to the original 401
|
|
115
|
+
JSON-RPC `Unauthorized.` response and default headers. A custom denial cannot
|
|
116
|
+
grant authority: it ends the request without later auth, route, policy or tool
|
|
117
|
+
handlers. Successful auth skips the refusal hook.
|
|
118
|
+
- Exported types: `McpHttpAuthRefusal`, `McpHttpAuthRefusalContext`,
|
|
119
|
+
`McpHttpAuthRefusalResponse`, `McpHttpAuthRefusalBody`, `McpHttpAuthRefusalHeader`.
|
|
120
|
+
|
|
73
121
|
## Tools read request headers from `ctx.request`
|
|
74
122
|
|
|
75
123
|
```ts
|
package/skills/index.md
CHANGED
|
@@ -18,4 +18,4 @@ A belt of prebuilt tools for `@warlock.js/ai` agents plus the Model Context Prot
|
|
|
18
18
|
- `fetchUrlTool` and `httpRequestTool` are policy-gated: set host allowlists and byte caps rather than opening them fully.
|
|
19
19
|
- `webSearchTool` needs a search provider choice and its API key (tavily, brave, or serpapi).
|
|
20
20
|
- Every error class extends the `AIError` of `@warlock.js/ai`; catch by subclass.
|
|
21
|
-
- Writing a custom tool with `ai.tool()`
|
|
21
|
+
- Writing a custom tool with `ai.tool()` is covered by the `define-ai-tool` topic of the `@warlock.js/ai` package.
|
package/skills/README.md
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
# `@warlock.js/ai-tools` — skills index
|
|
2
|
-
|
|
3
|
-
Per-task skills. Cross-references name the topic ("the `<topic>` topic"), and for another package add its skill ("the `<topic>` topic of the `warlock-js-<pkg>` skill").
|
|
4
|
-
|
|
5
|
-
## Skills
|
|
6
|
-
|
|
7
|
-
### `use-web-and-http-tools`
|
|
8
|
-
|
|
9
|
-
Wire the five ready-made tools — `ai.tools.webSearch` (`web_search`), `ai.tools.fetchUrl` (`fetch_url`), `ai.tools.http` (`http_request`), `ai.tools.calculator` (`calculator`), and `ai.tools.dateTime` (`date_time`) — into an agent under their construction-time rails: a search provider + API-key fallback, readability extract modes, the host/method allowlists and byte/timeout caps (the SSRF guardrails), the safe shunting-yard calculator, and the millisecond-unit date-time ops. Covers the option vs per-call-input split, the typed `WebToolError` / `HttpPolicyError` / `CalculatorError` / `DateTimeError`, and the errors-as-data contract. Load when giving an agent web search, fetch/scrape, REST, calculator, or date tools.
|
|
10
|
-
|
|
11
|
-
### `connect-mcp-server`
|
|
12
|
-
|
|
13
|
-
Connect an agent to an external Model Context Protocol server with `ai.mcp(server, options?)` and adapt each of its tools into a native `ToolContract` (Direction A). Covers the stdio (`node:child_process`, no dep) vs Streamable HTTP transport, `namePrefix` / `filter` / `timeoutMs`, the lazy-and-cached `client.tools()` handshake + `tools/list`, the JSON-Schema→Standard-Schema adaptation (Ajv optional peer), the `kind`→`type` translation, and the `McpTransportError` surface. Load when consuming an MCP server's tools inside a warlock agent.
|
|
14
|
-
|
|
15
|
-
### `expose-as-mcp-server`
|
|
16
|
-
|
|
17
|
-
Expose a warlock agent / supervisor / orchestrator (or a raw `ToolContract[]`) AS an MCP server with `ai.mcp.serve(source, options)` (Direction B). Covers the `source` shapes (array or `tools()`), the `name` / `version` / `transport` / `schemaTarget` options, how `initialize` / `tools/list` / `tools/call` map (via `extractJsonSchema` and the never-throwing `invoke()`), the auto-pumped stdio transport vs host-owned HTTP, and the `createServeHandler` pure-protocol seam for HTTP wiring and tests. Load when publishing local tools to Cursor / Claude Desktop / any MCP client.
|