@warlock.js/ai-tools 5.30.0 → 5.32.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.
Files changed (86) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +25 -13
  3. package/cjs/index.cjs +968 -122
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/http.type.d.mts +4 -5
  6. package/esm/contracts/http.type.d.mts.map +1 -1
  7. package/esm/contracts/index.d.mts +3 -1
  8. package/esm/contracts/mcp-serve.type.d.mts +285 -0
  9. package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
  10. package/esm/contracts/mcp.type.d.mts +44 -77
  11. package/esm/contracts/mcp.type.d.mts.map +1 -1
  12. package/esm/contracts/utility.type.d.mts +7 -8
  13. package/esm/contracts/utility.type.d.mts.map +1 -1
  14. package/esm/contracts/web.type.d.mts +9 -10
  15. package/esm/contracts/web.type.d.mts.map +1 -1
  16. package/esm/errors.d.mts +62 -22
  17. package/esm/errors.d.mts.map +1 -1
  18. package/esm/errors.mjs +20 -1
  19. package/esm/errors.mjs.map +1 -1
  20. package/esm/http/http-request.d.mts +2 -3
  21. package/esm/http/http-request.d.mts.map +1 -1
  22. package/esm/http/http-request.mjs +7 -5
  23. package/esm/http/http-request.mjs.map +1 -1
  24. package/esm/index.d.mts +10 -4
  25. package/esm/index.mjs +4 -2
  26. package/esm/mcp/client.mjs +37 -6
  27. package/esm/mcp/client.mjs.map +1 -1
  28. package/esm/mcp/index.d.mts +17 -9
  29. package/esm/mcp/index.d.mts.map +1 -1
  30. package/esm/mcp/index.mjs +3 -1
  31. package/esm/mcp/index.mjs.map +1 -1
  32. package/esm/mcp/json-schema-to-standard.d.mts +1 -3
  33. package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
  34. package/esm/mcp/protocol.d.mts +15 -0
  35. package/esm/mcp/protocol.d.mts.map +1 -0
  36. package/esm/mcp/protocol.mjs +23 -0
  37. package/esm/mcp/protocol.mjs.map +1 -0
  38. package/esm/mcp/result.d.mts +17 -0
  39. package/esm/mcp/result.d.mts.map +1 -0
  40. package/esm/mcp/result.mjs +85 -0
  41. package/esm/mcp/result.mjs.map +1 -0
  42. package/esm/mcp/serve-http.mjs +444 -0
  43. package/esm/mcp/serve-http.mjs.map +1 -0
  44. package/esm/mcp/serve.d.mts +46 -27
  45. package/esm/mcp/serve.d.mts.map +1 -1
  46. package/esm/mcp/serve.mjs +339 -96
  47. package/esm/mcp/serve.mjs.map +1 -1
  48. package/esm/mcp/transport.d.mts +4 -5
  49. package/esm/mcp/transport.d.mts.map +1 -1
  50. package/esm/mcp/transport.mjs +12 -6
  51. package/esm/mcp/transport.mjs.map +1 -1
  52. package/esm/mcp/transport.type.d.mts +2 -2
  53. package/esm/mcp/transport.type.d.mts.map +1 -1
  54. package/esm/register.d.mts +15 -5
  55. package/esm/register.d.mts.map +1 -1
  56. package/esm/register.mjs +3 -2
  57. package/esm/register.mjs.map +1 -1
  58. package/esm/schema.mjs.map +1 -1
  59. package/esm/utility/calculator.d.mts +1 -3
  60. package/esm/utility/calculator.d.mts.map +1 -1
  61. package/esm/utility/calculator.mjs.map +1 -1
  62. package/esm/utility/date-time.d.mts +3 -5
  63. package/esm/utility/date-time.d.mts.map +1 -1
  64. package/esm/utility/date-time.mjs +2 -1
  65. package/esm/utility/date-time.mjs.map +1 -1
  66. package/esm/utility/index.d.mts +2 -1
  67. package/esm/utility/index.mjs +1 -1
  68. package/esm/utility/schema.mjs.map +1 -1
  69. package/esm/web/fetch-url.d.mts +2 -3
  70. package/esm/web/fetch-url.d.mts.map +1 -1
  71. package/esm/web/fetch-url.mjs +2 -1
  72. package/esm/web/fetch-url.mjs.map +1 -1
  73. package/esm/web/index.d.mts +2 -1
  74. package/esm/web/index.mjs +1 -1
  75. package/esm/web/schema.mjs.map +1 -1
  76. package/esm/web/web-search.d.mts +2 -3
  77. package/esm/web/web-search.d.mts.map +1 -1
  78. package/esm/web/web-search.mjs +3 -4
  79. package/esm/web/web-search.mjs.map +1 -1
  80. package/llms-full.txt +245 -56
  81. package/llms.txt +3 -3
  82. package/package.json +2 -6
  83. package/skills/connect-mcp-server/SKILL.md +28 -6
  84. package/skills/expose-as-mcp-server/SKILL.md +168 -35
  85. package/skills/index.md +5 -2
  86. package/skills/use-web-and-http-tools/SKILL.md +49 -15
