@warlock.js/ai-tools 5.30.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -1
- package/README.md +25 -13
- package/cjs/index.cjs +843 -101
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -1
- package/esm/contracts/mcp-serve.type.d.mts +257 -0
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
- package/esm/contracts/mcp.type.d.mts +16 -59
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/errors.d.mts +36 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +20 -1
- package/esm/errors.mjs.map +1 -1
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +6 -4
- package/esm/index.mjs +3 -2
- package/esm/mcp/client.mjs +35 -5
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +12 -5
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +1 -0
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/protocol.d.mts +16 -0
- package/esm/mcp/protocol.d.mts.map +1 -0
- package/esm/mcp/protocol.mjs +23 -0
- package/esm/mcp/protocol.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +443 -0
- package/esm/mcp/serve-http.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +45 -26
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +325 -92
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/register.d.mts +12 -1
- package/esm/register.d.mts.map +1 -1
- package/esm/register.mjs +3 -2
- package/esm/register.mjs.map +1 -1
- package/esm/schema.mjs.map +1 -1
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +194 -57
- package/llms.txt +3 -3
- package/package.json +2 -6
- package/skills/connect-mcp-server/SKILL.md +28 -6
- package/skills/expose-as-mcp-server/SKILL.md +117 -36
- package/skills/index.md +5 -2
- package/skills/use-web-and-http-tools/SKILL.md +49 -15
package/esm/mcp/serve.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { McpTransportError } from "../errors.mjs";
|
|
1
|
+
import { McpServeError, McpTransportError } from "../errors.mjs";
|
|
2
|
+
import { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from "./protocol.mjs";
|
|
3
|
+
import { startHttpServer } from "./serve-http.mjs";
|
|
2
4
|
import { extractJsonSchema } from "@warlock.js/ai";
|
|
3
5
|
import { createInterface } from "node:readline";
|
|
4
6
|
|
|
@@ -9,59 +11,159 @@ const JSONRPC_VERSION = "2.0";
|
|
|
9
11
|
const DEFAULT_SCHEMA_TARGET = "draft-2020-12";
|
|
10
12
|
/** Default advertised server version when the caller omits one. */
|
|
11
13
|
const DEFAULT_VERSION = "4.4.0";
|
|
12
|
-
/** The MCP protocol version this server advertises in `initialize`. */
|
|
13
|
-
const PROTOCOL_VERSION = "2025-06-18";
|
|
14
14
|
/** JSON-RPC standard error codes we emit. */
|
|
15
15
|
const JSON_RPC_METHOD_NOT_FOUND = -32601;
|
|
16
16
|
const JSON_RPC_INVALID_PARAMS = -32602;
|
|
17
|
+
/** MCP's "resource not found" error code (server/resources, error handling). */
|
|
18
|
+
const MCP_RESOURCE_NOT_FOUND = -32002;
|
|
17
19
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
+
* A JSON-RPC failure raised inside a method handler, carrying the exact
|
|
21
|
+
* code to answer with (unknown resource, missing prompt argument, …).
|
|
20
22
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
var RpcFailure = class extends Error {
|
|
24
|
+
constructor(code, message, data) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.code = code;
|
|
27
|
+
this.data = data;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** Enumerate one tool set into a flat contract array. */
|
|
31
|
+
function toolsOf(source) {
|
|
32
|
+
return Array.isArray(source) ? source : source.tools();
|
|
33
|
+
}
|
|
34
|
+
/** Narrow a serve source to its grouped shape. */
|
|
35
|
+
function isGroupedSource(source) {
|
|
36
|
+
return !Array.isArray(source) && "groups" in source;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the serve source into the exposed tool list, applying
|
|
40
|
+
* `prefixToolNames` and refusing duplicate names. Throws
|
|
41
|
+
* {@link McpServeError} — `"config"` for `prefixToolNames` on an ungrouped
|
|
42
|
+
* source, `"collision"` when two tools would share a name.
|
|
43
|
+
*/
|
|
44
|
+
function resolveTools(source, options) {
|
|
45
|
+
const prefix = options.prefixToolNames ?? false;
|
|
46
|
+
const exposed = [];
|
|
47
|
+
if (isGroupedSource(source)) for (const [group, groupSource] of Object.entries(source.groups)) for (const contract of toolsOf(groupSource)) {
|
|
48
|
+
let name = contract.name;
|
|
49
|
+
if (prefix === true) name = `${group}_${contract.name}`;
|
|
50
|
+
else if (typeof prefix === "function") name = prefix(group, contract.name);
|
|
51
|
+
exposed.push({
|
|
52
|
+
name,
|
|
53
|
+
contract,
|
|
54
|
+
origin: `group "${group}"`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (prefix !== false) throw new McpServeError("prefixToolNames needs a grouped source: serve({ groups: { name: tools } }, …).", { type: "config" });
|
|
59
|
+
for (const contract of toolsOf(source)) exposed.push({
|
|
60
|
+
name: contract.name,
|
|
61
|
+
contract,
|
|
62
|
+
origin: "the source"
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const seen = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const tool of exposed) {
|
|
67
|
+
const previous = seen.get(tool.name);
|
|
68
|
+
if (previous !== void 0) throw new McpServeError(`MCP tool name "${tool.name}" is exposed twice (from ${previous} and ${tool.origin}). Rename one tool, or serve a grouped source with prefixToolNames.`, {
|
|
69
|
+
type: "collision",
|
|
70
|
+
context: { name: tool.name }
|
|
71
|
+
});
|
|
72
|
+
seen.set(tool.name, tool.origin);
|
|
73
|
+
}
|
|
74
|
+
return exposed.map(({ name, contract }) => ({
|
|
75
|
+
name,
|
|
76
|
+
contract
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
/** Throw a collision error when two entries share the same key. */
|
|
80
|
+
function assertUnique(entries, key, label) {
|
|
81
|
+
const seen = /* @__PURE__ */ new Set();
|
|
82
|
+
for (const entry of entries ?? []) {
|
|
83
|
+
const value = key(entry);
|
|
84
|
+
if (seen.has(value)) throw new McpServeError(`MCP ${label} "${value}" is declared twice.`, {
|
|
85
|
+
type: "collision",
|
|
86
|
+
context: { [label]: value }
|
|
87
|
+
});
|
|
88
|
+
seen.add(value);
|
|
89
|
+
}
|
|
24
90
|
}
|
|
25
91
|
/**
|
|
26
92
|
* The pure protocol core of `serve` — maps one JSON-RPC request to its
|
|
27
|
-
* response, with no I/O. Both the stdio and
|
|
93
|
+
* response, with no I/O. Both the stdio and HTTP serve-transports pump
|
|
28
94
|
* their inbound requests through this, and specs can drive it directly.
|
|
29
95
|
*
|
|
30
|
-
* Handles
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
96
|
+
* Handles `initialize`, `ping`, `tools/list`, `tools/call`, and — when
|
|
97
|
+
* configured — `resources/list`, `resources/templates/list`,
|
|
98
|
+
* `resources/read`, `prompts/list` and `prompts/get`. Any other method
|
|
99
|
+
* answers with a JSON-RPC `method not found` error.
|
|
33
100
|
*
|
|
34
101
|
* Constructed via {@link createServeHandler}.
|
|
35
102
|
*/
|
|
36
103
|
var McpServeHandler = class {
|
|
37
104
|
constructor(source, options) {
|
|
38
|
-
this.tools = resolveTools(source);
|
|
39
|
-
this.byName = new Map(this.tools.map((
|
|
105
|
+
this.tools = resolveTools(source, options);
|
|
106
|
+
this.byName = new Map(this.tools.map((tool) => [tool.name, tool.contract]));
|
|
107
|
+
assertUnique(options.resources, (resource) => resource.uri, "resource");
|
|
108
|
+
assertUnique(options.prompts, (prompt) => prompt.name, "prompt");
|
|
109
|
+
this.resources = options.resources ? new Map(options.resources.map((resource) => [resource.uri, resource])) : void 0;
|
|
110
|
+
this.prompts = options.prompts ? new Map(options.prompts.map((prompt) => [prompt.name, prompt])) : void 0;
|
|
40
111
|
this.options = options;
|
|
41
112
|
}
|
|
42
113
|
/**
|
|
43
114
|
* Dispatch one inbound JSON-RPC request to its handler and produce the
|
|
44
115
|
* response. A handler that throws is mapped to a JSON-RPC error response
|
|
45
116
|
* — the serve loop never crashes on a bad request.
|
|
117
|
+
*
|
|
118
|
+
* @param request - The JSON-RPC request.
|
|
119
|
+
* @param context - Per-call context: the inbound HTTP request (headers
|
|
120
|
+
* reach tools as `ctx.request`) and an abort signal.
|
|
46
121
|
*/
|
|
47
|
-
async handle(request) {
|
|
122
|
+
async handle(request, context = {}) {
|
|
48
123
|
try {
|
|
49
|
-
|
|
50
|
-
case "initialize": return this.ok(request.id, this.initializeResult());
|
|
51
|
-
case "tools/list": return this.ok(request.id, { tools: this.listTools() });
|
|
52
|
-
case "tools/call": return this.ok(request.id, await this.callTool(request.params));
|
|
53
|
-
default: return this.error(request.id, JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
|
|
54
|
-
}
|
|
124
|
+
return this.ok(request.id, await this.dispatch(request, context));
|
|
55
125
|
} catch (cause) {
|
|
126
|
+
if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
|
|
56
127
|
const message = cause instanceof Error ? cause.message : String(cause);
|
|
57
128
|
return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);
|
|
58
129
|
}
|
|
59
130
|
}
|
|
60
|
-
/**
|
|
61
|
-
|
|
131
|
+
/** Route a method to its result builder. */
|
|
132
|
+
async dispatch(request, context) {
|
|
133
|
+
switch (request.method) {
|
|
134
|
+
case "initialize": return this.initializeResult(request.params);
|
|
135
|
+
case "ping": return {};
|
|
136
|
+
case "tools/list": return { tools: this.listTools() };
|
|
137
|
+
case "tools/call": return this.callTool(request.params, context);
|
|
138
|
+
default: break;
|
|
139
|
+
}
|
|
140
|
+
if (this.resources) switch (request.method) {
|
|
141
|
+
case "resources/list": return { resources: this.listResources(this.resources) };
|
|
142
|
+
case "resources/templates/list": return { resourceTemplates: [] };
|
|
143
|
+
case "resources/read": return this.readResource(this.resources, request.params, context);
|
|
144
|
+
default: break;
|
|
145
|
+
}
|
|
146
|
+
if (this.prompts) switch (request.method) {
|
|
147
|
+
case "prompts/list": return { prompts: this.listPrompts(this.prompts) };
|
|
148
|
+
case "prompts/get": return this.getPrompt(this.prompts, request.params, context);
|
|
149
|
+
default: break;
|
|
150
|
+
}
|
|
151
|
+
throw new RpcFailure(JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Build the `initialize` result advertising name / version /
|
|
155
|
+
* capabilities, negotiating the protocol version: the client's requested
|
|
156
|
+
* version when supported, otherwise the newest this server speaks.
|
|
157
|
+
*/
|
|
158
|
+
initializeResult(params) {
|
|
159
|
+
const requested = readRecord(params).protocolVersion;
|
|
160
|
+
const protocolVersion = typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
|
|
161
|
+
const capabilities = { tools: {} };
|
|
162
|
+
if (this.resources) capabilities.resources = {};
|
|
163
|
+
if (this.prompts) capabilities.prompts = {};
|
|
62
164
|
return {
|
|
63
|
-
protocolVersion
|
|
64
|
-
capabilities
|
|
165
|
+
protocolVersion,
|
|
166
|
+
capabilities,
|
|
65
167
|
serverInfo: {
|
|
66
168
|
name: this.options.name,
|
|
67
169
|
version: this.options.version ?? DEFAULT_VERSION
|
|
@@ -70,19 +172,19 @@ var McpServeHandler = class {
|
|
|
70
172
|
}
|
|
71
173
|
/**
|
|
72
174
|
* Build the `tools/list` payload: one {@link McpToolDescriptor} per
|
|
73
|
-
*
|
|
74
|
-
* configured dialect (default `draft-2020-12` — overriding
|
|
175
|
+
* exposed tool, its `inputSchema` extracted via `extractJsonSchema` at
|
|
176
|
+
* the configured dialect (default `draft-2020-12` — overriding
|
|
75
177
|
* `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
|
|
76
178
|
*/
|
|
77
179
|
listTools() {
|
|
78
180
|
const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
|
|
79
|
-
return this.tools.map((contract) => {
|
|
181
|
+
return this.tools.map(({ name, contract }) => {
|
|
80
182
|
const inputSchema = extractJsonSchema(contract.input, { target }) ?? {
|
|
81
183
|
type: "object",
|
|
82
184
|
properties: {}
|
|
83
185
|
};
|
|
84
186
|
return {
|
|
85
|
-
name
|
|
187
|
+
name,
|
|
86
188
|
description: contract.description,
|
|
87
189
|
inputSchema
|
|
88
190
|
};
|
|
@@ -93,15 +195,27 @@ var McpServeHandler = class {
|
|
|
93
195
|
* never-throwing {@link import("@warlock.js/ai").ToolInvokeResult}: `data`
|
|
94
196
|
* → a text content block, `error` → an `isError: true` result. An unknown
|
|
95
197
|
* tool name throws (mapped to a JSON-RPC error by {@link handle}).
|
|
198
|
+
*
|
|
199
|
+
* The contract receives a `ToolContext` carrying the abort signal and,
|
|
200
|
+
* over HTTP, the inbound request's headers on `ctx.request`.
|
|
96
201
|
*/
|
|
97
|
-
async callTool(params) {
|
|
202
|
+
async callTool(params, context) {
|
|
98
203
|
const { name, args } = readCallParams(params);
|
|
99
204
|
const contract = this.byName.get(name);
|
|
100
205
|
if (!contract) throw new McpTransportError(`Unknown tool "${name}".`, {
|
|
101
206
|
type: "protocol",
|
|
102
207
|
method: "tools/call"
|
|
103
208
|
});
|
|
104
|
-
const
|
|
209
|
+
const toolContext = { artifacts: {} };
|
|
210
|
+
if (context.signal) toolContext.signal = context.signal;
|
|
211
|
+
if (context.request) {
|
|
212
|
+
const { headers, remoteAddress } = context.request;
|
|
213
|
+
toolContext.request = remoteAddress === void 0 ? { headers } : {
|
|
214
|
+
headers,
|
|
215
|
+
remoteAddress
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const result = await contract.invoke(args, toolContext);
|
|
105
219
|
if (result.error) return {
|
|
106
220
|
content: [{
|
|
107
221
|
type: "text",
|
|
@@ -117,6 +231,75 @@ var McpServeHandler = class {
|
|
|
117
231
|
isError: false
|
|
118
232
|
};
|
|
119
233
|
}
|
|
234
|
+
/** Build the `resources/list` payload (metadata only, no contents). */
|
|
235
|
+
listResources(resources) {
|
|
236
|
+
return [...resources.values()].map((resource) => compact({
|
|
237
|
+
uri: resource.uri,
|
|
238
|
+
name: resource.name,
|
|
239
|
+
title: resource.title,
|
|
240
|
+
description: resource.description,
|
|
241
|
+
mimeType: resource.mimeType
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Answer `resources/read`: run the resource's `read()` and wrap the
|
|
246
|
+
* result as one text or base64 blob content entry.
|
|
247
|
+
*/
|
|
248
|
+
async readResource(resources, params, context) {
|
|
249
|
+
const uri = readRecord(params).uri;
|
|
250
|
+
if (typeof uri !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "resources/read requires a string `uri`.");
|
|
251
|
+
const resource = resources.get(uri);
|
|
252
|
+
if (!resource) throw new RpcFailure(MCP_RESOURCE_NOT_FOUND, "Resource not found", { uri });
|
|
253
|
+
const body = await resource.read(context);
|
|
254
|
+
return { contents: [compact(typeof body === "string" ? {
|
|
255
|
+
uri,
|
|
256
|
+
mimeType: resource.mimeType,
|
|
257
|
+
text: body
|
|
258
|
+
} : {
|
|
259
|
+
uri,
|
|
260
|
+
mimeType: resource.mimeType,
|
|
261
|
+
blob: Buffer.from(body).toString("base64")
|
|
262
|
+
})] };
|
|
263
|
+
}
|
|
264
|
+
/** Build the `prompts/list` payload. */
|
|
265
|
+
listPrompts(prompts) {
|
|
266
|
+
return [...prompts.values()].map((prompt) => compact({
|
|
267
|
+
name: prompt.name,
|
|
268
|
+
title: prompt.title,
|
|
269
|
+
description: prompt.description,
|
|
270
|
+
arguments: prompt.arguments
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Answer `prompts/get`: validate the arguments (strings only; required
|
|
275
|
+
* ones present) and build the messages. An unknown prompt or a missing
|
|
276
|
+
* required argument answers invalid-params, per the MCP prompts spec.
|
|
277
|
+
*/
|
|
278
|
+
async getPrompt(prompts, params, context) {
|
|
279
|
+
const record = readRecord(params);
|
|
280
|
+
const name = record.name;
|
|
281
|
+
if (typeof name !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "prompts/get requires a string `name`.");
|
|
282
|
+
const prompt = prompts.get(name);
|
|
283
|
+
if (!prompt) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Unknown prompt "${name}".`);
|
|
284
|
+
const args = {};
|
|
285
|
+
for (const [key, value] of Object.entries(readRecord(record.arguments))) {
|
|
286
|
+
if (typeof value !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt argument "${key}" must be a string.`);
|
|
287
|
+
args[key] = value;
|
|
288
|
+
}
|
|
289
|
+
for (const argument of prompt.arguments ?? []) if (argument.required && args[argument.name] === void 0) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt "${name}" requires the "${argument.name}" argument.`);
|
|
290
|
+
const built = await prompt.get(args, context);
|
|
291
|
+
const messages = typeof built === "string" ? [{
|
|
292
|
+
role: "user",
|
|
293
|
+
content: {
|
|
294
|
+
type: "text",
|
|
295
|
+
text: built
|
|
296
|
+
}
|
|
297
|
+
}] : built;
|
|
298
|
+
return compact({
|
|
299
|
+
description: prompt.description,
|
|
300
|
+
messages
|
|
301
|
+
});
|
|
302
|
+
}
|
|
120
303
|
/** Build a JSON-RPC success response. */
|
|
121
304
|
ok(id, result) {
|
|
122
305
|
return {
|
|
@@ -126,17 +309,77 @@ var McpServeHandler = class {
|
|
|
126
309
|
};
|
|
127
310
|
}
|
|
128
311
|
/** Build a JSON-RPC error response. */
|
|
129
|
-
error(id, code, message) {
|
|
312
|
+
error(id, code, message, data) {
|
|
130
313
|
return {
|
|
131
314
|
jsonrpc: JSONRPC_VERSION,
|
|
132
315
|
id,
|
|
133
|
-
error: {
|
|
316
|
+
error: data === void 0 ? {
|
|
134
317
|
code,
|
|
135
318
|
message
|
|
319
|
+
} : {
|
|
320
|
+
code,
|
|
321
|
+
message,
|
|
322
|
+
data
|
|
136
323
|
}
|
|
137
324
|
};
|
|
138
325
|
}
|
|
139
326
|
};
|
|
327
|
+
/** JSON-RPC "Invalid Request" — the line is JSON but not a request object. */
|
|
328
|
+
const JSON_RPC_INVALID_REQUEST = -32600;
|
|
329
|
+
/** JSON-RPC "Internal error" — answering the request failed unexpectedly. */
|
|
330
|
+
const JSON_RPC_INTERNAL_ERROR = -32603;
|
|
331
|
+
/**
|
|
332
|
+
* Turn one stdio line into the line to write back, or `undefined` when
|
|
333
|
+
* nothing is answered: blank or non-JSON lines and notifications (no `id`).
|
|
334
|
+
* A JSON value that is not a request object (`null`, a number, an array) is
|
|
335
|
+
* answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
|
|
336
|
+
* Never rejects: a dispatch failure is answered as an internal error.
|
|
337
|
+
*/
|
|
338
|
+
async function answerStdioLine(line, dispatch) {
|
|
339
|
+
const trimmed = line.trim();
|
|
340
|
+
if (!trimmed) return;
|
|
341
|
+
let message;
|
|
342
|
+
try {
|
|
343
|
+
message = JSON.parse(trimmed);
|
|
344
|
+
} catch {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (typeof message !== "object" || message === null || Array.isArray(message)) return `${JSON.stringify({
|
|
348
|
+
jsonrpc: JSONRPC_VERSION,
|
|
349
|
+
id: null,
|
|
350
|
+
error: {
|
|
351
|
+
code: JSON_RPC_INVALID_REQUEST,
|
|
352
|
+
message: "Invalid request: not a JSON-RPC object."
|
|
353
|
+
}
|
|
354
|
+
})}\n`;
|
|
355
|
+
const request = message;
|
|
356
|
+
const id = request.id;
|
|
357
|
+
if (id === void 0 || id === null) return;
|
|
358
|
+
try {
|
|
359
|
+
const response = await dispatch(request);
|
|
360
|
+
return `${JSON.stringify(response)}\n`;
|
|
361
|
+
} catch {
|
|
362
|
+
return `${JSON.stringify({
|
|
363
|
+
jsonrpc: JSONRPC_VERSION,
|
|
364
|
+
id,
|
|
365
|
+
error: {
|
|
366
|
+
code: JSON_RPC_INTERNAL_ERROR,
|
|
367
|
+
message: "Internal error."
|
|
368
|
+
}
|
|
369
|
+
})}\n`;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** Read an unknown value as a plain record (non-objects read as empty). */
|
|
373
|
+
function readRecord(value) {
|
|
374
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
|
|
375
|
+
return value;
|
|
376
|
+
}
|
|
377
|
+
/** Drop `undefined` members so optional wire fields are omitted, not `null`. */
|
|
378
|
+
function compact(value) {
|
|
379
|
+
const result = {};
|
|
380
|
+
for (const [key, member] of Object.entries(value)) if (member !== void 0) result[key] = member;
|
|
381
|
+
return result;
|
|
382
|
+
}
|
|
140
383
|
/**
|
|
141
384
|
* Read and validate the `tools/call` params into `{ name, args }`. Throws
|
|
142
385
|
* a typed {@link McpTransportError} when `name` is missing — mapped to a
|
|
@@ -172,9 +415,12 @@ function serializeData(data) {
|
|
|
172
415
|
* {@link serve}) so callers and tests can drive the MCP protocol without an
|
|
173
416
|
* actual transport — feed it a JSON-RPC request, get the response.
|
|
174
417
|
*
|
|
175
|
-
* @
|
|
176
|
-
*
|
|
177
|
-
*
|
|
418
|
+
* Throws {@link McpServeError} on duplicate tool names / resource URIs /
|
|
419
|
+
* prompt names, or `prefixToolNames` on an ungrouped source.
|
|
420
|
+
*
|
|
421
|
+
* @param source - The tools to expose (one tool set or `{ groups }`).
|
|
422
|
+
* @param options - Serve options (name / version / schema target / resources / prompts).
|
|
423
|
+
* @returns An object whose `handle(request, context?)` maps a request to a response.
|
|
178
424
|
*/
|
|
179
425
|
function createServeHandler(source, options) {
|
|
180
426
|
return new McpServeHandler(source, options);
|
|
@@ -182,79 +428,66 @@ function createServeHandler(source, options) {
|
|
|
182
428
|
/**
|
|
183
429
|
* The internal {@link McpServer} — owns a {@link McpServeHandler} and a
|
|
184
430
|
* transport pump. For `stdio` it reads newline-delimited JSON-RPC requests
|
|
185
|
-
* from `process.stdin` and writes responses to `process.stdout`;
|
|
186
|
-
* `http`
|
|
187
|
-
* (a serve-over-HTTP needs a server the caller owns).
|
|
431
|
+
* from `process.stdin` and writes responses to `process.stdout`; for
|
|
432
|
+
* `http` it binds a Streamable HTTP endpoint (see `./serve-http`).
|
|
188
433
|
*
|
|
189
434
|
* Constructed via {@link serve}; the class itself is internal.
|
|
190
435
|
*/
|
|
191
436
|
var McpServerImpl = class {
|
|
192
437
|
constructor(source, options) {
|
|
193
|
-
this.running = false;
|
|
194
438
|
this.handler = new McpServeHandler(source, options);
|
|
195
439
|
this.options = options;
|
|
196
440
|
}
|
|
197
|
-
|
|
198
|
-
if (this.running)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
441
|
+
start() {
|
|
442
|
+
if (!this.running) {
|
|
443
|
+
const transport = this.options.transport ?? { type: "stdio" };
|
|
444
|
+
const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
|
|
445
|
+
this.running = starting.catch((error) => {
|
|
446
|
+
this.running = void 0;
|
|
447
|
+
throw error;
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return this.running;
|
|
451
|
+
}
|
|
452
|
+
/** Bind the HTTP endpoint; its `close()` also makes this server startable again. */
|
|
453
|
+
async startHttp(transport) {
|
|
454
|
+
const http = await startHttpServer(transport, (request, context) => this.handler.handle(request, context));
|
|
455
|
+
return {
|
|
456
|
+
url: http.url,
|
|
457
|
+
close: async () => {
|
|
458
|
+
await http.close();
|
|
459
|
+
this.running = void 0;
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
/** Wire the stdin line reader and answer each request on stdout. */
|
|
464
|
+
startStdio() {
|
|
465
|
+
const reader = createInterface({ input: process.stdin });
|
|
466
|
+
reader.on("line", (line) => {
|
|
467
|
+
this.onLine(line).catch(() => {});
|
|
204
468
|
});
|
|
469
|
+
return { close: async () => {
|
|
470
|
+
reader.close();
|
|
471
|
+
this.running = void 0;
|
|
472
|
+
} };
|
|
205
473
|
}
|
|
206
|
-
/**
|
|
207
|
-
* Parse one stdin line as a JSON-RPC request, dispatch it through the
|
|
208
|
-
* handler, and write the response as a single line to stdout. Non-JSON
|
|
209
|
-
* lines and notifications (no `id`) are ignored.
|
|
210
|
-
*/
|
|
474
|
+
/** Answer one stdin line on stdout (see {@link answerStdioLine}). */
|
|
211
475
|
async onLine(line) {
|
|
212
|
-
const
|
|
213
|
-
if (
|
|
214
|
-
|
|
476
|
+
const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
|
|
477
|
+
if (reply !== void 0) process.stdout.write(reply);
|
|
478
|
+
}
|
|
479
|
+
async stop() {
|
|
480
|
+
const running = this.running;
|
|
481
|
+
if (!running) return;
|
|
482
|
+
let handle;
|
|
215
483
|
try {
|
|
216
|
-
|
|
484
|
+
handle = await running;
|
|
217
485
|
} catch {
|
|
218
486
|
return;
|
|
219
487
|
}
|
|
220
|
-
|
|
221
|
-
const response = await this.handler.handle(request);
|
|
222
|
-
process.stdout.write(`${JSON.stringify(response)}\n`);
|
|
223
|
-
}
|
|
224
|
-
async stop() {
|
|
225
|
-
this.running = false;
|
|
226
|
-
this.reader?.close();
|
|
227
|
-
this.reader = void 0;
|
|
488
|
+
await handle.close();
|
|
228
489
|
}
|
|
229
490
|
};
|
|
230
|
-
/**
|
|
231
|
-
* Expose a built agent / supervisor / orchestrator (or a raw
|
|
232
|
-
* `ToolContract[]`) AS an MCP server (Direction B: local primitive → MCP
|
|
233
|
-
* server other clients consume).
|
|
234
|
-
*
|
|
235
|
-
* Enumerates `source.tools()` (or the literal array) once at construction.
|
|
236
|
-
* `tools/list` answers with each tool's `inputSchema` extracted via
|
|
237
|
-
* `extractJsonSchema` at the configured `schemaTarget` (default
|
|
238
|
-
* `draft-2020-12`). `tools/call` routes to the named contract's
|
|
239
|
-
* `invoke()` and maps the never-throwing result — `data` becomes a text
|
|
240
|
-
* content block, `error` becomes an `isError: true` result — so a failing
|
|
241
|
-
* tool surfaces as a normal MCP tool error rather than crashing the server.
|
|
242
|
-
*
|
|
243
|
-
* The default transport is `stdio`, pumped over `process.stdin` /
|
|
244
|
-
* `process.stdout`. Serving over HTTP is left to a host-owned server;
|
|
245
|
-
* `start()` rejects an `http` transport (the protocol core is available
|
|
246
|
-
* via {@link createServeHandler} for a caller's own HTTP wiring).
|
|
247
|
-
*
|
|
248
|
-
* @param source - The tools to expose.
|
|
249
|
-
* @param options - Server name, version, transport, and schema dialect.
|
|
250
|
-
* @returns An {@link McpServer} with `start()` / `stop()`.
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* serve(
|
|
254
|
-
* { tools: () => ws.allTools() },
|
|
255
|
-
* { name: "warlock-workspace", transport: { type: "stdio" } },
|
|
256
|
-
* ).start();
|
|
257
|
-
*/
|
|
258
491
|
function serve(source, options) {
|
|
259
492
|
return new McpServerImpl(source, options);
|
|
260
493
|
}
|
package/esm/mcp/serve.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { extractJsonSchema, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpServeOptions,\n McpServeSource,\n McpServer,\n McpToolCallResult,\n McpToolDescriptor,\n} from \"../contracts\";\nimport { McpTransportError } from \"../errors\";\n\n/** JSON-RPC version literal every outbound message carries. */\nconst JSONRPC_VERSION = \"2.0\";\n\n/** Default JSON-Schema dialect emitted for each tool's `inputSchema`. */\nconst DEFAULT_SCHEMA_TARGET = \"draft-2020-12\";\n\n/** Default advertised server version when the caller omits one. */\nconst DEFAULT_VERSION = \"4.4.0\";\n\n/** The MCP protocol version this server advertises in `initialize`. */\nconst PROTOCOL_VERSION = \"2025-06-18\";\n\n/** JSON-RPC standard error codes we emit. */\nconst JSON_RPC_METHOD_NOT_FOUND = -32601;\nconst JSON_RPC_INVALID_PARAMS = -32602;\n\n/**\n * Resolve the {@link McpServeSource} (either an object exposing `tools()`\n * or a literal `ToolContract[]`) into a flat contract array.\n */\nfunction resolveTools(source: McpServeSource): ToolContract[] {\n if (Array.isArray(source)) {\n return source;\n }\n\n return source.tools();\n}\n\n/**\n * The pure protocol core of `serve` — maps one JSON-RPC request to its\n * response, with no I/O. Both the stdio and http serve-transports pump\n * their inbound requests through this, and specs can drive it directly.\n *\n * Handles exactly the MCP slice this package serves: `initialize`,\n * `tools/list`, and `tools/call`. Any other method answers with a\n * JSON-RPC `method not found` error.\n *\n * Constructed via {@link createServeHandler}.\n */\nclass McpServeHandler {\n /** The tools this server exposes (snapshotted at construction). */\n private readonly tools: ToolContract[];\n\n /** Fast lookup by tool name for `tools/call` dispatch. */\n private readonly byName: Map<string, ToolContract>;\n\n /** Construction-time serve options (name / version / schema target). */\n private readonly options: McpServeOptions;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.tools = resolveTools(source);\n this.byName = new Map(this.tools.map((contract) => [contract.name, contract]));\n this.options = options;\n }\n\n /**\n * Dispatch one inbound JSON-RPC request to its handler and produce the\n * response. A handler that throws is mapped to a JSON-RPC error response\n * — the serve loop never crashes on a bad request.\n */\n public async handle(request: JsonRpcRequest): Promise<JsonRpcResponse> {\n try {\n switch (request.method) {\n case \"initialize\":\n return this.ok(request.id, this.initializeResult());\n case \"tools/list\":\n return this.ok(request.id, { tools: this.listTools() });\n case \"tools/call\":\n return this.ok(request.id, await this.callTool(request.params));\n default:\n return this.error(\n request.id,\n JSON_RPC_METHOD_NOT_FOUND,\n `Method \"${request.method}\" is not supported by this MCP server.`,\n );\n }\n } catch (cause) {\n const message = cause instanceof Error ? cause.message : String(cause);\n\n return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);\n }\n }\n\n /** Build the `initialize` result advertising name / version / capabilities. */\n private initializeResult(): Record<string, unknown> {\n return {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: {\n name: this.options.name,\n version: this.options.version ?? DEFAULT_VERSION,\n },\n };\n }\n\n /**\n * Build the `tools/list` payload: one {@link McpToolDescriptor} per\n * contract, its `inputSchema` extracted via `extractJsonSchema` at the\n * configured dialect (default `draft-2020-12` — overriding\n * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).\n */\n private listTools(): McpToolDescriptor[] {\n const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;\n\n return this.tools.map((contract) => {\n const inputSchema = extractJsonSchema(contract.input, { target }) ?? {\n type: \"object\",\n properties: {},\n };\n\n return {\n name: contract.name,\n description: contract.description,\n inputSchema,\n };\n });\n }\n\n /**\n * Route a `tools/call` to the named contract's `invoke()` and map the\n * never-throwing {@link import(\"@warlock.js/ai\").ToolInvokeResult}: `data`\n * → a text content block, `error` → an `isError: true` result. An unknown\n * tool name throws (mapped to a JSON-RPC error by {@link handle}).\n */\n private async callTool(params: unknown): Promise<McpToolCallResult> {\n const { name, args } = readCallParams(params);\n const contract = this.byName.get(name);\n\n if (!contract) {\n throw new McpTransportError(`Unknown tool \"${name}\".`, {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const result = await contract.invoke(args);\n\n if (result.error) {\n return {\n content: [{ type: \"text\", text: result.error.message }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: serializeData(result.data) }],\n isError: false,\n };\n }\n\n /** Build a JSON-RPC success response. */\n private ok(id: JsonRpcRequest[\"id\"], result: unknown): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, result };\n }\n\n /** Build a JSON-RPC error response. */\n private error(id: JsonRpcRequest[\"id\"], code: number, message: string): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, error: { code, message } };\n }\n}\n\n/**\n * Read and validate the `tools/call` params into `{ name, args }`. Throws\n * a typed {@link McpTransportError} when `name` is missing — mapped to a\n * JSON-RPC `invalid params` error by the handler.\n */\nfunction readCallParams(params: unknown): { name: string; args: unknown } {\n if (typeof params !== \"object\" || params === null) {\n throw new McpTransportError(\"tools/call params must be an object.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const record = params as { name?: unknown; arguments?: unknown };\n\n if (typeof record.name !== \"string\") {\n throw new McpTransportError(\"tools/call requires a string `name`.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n return { name: record.name, args: record.arguments ?? {} };\n}\n\n/**\n * Serialize a tool's `data` for an MCP text content block — a string is\n * passed verbatim, everything else is JSON-stringified so structured\n * output crosses the wire as text the consuming client can re-parse.\n */\nfunction serializeData(data: unknown): string {\n if (typeof data === \"string\") {\n return data;\n }\n\n if (data === undefined) {\n return \"\";\n }\n\n return JSON.stringify(data);\n}\n\n/**\n * Build the pure protocol handler for a serve source. Exposed (alongside\n * {@link serve}) so callers and tests can drive the MCP protocol without an\n * actual transport — feed it a JSON-RPC request, get the response.\n *\n * @param source - The tools to expose (an object with `tools()` or a literal array).\n * @param options - Serve options (name / version / schema target).\n * @returns An object whose `handle(request)` maps a request to a response.\n */\nexport function createServeHandler(\n source: McpServeSource,\n options: McpServeOptions,\n): { handle(request: JsonRpcRequest): Promise<JsonRpcResponse> } {\n return new McpServeHandler(source, options);\n}\n\n/**\n * The internal {@link McpServer} — owns a {@link McpServeHandler} and a\n * transport pump. For `stdio` it reads newline-delimited JSON-RPC requests\n * from `process.stdin` and writes responses to `process.stdout`; the\n * `http` transport is accepted but listening is deferred to the host\n * (a serve-over-HTTP needs a server the caller owns).\n *\n * Constructed via {@link serve}; the class itself is internal.\n */\nclass McpServerImpl implements McpServer {\n /** The pure protocol handler. */\n private readonly handler: McpServeHandler;\n\n /** Serve options (transport selection lives here). */\n private readonly options: McpServeOptions;\n\n /** The stdin line reader while serving over stdio. */\n private reader: Interface | undefined;\n\n /** Flipped while the server is actively reading the transport. */\n private running = false;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.handler = new McpServeHandler(source, options);\n this.options = options;\n }\n\n public async start(): Promise<void> {\n if (this.running) {\n return;\n }\n\n const transport = this.options.transport ?? { type: \"stdio\" };\n\n if (transport.type !== \"stdio\") {\n throw new McpTransportError(\n \"serve() over http requires a host-provided server; only stdio is auto-pumped.\",\n { type: \"connect\" },\n );\n }\n\n this.running = true;\n this.reader = createInterface({ input: process.stdin });\n\n this.reader.on(\"line\", (line) => {\n void this.onLine(line);\n });\n }\n\n /**\n * Parse one stdin line as a JSON-RPC request, dispatch it through the\n * handler, and write the response as a single line to stdout. Non-JSON\n * lines and notifications (no `id`) are ignored.\n */\n private async onLine(line: string): Promise<void> {\n const trimmed = line.trim();\n\n if (!trimmed) {\n return;\n }\n\n let request: JsonRpcRequest;\n\n try {\n request = JSON.parse(trimmed) as JsonRpcRequest;\n } catch {\n return;\n }\n\n if (request.id === undefined || request.id === null) {\n // A notification (e.g. notifications/initialized) — nothing to answer.\n return;\n }\n\n const response = await this.handler.handle(request);\n process.stdout.write(`${JSON.stringify(response)}\\n`);\n }\n\n public async stop(): Promise<void> {\n this.running = false;\n this.reader?.close();\n this.reader = undefined;\n }\n}\n\n/**\n * Expose a built agent / supervisor / orchestrator (or a raw\n * `ToolContract[]`) AS an MCP server (Direction B: local primitive → MCP\n * server other clients consume).\n *\n * Enumerates `source.tools()` (or the literal array) once at construction.\n * `tools/list` answers with each tool's `inputSchema` extracted via\n * `extractJsonSchema` at the configured `schemaTarget` (default\n * `draft-2020-12`). `tools/call` routes to the named contract's\n * `invoke()` and maps the never-throwing result — `data` becomes a text\n * content block, `error` becomes an `isError: true` result — so a failing\n * tool surfaces as a normal MCP tool error rather than crashing the server.\n *\n * The default transport is `stdio`, pumped over `process.stdin` /\n * `process.stdout`. Serving over HTTP is left to a host-owned server;\n * `start()` rejects an `http` transport (the protocol core is available\n * via {@link createServeHandler} for a caller's own HTTP wiring).\n *\n * @param source - The tools to expose.\n * @param options - Server name, version, transport, and schema dialect.\n * @returns An {@link McpServer} with `start()` / `stop()`.\n *\n * @example\n * serve(\n * { tools: () => ws.allTools() },\n * { name: \"warlock-workspace\", transport: { type: \"stdio\" } },\n * ).start();\n */\nexport function serve(source: McpServeSource, options: McpServeOptions): McpServer {\n return new McpServerImpl(source, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"serve.mjs","names":[],"sources":["../../../../../../../../packages/ai-tools/src/mcp/serve.ts"],"sourcesContent":["import { createInterface, type Interface } from \"node:readline\";\nimport { extractJsonSchema, type ToolContext, type ToolContract } from \"@warlock.js/ai\";\nimport type {\n JsonRpcRequest,\n JsonRpcResponse,\n McpGroupedToolSource,\n McpHttpServeHandle,\n McpHttpServeTransport,\n McpPromptMessage,\n McpServeCallContext,\n McpServeHandle,\n McpServeOptions,\n McpServePrompt,\n McpServeResource,\n McpServeSource,\n McpServer,\n McpToolCallResult,\n McpToolDescriptor,\n McpToolSource,\n} from \"../contracts\";\nimport { McpServeError, McpTransportError } from \"../errors\";\nimport { LATEST_PROTOCOL_VERSION, SUPPORTED_PROTOCOL_VERSIONS } from \"./protocol\";\nimport { startHttpServer } from \"./serve-http\";\n\n/** JSON-RPC version literal every outbound message carries. */\nconst JSONRPC_VERSION = \"2.0\";\n\n/** Default JSON-Schema dialect emitted for each tool's `inputSchema`. */\nconst DEFAULT_SCHEMA_TARGET = \"draft-2020-12\";\n\n/** Default advertised server version when the caller omits one. */\nconst DEFAULT_VERSION = \"4.4.0\";\n\n/** JSON-RPC standard error codes we emit. */\nconst JSON_RPC_METHOD_NOT_FOUND = -32601;\nconst JSON_RPC_INVALID_PARAMS = -32602;\n\n/** MCP's \"resource not found\" error code (server/resources, error handling). */\nconst MCP_RESOURCE_NOT_FOUND = -32002;\n\n/** One tool as exposed on the wire: its public name plus the contract behind it. */\ntype ExposedTool = {\n /** The name clients see in `tools/list` and send in `tools/call`. */\n name: string;\n /** The contract that runs. */\n contract: ToolContract;\n};\n\n/**\n * A JSON-RPC failure raised inside a method handler, carrying the exact\n * code to answer with (unknown resource, missing prompt argument, …).\n */\nclass RpcFailure extends Error {\n public readonly code: number;\n\n public readonly data: unknown;\n\n public constructor(code: number, message: string, data?: unknown) {\n super(message);\n this.code = code;\n this.data = data;\n }\n}\n\n/** Enumerate one tool set into a flat contract array. */\nfunction toolsOf(source: McpToolSource): ToolContract[] {\n return Array.isArray(source) ? source : source.tools();\n}\n\n/** Narrow a serve source to its grouped shape. */\nfunction isGroupedSource(source: McpServeSource): source is McpGroupedToolSource {\n return !Array.isArray(source) && \"groups\" in source;\n}\n\n/**\n * Resolve the serve source into the exposed tool list, applying\n * `prefixToolNames` and refusing duplicate names. Throws\n * {@link McpServeError} — `\"config\"` for `prefixToolNames` on an ungrouped\n * source, `\"collision\"` when two tools would share a name.\n */\nfunction resolveTools(source: McpServeSource, options: McpServeOptions): ExposedTool[] {\n const prefix = options.prefixToolNames ?? false;\n const exposed: Array<ExposedTool & { origin: string }> = [];\n\n if (isGroupedSource(source)) {\n for (const [group, groupSource] of Object.entries(source.groups)) {\n for (const contract of toolsOf(groupSource)) {\n let name = contract.name;\n\n if (prefix === true) {\n name = `${group}_${contract.name}`;\n } else if (typeof prefix === \"function\") {\n name = prefix(group, contract.name);\n }\n\n exposed.push({ name, contract, origin: `group \"${group}\"` });\n }\n }\n } else {\n if (prefix !== false) {\n throw new McpServeError(\n \"prefixToolNames needs a grouped source: serve({ groups: { name: tools } }, …).\",\n { type: \"config\" },\n );\n }\n\n for (const contract of toolsOf(source)) {\n exposed.push({ name: contract.name, contract, origin: \"the source\" });\n }\n }\n\n const seen = new Map<string, string>();\n\n for (const tool of exposed) {\n const previous = seen.get(tool.name);\n\n if (previous !== undefined) {\n throw new McpServeError(\n `MCP tool name \"${tool.name}\" is exposed twice (from ${previous} and ${tool.origin}). ` +\n \"Rename one tool, or serve a grouped source with prefixToolNames.\",\n { type: \"collision\", context: { name: tool.name } },\n );\n }\n\n seen.set(tool.name, tool.origin);\n }\n\n return exposed.map(({ name, contract }) => ({ name, contract }));\n}\n\n/** Throw a collision error when two entries share the same key. */\nfunction assertUnique<T>(\n entries: readonly T[] | undefined,\n key: (entry: T) => string,\n label: string,\n): void {\n const seen = new Set<string>();\n\n for (const entry of entries ?? []) {\n const value = key(entry);\n\n if (seen.has(value)) {\n throw new McpServeError(`MCP ${label} \"${value}\" is declared twice.`, {\n type: \"collision\",\n context: { [label]: value },\n });\n }\n\n seen.add(value);\n }\n}\n\n/**\n * The pure protocol core of `serve` — maps one JSON-RPC request to its\n * response, with no I/O. Both the stdio and HTTP serve-transports pump\n * their inbound requests through this, and specs can drive it directly.\n *\n * Handles `initialize`, `ping`, `tools/list`, `tools/call`, and — when\n * configured — `resources/list`, `resources/templates/list`,\n * `resources/read`, `prompts/list` and `prompts/get`. Any other method\n * answers with a JSON-RPC `method not found` error.\n *\n * Constructed via {@link createServeHandler}.\n */\nclass McpServeHandler {\n /** The tools this server exposes (snapshotted at construction). */\n private readonly tools: ExposedTool[];\n\n /** Fast lookup by exposed tool name for `tools/call` dispatch. */\n private readonly byName: Map<string, ToolContract>;\n\n /** Resources by URI, or `undefined` when the capability is off. */\n private readonly resources: Map<string, McpServeResource> | undefined;\n\n /** Prompts by name, or `undefined` when the capability is off. */\n private readonly prompts: Map<string, McpServePrompt> | undefined;\n\n /** Construction-time serve options (name / version / schema target). */\n private readonly options: McpServeOptions;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.tools = resolveTools(source, options);\n this.byName = new Map(this.tools.map((tool) => [tool.name, tool.contract]));\n\n assertUnique(options.resources, (resource) => resource.uri, \"resource\");\n assertUnique(options.prompts, (prompt) => prompt.name, \"prompt\");\n\n this.resources = options.resources\n ? new Map(options.resources.map((resource) => [resource.uri, resource]))\n : undefined;\n this.prompts = options.prompts\n ? new Map(options.prompts.map((prompt) => [prompt.name, prompt]))\n : undefined;\n this.options = options;\n }\n\n /**\n * Dispatch one inbound JSON-RPC request to its handler and produce the\n * response. A handler that throws is mapped to a JSON-RPC error response\n * — the serve loop never crashes on a bad request.\n *\n * @param request - The JSON-RPC request.\n * @param context - Per-call context: the inbound HTTP request (headers\n * reach tools as `ctx.request`) and an abort signal.\n */\n public async handle(\n request: JsonRpcRequest,\n context: McpServeCallContext = {},\n ): Promise<JsonRpcResponse> {\n try {\n return this.ok(request.id, await this.dispatch(request, context));\n } catch (cause) {\n if (cause instanceof RpcFailure) {\n return this.error(request.id, cause.code, cause.message, cause.data);\n }\n\n const message = cause instanceof Error ? cause.message : String(cause);\n\n return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);\n }\n }\n\n /** Route a method to its result builder. */\n private async dispatch(request: JsonRpcRequest, context: McpServeCallContext): Promise<unknown> {\n switch (request.method) {\n case \"initialize\":\n return this.initializeResult(request.params);\n case \"ping\":\n return {};\n case \"tools/list\":\n return { tools: this.listTools() };\n case \"tools/call\":\n return this.callTool(request.params, context);\n default:\n break;\n }\n\n if (this.resources) {\n switch (request.method) {\n case \"resources/list\":\n return { resources: this.listResources(this.resources) };\n case \"resources/templates/list\":\n return { resourceTemplates: [] };\n case \"resources/read\":\n return this.readResource(this.resources, request.params, context);\n default:\n break;\n }\n }\n\n if (this.prompts) {\n switch (request.method) {\n case \"prompts/list\":\n return { prompts: this.listPrompts(this.prompts) };\n case \"prompts/get\":\n return this.getPrompt(this.prompts, request.params, context);\n default:\n break;\n }\n }\n\n throw new RpcFailure(\n JSON_RPC_METHOD_NOT_FOUND,\n `Method \"${request.method}\" is not supported by this MCP server.`,\n );\n }\n\n /**\n * Build the `initialize` result advertising name / version /\n * capabilities, negotiating the protocol version: the client's requested\n * version when supported, otherwise the newest this server speaks.\n */\n private initializeResult(params: unknown): Record<string, unknown> {\n const requested = readRecord(params).protocolVersion;\n const protocolVersion =\n typeof requested === \"string\" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested)\n ? requested\n : LATEST_PROTOCOL_VERSION;\n\n const capabilities: Record<string, unknown> = { tools: {} };\n\n if (this.resources) {\n capabilities.resources = {};\n }\n\n if (this.prompts) {\n capabilities.prompts = {};\n }\n\n return {\n protocolVersion,\n capabilities,\n serverInfo: {\n name: this.options.name,\n version: this.options.version ?? DEFAULT_VERSION,\n },\n };\n }\n\n /**\n * Build the `tools/list` payload: one {@link McpToolDescriptor} per\n * exposed tool, its `inputSchema` extracted via `extractJsonSchema` at\n * the configured dialect (default `draft-2020-12` — overriding\n * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).\n */\n private listTools(): McpToolDescriptor[] {\n const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;\n\n return this.tools.map(({ name, contract }) => {\n const inputSchema = extractJsonSchema(contract.input, { target }) ?? {\n type: \"object\",\n properties: {},\n };\n\n return {\n name,\n description: contract.description,\n inputSchema,\n };\n });\n }\n\n /**\n * Route a `tools/call` to the named contract's `invoke()` and map the\n * never-throwing {@link import(\"@warlock.js/ai\").ToolInvokeResult}: `data`\n * → a text content block, `error` → an `isError: true` result. An unknown\n * tool name throws (mapped to a JSON-RPC error by {@link handle}).\n *\n * The contract receives a `ToolContext` carrying the abort signal and,\n * over HTTP, the inbound request's headers on `ctx.request`.\n */\n private async callTool(\n params: unknown,\n context: McpServeCallContext,\n ): Promise<McpToolCallResult> {\n const { name, args } = readCallParams(params);\n const contract = this.byName.get(name);\n\n if (!contract) {\n throw new McpTransportError(`Unknown tool \"${name}\".`, {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const toolContext: ToolContext = { artifacts: {} };\n\n if (context.signal) {\n toolContext.signal = context.signal;\n }\n\n if (context.request) {\n const { headers, remoteAddress } = context.request;\n\n toolContext.request = remoteAddress === undefined ? { headers } : { headers, remoteAddress };\n }\n\n const result = await contract.invoke(args, toolContext);\n\n if (result.error) {\n return {\n content: [{ type: \"text\", text: result.error.message }],\n isError: true,\n };\n }\n\n return {\n content: [{ type: \"text\", text: serializeData(result.data) }],\n isError: false,\n };\n }\n\n /** Build the `resources/list` payload (metadata only, no contents). */\n private listResources(resources: Map<string, McpServeResource>): Record<string, unknown>[] {\n return [...resources.values()].map((resource) =>\n compact({\n uri: resource.uri,\n name: resource.name,\n title: resource.title,\n description: resource.description,\n mimeType: resource.mimeType,\n }),\n );\n }\n\n /**\n * Answer `resources/read`: run the resource's `read()` and wrap the\n * result as one text or base64 blob content entry.\n */\n private async readResource(\n resources: Map<string, McpServeResource>,\n params: unknown,\n context: McpServeCallContext,\n ): Promise<Record<string, unknown>> {\n const uri = readRecord(params).uri;\n\n if (typeof uri !== \"string\") {\n throw new RpcFailure(JSON_RPC_INVALID_PARAMS, \"resources/read requires a string `uri`.\");\n }\n\n const resource = resources.get(uri);\n\n if (!resource) {\n throw new RpcFailure(MCP_RESOURCE_NOT_FOUND, \"Resource not found\", { uri });\n }\n\n const body = await resource.read(context);\n const entry =\n typeof body === \"string\"\n ? { uri, mimeType: resource.mimeType, text: body }\n : { uri, mimeType: resource.mimeType, blob: Buffer.from(body).toString(\"base64\") };\n\n return { contents: [compact(entry)] };\n }\n\n /** Build the `prompts/list` payload. */\n private listPrompts(prompts: Map<string, McpServePrompt>): Record<string, unknown>[] {\n return [...prompts.values()].map((prompt) =>\n compact({\n name: prompt.name,\n title: prompt.title,\n description: prompt.description,\n arguments: prompt.arguments,\n }),\n );\n }\n\n /**\n * Answer `prompts/get`: validate the arguments (strings only; required\n * ones present) and build the messages. An unknown prompt or a missing\n * required argument answers invalid-params, per the MCP prompts spec.\n */\n private async getPrompt(\n prompts: Map<string, McpServePrompt>,\n params: unknown,\n context: McpServeCallContext,\n ): Promise<Record<string, unknown>> {\n const record = readRecord(params);\n const name = record.name;\n\n if (typeof name !== \"string\") {\n throw new RpcFailure(JSON_RPC_INVALID_PARAMS, \"prompts/get requires a string `name`.\");\n }\n\n const prompt = prompts.get(name);\n\n if (!prompt) {\n throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Unknown prompt \"${name}\".`);\n }\n\n const args: Record<string, string> = {};\n\n for (const [key, value] of Object.entries(readRecord(record.arguments))) {\n if (typeof value !== \"string\") {\n throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt argument \"${key}\" must be a string.`);\n }\n\n args[key] = value;\n }\n\n for (const argument of prompt.arguments ?? []) {\n if (argument.required && args[argument.name] === undefined) {\n throw new RpcFailure(\n JSON_RPC_INVALID_PARAMS,\n `Prompt \"${name}\" requires the \"${argument.name}\" argument.`,\n );\n }\n }\n\n const built = await prompt.get(args, context);\n const messages: McpPromptMessage[] =\n typeof built === \"string\"\n ? [{ role: \"user\", content: { type: \"text\", text: built } }]\n : built;\n\n return compact({ description: prompt.description, messages });\n }\n\n /** Build a JSON-RPC success response. */\n private ok(id: JsonRpcRequest[\"id\"], result: unknown): JsonRpcResponse {\n return { jsonrpc: JSONRPC_VERSION, id, result };\n }\n\n /** Build a JSON-RPC error response. */\n private error(\n id: JsonRpcRequest[\"id\"],\n code: number,\n message: string,\n data?: unknown,\n ): JsonRpcResponse {\n const error = data === undefined ? { code, message } : { code, message, data };\n\n return { jsonrpc: JSONRPC_VERSION, id, error };\n }\n}\n\n/** JSON-RPC \"Invalid Request\" — the line is JSON but not a request object. */\nconst JSON_RPC_INVALID_REQUEST = -32600;\n\n/** JSON-RPC \"Internal error\" — answering the request failed unexpectedly. */\nconst JSON_RPC_INTERNAL_ERROR = -32603;\n\n/**\n * Turn one stdio line into the line to write back, or `undefined` when\n * nothing is answered: blank or non-JSON lines and notifications (no `id`).\n * A JSON value that is not a request object (`null`, a number, an array) is\n * answered with a JSON-RPC \"Invalid Request\" error whose `id` is `null`.\n * Never rejects: a dispatch failure is answered as an internal error.\n */\nexport async function answerStdioLine(\n line: string,\n dispatch: (request: JsonRpcRequest) => Promise<JsonRpcResponse>,\n): Promise<string | undefined> {\n const trimmed = line.trim();\n\n if (!trimmed) {\n return undefined;\n }\n\n let message: unknown;\n\n try {\n message = JSON.parse(trimmed);\n } catch {\n return undefined;\n }\n\n if (typeof message !== \"object\" || message === null || Array.isArray(message)) {\n const invalid = {\n jsonrpc: JSONRPC_VERSION,\n id: null,\n error: { code: JSON_RPC_INVALID_REQUEST, message: \"Invalid request: not a JSON-RPC object.\" },\n };\n\n return `${JSON.stringify(invalid)}\\n`;\n }\n\n const request = message as JsonRpcRequest;\n const id: unknown = request.id;\n\n if (id === undefined || id === null) {\n // A notification (e.g. notifications/initialized) — nothing to answer.\n return undefined;\n }\n\n try {\n const response = await dispatch(request);\n\n return `${JSON.stringify(response)}\\n`;\n } catch {\n const failed = {\n jsonrpc: JSONRPC_VERSION,\n id,\n error: { code: JSON_RPC_INTERNAL_ERROR, message: \"Internal error.\" },\n };\n\n return `${JSON.stringify(failed)}\\n`;\n }\n}\n\n/** Read an unknown value as a plain record (non-objects read as empty). */\nfunction readRecord(value: unknown): Record<string, unknown> {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return {};\n }\n\n return value as Record<string, unknown>;\n}\n\n/** Drop `undefined` members so optional wire fields are omitted, not `null`. */\nfunction compact(value: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {};\n\n for (const [key, member] of Object.entries(value)) {\n if (member !== undefined) {\n result[key] = member;\n }\n }\n\n return result;\n}\n\n/**\n * Read and validate the `tools/call` params into `{ name, args }`. Throws\n * a typed {@link McpTransportError} when `name` is missing — mapped to a\n * JSON-RPC `invalid params` error by the handler.\n */\nfunction readCallParams(params: unknown): { name: string; args: unknown } {\n if (typeof params !== \"object\" || params === null) {\n throw new McpTransportError(\"tools/call params must be an object.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n const record = params as { name?: unknown; arguments?: unknown };\n\n if (typeof record.name !== \"string\") {\n throw new McpTransportError(\"tools/call requires a string `name`.\", {\n type: \"protocol\",\n method: \"tools/call\",\n });\n }\n\n return { name: record.name, args: record.arguments ?? {} };\n}\n\n/**\n * Serialize a tool's `data` for an MCP text content block — a string is\n * passed verbatim, everything else is JSON-stringified so structured\n * output crosses the wire as text the consuming client can re-parse.\n */\nfunction serializeData(data: unknown): string {\n if (typeof data === \"string\") {\n return data;\n }\n\n if (data === undefined) {\n return \"\";\n }\n\n return JSON.stringify(data);\n}\n\n/**\n * The pure protocol handler: one JSON-RPC request in, one response out.\n */\nexport interface McpServeRequestHandler {\n /**\n * Map one request to its response.\n *\n * @param request - The JSON-RPC request.\n * @param context - Optional per-call context (inbound HTTP request, abort signal).\n */\n handle(request: JsonRpcRequest, context?: McpServeCallContext): Promise<JsonRpcResponse>;\n}\n\n/**\n * Build the pure protocol handler for a serve source. Exposed (alongside\n * {@link serve}) so callers and tests can drive the MCP protocol without an\n * actual transport — feed it a JSON-RPC request, get the response.\n *\n * Throws {@link McpServeError} on duplicate tool names / resource URIs /\n * prompt names, or `prefixToolNames` on an ungrouped source.\n *\n * @param source - The tools to expose (one tool set or `{ groups }`).\n * @param options - Serve options (name / version / schema target / resources / prompts).\n * @returns An object whose `handle(request, context?)` maps a request to a response.\n */\nexport function createServeHandler(\n source: McpServeSource,\n options: McpServeOptions,\n): McpServeRequestHandler {\n return new McpServeHandler(source, options);\n}\n\n/**\n * The internal {@link McpServer} — owns a {@link McpServeHandler} and a\n * transport pump. For `stdio` it reads newline-delimited JSON-RPC requests\n * from `process.stdin` and writes responses to `process.stdout`; for\n * `http` it binds a Streamable HTTP endpoint (see `./serve-http`).\n *\n * Constructed via {@link serve}; the class itself is internal.\n */\nclass McpServerImpl implements McpServer {\n /** The pure protocol handler. */\n private readonly handler: McpServeHandler;\n\n /** Serve options (transport selection lives here). */\n private readonly options: McpServeOptions;\n\n /** The running handle (stdio or http), while started or starting. */\n private running: Promise<McpServeHandle> | undefined;\n\n public constructor(source: McpServeSource, options: McpServeOptions) {\n this.handler = new McpServeHandler(source, options);\n this.options = options;\n }\n\n public start(): Promise<McpServeHandle> {\n if (!this.running) {\n const transport = this.options.transport ?? { type: \"stdio\" };\n const starting =\n transport.type === \"http\" ? this.startHttp(transport) : Promise.resolve(this.startStdio());\n\n // A failed start leaves the server startable again.\n this.running = starting.catch((error: unknown) => {\n this.running = undefined;\n throw error;\n });\n }\n\n return this.running;\n }\n\n /** Bind the HTTP endpoint; its `close()` also makes this server startable again. */\n private async startHttp(transport: McpHttpServeTransport): Promise<McpHttpServeHandle> {\n const http = await startHttpServer(transport, (request, context) =>\n this.handler.handle(request, context),\n );\n\n return {\n url: http.url,\n close: async () => {\n await http.close();\n this.running = undefined;\n },\n };\n }\n\n /** Wire the stdin line reader and answer each request on stdout. */\n private startStdio(): McpServeHandle {\n const reader: Interface = createInterface({ input: process.stdin });\n\n reader.on(\"line\", (line) => {\n // A failure answering one line must never become an unhandled\n // rejection: that would take the whole server process down.\n void this.onLine(line).catch(() => {});\n });\n\n return {\n close: async () => {\n reader.close();\n this.running = undefined;\n },\n };\n }\n\n /** Answer one stdin line on stdout (see {@link answerStdioLine}). */\n private async onLine(line: string): Promise<void> {\n const reply = await answerStdioLine(line, (request) => this.handler.handle(request));\n\n if (reply !== undefined) {\n process.stdout.write(reply);\n }\n }\n\n public async stop(): Promise<void> {\n const running = this.running;\n\n if (!running) {\n return;\n }\n\n let handle: McpServeHandle;\n\n try {\n handle = await running;\n } catch {\n // A start that failed has nothing to close.\n return;\n }\n\n await handle.close();\n }\n}\n\n/**\n * Expose a built agent / supervisor / orchestrator (or a raw\n * `ToolContract[]`, or several named groups) AS an MCP server (Direction B:\n * local primitive → MCP server other clients consume).\n *\n * Enumerates the tools once, here. Every exposed name must be unique — a\n * duplicate throws {@link McpServeError} (`type: \"collision\"`) right away;\n * serve `{ groups: { … } }` with `prefixToolNames: true` to namespace\n * same-named tools from different sets. `tools/list` answers with each\n * tool's `inputSchema` extracted via `extractJsonSchema` at the configured\n * `schemaTarget` (default `draft-2020-12`). `tools/call` routes to the\n * contract's `invoke()` and maps the never-throwing result — `data`\n * becomes a text content block, `error` an `isError: true` result.\n *\n * Transports: `stdio` (default) pumps `process.stdin` / `process.stdout`;\n * `http` binds an MCP Streamable HTTP endpoint on `127.0.0.1` by default,\n * with bearer / custom auth (required off loopback), `Origin` and `Host`\n * validation against DNS rebinding, a body-size cap, and a draining\n * `close()`. `start()` resolves `{ url, close }`.\n *\n * @param source - The tools to expose.\n * @param options - Server name, version, transport, schema dialect, resources, prompts.\n * @returns An {@link McpServer} with `start()` / `stop()`.\n *\n * @example\n * const { url, close } = await serve([calc, clock], {\n * name: \"session-tools\",\n * transport: { type: \"http\", auth: { bearer: sessionToken } },\n * }).start();\n */\nexport function serve(\n source: McpServeSource,\n options: McpServeOptions & { transport: McpHttpServeTransport },\n): McpServer<McpHttpServeHandle>;\nexport function serve(source: McpServeSource, options: McpServeOptions): McpServer;\nexport function serve(source: McpServeSource, options: McpServeOptions): McpServer {\n return new McpServerImpl(source, options);\n}\n"],"mappings":"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"}
|