@corbits/mcp 0.3.1 → 0.4.1

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 (124) hide show
  1. package/README.md +162 -28
  2. package/dist/auth/callback.d.ts +14 -0
  3. package/dist/auth/callback.js +79 -0
  4. package/dist/auth/challenge.d.ts +9 -0
  5. package/dist/auth/challenge.js +52 -0
  6. package/dist/auth/contracts.d.ts +18 -0
  7. package/dist/auth/contracts.js +19 -0
  8. package/dist/auth/http.d.ts +2 -0
  9. package/dist/auth/http.js +2 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.js +6 -0
  12. package/dist/auth/login.d.ts +62 -0
  13. package/dist/auth/login.js +175 -0
  14. package/dist/auth/oauth-http.d.ts +6 -0
  15. package/dist/auth/oauth-http.js +32 -0
  16. package/dist/auth/resource-server.d.ts +25 -0
  17. package/dist/auth/resource-server.js +94 -0
  18. package/dist/auth/url-policy.d.ts +10 -0
  19. package/dist/auth/url-policy.js +35 -0
  20. package/dist/client/resource-budget.d.ts +1 -0
  21. package/dist/client/resource-budget.js +1 -0
  22. package/dist/client/transport.d.ts +10 -0
  23. package/dist/client/transport.js +1 -0
  24. package/dist/client.d.ts +208 -26
  25. package/dist/client.js +1128 -90
  26. package/dist/extensions/apps.d.ts +58 -0
  27. package/dist/extensions/apps.js +55 -0
  28. package/dist/extensions/elicitation.d.ts +18 -0
  29. package/dist/extensions/elicitation.js +55 -0
  30. package/dist/extensions/index.d.ts +5 -0
  31. package/dist/extensions/index.js +5 -0
  32. package/dist/extensions/maturity.d.ts +44 -0
  33. package/dist/extensions/maturity.js +24 -0
  34. package/dist/extensions/skills/contracts.d.ts +53 -0
  35. package/dist/extensions/skills/contracts.js +1 -0
  36. package/dist/extensions/skills/index.d.ts +52 -0
  37. package/dist/extensions/skills/index.js +402 -0
  38. package/dist/extensions/tasks/contracts.d.ts +39 -0
  39. package/dist/extensions/tasks/contracts.js +1 -0
  40. package/dist/extensions/tasks/file-store.d.ts +12 -0
  41. package/dist/extensions/tasks/file-store.js +152 -0
  42. package/dist/extensions/tasks/index.d.ts +82 -0
  43. package/dist/extensions/tasks/index.js +221 -0
  44. package/dist/extensions/tasks/record.d.ts +3 -0
  45. package/dist/extensions/tasks/record.js +37 -0
  46. package/dist/hub/discover.d.ts +5 -1
  47. package/dist/hub/discover.js +36 -13
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.js +4 -2
  50. package/dist/interchange/client/capabilities.d.ts +4 -0
  51. package/dist/interchange/client/capabilities.js +53 -0
  52. package/dist/interchange/client/catalog.d.ts +14 -0
  53. package/dist/interchange/client/catalog.js +52 -0
  54. package/dist/interchange/client/contracts.d.ts +47 -0
  55. package/dist/interchange/client/contracts.js +1 -0
  56. package/dist/interchange/client/projection.d.ts +6 -0
  57. package/dist/interchange/client/projection.js +92 -0
  58. package/dist/interchange/client/skills.d.ts +16 -0
  59. package/dist/interchange/client/skills.js +87 -0
  60. package/dist/interchange/client.d.ts +8 -0
  61. package/dist/interchange/client.js +6 -0
  62. package/dist/interchange/server/contracts.d.ts +65 -0
  63. package/dist/interchange/server/contracts.js +1 -0
  64. package/dist/interchange/server.d.ts +5 -0
  65. package/dist/interchange/server.js +172 -0
  66. package/dist/naming.d.ts +2 -0
  67. package/dist/naming.js +7 -0
  68. package/dist/oauth-discovery.d.ts +107 -1
  69. package/dist/oauth-discovery.js +292 -56
  70. package/dist/protocol/content.d.ts +35 -0
  71. package/dist/protocol/content.js +64 -0
  72. package/dist/protocol/envelopes.d.ts +14 -0
  73. package/dist/protocol/envelopes.js +22 -0
  74. package/dist/protocol/errors.d.ts +26 -0
  75. package/dist/protocol/errors.js +31 -0
  76. package/dist/protocol/extensions.d.ts +34 -0
  77. package/dist/protocol/extensions.js +86 -0
  78. package/dist/protocol/headers.d.ts +12 -0
  79. package/dist/protocol/headers.js +128 -0
  80. package/dist/protocol/index.d.ts +11 -0
  81. package/dist/protocol/index.js +11 -0
  82. package/dist/protocol/json-schema.d.ts +8 -0
  83. package/dist/protocol/json-schema.js +152 -0
  84. package/dist/protocol/legacy.d.ts +12 -0
  85. package/dist/protocol/legacy.js +18 -0
  86. package/dist/protocol/methods.d.ts +116 -0
  87. package/dist/protocol/methods.js +78 -0
  88. package/dist/protocol/resource-budget.d.ts +9 -0
  89. package/dist/protocol/resource-budget.js +206 -0
  90. package/dist/protocol/schemas.d.ts +115 -0
  91. package/dist/protocol/schemas.js +553 -0
  92. package/dist/protocol/types.d.ts +295 -0
  93. package/dist/protocol/types.js +3 -0
  94. package/dist/protocol/ui.d.ts +3 -0
  95. package/dist/protocol/ui.js +18 -0
  96. package/dist/server/contracts.d.ts +67 -0
  97. package/dist/server/contracts.js +1 -0
  98. package/dist/server/errors.d.ts +6 -0
  99. package/dist/server/errors.js +21 -0
  100. package/dist/server/extensions.d.ts +7 -0
  101. package/dist/server/extensions.js +61 -0
  102. package/dist/server/fixtures.d.ts +2 -0
  103. package/dist/server/fixtures.js +34 -0
  104. package/dist/server/legacy.d.ts +9 -0
  105. package/dist/server/legacy.js +435 -0
  106. package/dist/server/tools.d.ts +17 -0
  107. package/dist/server/tools.js +93 -0
  108. package/dist/server.d.ts +14 -0
  109. package/dist/server.js +406 -0
  110. package/dist/sidecar-bundle.d.ts +7 -7
  111. package/dist/sidecar-bundle.js +36 -29
  112. package/dist/test-server.js +12 -3
  113. package/dist/tool.d.ts +8 -0
  114. package/dist/tool.js +35 -19
  115. package/dist/url.js +8 -3
  116. package/docs/conformance.md +135 -0
  117. package/docs/integration-status.md +100 -0
  118. package/docs/legacy-server.md +58 -0
  119. package/docs/migration.md +143 -0
  120. package/docs/public-api.md +105 -0
  121. package/examples/README.md +28 -0
  122. package/examples/standalone-client.ts +14 -0
  123. package/examples/standalone-server.ts +37 -0
  124. package/package.json +44 -9