package/esm/mcp/serve.mjs CHANGED
@@ -1,4 +1,7 @@
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";
4
+ import { assertMcpResult, isMcpResult, isRecord, looksLikeMcpResult, unwrapMcpResult } from "./result.mjs";
2
5
  import { extractJsonSchema } from "@warlock.js/ai";
3
6
  import { createInterface } from "node:readline";
4
7
 
@@ -9,59 +12,156 @@ const JSONRPC_VERSION = "2.0";
9
12
  const DEFAULT_SCHEMA_TARGET = "draft-2020-12";
10
13
  /** Default advertised server version when the caller omits one. */
11
14
  const DEFAULT_VERSION = "4.4.0";
12
- /** The MCP protocol version this server advertises in `initialize`. */
13
- const PROTOCOL_VERSION = "2025-06-18";
14
15
  /** JSON-RPC standard error codes we emit. */
15
16
  const JSON_RPC_METHOD_NOT_FOUND = -32601;
16
17
  const JSON_RPC_INVALID_PARAMS = -32602;
18
+ /** MCP's "resource not found" error code (server/resources, error handling). */
19
+ const MCP_RESOURCE_NOT_FOUND = -32002;
17
20
  /**
18
- * Resolve the {@link McpServeSource} (either an object exposing `tools()`
19
- * or a literal `ToolContract[]`) into a flat contract array.
21
+ * A JSON-RPC failure raised inside a method handler, carrying the exact
22
+ * code to answer with (unknown resource, missing prompt argument, …).
20
23
  */
