@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.
- package/CHANGELOG.md +53 -1
- package/README.md +25 -13
- package/cjs/index.cjs +968 -122
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/http.type.d.mts +4 -5
- package/esm/contracts/http.type.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +3 -1
- package/esm/contracts/mcp-serve.type.d.mts +285 -0
- package/esm/contracts/mcp-serve.type.d.mts.map +1 -0
- package/esm/contracts/mcp.type.d.mts +44 -77
- package/esm/contracts/mcp.type.d.mts.map +1 -1
- package/esm/contracts/utility.type.d.mts +7 -8
- package/esm/contracts/utility.type.d.mts.map +1 -1
- package/esm/contracts/web.type.d.mts +9 -10
- package/esm/contracts/web.type.d.mts.map +1 -1
- package/esm/errors.d.mts +62 -22
- 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 +2 -3
- package/esm/http/http-request.d.mts.map +1 -1
- package/esm/http/http-request.mjs +7 -5
- package/esm/http/http-request.mjs.map +1 -1
- package/esm/index.d.mts +10 -4
- package/esm/index.mjs +4 -2
- package/esm/mcp/client.mjs +37 -6
- package/esm/mcp/client.mjs.map +1 -1
- package/esm/mcp/index.d.mts +17 -9
- package/esm/mcp/index.d.mts.map +1 -1
- package/esm/mcp/index.mjs +3 -1
- package/esm/mcp/index.mjs.map +1 -1
- package/esm/mcp/json-schema-to-standard.d.mts +1 -3
- package/esm/mcp/json-schema-to-standard.d.mts.map +1 -1
- package/esm/mcp/protocol.d.mts +15 -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/result.d.mts +17 -0
- package/esm/mcp/result.d.mts.map +1 -0
- package/esm/mcp/result.mjs +85 -0
- package/esm/mcp/result.mjs.map +1 -0
- package/esm/mcp/serve-http.mjs +444 -0
- package/esm/mcp/serve-http.mjs.map +1 -0
- package/esm/mcp/serve.d.mts +46 -27
- package/esm/mcp/serve.d.mts.map +1 -1
- package/esm/mcp/serve.mjs +339 -96
- package/esm/mcp/serve.mjs.map +1 -1
- package/esm/mcp/transport.d.mts +4 -5
- package/esm/mcp/transport.d.mts.map +1 -1
- package/esm/mcp/transport.mjs +12 -6
- package/esm/mcp/transport.mjs.map +1 -1
- package/esm/mcp/transport.type.d.mts +2 -2
- package/esm/mcp/transport.type.d.mts.map +1 -1
- package/esm/register.d.mts +15 -5
- 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 +1 -3
- package/esm/utility/calculator.d.mts.map +1 -1
- package/esm/utility/calculator.mjs.map +1 -1
- package/esm/utility/date-time.d.mts +3 -5
- package/esm/utility/date-time.d.mts.map +1 -1
- package/esm/utility/date-time.mjs +2 -1
- package/esm/utility/date-time.mjs.map +1 -1
- package/esm/utility/index.d.mts +2 -1
- package/esm/utility/index.mjs +1 -1
- package/esm/utility/schema.mjs.map +1 -1
- package/esm/web/fetch-url.d.mts +2 -3
- package/esm/web/fetch-url.d.mts.map +1 -1
- package/esm/web/fetch-url.mjs +2 -1
- package/esm/web/fetch-url.mjs.map +1 -1
- package/esm/web/index.d.mts +2 -1
- package/esm/web/index.mjs +1 -1
- package/esm/web/schema.mjs.map +1 -1
- package/esm/web/web-search.d.mts +2 -3
- package/esm/web/web-search.d.mts.map +1 -1
- package/esm/web/web-search.mjs +3 -4
- package/esm/web/web-search.mjs.map +1 -1
- package/llms-full.txt +245 -56
- 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 +168 -35
- 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,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
|
-
*
|
|
19
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
31
|
-
* `
|
|
32
|
-
*
|
|
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((
|
|
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
|
-
|
|
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
|
-
/**
|
|
61
|
-
|
|
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
|
|
64
|
-
capabilities
|
|
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
|
-
*
|
|
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
|
|
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
|
|
95
|
-
* tool name throws (mapped to a
|
|
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
|
|
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
|
-
* @
|
|
176
|
-
*
|
|
177
|
-
*
|
|
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`;
|
|
186
|
-
* `http`
|
|
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
|
-
|
|
198
|
-
if (this.running)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
|
213
|
-
if (
|
|
214
|
-
|
|
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
|
-
|
|
494
|
+
handle = await running;
|
|
217
495
|
} catch {
|
|
218
496
|
return;
|
|
219
497
|
}
|
|
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;
|
|
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
|