@@ -0,0 +1,435 @@
1
+ import { validateMcpIdentity } from "../auth/contracts.js";
2
+ import { MCP_PROTOCOL_VERSION, McpError, validateLegacyInitialize, validateJsonRpcRequest, validateRequest, validateRequestHeaders, validateResult, validateNotification, record, } from "../protocol/index.js";
3
+ import { errorPayload } from "./errors.js";
4
+ const REVISIONS = [
5
+ "2025-11-25",
6
+ "2025-06-18",
7
+ "2025-03-26",
8
+ ];
9
+ const METHODS = new Set([
10
+ "tools/list",
11
+ "tools/call",
12
+ "prompts/list",
13
+ "prompts/get",
14
+ "resources/list",
15
+ "resources/templates/list",
16
+ "resources/read",
17
+ "completion/complete",
18
+ ]);
19
+ const json = (value, status = 200, headers = {}) => Response.json(value, {
20
+ status,
21
+ headers: { "Cache-Control": "no-store", ...headers },
22
+ });
23
+ const failed = (error, id) => ({
24
+ jsonrpc: "2.0",
25
+ id: id ?? null,
26
+ error: errorPayload(error, "Legacy request failed"),
27
+ });
28
+ const partition = (identity) => identity === null
29
+ ? "public"
30
+ : JSON.stringify([
31
+ identity.tenantId,
32
+ identity.principalId,
33
+ identity.authorizationContextId,
34
+ ]);
35
+ /** Isolated legacy transport. No HTTP+SSE endpoint, GET stream, resumption, MRTR or extensions. */
36
+ export function createLegacyServer(options, providers) {
37
+ const configured = options.legacy?.versions ?? REVISIONS;
38
+ if (!configured.length ||
39
+ configured.some((version) => !REVISIONS.includes(version)))
40
+ throw new Error("Unsupported legacy revision");
41
+ const versions = REVISIONS.filter((version) => configured.includes(version));
42
+ const ttl = options.legacy?.sessionTtlMs ?? 30 * 60_000;
43
+ const maxSessions = options.legacy?.maxSessions ?? 1024;
44
+ if (!Number.isSafeInteger(ttl) ||
45
+ ttl <= 0 ||
46
+ ttl > 2_147_483_647 ||
47
+ !Number.isSafeInteger(maxSessions) ||
48
+ maxSessions <= 0)
49
+ throw new Error("Invalid legacy session limits");
50
+ let closed = false;
51
+ function assertOpen() {
52
+ if (closed)
53
+ throw new McpError("Legacy server is closed", 503, -32603);
54
+ }
55
+ const sessions = new Map();
56
+ const capabilities = {};
57
+ for (const method of METHODS)
58
+ if (providers[method]) {
59
+ const family = method.split("/")[0];
60
+ capabilities[family === "completion" ? "completions" : family] = {};
61
+ }
62
+ function remove(id, session) {
63
+ session.closed = true;
64
+ clearTimeout(session.timer);
65
+ sessions.delete(id);
66
+ for (const controller of session.active.values())
67
+ controller.abort();
68
+ session.active.clear();
69
+ }
70
+ function lookup(request, identity) {
71
+ const id = request.headers.get("Mcp-Session-Id");
72
+ if (!id)
73
+ throw new McpError("Session ID required", 400, -32600);
74
+ const session = sessions.get(id);
75
+ if (session && session.expires <= Date.now())
76
+ remove(id, session);
77
+ if (!session ||
78
+ !sessions.has(id) ||
79
+ partition(session.identity) !== partition(identity))
80
+ throw new McpError("Session not found", 404, -32600);
81
+ const version = request.headers.get("MCP-Protocol-Version");
82
+ if ((version !== null && version !== session.version) ||
83
+ (version === null && session.version !== "2025-03-26"))
84
+ throw new McpError("Protocol version does not match session", 400, -32600);
85
+ return [id, session];
86
+ }
87
+ async function authorized(identity, resource, action) {
88
+ return (options.auth === "public" ||
89
+ (identity !== null &&
90
+ (await options.auth.authorize(identity, { resource, action }))));
91
+ }
92
+ function validation(session, method, params) {
93
+ return {
94
+ era: "legacy",
95
+ protocolVersion: session.version,
96
+ extensions: {},
97
+ clientCapabilities: session.capabilities,
98
+ serverCapabilities: capabilities,
99
+ ...providers[method]?.schemas?.(params),
100
+ };
101
+ }
102
+ function notification(message, session) {
103
+ const params = message.params ?? {};
104
+ if (message.method !== "notifications/initialized" &&
105
+ message.method !== "notifications/cancelled")
106
+ throw new McpError("Unsupported notification", 400, -32601);
107
+ validateNotification(message.method, params, {
108
+ era: "legacy",
109
+ extensions: {},
110
+ });
111
+ if (message.method === "notifications/initialized")
112
+ session.initialized = true;
113
+ else
114
+ session.active.get(params.requestId)?.abort();
115
+ }
116
+ async function run(message, request, session, identity, send) {
117
+ const id = message.id;
118
+ let controller;
119
+ let abortListener;
120
+ try {
121
+ if (session.used.has(id))
122
+ throw new McpError("Request ID already used in session", 400, -32600);
123
+ if (session.used.size >= 10_000)
124
+ throw new McpError("Session request limit reached; initialize a new session", 429, -32600);
125
+ session.used.add(id);
126
+ if (message.method === "ping")
127
+ return { jsonrpc: "2.0", id, result: {} };
128
+ if (!session.initialized)
129
+ throw new McpError("Session not initialized", 400, -32600);
130
+ if (!METHODS.has(message.method) || !providers[message.method])
131
+ throw new McpError("Method not available in legacy mode", 404, -32601);
132
+ const provider = providers[message.method];
133
+ const params = message.params ?? {};
134
+ const pair = provider.authorization
135
+ ? provider.authorization(params)
136
+ : { resource: provider.resource, action: provider.action };
137
+ controller = new AbortController();
138
+ session.active.set(id, controller);
139
+ if (session.expires <= Date.now() || session.closed)
140
+ controller.abort();
141
+ const signal = controller.signal;
142
+ signal.throwIfAborted();
143
+ const cancelled = new Promise((_resolve, reject) => {
144
+ abortListener = () => reject(new McpError("Request cancelled", undefined, -32603));
145
+ signal.addEventListener("abort", abortListener, { once: true });
146
+ });
147
+ if ((pair === false && options.auth !== "public") ||
148
+ (pair !== null &&
149
+ pair !== false &&
150
+ !(await Promise.race([
151
+ authorized(identity, pair.resource, pair.action),
152
+ cancelled,
153
+ ]))))
154
+ throw new McpError("Operation forbidden", 403, -32602);
155
+ const context = validation(session, message.method, params);
156
+ validateRequest(message.method, params, context);
157
+ validateRequestHeaders(request.headers, message.method, params, context);
158
+ let lastProgress = -Infinity;
159
+ const providerContext = {
160
+ identity,
161
+ auth: options.auth,
162
+ requestId: id,
163
+ protocolVersion: session.version,
164
+ clientCapabilities: session.capabilities,
165
+ signal,
166
+ notify() {
167
+ throw new McpError("Legacy change notifications are not supported", 400, -32601);
168
+ },
169
+ reportProgress(progress, total, message) {
170
+ const token = params._meta === undefined
171
+ ? undefined
172
+ : record(params._meta).progressToken;
173
+ if (token === undefined || signal.aborted)
174
+ return;
175
+ if (progress <= lastProgress)
176
+ throw new McpError("Progress must increase", 500, -32603);
177
+ const notification = {
178
+ progressToken: token,
179
+ progress,
180
+ ...(total === undefined ? {} : { total }),
181
+ ...(message === undefined ? {} : { message }),
182
+ };
183
+ validateNotification("notifications/progress", notification, context);
184
+ lastProgress = progress;
185
+ send({
186
+ jsonrpc: "2.0",
187
+ method: "notifications/progress",
188
+ params: notification,
189
+ });
190
+ },
191
+ };
192
+ signal.throwIfAborted();
193
+ const result = await Promise.race([
194
+ Promise.resolve().then(() => {
195
+ signal.throwIfAborted();
196
+ return provider.handle(params, providerContext);
197
+ }),
198
+ cancelled,
199
+ ]);
200
+ const validated = validateResult(message.method, result, context);
201
+ // Ordinary results only; the shared validator rejects task/MRTR before projection.
202
+ const { resultType: _type, ttlMs: _ttl, cacheScope: _cache, ...ordinary } = validated;
203
+ if (session.version === "2025-03-26") {
204
+ // Structured tool output and resource links arrived in 2025-06-18.
205
+ // Preserve their information in text for March clients rather than emit
206
+ // a newer content discriminator or silently discard structured data.
207
+ const content = (value) => {
208
+ const block = record(value);
209
+ return block.type === "resource_link"
210
+ ? {
211
+ type: "text",
212
+ text: `${String(block.name)}: ${String(block.uri)}`,
213
+ }
214
+ : block;
215
+ };
216
+ if (Array.isArray(ordinary.content)) {
217
+ const compatibleContent = ordinary.content.map(content);
218
+ if (ordinary.structuredContent !== undefined)
219
+ compatibleContent.push({
220
+ type: "text",
221
+ text: JSON.stringify(ordinary.structuredContent),
222
+ });
223
+ ordinary.content = compatibleContent;
224
+ }
225
+ if (Array.isArray(ordinary.messages))
226
+ ordinary.messages = ordinary.messages.map((value) => {
227
+ const message = record(value);
228
+ return { ...message, content: content(message.content) };
229
+ });
230
+ delete ordinary.structuredContent;
231
+ if (Array.isArray(ordinary.tools))
232
+ ordinary.tools = ordinary.tools.map((tool) => {
233
+ const { outputSchema: _output, ...definition } = record(tool);
234
+ return definition;
235
+ });
236
+ }
237
+ return { jsonrpc: "2.0", id, result: ordinary };
238
+ }
239
+ catch (error) {
240
+ return failed(error, id);
241
+ }
242
+ finally {
243
+ if (controller && abortListener)
244
+ controller.signal.removeEventListener("abort", abortListener);
245
+ if (controller)
246
+ session.active.delete(id);
247
+ }
248
+ }
249
+ return {
250
+ versions,
251
+ async lifecycle(request) {
252
+ try {
253
+ assertOpen();
254
+ const rawIdentity = options.auth === "public"
255
+ ? null
256
+ : await options.auth.authenticate(request);
257
+ assertOpen();
258
+ if (options.auth !== "public" && !rawIdentity)
259
+ return (options.challenge?.(401) ?? new Response(null, { status: 401 }));
260
+ const identity = rawIdentity === null ? null : validateMcpIdentity(rawIdentity);
261
+ const [id, session] = lookup(request, identity);
262
+ if (request.method === "DELETE") {
263
+ remove(id, session);
264
+ return new Response(null, { status: 204 });
265
+ }
266
+ return new Response(null, {
267
+ status: 405,
268
+ headers: { Allow: "POST, DELETE" },
269
+ });
270
+ }
271
+ catch (error) {
272
+ return json(failed(error), error instanceof McpError ? (error.status ?? 400) : 500);
273
+ }
274
+ },
275
+ async handle(request, body, identity) {
276
+ const entries = Array.isArray(body) ? body : [body];
277
+ const modern = request.headers.get("MCP-Protocol-Version") === MCP_PROTOCOL_VERSION ||
278
+ entries.some((entry) => {
279
+ if (entry === null || typeof entry !== "object")
280
+ return false;
281
+ const params = entry.params;
282
+ if (params === null || typeof params !== "object")
283
+ return false;
284
+ const meta = params._meta;
285
+ return (meta !== null &&
286
+ typeof meta === "object" &&
287
+ meta["io.modelcontextprotocol/protocolVersion"] === MCP_PROTOCOL_VERSION);
288
+ });
289
+ if (modern)
290
+ return undefined;
291
+ const first = entries[0];
292
+ const initializing = first !== null &&
293
+ typeof first === "object" &&
294
+ first.method === "initialize";
295
+ if (!initializing &&
296
+ !request.headers.has("Mcp-Session-Id") &&
297
+ request.headers.has("Mcp-Method"))
298
+ return undefined;
299
+ let id;
300
+ try {
301
+ assertOpen();
302
+ if (initializing && !Array.isArray(body)) {
303
+ const message = validateJsonRpcRequest(body);
304
+ id = message.id;
305
+ if (request.headers.has("Mcp-Session-Id"))
306
+ throw new McpError("Initialize must start a new session", 400, -32600);
307
+ const headerVersion = request.headers.get("MCP-Protocol-Version");
308
+ if (headerVersion !== null &&
309
+ !versions.some((version) => version === headerVersion))
310
+ throw new McpError("Unsupported legacy protocol header", 400, -32600);
311
+ const params = validateLegacyInitialize(message.params);
312
+ const permitted = await authorized(identity, "mcp:server", "initialize");
313
+ assertOpen();
314
+ if (!permitted)
315
+ return (options.challenge?.(403) ?? new Response(null, { status: 403 }));
316
+ for (const [id, session] of sessions)
317
+ if (session.expires <= Date.now())
318
+ remove(id, session);
319
+ if (sessions.size >= maxSessions)
320
+ throw new McpError("Legacy session capacity reached", 503, -32603);
321
+ const version = versions.find((version) => version === params.protocolVersion) ??
322
+ versions[0];
323
+ const sessionId = crypto.randomUUID();
324
+ const session = {
325
+ identity,
326
+ version,
327
+ capabilities: params.capabilities,
328
+ initialized: false,
329
+ closed: false,
330
+ used: new Set([id]),
331
+ active: new Map(),
332
+ expires: Date.now() + ttl,
333
+ timer: setTimeout(() => {
334
+ const current = sessions.get(sessionId);
335
+ if (current)
336
+ remove(sessionId, current);
337
+ }, ttl),
338
+ };
339
+ session.timer.unref();
340
+ sessions.set(sessionId, session);
341
+ return json({
342
+ jsonrpc: "2.0",
343
+ id,
344
+ result: {
345
+ protocolVersion: version,
346
+ capabilities,
347
+ serverInfo: options.serverInfo,
348
+ },
349
+ }, 200, { "Mcp-Session-Id": sessionId });
350
+ }
351
+ const [, session] = lookup(request, identity);
352
+ if (Array.isArray(body) &&
353
+ (session.version !== "2025-03-26" || body.length === 0))
354
+ throw new McpError("Batch unsupported for this revision", 400, -32600);
355
+ if (entries.some((entry) => entry !== null &&
356
+ typeof entry === "object" &&
357
+ entry.method === "initialize"))
358
+ throw new McpError("Initialize cannot be batched or repeated", 400, -32600);
359
+ // This shim never issues server-to-client requests, so unsolicited responses
360
+ // cannot be accepted (including the March response-batch shape).
361
+ if (entries.some((entry) => entry !== null &&
362
+ typeof entry === "object" &&
363
+ (Object.hasOwn(entry, "result") || Object.hasOwn(entry, "error"))))
364
+ throw new McpError("Unsolicited JSON-RPC response", 400, -32600);
365
+ const messages = entries.map((entry) => {
366
+ try {
367
+ return { message: validateJsonRpcRequest(entry, true) };
368
+ }
369
+ catch (error) {
370
+ return { error: failed(error) };
371
+ }
372
+ });
373
+ if (!Array.isArray(body) && messages[0]?.error)
374
+ return json(messages[0].error, 400);
375
+ if (!messages.some((item) => item.error || (item.message && "id" in item.message))) {
376
+ for (const item of messages)
377
+ if (item.message)
378
+ notification(item.message, session);
379
+ return new Response(null, { status: 202 });
380
+ }
381
+ let disconnected = false;
382
+ const stream = new ReadableStream({
383
+ start(sink) {
384
+ const send = (value) => {
385
+ if (!disconnected)
386
+ sink.enqueue(new TextEncoder().encode(`data: ${JSON.stringify(value)}\n\n`));
387
+ };
388
+ void Promise.all(messages.map(async (item) => {
389
+ if (item.error) {
390
+ send(item.error);
391
+ return;
392
+ }
393
+ if (!item.message)
394
+ return;
395
+ if (!("id" in item.message)) {
396
+ try {
397
+ notification(item.message, session);
398
+ }
399
+ catch {
400
+ /* Notifications never receive JSON-RPC responses. */
401
+ }
402
+ return;
403
+ }
404
+ send(await run(item.message, request, session, identity, send));
405
+ })).finally(() => {
406
+ if (!disconnected) {
407
+ disconnected = true;
408
+ sink.close();
409
+ }
410
+ });
411
+ },
412
+ // In legacy HTTP a lost connection is NOT a cancellation notification.
413
+ cancel() {
414
+ disconnected = true;
415
+ },
416
+ });
417
+ return new Response(stream, {
418
+ headers: {
419
+ "Content-Type": "text/event-stream",
420
+ "Cache-Control": "no-store",
421
+ "X-Accel-Buffering": "no",
422
+ },
423
+ });
424
+ }
425
+ catch (error) {
426
+ return json(failed(error, id), error instanceof McpError ? (error.status ?? 400) : 500);
427
+ }
428
+ },
429
+ close() {
430
+ closed = true;
431
+ for (const [id, session] of sessions)
432
+ remove(id, session);
433
+ },
434
+ };
435
+ }
@@ -0,0 +1,17 @@
1
+ import type { McpAuthPolicy } from "../auth/contracts.js";
2
+ import type { McpOperationOutcome, McpProviders, McpRequestContext } from "./contracts.js";
3
+ export type McpToolBinding = {
4
+ definition: {
5
+ name: string;
6
+ description?: string;
7
+ inputSchema: Record<string, unknown>;
8
+ outputSchema?: Record<string, unknown>;
9
+ [key: string]: unknown;
10
+ };
11
+ resource: string;
12
+ action: string;
13
+ visible?: (context: McpRequestContext) => boolean;
14
+ execute: (args: Record<string, unknown>, context: McpRequestContext, params: Record<string, unknown>) => McpOperationOutcome | Promise<McpOperationOutcome>;
15
+ };
16
+ /** Finite catalog; authorization is rechecked even after successful discovery. */
17
+ export declare function createToolProviders(tools: readonly McpToolBinding[], auth: McpAuthPolicy | "public"): McpProviders;
@@ -0,0 +1,93 @@
1
+ import { McpError, validateSchema, validateInstance, } from "../protocol/index.js";
2
+ function activePolicy(providerAuth, serverAuth) {
3
+ if (providerAuth === "public")
4
+ return serverAuth;
5
+ if (serverAuth === "public" || serverAuth === providerAuth)
6
+ return providerAuth;
7
+ return {
8
+ authorize: async (identity, operation) => (await providerAuth.authorize(identity, operation)) &&
9
+ (await serverAuth.authorize(identity, operation)),
10
+ };
11
+ }
12
+ /** Finite catalog; authorization is rechecked even after successful discovery. */
13
+ export function createToolProviders(tools, auth) {
14
+ const byName = new Map();
15
+ for (const tool of tools) {
16
+ if (byName.has(tool.definition.name))
17
+ throw new Error(`Duplicate tool: ${tool.definition.name}`);
18
+ validateSchema(tool.definition.inputSchema);
19
+ if (tool.definition.outputSchema)
20
+ validateSchema(tool.definition.outputSchema);
21
+ byName.set(tool.definition.name, tool);
22
+ }
23
+ const authorizationPairs = [...byName.values()].map((tool) => ({
24
+ resource: tool.resource,
25
+ action: tool.action,
26
+ }));
27
+ const fallback = authorizationPairs[0] ?? {
28
+ resource: "mcp:server",
29
+ action: "discover",
30
+ };
31
+ const allowed = async (tool, context) => {
32
+ const policy = activePolicy(auth, context.auth);
33
+ return ((tool.visible?.(context) ?? true) &&
34
+ (policy === "public" ||
35
+ (context.identity !== null &&
36
+ (await policy.authorize(context.identity, {
37
+ resource: tool.resource,
38
+ action: tool.action,
39
+ })))));
40
+ };
41
+ return {
42
+ "tools/list": {
43
+ resource: fallback.resource,
44
+ action: fallback.action,
45
+ authorizationPairs,
46
+ authorization: () => null,
47
+ async handle(params, context) {
48
+ if (params.cursor !== undefined)
49
+ throw new McpError("Unknown cursor", 400, -32602);
50
+ const visible = [];
51
+ for (const tool of byName.values())
52
+ if (await allowed(tool, context))
53
+ visible.push(tool.definition);
54
+ return { tools: visible };
55
+ },
56
+ },
57
+ "tools/call": {
58
+ resource: fallback.resource,
59
+ action: fallback.action,
60
+ authorizationPairs,
61
+ authorization(params) {
62
+ const tool = byName.get(String(params.name));
63
+ // Null would skip the gate and reveal whether the name is in the catalog.
64
+ return tool ? { resource: tool.resource, action: tool.action } : false;
65
+ },
66
+ schemas(params) {
67
+ const definition = byName.get(String(params.name))?.definition;
68
+ return definition
69
+ ? {
70
+ inputSchema: definition.inputSchema,
71
+ ...(definition.outputSchema
72
+ ? { outputSchema: definition.outputSchema }
73
+ : {}),
74
+ }
75
+ : {};
76
+ },
77
+ async handle(params, context) {
78
+ const tool = byName.get(String(params.name));
79
+ if (!tool || !(await allowed(tool, context)))
80
+ throw new McpError("Tool unavailable", 403, -32602);
81
+ const args = (params.arguments ?? {});
82
+ validateInstance(tool.definition.inputSchema, args);
83
+ const result = await tool.execute(args, context, params);
84
+ if ((result.resultType === undefined ||
85
+ result.resultType === "complete") &&
86
+ result.isError !== true &&
87
+ tool.definition.outputSchema)
88
+ validateInstance(tool.definition.outputSchema, result.structuredContent);
89
+ return result;
90
+ },
91
+ },
92
+ };
93
+ }
@@ -0,0 +1,14 @@
1
+ import type { McpServerOptions, McpServer } from "./server/contracts.js";
2
+ export type * from "./server/contracts.js";
3
+ export { createTaskProviders, createSkillProviders, } from "./server/extensions.js";
4
+ export { createToolProviders } from "./server/tools.js";
5
+ export type { McpToolBinding } from "./server/tools.js";
6
+ /** Stateless modern endpoint with an independently gated legacy session transport. */
7
+ export declare function createMcpServer(options: McpServerOptions): McpServer;
8
+ export { createSkillPublisher } from "./extensions/skills/index.js";
9
+ export type * from "./extensions/skills/contracts.js";
10
+ export { createTaskRuntime, validateTaskRecord, } from "./extensions/tasks/index.js";
11
+ export type * from "./extensions/tasks/contracts.js";
12
+ export { createMcpResourceServerAuth } from "./auth/resource-server.js";
13
+ export type { McpResourceServerOptions } from "./auth/resource-server.js";
14
+ export type { McpAuthPolicy, McpIdentity } from "./auth/contracts.js";