21
- function resolveTools(source) {
22
- if (Array.isArray(source)) return source;
23
- return source.tools();
24
+ var RpcFailure = class extends Error {
25
+ constructor(code, message, data) {
26
+ super(message);
27
+ this.code = code;
28
+ this.data = data;
29
+ }
30
+ };
31
+ /** Enumerate one tool set into a flat contract array. */
32
+ function toolsOf(source) {
33
+ return Array.isArray(source) ? source : source.tools();
34
+ }
35
+ /** Narrow a serve source to its grouped shape. */
36
+ function isGroupedSource(source) {
37
+ return !Array.isArray(source) && "groups" in source;
38
+ }
39
+ /**
40
+ * Resolve the serve source into the exposed tool list, applying
41
+ * `prefixToolNames` and refusing duplicate names. Throws
42
+ * {@link McpServeError} — `"config"` for `prefixToolNames` on an ungrouped
43
+ * source, `"collision"` when two tools would share a name.
44
+ */
45
+ function resolveTools(source, options) {
46
+ const prefix = options.prefixToolNames ?? false;
47
+ const exposed = [];
48
+ if (isGroupedSource(source)) for (const [group, groupSource] of Object.entries(source.groups)) for (const contract of toolsOf(groupSource)) {
49
+ let name = contract.name;
50
+ if (prefix === true) name = `${group}_${contract.name}`;
51
+ else if (typeof prefix === "function") name = prefix(group, contract.name);
52
+ exposed.push({
53
+ name,
54
+ contract,
55
+ origin: `group "${group}"`
56
+ });
57
+ }
58
+ else {
59
+ if (prefix !== false) throw new McpServeError("prefixToolNames needs a grouped source: serve({ groups: { name: tools } }, …).", { type: "config" });
60
+ for (const contract of toolsOf(source)) exposed.push({
61
+ name: contract.name,
62
+ contract,
63
+ origin: "the source"
64
+ });
65
+ }
66
+ const seen = /* @__PURE__ */ new Map();
67
+ for (const tool of exposed) {
68
+ const previous = seen.get(tool.name);
69
+ 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.`, {
70
+ type: "collision",
71
+ context: { name: tool.name }
72
+ });
73
+ seen.set(tool.name, tool.origin);
74
+ }
75
+ return exposed.map(({ name, contract }) => ({
76
+ name,
77
+ contract
78
+ }));
79
+ }
80
+ /** Throw a collision error when two entries share the same key. */
81
+ function assertUnique(entries, key, label) {
82
+ const seen = /* @__PURE__ */ new Set();
83
+ for (const entry of entries ?? []) {
84
+ const value = key(entry);
85
+ if (seen.has(value)) throw new McpServeError(`MCP ${label} "${value}" is declared twice.`, {
86
+ type: "collision",
87
+ context: { [label]: value }
88
+ });
89
+ seen.add(value);
90
+ }
24
91
  }
25
92
  /**
26
93
  * The pure protocol core of `serve` — maps one JSON-RPC request to its
27
- * response, with no I/O. Both the stdio and http serve-transports pump
94
+ * response, with no I/O. Both the stdio and HTTP serve-transports pump
28
95
  * their inbound requests through this, and specs can drive it directly.
29
96
  *
30
- * Handles exactly the MCP slice this package serves: `initialize`,
31
- * `tools/list`, and `tools/call`. Any other method answers with a
32
- * JSON-RPC `method not found` error.
97
+ * Handles `initialize`, `ping`, `tools/list`, `tools/call`, and — when
98
+ * configured — `resources/list`, `resources/templates/list`,
99
+ * `resources/read`, `prompts/list` and `prompts/get`. Any other method
100
+ * answers with a JSON-RPC `method not found` error.
33
101
  *
34
102
  * Constructed via {@link createServeHandler}.
35
103
  */
36
104
  var McpServeHandler = class {
37
105
  constructor(source, options) {
38
- this.tools = resolveTools(source);
39
- this.byName = new Map(this.tools.map((contract) => [contract.name, contract]));
106
+ this.tools = resolveTools(source, options);
107
+ this.byName = new Map(this.tools.map((tool) => [tool.name, tool.contract]));
108
+ assertUnique(options.resources, (resource) => resource.uri, "resource");
109
+ assertUnique(options.prompts, (prompt) => prompt.name, "prompt");
110
+ this.resources = options.resources ? new Map(options.resources.map((resource) => [resource.uri, resource])) : void 0;
111
+ this.prompts = options.prompts ? new Map(options.prompts.map((prompt) => [prompt.name, prompt])) : void 0;
40
112
  this.options = options;
41
113
  }
42
114
  /**
43
115
  * Dispatch one inbound JSON-RPC request to its handler and produce the
44
116
  * response. A handler that throws is mapped to a JSON-RPC error response
45
117
  * — the serve loop never crashes on a bad request.
118
+ *
119
+ * @param request - The JSON-RPC request.
120
+ * @param context - Per-call context: the inbound HTTP request (headers
121
+ * reach tools as `ctx.request`) and an abort signal.
46
122
  */
47
- async handle(request) {
123
+ async handle(request, context = {}) {
48
124
  try {
49
- switch (request.method) {
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
- }
125
+ return this.ok(request.id, await this.dispatch(request, context));
55
126
  } catch (cause) {
127
+ if (cause instanceof RpcFailure) return this.error(request.id, cause.code, cause.message, cause.data);
56
128
  const message = cause instanceof Error ? cause.message : String(cause);
57
129
  return this.error(request.id, JSON_RPC_INVALID_PARAMS, message);
58
130
  }
59
131
  }
60
- /** Build the `initialize` result advertising name / version / capabilities. */
61
- initializeResult() {
132
+ /** Route a method to its result builder. */
133
+ async dispatch(request, context) {
134
+ switch (request.method) {
135
+ case "initialize": return this.initializeResult(request.params);
136
+ case "ping": return {};
137
+ case "tools/list": return { tools: this.listTools() };
138
+ case "tools/call": return this.callTool(request.params, context);
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
+ }
145
+ if (this.prompts) switch (request.method) {
146
+ case "prompts/list": return { prompts: this.listPrompts(this.prompts) };
147
+ case "prompts/get": return this.getPrompt(this.prompts, request.params, context);
148
+ }
149
+ throw new RpcFailure(JSON_RPC_METHOD_NOT_FOUND, `Method "${request.method}" is not supported by this MCP server.`);
150
+ }
151
+ /**
152
+ * Build the `initialize` result advertising name / version /
153
+ * capabilities, negotiating the protocol version: the client's requested
154
+ * version when supported, otherwise the newest this server speaks.
155
+ */
156
+ initializeResult(params) {
157
+ const requested = readRecord(params).protocolVersion;
158
+ const protocolVersion = typeof requested === "string" && SUPPORTED_PROTOCOL_VERSIONS.includes(requested) ? requested : LATEST_PROTOCOL_VERSION;
159
+ const capabilities = { tools: {} };
160
+ if (this.resources) capabilities.resources = {};
161
+ if (this.prompts) capabilities.prompts = {};
62
162
  return {
63
- protocolVersion: PROTOCOL_VERSION,
64
- capabilities: { tools: {} },
163
+ protocolVersion,
164
+ capabilities,
65
165
  serverInfo: {
66
166
  name: this.options.name,
67
167
  version: this.options.version ?? DEFAULT_VERSION
@@ -70,38 +170,52 @@ var McpServeHandler = class {
70
170
  }
71
171
  /**
72
172
  * Build the `tools/list` payload: one {@link McpToolDescriptor} per
73
- * contract, its `inputSchema` extracted via `extractJsonSchema` at the
74
- * configured dialect (default `draft-2020-12` — overriding
173
+ * exposed tool, its `inputSchema` extracted via `extractJsonSchema` at
174
+ * the configured dialect (default `draft-2020-12` — overriding
75
175
  * `extractJsonSchema`'s own `openai-strict` default to a neutral MCP draft).
76
176
  */
77
177
  listTools() {
78
178
  const target = this.options.schemaTarget ?? DEFAULT_SCHEMA_TARGET;
79
- return this.tools.map((contract) => {
179
+ return this.tools.map(({ name, contract }) => {
80
180
  const inputSchema = extractJsonSchema(contract.input, { target }) ?? {
81
181
  type: "object",
82
182
  properties: {}
83
183
  };
84
184
  return {
85
- name: contract.name,
185
+ name,
86
186
  description: contract.description,
87
- inputSchema
187
+ inputSchema,
188
+ ...isRecord(contract.meta?.outputSchema) ? { outputSchema: contract.meta.outputSchema } : {}
88
189
  };
89
190
  });
90
191
  }
91
192
  /**
92
193
  * Route a `tools/call` to the named contract's `invoke()` and map the
93
194
  * never-throwing {@link import("@warlock.js/ai").ToolInvokeResult}: `data`
94
- * → a text content block, `error` → an `isError: true` result. An unknown
95
- * tool name throws (mapped to a JSON-RPC error by {@link handle}).
195
+ * → a text content block (or an opted-in native result), `error` → an
196
+ * `isError: true` result. An unknown tool name throws (mapped to a
197
+ * 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 result = await contract.invoke(args);
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",
@@ -109,6 +223,16 @@ var McpServeHandler = class {
109
223
  }],
110
224
  isError: true
111
225
  };
226
+ const mode = contract.meta?.mcpResults ?? this.options.results ?? "text";
227
+ if (isMcpResult(result.data) || mode === "native" && looksLikeMcpResult(result.data)) {
228
+ const native = isMcpResult(result.data) ? unwrapMcpResult(result.data) : result.data;
229
+ try {
230
+ assertMcpResult(native);
231
+ } catch {
232
+ throw new RpcFailure(JSON_RPC_INTERNAL_ERROR, "Invalid native MCP CallToolResult.");
233
+ }
234
+ return native;
235
+ }
112
236
  return {
113
237
  content: [{
114
238
  type: "text",
@@ -117,6 +241,75 @@ var McpServeHandler = class {
117
241
  isError: false
118
242
  };
119
243
  }
244
+ /** Build the `resources/list` payload (metadata only, no contents). */
245
+ listResources(resources) {
246
+ return [...resources.values()].map((resource) => compact({
247
+ uri: resource.uri,
248
+ name: resource.name,
249
+ title: resource.title,
250
+ description: resource.description,
251
+ mimeType: resource.mimeType
252
+ }));
253
+ }
254
+ /**
255
+ * Answer `resources/read`: run the resource's `read()` and wrap the
256
+ * result as one text or base64 blob content entry.
257
+ */
258
+ async readResource(resources, params, context) {
259
+ const uri = readRecord(params).uri;
260
+ if (typeof uri !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "resources/read requires a string `uri`.");
261
+ const resource = resources.get(uri);
262
+ if (!resource) throw new RpcFailure(MCP_RESOURCE_NOT_FOUND, "Resource not found", { uri });
263
+ const body = await resource.read(context);
264
+ return { contents: [compact(typeof body === "string" ? {
265
+ uri,
266
+ mimeType: resource.mimeType,
267
+ text: body
268
+ } : {
269
+ uri,
270
+ mimeType: resource.mimeType,
271
+ blob: Buffer.from(body).toString("base64")
272
+ })] };
273
+ }
274
+ /** Build the `prompts/list` payload. */
275
+ listPrompts(prompts) {
276
+ return [...prompts.values()].map((prompt) => compact({
277
+ name: prompt.name,
278
+ title: prompt.title,
279
+ description: prompt.description,
280
+ arguments: prompt.arguments
281
+ }));
282
+ }
283
+ /**
284
+ * Answer `prompts/get`: validate the arguments (strings only; required
285
+ * ones present) and build the messages. An unknown prompt or a missing
286
+ * required argument answers invalid-params, per the MCP prompts spec.
287
+ */
288
+ async getPrompt(prompts, params, context) {
289
+ const record = readRecord(params);
290
+ const name = record.name;
291
+ if (typeof name !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, "prompts/get requires a string `name`.");
292
+ const prompt = prompts.get(name);
293
+ if (!prompt) throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Unknown prompt "${name}".`);
294
+ const args = {};
295
+ for (const [key, value] of Object.entries(readRecord(record.arguments))) {
296
+ if (typeof value !== "string") throw new RpcFailure(JSON_RPC_INVALID_PARAMS, `Prompt argument "${key}" must be a string.`);
297
+ args[key] = value;
298
+ }
299
+ 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.`);
300
+ const built = await prompt.get(args, context);
301
+ const messages = typeof built === "string" ? [{
302
+ role: "user",
303
+ content: {
304
+ type: "text",
305
+ text: built
306
+ }
307
+ }] : built;
308
+ return compact({
309
+ description: prompt.description,
310
+ messages
311
+ });
312
+ }
120
313
  /** Build a JSON-RPC success response. */
121
314
  ok(id, result) {
122
315
  return {
@@ -126,17 +319,77 @@ var McpServeHandler = class {
126
319
  };
127
320
  }
128
321
  /** Build a JSON-RPC error response. */
129
- error(id, code, message) {
322
+ error(id, code, message, data) {
130
323
  return {
131
324
  jsonrpc: JSONRPC_VERSION,
132
325
  id,
133
- error: {
326
+ error: data === void 0 ? {
134
327
  code,
135
328
  message
329
+ } : {
330
+ code,
331
+ message,
332
+ data
136
333
  }
137
334
  };
138
335
  }
139
336
  };
337
+ /** JSON-RPC "Invalid Request" — the line is JSON but not a request object. */
338
+ const JSON_RPC_INVALID_REQUEST = -32600;
339
+ /** JSON-RPC "Internal error" — answering the request failed unexpectedly. */
340
+ const JSON_RPC_INTERNAL_ERROR = -32603;
341
+ /**
342
+ * Turn one stdio line into the line to write back, or `undefined` when
343
+ * nothing is answered: blank or non-JSON lines and notifications (no `id`).
344
+ * A JSON value that is not a request object (`null`, a number, an array) is
345
+ * answered with a JSON-RPC "Invalid Request" error whose `id` is `null`.
346
+ * Never rejects: a dispatch failure is answered as an internal error.
347
+ */
348
+ async function answerStdioLine(line, dispatch) {
349
+ const trimmed = line.trim();
350
+ if (!trimmed) return;
351
+ let message;
352
+ try {
353
+ message = JSON.parse(trimmed);
354
+ } catch {
355
+ return;
356
+ }
357
+ if (typeof message !== "object" || message === null || Array.isArray(message)) return `${JSON.stringify({
358
+ jsonrpc: JSONRPC_VERSION,
359
+ id: null,
360
+ error: {
361
+ code: JSON_RPC_INVALID_REQUEST,
362
+ message: "Invalid request: not a JSON-RPC object."
363
+ }
364
+ })}\n`;
365
+ const request = message;
366
+ const id = request.id;
367
+ if (id === void 0 || id === null) return;
368
+ try {
369
+ const response = await dispatch(request);
370
+ return `${JSON.stringify(response)}\n`;
371
+ } catch {
372
+ return `${JSON.stringify({
373
+ jsonrpc: JSONRPC_VERSION,
374
+ id,
375
+ error: {
376
+ code: JSON_RPC_INTERNAL_ERROR,
377
+ message: "Internal error."
378
+ }
379
+ })}\n`;
380
+ }
381
+ }
382
+ /** Read an unknown value as a plain record (non-objects read as empty). */
383
+ function readRecord(value) {
384
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return {};
385
+ return value;
386
+ }
387
+ /** Drop `undefined` members so optional wire fields are omitted, not `null`. */
388
+ function compact(value) {
389
+ const result = {};
390
+ for (const [key, member] of Object.entries(value)) if (member !== void 0) result[key] = member;
391
+ return result;
392
+ }
140
393
  /**
141
394
  * Read and validate the `tools/call` params into `{ name, args }`. Throws
142
395
  * a typed {@link McpTransportError} when `name` is missing — mapped to a
@@ -172,9 +425,12 @@ function serializeData(data) {
172
425
  * {@link serve}) so callers and tests can drive the MCP protocol without an
173
426
  * actual transport — feed it a JSON-RPC request, get the response.
174
427
  *
175
- * @param source - The tools to expose (an object with `tools()` or a literal array).
176
- * @param options - Serve options (name / version / schema target).
177
- * @returns An object whose `handle(request)` maps a request to a response.
428
+ * Throws {@link McpServeError} on duplicate tool names / resource URIs /
429
+ * prompt names, or `prefixToolNames` on an ungrouped source.
430
+ *
431
+ * @param source - The tools to expose (one tool set or `{ groups }`).
432
+ * @param options - Serve options (name / version / schema target / resources / prompts).
433
+ * @returns An object whose `handle(request, context?)` maps a request to a response.
178
434
  */
179
435
  function createServeHandler(source, options) {
180
436
  return new McpServeHandler(source, options);
@@ -182,83 +438,70 @@ function createServeHandler(source, options) {
182
438
  /**
183
439
  * The internal {@link McpServer} — owns a {@link McpServeHandler} and a
184
440
  * transport pump. For `stdio` it reads newline-delimited JSON-RPC requests
185
- * from `process.stdin` and writes responses to `process.stdout`; the
186
- * `http` transport is accepted but listening is deferred to the host
187
- * (a serve-over-HTTP needs a server the caller owns).
441
+ * from `process.stdin` and writes responses to `process.stdout`; for
442
+ * `http` it binds a Streamable HTTP endpoint (see `./serve-http`).
188
443
  *
189
444
  * Constructed via {@link serve}; the class itself is internal.
190
445
  */
191
446
  var McpServerImpl = class {
192
447
  constructor(source, options) {
193
- this.running = false;
194
448
  this.handler = new McpServeHandler(source, options);
195
449
  this.options = options;
196
450
  }
197
- async start() {
198
- if (this.running) return;
199
- if ((this.options.transport ?? { type: "stdio" }).type !== "stdio") throw new McpTransportError("serve() over http requires a host-provided server; only stdio is auto-pumped.", { type: "connect" });
200
- this.running = true;
201
- this.reader = createInterface({ input: process.stdin });
202
- this.reader.on("line", (line) => {
203
- this.onLine(line);
451
+ start() {
452
+ if (!this.running) {
453
+ const transport = this.options.transport ?? { type: "stdio" };
454
+ const starting = transport.type === "http" ? this.startHttp(transport) : Promise.resolve(this.startStdio());
455
+ this.running = starting.catch((error) => {
456
+ this.running = void 0;
457
+ throw error;
458
+ });
459
+ }
460
+ return this.running;
461
+ }
462
+ /** Bind the HTTP endpoint; its `close()` also makes this server startable again. */
463
+ async startHttp(transport) {
464
+ const http = await startHttpServer(transport, (request, context) => this.handler.handle(request, context));
465
+ return {
466
+ url: http.url,
467
+ close: async () => {
468
+ await http.close();
469
+ this.running = void 0;
470
+ }
471
+ };
472
+ }
473
+ /** Wire the stdin line reader and answer each request on stdout. */
474
+ startStdio() {
475
+ const reader = createInterface({ input: process.stdin });
476
+ reader.on("line", (line) => {
477
+ this.onLine(line).catch(() => {});
204
478
  });
479
+ return { close: async () => {
480
+ reader.close();
481
+ this.running = void 0;
482
+ } };
205
483
  }
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
- */
484
+ /** Answer one stdin line on stdout (see {@link answerStdioLine}). */
211
485
  async onLine(line) {
212
- const trimmed = line.trim();
213
- if (!trimmed) return;
214
- let request;
486
+ const reply = await answerStdioLine(line, (request) => this.handler.handle(request));
487
+ if (reply !== void 0) process.stdout.write(reply);
488
+ }
489
+ async stop() {
490
+ const running = this.running;
491
+ if (!running) return;
492
+ let handle;
215
493
  try {
216
- request = JSON.parse(trimmed);
494
+ handle = await running;
217
495
  } catch {
218
496
  return;
219
497
  }
220
- if (request.id === void 0 || request.id === null) return;
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;
498
+ await handle.close();
228
499
  }
229
500
  };
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
501
  function serve(source, options) {
259
502
  return new McpServerImpl(source, options);
260
503
  }
261
504
 
262
505
  //#endregion
263
- export { createServeHandler, serve };
506
+ export { answerStdioLine, createServeHandler, serve };
264
507
  //# sourceMappingURL=serve.mjs.map