@corbits/mcp 0.3.1 → 0.4.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/README.md +120 -26
- package/dist/auth/callback.d.ts +14 -0
- package/dist/auth/callback.js +79 -0
- package/dist/auth/challenge.d.ts +9 -0
- package/dist/auth/challenge.js +52 -0
- package/dist/auth/contracts.d.ts +18 -0
- package/dist/auth/contracts.js +19 -0
- package/dist/auth/http.d.ts +2 -0
- package/dist/auth/http.js +2 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +6 -0
- package/dist/auth/login.d.ts +62 -0
- package/dist/auth/login.js +175 -0
- package/dist/auth/oauth-http.d.ts +6 -0
- package/dist/auth/oauth-http.js +32 -0
- package/dist/auth/resource-server.d.ts +25 -0
- package/dist/auth/resource-server.js +94 -0
- package/dist/auth/url-policy.d.ts +10 -0
- package/dist/auth/url-policy.js +35 -0
- package/dist/client/resource-budget.d.ts +1 -0
- package/dist/client/resource-budget.js +1 -0
- package/dist/client/transport.d.ts +10 -0
- package/dist/client/transport.js +1 -0
- package/dist/client.d.ts +208 -26
- package/dist/client.js +1128 -90
- package/dist/extensions/apps.d.ts +58 -0
- package/dist/extensions/apps.js +55 -0
- package/dist/extensions/elicitation.d.ts +18 -0
- package/dist/extensions/elicitation.js +55 -0
- package/dist/extensions/index.d.ts +5 -0
- package/dist/extensions/index.js +5 -0
- package/dist/extensions/maturity.d.ts +44 -0
- package/dist/extensions/maturity.js +24 -0
- package/dist/extensions/skills/contracts.d.ts +53 -0
- package/dist/extensions/skills/contracts.js +1 -0
- package/dist/extensions/skills/index.d.ts +52 -0
- package/dist/extensions/skills/index.js +402 -0
- package/dist/extensions/tasks/contracts.d.ts +39 -0
- package/dist/extensions/tasks/contracts.js +1 -0
- package/dist/extensions/tasks/file-store.d.ts +12 -0
- package/dist/extensions/tasks/file-store.js +152 -0
- package/dist/extensions/tasks/index.d.ts +82 -0
- package/dist/extensions/tasks/index.js +221 -0
- package/dist/extensions/tasks/record.d.ts +3 -0
- package/dist/extensions/tasks/record.js +37 -0
- package/dist/hub/discover.d.ts +5 -1
- package/dist/hub/discover.js +36 -13
- package/dist/index.d.ts +3 -2
- package/dist/index.js +4 -2
- package/dist/interchange/client/capabilities.d.ts +4 -0
- package/dist/interchange/client/capabilities.js +53 -0
- package/dist/interchange/client/catalog.d.ts +14 -0
- package/dist/interchange/client/catalog.js +52 -0
- package/dist/interchange/client/contracts.d.ts +47 -0
- package/dist/interchange/client/contracts.js +1 -0
- package/dist/interchange/client/projection.d.ts +6 -0
- package/dist/interchange/client/projection.js +92 -0
- package/dist/interchange/client/skills.d.ts +16 -0
- package/dist/interchange/client/skills.js +87 -0
- package/dist/interchange/client.d.ts +8 -0
- package/dist/interchange/client.js +6 -0
- package/dist/interchange/server/contracts.d.ts +65 -0
- package/dist/interchange/server/contracts.js +1 -0
- package/dist/interchange/server.d.ts +5 -0
- package/dist/interchange/server.js +172 -0
- package/dist/naming.d.ts +2 -0
- package/dist/naming.js +7 -0
- package/dist/oauth-discovery.d.ts +107 -1
- package/dist/oauth-discovery.js +292 -56
- package/dist/protocol/content.d.ts +35 -0
- package/dist/protocol/content.js +64 -0
- package/dist/protocol/envelopes.d.ts +14 -0
- package/dist/protocol/envelopes.js +22 -0
- package/dist/protocol/errors.d.ts +26 -0
- package/dist/protocol/errors.js +31 -0
- package/dist/protocol/extensions.d.ts +34 -0
- package/dist/protocol/extensions.js +86 -0
- package/dist/protocol/headers.d.ts +12 -0
- package/dist/protocol/headers.js +128 -0
- package/dist/protocol/index.d.ts +11 -0
- package/dist/protocol/index.js +11 -0
- package/dist/protocol/json-schema.d.ts +8 -0
- package/dist/protocol/json-schema.js +152 -0
- package/dist/protocol/legacy.d.ts +12 -0
- package/dist/protocol/legacy.js +18 -0
- package/dist/protocol/methods.d.ts +116 -0
- package/dist/protocol/methods.js +78 -0
- package/dist/protocol/resource-budget.d.ts +9 -0
- package/dist/protocol/resource-budget.js +206 -0
- package/dist/protocol/schemas.d.ts +115 -0
- package/dist/protocol/schemas.js +553 -0
- package/dist/protocol/types.d.ts +295 -0
- package/dist/protocol/types.js +3 -0
- package/dist/protocol/ui.d.ts +3 -0
- package/dist/protocol/ui.js +18 -0
- package/dist/server/contracts.d.ts +67 -0
- package/dist/server/contracts.js +1 -0
- package/dist/server/errors.d.ts +6 -0
- package/dist/server/errors.js +21 -0
- package/dist/server/extensions.d.ts +7 -0
- package/dist/server/extensions.js +61 -0
- package/dist/server/fixtures.d.ts +2 -0
- package/dist/server/fixtures.js +34 -0
- package/dist/server/legacy.d.ts +9 -0
- package/dist/server/legacy.js +435 -0
- package/dist/server/tools.d.ts +17 -0
- package/dist/server/tools.js +93 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.js +406 -0
- package/dist/sidecar-bundle.d.ts +7 -7
- package/dist/sidecar-bundle.js +36 -29
- package/dist/test-server.js +12 -3
- package/dist/tool.d.ts +8 -0
- package/dist/tool.js +35 -19
- package/dist/url.js +8 -3
- package/docs/conformance.md +135 -0
- package/docs/integration-status.md +98 -0
- package/docs/legacy-server.md +58 -0
- package/docs/migration.md +143 -0
- package/docs/public-api.md +106 -0
- package/examples/README.md +28 -0
- package/examples/standalone-client.ts +14 -0
- package/examples/standalone-server.ts +37 -0
- package/package.json +40 -5
package/dist/server.js
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import { errorPayload } from "./server/errors.js";
|
|
2
|
+
import { createLegacyServer } from "./server/legacy.js";
|
|
3
|
+
import { createResourceBudget } from "./protocol/resource-budget.js";
|
|
4
|
+
import { validateMcpIdentity } from "./auth/contracts.js";
|
|
5
|
+
import { MCP_PROTOCOL_VERSION, MCP_EXT_TASKS, MCP_EXT_SKILLS, methodDescriptor, validateExtensionSettings, negotiateExtensions, McpError, validateRequest, validateJsonRpcRequest, validateRequestHeaders, validateResult, validateNotification, record as protocolRecord, } from "./protocol/index.js";
|
|
6
|
+
export { createTaskProviders, createSkillProviders, } from "./server/extensions.js";
|
|
7
|
+
export { createToolProviders } from "./server/tools.js";
|
|
8
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
9
|
+
? value
|
|
10
|
+
: {};
|
|
11
|
+
function methodAuthorization(provider, params) {
|
|
12
|
+
return provider.authorization
|
|
13
|
+
? provider.authorization(params)
|
|
14
|
+
: { resource: provider.resource, action: provider.action };
|
|
15
|
+
}
|
|
16
|
+
/** Pairs a private discover check may satisfy. No providers means mcp:server/discover. */
|
|
17
|
+
function discoverAuthorizations(providers) {
|
|
18
|
+
const pairs = [];
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const provider of Object.values(providers)) {
|
|
21
|
+
const listed = provider.authorizationPairs ?? [
|
|
22
|
+
{ resource: provider.resource, action: provider.action },
|
|
23
|
+
];
|
|
24
|
+
for (const pair of listed) {
|
|
25
|
+
const key = `${pair.resource}\0${pair.action}`;
|
|
26
|
+
if (seen.has(key))
|
|
27
|
+
continue;
|
|
28
|
+
seen.add(key);
|
|
29
|
+
pairs.push(pair);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (pairs.length === 0)
|
|
33
|
+
pairs.push({ resource: "mcp:server", action: "discover" });
|
|
34
|
+
return pairs;
|
|
35
|
+
}
|
|
36
|
+
/** Stateless modern endpoint with an independently gated legacy session transport. */
|
|
37
|
+
export function createMcpServer(options) {
|
|
38
|
+
const providers = Object.freeze({ ...options.providers });
|
|
39
|
+
const extensions = validateExtensionSettings(options.extensions ?? {});
|
|
40
|
+
for (const [extension, methods] of [
|
|
41
|
+
[MCP_EXT_TASKS, ["tasks/get", "tasks/update", "tasks/cancel"]],
|
|
42
|
+
[MCP_EXT_SKILLS, ["skills/list", "skills/get", "resources/read"]],
|
|
43
|
+
]) {
|
|
44
|
+
if (extensions[extension] && methods.some((method) => !providers[method]))
|
|
45
|
+
throw new Error(`Missing providers for ${extension}`);
|
|
46
|
+
}
|
|
47
|
+
if (extensions[MCP_EXT_SKILLS]?.directoryRead === true &&
|
|
48
|
+
!providers["resources/directory/read"])
|
|
49
|
+
throw new Error("Missing directory read provider");
|
|
50
|
+
const active = new Set();
|
|
51
|
+
let closed = false;
|
|
52
|
+
const capabilities = {};
|
|
53
|
+
for (const method of Object.keys(providers)) {
|
|
54
|
+
if (method === "server/discover")
|
|
55
|
+
throw new Error("Discovery is server-owned");
|
|
56
|
+
const descriptor = methodDescriptor(method);
|
|
57
|
+
if (!descriptor)
|
|
58
|
+
throw new Error(`Unknown provider method: ${method}`);
|
|
59
|
+
if (descriptor.extension && !options.extensions?.[descriptor.extension])
|
|
60
|
+
throw new Error(`Extension not enabled: ${descriptor.extension}`);
|
|
61
|
+
if (descriptor.capability)
|
|
62
|
+
capabilities[descriptor.capability] = {};
|
|
63
|
+
const family = method.split("/")[0];
|
|
64
|
+
if (family === "tools" || family === "prompts" || family === "resources")
|
|
65
|
+
capabilities[family] = {};
|
|
66
|
+
if (family === "completion")
|
|
67
|
+
capabilities.completions = {};
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(extensions).length)
|
|
70
|
+
capabilities.extensions = extensions;
|
|
71
|
+
const legacy = options.legacy
|
|
72
|
+
? createLegacyServer(options, providers)
|
|
73
|
+
: undefined;
|
|
74
|
+
return {
|
|
75
|
+
async fetch(request) {
|
|
76
|
+
const origin = request.headers.get("origin");
|
|
77
|
+
if (origin && !options.allowedOrigins?.includes(origin))
|
|
78
|
+
return new Response(null, { status: 403 });
|
|
79
|
+
if (closed)
|
|
80
|
+
return new Response(null, { status: 503 });
|
|
81
|
+
if (request.method !== "POST") {
|
|
82
|
+
if (legacy &&
|
|
83
|
+
request.headers.get("MCP-Protocol-Version") !==
|
|
84
|
+
MCP_PROTOCOL_VERSION &&
|
|
85
|
+
(request.method === "DELETE" || request.headers.has("Mcp-Session-Id")))
|
|
86
|
+
return legacy.lifecycle(request);
|
|
87
|
+
return new Response(null, { status: 405, headers: { Allow: "POST" } });
|
|
88
|
+
}
|
|
89
|
+
if (request.headers.get("content-type")?.split(";")[0]?.trim() !==
|
|
90
|
+
"application/json")
|
|
91
|
+
return new Response(null, { status: 415 });
|
|
92
|
+
const accept = request.headers.get("accept") ?? "";
|
|
93
|
+
if (!accept.includes("application/json") ||
|
|
94
|
+
!accept.includes("text/event-stream"))
|
|
95
|
+
return new Response(null, { status: 406 });
|
|
96
|
+
let id;
|
|
97
|
+
try {
|
|
98
|
+
const authenticated = options.auth === "public"
|
|
99
|
+
? null
|
|
100
|
+
: await options.auth.authenticate(request);
|
|
101
|
+
if (closed)
|
|
102
|
+
return new Response(null, { status: 503 });
|
|
103
|
+
if (options.auth !== "public" && !authenticated)
|
|
104
|
+
return (options.challenge?.(401) ?? new Response(null, { status: 401 }));
|
|
105
|
+
const identity = authenticated === null ? null : validateMcpIdentity(authenticated);
|
|
106
|
+
const limit = options.maxRequestBytes ?? 1024 * 1024;
|
|
107
|
+
const reader = request.body?.getReader();
|
|
108
|
+
if (!reader)
|
|
109
|
+
throw new McpError("Missing request body", 400, -32600);
|
|
110
|
+
const chunks = [];
|
|
111
|
+
let size = 0;
|
|
112
|
+
while (true) {
|
|
113
|
+
const chunk = await reader.read();
|
|
114
|
+
if (chunk.done)
|
|
115
|
+
break;
|
|
116
|
+
size += chunk.value.byteLength;
|
|
117
|
+
if (size > limit) {
|
|
118
|
+
await reader.cancel();
|
|
119
|
+
throw new McpError("Request too large", 413, -32600);
|
|
120
|
+
}
|
|
121
|
+
chunks.push(chunk.value);
|
|
122
|
+
}
|
|
123
|
+
const bytes = new Uint8Array(size);
|
|
124
|
+
let offset = 0;
|
|
125
|
+
for (const chunk of chunks) {
|
|
126
|
+
bytes.set(chunk, offset);
|
|
127
|
+
offset += chunk.byteLength;
|
|
128
|
+
}
|
|
129
|
+
let parsed;
|
|
130
|
+
try {
|
|
131
|
+
parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new McpError("Invalid JSON", 400, -32700);
|
|
135
|
+
}
|
|
136
|
+
const readableId = record(parsed).id;
|
|
137
|
+
if (typeof readableId === "string" ||
|
|
138
|
+
(typeof readableId === "number" && Number.isSafeInteger(readableId)))
|
|
139
|
+
id = readableId;
|
|
140
|
+
const compatibility = await legacy?.handle(request, parsed, identity);
|
|
141
|
+
if (closed)
|
|
142
|
+
return new Response(null, { status: 503 });
|
|
143
|
+
if (compatibility)
|
|
144
|
+
return compatibility;
|
|
145
|
+
const envelope = validateJsonRpcRequest(parsed);
|
|
146
|
+
id = envelope.id;
|
|
147
|
+
const params = record(envelope.params);
|
|
148
|
+
const meta = record(params._meta);
|
|
149
|
+
if (envelope.method !== "server/discover") {
|
|
150
|
+
const gated = providers[envelope.method];
|
|
151
|
+
if (gated) {
|
|
152
|
+
const pair = methodAuthorization(gated, params);
|
|
153
|
+
if (pair !== null && options.auth !== "public" && identity) {
|
|
154
|
+
const permitted = pair !== false &&
|
|
155
|
+
(await options.auth.authorize(identity, pair));
|
|
156
|
+
if (closed)
|
|
157
|
+
return new Response(null, { status: 503 });
|
|
158
|
+
if (!permitted)
|
|
159
|
+
return (options.challenge?.(403) ??
|
|
160
|
+
new Response(null, { status: 403 }));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const validation = {
|
|
165
|
+
era: "modern",
|
|
166
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
167
|
+
clientCapabilities: record(meta["io.modelcontextprotocol/clientCapabilities"]),
|
|
168
|
+
serverCapabilities: capabilities,
|
|
169
|
+
extensions: negotiateExtensions(extensions, record(record(meta["io.modelcontextprotocol/clientCapabilities"])
|
|
170
|
+
.extensions)),
|
|
171
|
+
...providers[envelope.method]?.schemas?.(params),
|
|
172
|
+
// Explicit Skills publication supports bounded resource payloads only.
|
|
173
|
+
maxDecodedBytes: envelope.method === "resources/read" && extensions[MCP_EXT_SKILLS]
|
|
174
|
+
? 16 * 1024 * 1024
|
|
175
|
+
: undefined,
|
|
176
|
+
};
|
|
177
|
+
validateRequestHeaders(request.headers, envelope.method, params, validation);
|
|
178
|
+
validateRequest(envelope.method, params, validation);
|
|
179
|
+
if (envelope.method === "server/discover") {
|
|
180
|
+
if (options.auth !== "public") {
|
|
181
|
+
let permitted = false;
|
|
182
|
+
for (const pair of discoverAuthorizations(providers)) {
|
|
183
|
+
if (identity && (await options.auth.authorize(identity, pair))) {
|
|
184
|
+
permitted = true;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (closed)
|
|
189
|
+
return new Response(null, { status: 503 });
|
|
190
|
+
if (!permitted)
|
|
191
|
+
return (options.challenge?.(403) ?? new Response(null, { status: 403 }));
|
|
192
|
+
}
|
|
193
|
+
const result = validateResult(envelope.method, {
|
|
194
|
+
resultType: "complete",
|
|
195
|
+
supportedVersions: [
|
|
196
|
+
MCP_PROTOCOL_VERSION,
|
|
197
|
+
...(legacy?.versions ?? []),
|
|
198
|
+
],
|
|
199
|
+
capabilities,
|
|
200
|
+
_meta: {
|
|
201
|
+
"io.modelcontextprotocol/serverInfo": options.serverInfo,
|
|
202
|
+
},
|
|
203
|
+
}, validation);
|
|
204
|
+
return Response.json({ jsonrpc: "2.0", id, result }, { headers: { "Cache-Control": "no-store" } });
|
|
205
|
+
}
|
|
206
|
+
const provider = providers[envelope.method];
|
|
207
|
+
if (!provider)
|
|
208
|
+
throw new McpError("Method not found", 404, -32601);
|
|
209
|
+
const controller = new AbortController();
|
|
210
|
+
active.add(controller);
|
|
211
|
+
const abort = () => controller.abort(request.signal.reason);
|
|
212
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
213
|
+
if (request.signal.aborted)
|
|
214
|
+
abort();
|
|
215
|
+
const requestId = id;
|
|
216
|
+
const subscription = envelope.method === "subscriptions/listen";
|
|
217
|
+
let acceptedFilter;
|
|
218
|
+
const correlate = (payload) => subscription
|
|
219
|
+
? {
|
|
220
|
+
...payload,
|
|
221
|
+
_meta: {
|
|
222
|
+
...(payload._meta === undefined
|
|
223
|
+
? {}
|
|
224
|
+
: protocolRecord(payload._meta)),
|
|
225
|
+
"io.modelcontextprotocol/subscriptionId": requestId,
|
|
226
|
+
},
|
|
227
|
+
}
|
|
228
|
+
: payload;
|
|
229
|
+
let ended = false;
|
|
230
|
+
let lastProgress = -Infinity;
|
|
231
|
+
const encoder = new TextEncoder();
|
|
232
|
+
const stream = new ReadableStream({
|
|
233
|
+
start(sink) {
|
|
234
|
+
const send = (message) => {
|
|
235
|
+
if (!ended) {
|
|
236
|
+
const wire = encoder.encode(`data: ${JSON.stringify(message)}\n\n`);
|
|
237
|
+
if (validation.maxDecodedBytes !== undefined &&
|
|
238
|
+
record(message).result !== undefined) {
|
|
239
|
+
const budget = createResourceBudget(validation.maxDecodedBytes, true);
|
|
240
|
+
budget.push(wire);
|
|
241
|
+
budget.finish();
|
|
242
|
+
}
|
|
243
|
+
sink.enqueue(wire);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const finish = () => {
|
|
247
|
+
if (!ended) {
|
|
248
|
+
ended = true;
|
|
249
|
+
sink.close();
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
controller.signal.addEventListener("abort", finish, { once: true });
|
|
253
|
+
if (controller.signal.aborted)
|
|
254
|
+
finish();
|
|
255
|
+
const context = {
|
|
256
|
+
identity,
|
|
257
|
+
auth: options.auth,
|
|
258
|
+
requestId,
|
|
259
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
260
|
+
clientCapabilities: validation.clientCapabilities ?? {},
|
|
261
|
+
signal: controller.signal,
|
|
262
|
+
reportProgress(progress, total, message) {
|
|
263
|
+
const token = meta.progressToken;
|
|
264
|
+
if (subscription ||
|
|
265
|
+
token === undefined ||
|
|
266
|
+
controller.signal.aborted)
|
|
267
|
+
return;
|
|
268
|
+
if (progress <= lastProgress)
|
|
269
|
+
throw new McpError("Progress must increase", 500, -32603);
|
|
270
|
+
const notification = {
|
|
271
|
+
progressToken: token,
|
|
272
|
+
progress,
|
|
273
|
+
...(total === undefined ? {} : { total }),
|
|
274
|
+
...(message === undefined ? {} : { message }),
|
|
275
|
+
};
|
|
276
|
+
validateNotification("notifications/progress", notification, validation);
|
|
277
|
+
lastProgress = progress;
|
|
278
|
+
send({
|
|
279
|
+
jsonrpc: "2.0",
|
|
280
|
+
method: "notifications/progress",
|
|
281
|
+
params: notification,
|
|
282
|
+
});
|
|
283
|
+
},
|
|
284
|
+
notify(method, notification) {
|
|
285
|
+
if (envelope.method !== "subscriptions/listen")
|
|
286
|
+
throw new Error("Change notifications require subscriptions/listen");
|
|
287
|
+
if (acceptedFilter === undefined)
|
|
288
|
+
throw new Error("Subscription is not yet acknowledged");
|
|
289
|
+
const outgoing = correlate(notification);
|
|
290
|
+
validateNotification(method, outgoing, validation);
|
|
291
|
+
const filter = acceptedFilter;
|
|
292
|
+
const listFlag = {
|
|
293
|
+
"notifications/tools/list_changed": "toolsListChanged",
|
|
294
|
+
"notifications/prompts/list_changed": "promptsListChanged",
|
|
295
|
+
"notifications/resources/list_changed": "resourcesListChanged",
|
|
296
|
+
}[method];
|
|
297
|
+
const permitted = listFlag
|
|
298
|
+
? filter[listFlag] === true
|
|
299
|
+
: method === "notifications/resources/updated"
|
|
300
|
+
? Array.isArray(filter.resourceSubscriptions) &&
|
|
301
|
+
filter.resourceSubscriptions.includes(String(notification.uri))
|
|
302
|
+
: method === "notifications/tasks"
|
|
303
|
+
? Array.isArray(filter.taskIds) &&
|
|
304
|
+
filter.taskIds.includes(String(notification.taskId))
|
|
305
|
+
: false;
|
|
306
|
+
if (!permitted)
|
|
307
|
+
return;
|
|
308
|
+
send({ jsonrpc: "2.0", method, params: outgoing });
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
void Promise.resolve()
|
|
312
|
+
.then(async () => {
|
|
313
|
+
controller.signal.throwIfAborted();
|
|
314
|
+
if (subscription) {
|
|
315
|
+
const requested = structuredClone(record(params.notifications));
|
|
316
|
+
const accepted = structuredClone((await provider.acceptSubscription?.(params, context)) ??
|
|
317
|
+
{});
|
|
318
|
+
controller.signal.throwIfAborted();
|
|
319
|
+
const acknowledged = correlate({ notifications: accepted });
|
|
320
|
+
validateNotification("notifications/subscriptions/acknowledged", acknowledged, validation);
|
|
321
|
+
for (const [key, value] of Object.entries(accepted)) {
|
|
322
|
+
const wanted = requested[key];
|
|
323
|
+
if (Array.isArray(value)
|
|
324
|
+
? !Array.isArray(wanted) ||
|
|
325
|
+
value.some((item) => !wanted.includes(item))
|
|
326
|
+
: value === true && wanted !== true) {
|
|
327
|
+
throw new McpError("Accepted subscription exceeds requested filter", 500, -32603);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
acceptedFilter = accepted;
|
|
331
|
+
send({
|
|
332
|
+
jsonrpc: "2.0",
|
|
333
|
+
method: "notifications/subscriptions/acknowledged",
|
|
334
|
+
params: acknowledged,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return provider.handle(params, context);
|
|
338
|
+
})
|
|
339
|
+
.then((result) => {
|
|
340
|
+
const outgoing = {
|
|
341
|
+
jsonrpc: "2.0",
|
|
342
|
+
id: requestId,
|
|
343
|
+
result: validateResult(envelope.method, correlate({
|
|
344
|
+
...protocolRecord(result),
|
|
345
|
+
resultType: result.resultType === undefined
|
|
346
|
+
? "complete"
|
|
347
|
+
: result.resultType,
|
|
348
|
+
}), validation),
|
|
349
|
+
};
|
|
350
|
+
send(outgoing);
|
|
351
|
+
})
|
|
352
|
+
.catch((error) => {
|
|
353
|
+
send({
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
id: requestId,
|
|
356
|
+
error: errorPayload(error, "Provider execution failed"),
|
|
357
|
+
});
|
|
358
|
+
})
|
|
359
|
+
.finally(() => {
|
|
360
|
+
active.delete(controller);
|
|
361
|
+
request.signal.removeEventListener("abort", abort);
|
|
362
|
+
controller.signal.removeEventListener("abort", finish);
|
|
363
|
+
finish();
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
cancel(reason) {
|
|
367
|
+
ended = true;
|
|
368
|
+
controller.abort(reason);
|
|
369
|
+
active.delete(controller);
|
|
370
|
+
request.signal.removeEventListener("abort", abort);
|
|
371
|
+
},
|
|
372
|
+
});
|
|
373
|
+
return new Response(stream, {
|
|
374
|
+
headers: {
|
|
375
|
+
"Content-Type": "text/event-stream",
|
|
376
|
+
"Cache-Control": "no-store",
|
|
377
|
+
"X-Accel-Buffering": "no",
|
|
378
|
+
},
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
const known = error instanceof McpError;
|
|
383
|
+
return Response.json({
|
|
384
|
+
jsonrpc: "2.0",
|
|
385
|
+
...(id === undefined ? {} : { id }),
|
|
386
|
+
error: errorPayload(error, "Request failed"),
|
|
387
|
+
}, {
|
|
388
|
+
status: known
|
|
389
|
+
? (error.status ?? (error.code === -32601 ? 404 : 400))
|
|
390
|
+
: 500,
|
|
391
|
+
headers: { "Cache-Control": "no-store" },
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
async close() {
|
|
396
|
+
closed = true;
|
|
397
|
+
legacy?.close();
|
|
398
|
+
for (const controller of active)
|
|
399
|
+
controller.abort();
|
|
400
|
+
active.clear();
|
|
401
|
+
},
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
export { createSkillPublisher } from "./extensions/skills/index.js";
|
|
405
|
+
export { createTaskRuntime, validateTaskRecord, } from "./extensions/tasks/index.js";
|
|
406
|
+
export { createMcpResourceServerAuth } from "./auth/resource-server.js";
|
package/dist/sidecar-bundle.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type AnnotatedToolFactory, type BaseEnv } from "@intx/agent";
|
|
2
2
|
import { type McpTool } from "./client.js";
|
|
3
|
+
import type { McpHostServices } from "./interchange/client/contracts.js";
|
|
4
|
+
export { shapeMcpContent } from "./interchange/client/projection.js";
|
|
3
5
|
/** The consumer key a host matches a credential binding against. */
|
|
4
6
|
export declare const SIDECAR_BUNDLE_ID = "@corbits/mcp/sidecar-bundle";
|
|
5
7
|
type MediatedFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
@@ -32,19 +34,17 @@ export interface McpBoundServer {
|
|
|
32
34
|
readonly allowWithoutAsk?: readonly string[];
|
|
33
35
|
}
|
|
34
36
|
export interface McpServersConfig {
|
|
37
|
+
/** Explicit host lifecycle and presentation services. */
|
|
38
|
+
readonly host?: McpHostServices;
|
|
35
39
|
readonly servers: readonly McpBoundServer[];
|
|
36
|
-
/** Per-request bound on
|
|
40
|
+
/** Per-request bound on connect and `tools/call`; defaults to 60s. */
|
|
37
41
|
readonly timeoutMs?: number;
|
|
42
|
+
/** Extension ids advertised on every request. Off unless listed. */
|
|
43
|
+
readonly extensions?: readonly string[];
|
|
38
44
|
}
|
|
39
|
-
/**
|
|
40
|
-
* MCP results are never interpreted: text blocks come back as their text so
|
|
41
|
-
* the model reads prose, and anything else is handed over verbatim as JSON.
|
|
42
|
-
*/
|
|
43
|
-
export declare function shapeMcpContent(content: unknown): string;
|
|
44
45
|
/**
|
|
45
46
|
* The sidecar bundle: one `@intx/agent` tool per catalog entry, named
|
|
46
47
|
* `<handle>.<tool>` so each is grantable on its own. Synchronous, with
|
|
47
48
|
* static definitions — the catalog is already in hand.
|
|
48
49
|
*/
|
|
49
50
|
export declare function mcpServers(config: McpServersConfig): AnnotatedToolFactory<McpServersEnv>;
|
|
50
|
-
export {};
|
package/dist/sidecar-bundle.js
CHANGED
|
@@ -11,8 +11,11 @@
|
|
|
11
11
|
// point at which this bundle could do a `tools/list` round trip.
|
|
12
12
|
import { defineTool, } from "@intx/agent";
|
|
13
13
|
import { type } from "arktype";
|
|
14
|
-
import { DEFAULT_TIMEOUT_MS, mcpCallTool,
|
|
15
|
-
import { assertCatalogNames, assertServers, isAskExempt, qualifiedName, toolDescription, } from "./naming.js";
|
|
14
|
+
import { DEFAULT_TIMEOUT_MS, mcpCallTool, mcpConnect, McpToolSchema, } from "./client.js";
|
|
15
|
+
import { assertCatalogNames, assertServers, isAskExempt, isModelVisible, qualifiedName, toolDescription, } from "./naming.js";
|
|
16
|
+
import { assertHostExtensions, hostClientOptions, } from "./interchange/client/capabilities.js";
|
|
17
|
+
import { projectToolResult } from "./interchange/client/projection.js";
|
|
18
|
+
export { shapeMcpContent } from "./interchange/client/projection.js";
|
|
16
19
|
/** The consumer key a host matches a credential binding against. */
|
|
17
20
|
export const SIDECAR_BUNDLE_ID = "@corbits/mcp/sidecar-bundle";
|
|
18
21
|
const BoundServerSchema = type({
|
|
@@ -24,6 +27,7 @@ const BoundServerSchema = type({
|
|
|
24
27
|
const ConfigSchema = type({
|
|
25
28
|
servers: BoundServerSchema.array(),
|
|
26
29
|
"timeoutMs?": "number > 0",
|
|
30
|
+
"extensions?": "string[]",
|
|
27
31
|
});
|
|
28
32
|
/** Validate at construction: a malformed deploy config is a deploy bug, not a
|
|
29
33
|
* tool error the model should see. */
|
|
@@ -42,20 +46,6 @@ function assertConfig(config) {
|
|
|
42
46
|
throw new Error(`invalid @corbits/mcp server config: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
|
-
const TextBlock = type({ type: "'text'", text: "string" }).array();
|
|
46
|
-
/**
|
|
47
|
-
* MCP results are never interpreted: text blocks come back as their text so
|
|
48
|
-
* the model reads prose, and anything else is handed over verbatim as JSON.
|
|
49
|
-
*/
|
|
50
|
-
export function shapeMcpContent(content) {
|
|
51
|
-
if (typeof content === "string")
|
|
52
|
-
return content;
|
|
53
|
-
const blocks = TextBlock(content);
|
|
54
|
-
if (!(blocks instanceof type.errors)) {
|
|
55
|
-
return blocks.map((block) => block.text).join("\n");
|
|
56
|
-
}
|
|
57
|
-
return JSON.stringify(content);
|
|
58
|
-
}
|
|
59
49
|
/** A mediated handle resolves a relative path against the origin it is pinned
|
|
60
50
|
* to, so the bundle passes the server's path and never names a host. */
|
|
61
51
|
function requestPath(url) {
|
|
@@ -70,9 +60,10 @@ function requestPath(url) {
|
|
|
70
60
|
export function mcpServers(config) {
|
|
71
61
|
assertConfig(config);
|
|
72
62
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
63
|
+
assertHostExtensions(config.extensions, config.host);
|
|
64
|
+
const entries = config.servers.flatMap((server) => server.tools.filter(isModelVisible).map((tool) => ({
|
|
65
|
+
server: { ...server },
|
|
66
|
+
tool: structuredClone(tool),
|
|
76
67
|
qualified: qualifiedName(server.handle, tool.name),
|
|
77
68
|
})));
|
|
78
69
|
const definitions = entries.map(({ qualified, tool, server }) => ({
|
|
@@ -108,11 +99,16 @@ export function mcpServers(config) {
|
|
|
108
99
|
throw new Error(`the "${runtime.server.handle}" credential is a ${credential.kind} handle; an MCP server needs an http one`);
|
|
109
100
|
}
|
|
110
101
|
const mediated = (input, init) => credential.fetch(input, init);
|
|
111
|
-
const
|
|
102
|
+
const opts = {
|
|
112
103
|
fetch: mediated,
|
|
113
104
|
timeoutMs,
|
|
114
|
-
|
|
115
|
-
|
|
105
|
+
...hostClientOptions(config.host, config.extensions),
|
|
106
|
+
...(config.extensions !== undefined
|
|
107
|
+
? { extensions: config.extensions }
|
|
108
|
+
: {}),
|
|
109
|
+
};
|
|
110
|
+
const session = await mcpConnect(requestPath(runtime.server.url), opts);
|
|
111
|
+
return { ...opts, session };
|
|
116
112
|
})().catch((cause) => {
|
|
117
113
|
runtime.ready = undefined;
|
|
118
114
|
runtime.handle = undefined;
|
|
@@ -133,17 +129,28 @@ export function mcpServers(config) {
|
|
|
133
129
|
};
|
|
134
130
|
}
|
|
135
131
|
try {
|
|
136
|
-
const
|
|
137
|
-
|
|
132
|
+
const context = {
|
|
133
|
+
server: found.server.handle,
|
|
134
|
+
url: requestPath(found.server.url),
|
|
135
|
+
tool: found.qualified,
|
|
136
|
+
callId: call.id,
|
|
137
|
+
signal,
|
|
138
|
+
};
|
|
139
|
+
await config.host?.validateCatalog?.(context, found.tool);
|
|
140
|
+
const client = {
|
|
141
|
+
...(await connect(runtime)),
|
|
142
|
+
signal,
|
|
143
|
+
inputSchema: found.tool.inputSchema,
|
|
144
|
+
...(found.tool.outputSchema === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { outputSchema: found.tool.outputSchema }),
|
|
147
|
+
};
|
|
148
|
+
const result = await mcpCallTool(requestPath(found.server.url), found.tool.name, call.arguments, client).catch((cause) => {
|
|
138
149
|
// The session may have expired; the next call opens a new one.
|
|
139
150
|
runtime.ready = undefined;
|
|
140
151
|
throw cause;
|
|
141
152
|
});
|
|
142
|
-
return
|
|
143
|
-
callId: call.id,
|
|
144
|
-
isError: result.isError === true,
|
|
145
|
-
content: shapeMcpContent(result.content),
|
|
146
|
-
};
|
|
153
|
+
return await projectToolResult(context, result, client, config.host);
|
|
147
154
|
}
|
|
148
155
|
catch (cause) {
|
|
149
156
|
// One server's failure is that server's tool error, never the
|
package/dist/test-server.js
CHANGED
|
@@ -72,12 +72,21 @@ export function startTestMcpServer(opts = {}) {
|
|
|
72
72
|
return new Response("bad request", { status: 400 });
|
|
73
73
|
}
|
|
74
74
|
if (params.name === "echo") {
|
|
75
|
-
return reply({
|
|
75
|
+
return reply({
|
|
76
|
+
content: [{ type: "text", text: String(params.arguments.text) }],
|
|
77
|
+
});
|
|
76
78
|
}
|
|
77
79
|
if (params.name === "delete_thing") {
|
|
78
|
-
return reply({
|
|
80
|
+
return reply({
|
|
81
|
+
content: [
|
|
82
|
+
{ type: "text", text: `deleted ${String(params.arguments.id)}` },
|
|
83
|
+
],
|
|
84
|
+
});
|
|
79
85
|
}
|
|
80
|
-
return reply({
|
|
86
|
+
return reply({
|
|
87
|
+
content: [{ type: "text", text: `unknown tool ${params.name}` }],
|
|
88
|
+
isError: true,
|
|
89
|
+
});
|
|
81
90
|
}
|
|
82
91
|
return new Response("not found", { status: 404 });
|
|
83
92
|
},
|
package/dist/tool.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AnnotatedToolFactory, type BaseEnv } from "@intx/agent";
|
|
2
2
|
import type { CredentialCapability } from "@intx/types";
|
|
3
|
+
import type { McpHostServices } from "./interchange/client/contracts.js";
|
|
3
4
|
export interface McpServerConfig {
|
|
4
5
|
/** Namespace prefix for this server's tools: `<name>.<remote-tool-name>`. */
|
|
5
6
|
name: string;
|
|
@@ -17,6 +18,8 @@ export interface McpToolsEnv extends BaseEnv {
|
|
|
17
18
|
}
|
|
18
19
|
export interface McpToolsOptions {
|
|
19
20
|
servers: McpServerConfig[];
|
|
21
|
+
/** Explicit host lifecycle and presentation services. */
|
|
22
|
+
host?: McpHostServices;
|
|
20
23
|
/**
|
|
21
24
|
* `<server>.<tool>` names allowed to float below the `ask` floor onto
|
|
22
25
|
* `allow` from a resolved `allow` grant. Every generated tool starts
|
|
@@ -27,6 +30,11 @@ export interface McpToolsOptions {
|
|
|
27
30
|
allowWithoutAsk?: string[];
|
|
28
31
|
/** Per-request bound on every MCP round trip; defaults to 60s. */
|
|
29
32
|
timeoutMs?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Extension ids advertised on every request. Off unless listed. Tools stay
|
|
35
|
+
* the only grantable unit; extensions are not projected into tools.
|
|
36
|
+
*/
|
|
37
|
+
extensions?: readonly string[];
|
|
30
38
|
}
|
|
31
39
|
/**
|
|
32
40
|
* Discover every server's tool catalog and build the `@corbits/mcp` tool
|