@absolutejs/mcp 0.0.1 → 0.1.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 +6 -8
- package/dist/index.js +228 -37
- package/dist/src/client.d.ts +50 -0
- package/dist/src/core.d.ts +11 -0
- package/dist/src/dispatch.d.ts +4 -3
- package/dist/src/handler.d.ts +2 -0
- package/dist/src/index.d.ts +22 -13
- package/dist/src/types.d.ts +43 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,14 +114,12 @@ different scope, a stricter `authorize` (role + MFA + a kill switch, re-checked
|
|
|
114
114
|
live), a rate-limit `beforeCall`, and an audit `onCall`:
|
|
115
115
|
|
|
116
116
|
```ts
|
|
117
|
-
app
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}),
|
|
124
|
-
);
|
|
117
|
+
app.use(mcpServer({ path: "/mcp" /* member */ })).use(
|
|
118
|
+
mcpServer({
|
|
119
|
+
path: "/mcp/admin",
|
|
120
|
+
scopesSupported: ["openid", "mcp:admin"] /* stricter */,
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
125
123
|
```
|
|
126
124
|
|
|
127
125
|
Only one endpoint per app should set `serveRootMetadata` (the un-suffixed alias).
|
package/dist/index.js
CHANGED
|
@@ -29,6 +29,155 @@ var verifyBearer = async (config) => {
|
|
|
29
29
|
return { error: "Token has no subject" };
|
|
30
30
|
return { payload, scopes, subject };
|
|
31
31
|
};
|
|
32
|
+
// src/client.ts
|
|
33
|
+
var DEFAULT_TIMEOUT_MS = 30000;
|
|
34
|
+
var DEFAULT_PROTOCOL = "2025-06-18";
|
|
35
|
+
|
|
36
|
+
class McpClientError extends Error {
|
|
37
|
+
code;
|
|
38
|
+
status;
|
|
39
|
+
constructor(message, options = {}) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "McpClientError";
|
|
42
|
+
this.code = options.code;
|
|
43
|
+
this.status = options.status;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
var parseBody = async (response, maxBytes) => {
|
|
47
|
+
const text = await response.text();
|
|
48
|
+
if (maxBytes > 0 && text.length > maxBytes) {
|
|
49
|
+
throw new McpClientError("Response exceeded the size cap", {
|
|
50
|
+
status: response.status
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
54
|
+
if (!contentType.includes("text/event-stream")) {
|
|
55
|
+
return JSON.parse(text);
|
|
56
|
+
}
|
|
57
|
+
const messages = text.split(/\r?\n/).filter((line) => line.startsWith("data:")).map((line) => line.slice("data:".length).trim()).filter((chunk) => chunk.length > 0);
|
|
58
|
+
for (let index = messages.length - 1;index >= 0; index -= 1) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(messages[index] ?? "");
|
|
61
|
+
if (isRecord(parsed) && (("result" in parsed) || ("error" in parsed))) {
|
|
62
|
+
return parsed;
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
throw new McpClientError("No JSON-RPC response in the event stream");
|
|
67
|
+
};
|
|
68
|
+
var createMcpClient = (options) => {
|
|
69
|
+
const doFetch = options.request ?? fetch;
|
|
70
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
71
|
+
const maxBytes = options.maxResponseBytes ?? 0;
|
|
72
|
+
let protocolVersion = options.protocolVersion ?? DEFAULT_PROTOCOL;
|
|
73
|
+
let sessionId = null;
|
|
74
|
+
let nextId = 1;
|
|
75
|
+
const rpc = async (method, params) => {
|
|
76
|
+
const controller = new AbortController;
|
|
77
|
+
const timer = setTimeout(() => {
|
|
78
|
+
controller.abort();
|
|
79
|
+
}, timeoutMs);
|
|
80
|
+
try {
|
|
81
|
+
const headers = {
|
|
82
|
+
accept: "application/json, text/event-stream",
|
|
83
|
+
"content-type": "application/json",
|
|
84
|
+
"mcp-protocol-version": protocolVersion,
|
|
85
|
+
...options.headers
|
|
86
|
+
};
|
|
87
|
+
if (sessionId !== null)
|
|
88
|
+
headers["mcp-session-id"] = sessionId;
|
|
89
|
+
const response = await doFetch(options.url, {
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
id: nextId++,
|
|
92
|
+
jsonrpc: "2.0",
|
|
93
|
+
method,
|
|
94
|
+
...params === undefined ? {} : { params }
|
|
95
|
+
}),
|
|
96
|
+
headers,
|
|
97
|
+
method: "POST",
|
|
98
|
+
signal: controller.signal
|
|
99
|
+
});
|
|
100
|
+
const captured = response.headers.get("mcp-session-id");
|
|
101
|
+
if (captured)
|
|
102
|
+
sessionId = captured;
|
|
103
|
+
if (response.status === 401) {
|
|
104
|
+
throw new McpClientError("The MCP server rejected the credentials", {
|
|
105
|
+
status: 401
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const payload = await parseBody(response, maxBytes);
|
|
109
|
+
if (!isRecord(payload)) {
|
|
110
|
+
throw new McpClientError("Malformed JSON-RPC response");
|
|
111
|
+
}
|
|
112
|
+
if (isRecord(payload.error)) {
|
|
113
|
+
const message = typeof payload.error.message === "string" ? payload.error.message : "MCP error";
|
|
114
|
+
const code = typeof payload.error.code === "number" ? payload.error.code : undefined;
|
|
115
|
+
throw new McpClientError(message, { code });
|
|
116
|
+
}
|
|
117
|
+
return payload.result;
|
|
118
|
+
} finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const notify = async (method) => {
|
|
123
|
+
const headers = {
|
|
124
|
+
"content-type": "application/json",
|
|
125
|
+
"mcp-protocol-version": protocolVersion,
|
|
126
|
+
...options.headers
|
|
127
|
+
};
|
|
128
|
+
if (sessionId !== null)
|
|
129
|
+
headers["mcp-session-id"] = sessionId;
|
|
130
|
+
await doFetch(options.url, {
|
|
131
|
+
body: JSON.stringify({ jsonrpc: "2.0", method }),
|
|
132
|
+
headers,
|
|
133
|
+
method: "POST"
|
|
134
|
+
}).catch(() => {
|
|
135
|
+
return;
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
const initialize = async () => {
|
|
139
|
+
const result = await rpc("initialize", {
|
|
140
|
+
capabilities: {},
|
|
141
|
+
clientInfo: options.clientInfo ?? {
|
|
142
|
+
name: "@absolutejs/mcp",
|
|
143
|
+
version: "0"
|
|
144
|
+
},
|
|
145
|
+
protocolVersion
|
|
146
|
+
});
|
|
147
|
+
if (isRecord(result) && typeof result.protocolVersion === "string") {
|
|
148
|
+
protocolVersion = result.protocolVersion;
|
|
149
|
+
}
|
|
150
|
+
await notify("notifications/initialized");
|
|
151
|
+
return isRecord(result) ? result : {};
|
|
152
|
+
};
|
|
153
|
+
const listTools = async () => {
|
|
154
|
+
const result = await rpc("tools/list");
|
|
155
|
+
const tools = isRecord(result) && Array.isArray(result.tools) ? result.tools : [];
|
|
156
|
+
return tools.filter(isRecord).map((tool) => ({
|
|
157
|
+
annotations: isRecord(tool.annotations) ? tool.annotations : undefined,
|
|
158
|
+
description: typeof tool.description === "string" ? tool.description : undefined,
|
|
159
|
+
inputSchema: isRecord(tool.inputSchema) ? tool.inputSchema : undefined,
|
|
160
|
+
name: typeof tool.name === "string" ? tool.name : "",
|
|
161
|
+
outputSchema: isRecord(tool.outputSchema) ? tool.outputSchema : undefined
|
|
162
|
+
}));
|
|
163
|
+
};
|
|
164
|
+
const callTool = async (name, args) => {
|
|
165
|
+
const result = await rpc("tools/call", { arguments: args ?? {}, name });
|
|
166
|
+
if (isRecord(result) && Array.isArray(result.content)) {
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
return { content: [], isError: false };
|
|
170
|
+
};
|
|
171
|
+
const listResources = async () => {
|
|
172
|
+
const result = await rpc("resources/list");
|
|
173
|
+
return isRecord(result) && Array.isArray(result.resources) ? result.resources : [];
|
|
174
|
+
};
|
|
175
|
+
const readResource = async (uri) => rpc("resources/read", { uri });
|
|
176
|
+
const ping = async () => {
|
|
177
|
+
await rpc("ping");
|
|
178
|
+
};
|
|
179
|
+
return { callTool, initialize, listResources, listTools, ping, readResource };
|
|
180
|
+
};
|
|
32
181
|
// src/jsonrpc.ts
|
|
33
182
|
var JSONRPC_PARSE_ERROR = -32700;
|
|
34
183
|
var JSONRPC_INVALID_REQUEST = -32600;
|
|
@@ -69,6 +218,15 @@ var negotiateProtocol = (supported, params) => {
|
|
|
69
218
|
const requested = isRecord(params) && typeof params.protocolVersion === "string" ? params.protocolVersion : preferred;
|
|
70
219
|
return supported.includes(requested) ? requested : preferred;
|
|
71
220
|
};
|
|
221
|
+
var scopeAllows = (tool, scopes) => tool.scope === undefined || scopes.includes(tool.scope);
|
|
222
|
+
var normalizeResult = (value) => {
|
|
223
|
+
if (typeof value === "string") {
|
|
224
|
+
return { content: [{ text: value, type: "text" }], isError: false };
|
|
225
|
+
}
|
|
226
|
+
if (Array.isArray(value))
|
|
227
|
+
return { content: value, isError: false };
|
|
228
|
+
return { isError: false, ...value };
|
|
229
|
+
};
|
|
72
230
|
var initialize = (config, id, params) => {
|
|
73
231
|
const supported = config.supportedProtocols ?? DEFAULT_PROTOCOLS;
|
|
74
232
|
const capabilities = {
|
|
@@ -86,19 +244,20 @@ var initialize = (config, id, params) => {
|
|
|
86
244
|
serverInfo: config.serverInfo
|
|
87
245
|
});
|
|
88
246
|
};
|
|
89
|
-
var toolsList = async (config, caller, id) => {
|
|
247
|
+
var toolsList = async (config, caller, scopes, id) => {
|
|
90
248
|
const tools = await config.tools({ caller, meta: {} });
|
|
91
249
|
return rpcResult(id, {
|
|
92
|
-
tools: Object.entries(tools).map(([name, tool]) => ({
|
|
250
|
+
tools: Object.entries(tools).filter(([, tool]) => scopeAllows(tool, scopes)).map(([name, tool]) => ({
|
|
93
251
|
annotations: tool.annotations,
|
|
94
252
|
description: tool.description,
|
|
95
253
|
inputSchema: tool.inputSchema,
|
|
96
|
-
name
|
|
254
|
+
name,
|
|
255
|
+
...tool.outputSchema === undefined ? {} : { outputSchema: tool.outputSchema }
|
|
97
256
|
}))
|
|
98
257
|
});
|
|
99
258
|
};
|
|
100
259
|
var errorResult = (id, text) => rpcResult(id, { content: [{ text, type: "text" }], isError: true });
|
|
101
|
-
var toolsCall = async (config, caller, id, params) => {
|
|
260
|
+
var toolsCall = async (config, caller, scopes, id, params) => {
|
|
102
261
|
if (!isRecord(params) || typeof params.name !== "string") {
|
|
103
262
|
return rpcError(id, JSONRPC_INVALID_PARAMS, "tools/call needs a name");
|
|
104
263
|
}
|
|
@@ -112,17 +271,15 @@ var toolsCall = async (config, caller, id, params) => {
|
|
|
112
271
|
}
|
|
113
272
|
const tools = await config.tools({ caller, meta });
|
|
114
273
|
const tool = tools[name];
|
|
115
|
-
if (!tool)
|
|
274
|
+
if (!tool || !scopeAllows(tool, scopes)) {
|
|
116
275
|
return rpcError(id, JSONRPC_INVALID_PARAMS, `Unknown tool: ${name}`);
|
|
276
|
+
}
|
|
117
277
|
let ok = false;
|
|
118
278
|
let response;
|
|
119
279
|
try {
|
|
120
|
-
const
|
|
121
|
-
ok = true;
|
|
122
|
-
response = rpcResult(id,
|
|
123
|
-
content: [{ text, type: "text" }],
|
|
124
|
-
isError: false
|
|
125
|
-
});
|
|
280
|
+
const result = normalizeResult(await tool.handler(args));
|
|
281
|
+
ok = result.isError !== true;
|
|
282
|
+
response = rpcResult(id, result);
|
|
126
283
|
} catch (error) {
|
|
127
284
|
const detail = error instanceof Error ? error.message : "unknown error";
|
|
128
285
|
response = errorResult(id, `Tool failed: ${detail}`);
|
|
@@ -187,7 +344,7 @@ var resourcesRead = async (config, caller, id, params) => {
|
|
|
187
344
|
]
|
|
188
345
|
});
|
|
189
346
|
};
|
|
190
|
-
var dispatchMcp = async (config, caller, message) => {
|
|
347
|
+
var dispatchMcp = async (config, caller, scopes, message) => {
|
|
191
348
|
if (!isRecord(message) || message.jsonrpc !== "2.0") {
|
|
192
349
|
return rpcError(null, JSONRPC_INVALID_REQUEST, "Not a JSON-RPC 2.0 message");
|
|
193
350
|
}
|
|
@@ -201,9 +358,10 @@ var dispatchMcp = async (config, caller, message) => {
|
|
|
201
358
|
if (method === "ping")
|
|
202
359
|
return rpcResult(id, {});
|
|
203
360
|
if (method === "tools/list")
|
|
204
|
-
return toolsList(config, caller, id);
|
|
205
|
-
if (method === "tools/call")
|
|
206
|
-
return toolsCall(config, caller, id, params);
|
|
361
|
+
return toolsList(config, caller, scopes, id);
|
|
362
|
+
if (method === "tools/call") {
|
|
363
|
+
return toolsCall(config, caller, scopes, id, params);
|
|
364
|
+
}
|
|
207
365
|
if (method === "prompts/list")
|
|
208
366
|
return promptsList(config, id);
|
|
209
367
|
if (method === "prompts/get")
|
|
@@ -222,31 +380,61 @@ var protectedResourceMetadata = (input) => ({
|
|
|
222
380
|
scopes_supported: input.scopes ?? []
|
|
223
381
|
});
|
|
224
382
|
var metadataPathFor = (path) => `/.well-known/oauth-protected-resource${path}`;
|
|
225
|
-
|
|
226
|
-
|
|
383
|
+
|
|
384
|
+
// src/core.ts
|
|
227
385
|
var ROOT_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
228
|
-
var
|
|
386
|
+
var JSON_HEADERS = {
|
|
387
|
+
"content-type": "application/json"
|
|
388
|
+
};
|
|
389
|
+
var metadataResponse = (config) => new Response(JSON.stringify(protectedResourceMetadata({
|
|
390
|
+
issuer: config.issuer,
|
|
391
|
+
resource: `${config.issuer}${config.path}`,
|
|
392
|
+
scopes: config.scopesSupported
|
|
393
|
+
})), { headers: JSON_HEADERS });
|
|
394
|
+
var runMcpPost = async (config, request, body) => {
|
|
395
|
+
const auth = await config.authorize(request);
|
|
396
|
+
if (!auth.ok) {
|
|
397
|
+
return unauthorized(`${config.issuer}${metadataPathFor(config.path)}`, auth.reason);
|
|
398
|
+
}
|
|
399
|
+
if (body === undefined || body === null) {
|
|
400
|
+
return rpcError(null, JSONRPC_PARSE_ERROR, "Invalid JSON");
|
|
401
|
+
}
|
|
402
|
+
if (Array.isArray(body)) {
|
|
403
|
+
return rpcError(null, JSONRPC_INVALID_REQUEST, "Batching is not supported");
|
|
404
|
+
}
|
|
405
|
+
return dispatchMcp(config, auth.caller, auth.scopes ?? [], body).catch(() => rpcError(null, JSONRPC_INVALID_REQUEST, "Internal error"));
|
|
406
|
+
};
|
|
407
|
+
var handleMcpRequest = async (config, request) => {
|
|
408
|
+
const { pathname } = new URL(request.url);
|
|
229
409
|
const metadataPath = metadataPathFor(config.path);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
});
|
|
236
|
-
const base = new Elysia().get(metadataPath, metadata).get(config.path, () => new Response(null, { status: HTTP_METHOD_NOT_ALLOWED })).post(config.path, async ({ body, request }) => {
|
|
237
|
-
const auth = await config.authorize(request);
|
|
238
|
-
if (!auth.ok)
|
|
239
|
-
return unauthorized(metadataUrl, auth.reason);
|
|
240
|
-
const message = body;
|
|
241
|
-
if (message === undefined || message === null) {
|
|
242
|
-
return rpcError(null, JSONRPC_PARSE_ERROR, "Invalid JSON");
|
|
410
|
+
if (request.method === "GET") {
|
|
411
|
+
if (pathname === metadataPath)
|
|
412
|
+
return metadataResponse(config);
|
|
413
|
+
if (config.serveRootMetadata && pathname === ROOT_METADATA_PATH) {
|
|
414
|
+
return metadataResponse(config);
|
|
243
415
|
}
|
|
244
|
-
if (
|
|
245
|
-
return
|
|
416
|
+
if (pathname === config.path) {
|
|
417
|
+
return new Response(null, { status: HTTP_METHOD_NOT_ALLOWED });
|
|
246
418
|
}
|
|
247
|
-
return
|
|
248
|
-
}
|
|
249
|
-
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
if (request.method === "POST" && pathname === config.path) {
|
|
422
|
+
const body = await request.json().catch(() => {
|
|
423
|
+
return;
|
|
424
|
+
});
|
|
425
|
+
return runMcpPost(config, request, body);
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/handler.ts
|
|
431
|
+
var createMcpHandler = (config) => (request) => handleMcpRequest(config, request);
|
|
432
|
+
// src/server.ts
|
|
433
|
+
import { Elysia } from "elysia";
|
|
434
|
+
var mcpServer = (config) => {
|
|
435
|
+
const metadataPath = metadataPathFor(config.path);
|
|
436
|
+
const base = new Elysia().get(metadataPath, () => metadataResponse(config)).get(config.path, () => new Response(null, { status: HTTP_METHOD_NOT_ALLOWED })).post(config.path, ({ body, request }) => runMcpPost(config, request, body));
|
|
437
|
+
const app = config.serveRootMetadata ? base.get(ROOT_METADATA_PATH, () => metadataResponse(config)) : base;
|
|
250
438
|
return app;
|
|
251
439
|
};
|
|
252
440
|
export {
|
|
@@ -254,5 +442,8 @@ export {
|
|
|
254
442
|
protectedResourceMetadata,
|
|
255
443
|
metadataPathFor,
|
|
256
444
|
mcpServer,
|
|
257
|
-
dispatchMcp
|
|
445
|
+
dispatchMcp,
|
|
446
|
+
createMcpHandler,
|
|
447
|
+
createMcpClient,
|
|
448
|
+
McpClientError
|
|
258
449
|
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { McpToolAnnotations, McpToolResult } from "./types";
|
|
2
|
+
export declare class McpClientError extends Error {
|
|
3
|
+
readonly code: number | undefined;
|
|
4
|
+
readonly status: number | undefined;
|
|
5
|
+
constructor(message: string, options?: {
|
|
6
|
+
code?: number;
|
|
7
|
+
status?: number;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
export type McpClientOptions = {
|
|
11
|
+
clientInfo?: {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
};
|
|
15
|
+
/** Sent on every request (e.g. `{ authorization: "Bearer …" }`). */
|
|
16
|
+
headers?: Record<string, string>;
|
|
17
|
+
/** Reject responses whose body exceeds this many bytes (0 = no cap). */
|
|
18
|
+
maxResponseBytes?: number;
|
|
19
|
+
protocolVersion?: string;
|
|
20
|
+
/** Inject a custom fetch (tests, proxies). Defaults to global fetch. */
|
|
21
|
+
request?: typeof fetch;
|
|
22
|
+
timeoutMs?: number;
|
|
23
|
+
url: string;
|
|
24
|
+
};
|
|
25
|
+
export type McpRemoteTool = {
|
|
26
|
+
annotations?: McpToolAnnotations;
|
|
27
|
+
description?: string;
|
|
28
|
+
inputSchema?: Record<string, unknown>;
|
|
29
|
+
name: string;
|
|
30
|
+
outputSchema?: Record<string, unknown>;
|
|
31
|
+
};
|
|
32
|
+
export type McpInitializeResult = {
|
|
33
|
+
capabilities?: Record<string, unknown>;
|
|
34
|
+
instructions?: string;
|
|
35
|
+
protocolVersion: string;
|
|
36
|
+
serverInfo?: {
|
|
37
|
+
name?: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
export type McpClient = {
|
|
43
|
+
callTool: (name: string, args?: unknown) => Promise<McpToolResult>;
|
|
44
|
+
initialize: () => Promise<McpInitializeResult>;
|
|
45
|
+
listResources: () => Promise<unknown[]>;
|
|
46
|
+
listTools: () => Promise<McpRemoteTool[]>;
|
|
47
|
+
ping: () => Promise<void>;
|
|
48
|
+
readResource: (uri: string) => Promise<unknown>;
|
|
49
|
+
};
|
|
50
|
+
export declare const createMcpClient: (options: McpClientOptions) => McpClient;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { McpServerConfig } from "./types";
|
|
2
|
+
export declare const ROOT_METADATA_PATH = "/.well-known/oauth-protected-resource";
|
|
3
|
+
export declare const metadataResponse: <Caller>(config: McpServerConfig<Caller>) => Response;
|
|
4
|
+
/** Run one POST: authorize → validate the (already-decoded) body → dispatch.
|
|
5
|
+
* The body is passed in because Elysia pre-parses it while a raw handler must
|
|
6
|
+
* parse it itself; both funnel through here so the logic lives in one place. */
|
|
7
|
+
export declare const runMcpPost: <Caller>(config: McpServerConfig<Caller>, request: Request, body: unknown) => Promise<Response>;
|
|
8
|
+
/** The full path-aware handler over web-standard Request/Response. Returns a
|
|
9
|
+
* Response for any MCP route (POST endpoint, GET 405, discovery metadata) and
|
|
10
|
+
* `null` for anything else, so a host can compose it with its own routes. */
|
|
11
|
+
export declare const handleMcpRequest: <Caller>(config: McpServerConfig<Caller>, request: Request) => Promise<Response | null>;
|
package/dist/src/dispatch.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServerConfig } from "./types";
|
|
2
|
-
/** Route one decoded JSON-RPC message to its handler.
|
|
3
|
-
*
|
|
4
|
-
|
|
2
|
+
/** Route one decoded JSON-RPC message to its handler. `scopes` are the caller's
|
|
3
|
+
* granted scopes (from `authorize`); they gate scope-restricted tools.
|
|
4
|
+
* Notifications (no `id`) get a bare 202; unknown methods get method-not-found. */
|
|
5
|
+
export declare const dispatchMcp: <Caller>(config: McpServerConfig<Caller>, caller: Caller, scopes: string[], message: unknown) => Promise<Response>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `@absolutejs/mcp` — serve a
|
|
3
|
-
*
|
|
2
|
+
* `@absolutejs/mcp` — serve (and consume) a Model Context Protocol endpoint
|
|
3
|
+
* over streamable HTTP, from a tool/prompt/resource registry.
|
|
4
4
|
*
|
|
5
|
-
* Define the endpoint once
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* **Serve.** Define the endpoint once and mount it: {@link mcpServer} returns an
|
|
6
|
+
* Elysia plugin, or {@link createMcpHandler} returns a framework-agnostic
|
|
7
|
+
* `(request) => Response | null` for Bun.serve / Hono / Next.js / Workers. You
|
|
8
|
+
* supply WHICH tools to expose and HOW to authorize a request into a caller
|
|
9
|
+
* ({@link verifyBearer} does the standard OAuth bearer checks against any
|
|
10
|
+
* authorization server); the package owns the JSON-RPC protocol, protocol
|
|
9
11
|
* negotiation, RFC 9728 discovery metadata, and the 401 challenge. Per-call
|
|
10
|
-
* guards (`beforeCall`
|
|
11
|
-
*
|
|
12
|
-
* billing, storage, or
|
|
12
|
+
* guards (`beforeCall`, `onCall`), a per-call `meta` scratchpad, per-tool
|
|
13
|
+
* `scope` gating, and rich tool results (text/image/structured) are all
|
|
14
|
+
* built in; the package ships no opinion about billing, storage, or access.
|
|
13
15
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* **Consume.** {@link createMcpClient} is a streamable-HTTP client for calling
|
|
17
|
+
* OTHER MCP servers — the half you need to expose a user's own connected tools
|
|
18
|
+
* to your agent. Safety wrapping around untrusted remote tools (namespacing,
|
|
19
|
+
* injection defense, approval gating) is the host's job.
|
|
20
|
+
*
|
|
21
|
+
* The tool shape is structurally compatible with `@absolutejs/ai`'s `AIToolMap`,
|
|
22
|
+
* so an AI tool registry serves over MCP without conversion — but nothing here
|
|
23
|
+
* depends on a model.
|
|
17
24
|
*/
|
|
18
25
|
export { verifyBearer, type BearerResult, type BearerVerifier, type VerifiedJwt, type VerifyBearerConfig, } from "./auth";
|
|
26
|
+
export { createMcpClient, McpClientError, type McpClient, type McpClientOptions, type McpInitializeResult, type McpRemoteTool, } from "./client";
|
|
19
27
|
export { dispatchMcp } from "./dispatch";
|
|
28
|
+
export { createMcpHandler } from "./handler";
|
|
20
29
|
export { metadataPathFor, protectedResourceMetadata, type ProtectedResourceMetadata, } from "./metadata";
|
|
21
30
|
export { mcpServer } from "./server";
|
|
22
|
-
export type { McpAuthResult, McpCallGate, McpCallMeta, McpPromptArgument, McpPromptDefinition, McpPrompts, McpResource, McpResources, McpServerConfig, McpServerInfo, McpTool, McpToolAnnotations, McpToolContext, McpToolRegistry, } from "./types";
|
|
31
|
+
export type { McpAudioContent, McpAuthResult, McpCallGate, McpCallMeta, McpContent, McpImageContent, McpPromptArgument, McpPromptDefinition, McpPrompts, McpResource, McpResourceLink, McpResources, McpServerConfig, McpServerInfo, McpTextContent, McpTool, McpToolAnnotations, McpToolContext, McpToolRegistry, McpToolResult, McpToolReturn, } from "./types";
|
package/dist/src/types.d.ts
CHANGED
|
@@ -8,13 +8,50 @@ export type McpToolAnnotations = {
|
|
|
8
8
|
readOnlyHint?: boolean;
|
|
9
9
|
title?: string;
|
|
10
10
|
};
|
|
11
|
-
/**
|
|
12
|
-
*
|
|
11
|
+
/** Rich tool-result content blocks. A handler may return a bare string (wrapped
|
|
12
|
+
* as one text block), an array of these, or a full {@link McpToolResult}. */
|
|
13
|
+
export type McpTextContent = {
|
|
14
|
+
text: string;
|
|
15
|
+
type: "text";
|
|
16
|
+
};
|
|
17
|
+
export type McpImageContent = {
|
|
18
|
+
data: string;
|
|
19
|
+
mimeType: string;
|
|
20
|
+
type: "image";
|
|
21
|
+
};
|
|
22
|
+
export type McpAudioContent = {
|
|
23
|
+
data: string;
|
|
24
|
+
mimeType: string;
|
|
25
|
+
type: "audio";
|
|
26
|
+
};
|
|
27
|
+
export type McpResourceLink = {
|
|
28
|
+
description?: string;
|
|
29
|
+
mimeType?: string;
|
|
30
|
+
name?: string;
|
|
31
|
+
type: "resource_link";
|
|
32
|
+
uri: string;
|
|
33
|
+
};
|
|
34
|
+
export type McpContent = McpAudioContent | McpImageContent | McpResourceLink | McpTextContent;
|
|
35
|
+
export type McpToolResult = {
|
|
36
|
+
content: McpContent[];
|
|
37
|
+
isError?: boolean;
|
|
38
|
+
/** Structured output validated against the tool's `outputSchema`, if any. */
|
|
39
|
+
structuredContent?: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
/** What a tool handler may return. A bare string is the common case. */
|
|
42
|
+
export type McpToolReturn = McpContent[] | McpToolResult | string;
|
|
43
|
+
/** One callable tool. `inputSchema` is a JSON Schema object. */
|
|
13
44
|
export type McpTool = {
|
|
14
45
|
annotations?: McpToolAnnotations;
|
|
15
46
|
description: string;
|
|
16
|
-
handler: (args: unknown) =>
|
|
47
|
+
handler: (args: unknown) => McpToolReturn | Promise<McpToolReturn>;
|
|
17
48
|
inputSchema: Record<string, unknown>;
|
|
49
|
+
/** JSON Schema for `structuredContent`, advertised on `tools/list`. */
|
|
50
|
+
outputSchema?: Record<string, unknown>;
|
|
51
|
+
/** If set, the tool is only listed and callable when the caller's scopes
|
|
52
|
+
* include this. Tools without a scope are always available. Fails closed:
|
|
53
|
+
* a scoped tool is hidden when the caller's scopes are unknown. */
|
|
54
|
+
scope?: string;
|
|
18
55
|
};
|
|
19
56
|
export type McpToolRegistry = Record<string, McpTool>;
|
|
20
57
|
/** A resource the client can list and read (`resources/list` / `resources/read`). */
|
|
@@ -38,10 +75,12 @@ export type McpPromptDefinition = {
|
|
|
38
75
|
* one `tools/call` request. A tool handler can write to it (e.g. record which
|
|
39
76
|
* entity it touched) and `onCall` can read it back for the audit row. */
|
|
40
77
|
export type McpCallMeta = Record<string, unknown>;
|
|
41
|
-
/** What `authorize` returns: the resolved caller
|
|
78
|
+
/** What `authorize` returns: the resolved caller (plus the caller's scopes, if
|
|
79
|
+
* any tools are scope-gated), or a reason for the 401. */
|
|
42
80
|
export type McpAuthResult<Caller> = {
|
|
43
81
|
caller: Caller;
|
|
44
82
|
ok: true;
|
|
83
|
+
scopes?: string[];
|
|
45
84
|
} | {
|
|
46
85
|
ok: false;
|
|
47
86
|
reason: string;
|
package/package.json
CHANGED