toolnexus 0.24.0 → 0.25.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 +116 -1
- package/dist/acp.d.ts +37 -6
- package/dist/acp.js +239 -52
- package/dist/client.d.ts +32 -8
- package/dist/client.js +102 -23
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp.d.ts +32 -7
- package/dist/mcp.js +125 -36
- package/dist/mcp2026/auth.d.ts +137 -0
- package/dist/mcp2026/auth.js +446 -0
- package/dist/mcp2026/client.d.ts +48 -0
- package/dist/mcp2026/client.js +160 -0
- package/dist/mcp2026/protocol.d.ts +54 -0
- package/dist/mcp2026/protocol.js +113 -0
- package/dist/mcp2026/skills.d.ts +5 -0
- package/dist/mcp2026/skills.js +25 -0
- package/dist/mcp2026/source.d.ts +86 -0
- package/dist/mcp2026/source.js +372 -0
- package/dist/mcp2026/stdio.d.ts +24 -0
- package/dist/mcp2026/stdio.js +99 -0
- package/dist/mcp2026/toolsearch.d.ts +5 -0
- package/dist/mcp2026/toolsearch.js +58 -0
- package/dist/mcphandler.d.ts +169 -0
- package/dist/mcphandler.js +1145 -0
- package/dist/mcpheader.d.ts +5 -0
- package/dist/mcpheader.js +18 -0
- package/dist/mcpserve.d.ts +24 -15
- package/dist/mcpserve.js +105 -2
- package/dist/mcpserverauth.d.ts +48 -0
- package/dist/mcpserverauth.js +203 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +44 -0
- package/dist/toolkit.d.ts +20 -1
- package/dist/toolkit.js +25 -3
- package/dist/types.d.ts +22 -0
- package/package.json +4 -3
- package/src/acp.ts +258 -55
- package/src/client.ts +124 -27
- package/src/index.ts +2 -0
- package/src/mcp.ts +162 -43
- package/src/mcp2026/auth.ts +473 -0
- package/src/mcp2026/client.ts +161 -0
- package/src/mcp2026/protocol.ts +135 -0
- package/src/mcp2026/skills.ts +22 -0
- package/src/mcp2026/source.ts +407 -0
- package/src/mcp2026/stdio.ts +102 -0
- package/src/mcp2026/toolsearch.ts +56 -0
- package/src/mcphandler.ts +1252 -0
- package/src/mcpheader.ts +19 -0
- package/src/mcpserve.ts +115 -2
- package/src/mcpserverauth.ts +216 -0
- package/src/skill.ts +50 -0
- package/src/toolkit.ts +31 -4
- package/src/types.ts +23 -0
package/dist/client.d.ts
CHANGED
|
@@ -358,12 +358,21 @@ export interface RunResult {
|
|
|
358
358
|
export type StreamEvent = {
|
|
359
359
|
type: "text";
|
|
360
360
|
delta: string;
|
|
361
|
-
}
|
|
361
|
+
}
|
|
362
|
+
/** Model reasoning narration (e.g. an ACP agent's `agent_thought_chunk`). Never part of
|
|
363
|
+
* the answer text or the RunResult. */
|
|
364
|
+
| {
|
|
365
|
+
type: "thought";
|
|
366
|
+
delta: string;
|
|
367
|
+
via?: "acp";
|
|
368
|
+
}
|
|
369
|
+
/** `via: "acp"` marks a tool the ACP agent ran itself: reported, never executed by the loop. */
|
|
370
|
+
| {
|
|
362
371
|
type: "tool_call";
|
|
363
372
|
id: string;
|
|
364
373
|
name: string;
|
|
365
374
|
args: Record<string, unknown>;
|
|
366
|
-
via?: "call_tool";
|
|
375
|
+
via?: "call_tool" | "acp";
|
|
367
376
|
} | {
|
|
368
377
|
type: "tool_result";
|
|
369
378
|
id: string;
|
|
@@ -371,7 +380,7 @@ export type StreamEvent = {
|
|
|
371
380
|
output: string;
|
|
372
381
|
isError: boolean;
|
|
373
382
|
metadata?: Record<string, unknown>;
|
|
374
|
-
via?: "call_tool";
|
|
383
|
+
via?: "call_tool" | "acp";
|
|
375
384
|
} | {
|
|
376
385
|
type: "usage";
|
|
377
386
|
usage: Usage;
|
|
@@ -582,11 +591,22 @@ export interface InProcessResponse {
|
|
|
582
591
|
totalTokens?: number;
|
|
583
592
|
};
|
|
584
593
|
}
|
|
594
|
+
/** A streaming in-process model: `emit` each event as it happens — `text` deltas become
|
|
595
|
+
* the streamed answer as they arrive; any other event (thought, an agent's own tool_call /
|
|
596
|
+
* tool_result) goes straight to the stream consumer and is never executed by the loop.
|
|
597
|
+
* Return the whole message when done (its `toolCalls`, if any, are what the loop runs). */
|
|
598
|
+
export type InProcessGenerateStream = (request: InProcessRequest, emit: (event: StreamEvent) => void) => InProcessResponse | Promise<InProcessResponse>;
|
|
599
|
+
type InProcessGenerate = (request: InProcessRequest) => InProcessResponse | Promise<InProcessResponse>;
|
|
585
600
|
/** Options for {@link createInProcessClient}: every client option EXCEPT the three
|
|
586
|
-
* that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate
|
|
601
|
+
* that describe a wire — `baseUrl`, `apiKey` and `style` — plus `generate` and/or
|
|
602
|
+
* `generateStream` (at least one). */
|
|
587
603
|
export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style" | "fetch"> & {
|
|
588
604
|
/** Your model. Receives the assembled request, returns one assistant message. */
|
|
589
|
-
generate
|
|
605
|
+
generate?: InProcessGenerate;
|
|
606
|
+
/** Your model, streaming for real. With it, `stream()` is incremental; without it a
|
|
607
|
+
* `stream()` on a plain `generate` is refused. When both are set, `generate` serves
|
|
608
|
+
* non-streaming calls; alone, it serves them too (with a no-op `emit`). */
|
|
609
|
+
generateStream?: InProcessGenerateStream;
|
|
590
610
|
};
|
|
591
611
|
/**
|
|
592
612
|
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
@@ -596,9 +616,11 @@ export type InProcessOptions = Omit<ClientOptions, "baseUrl" | "apiKey" | "style
|
|
|
596
616
|
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
597
617
|
* is no second copy of the request/response assembly to drift.
|
|
598
618
|
*
|
|
599
|
-
* Streaming is refused rather than faked — see the thrown
|
|
619
|
+
* Streaming with only a plain `generate` is refused rather than faked — see the thrown
|
|
620
|
+
* error. With `generateStream` it streams for real: each text event is written to the
|
|
621
|
+
* SSE body the moment it is emitted.
|
|
600
622
|
*/
|
|
601
|
-
export declare function createInProcessFetch(generate
|
|
623
|
+
export declare function createInProcessFetch(generate?: InProcessGenerate, generateStream?: InProcessGenerateStream): typeof fetch;
|
|
602
624
|
/**
|
|
603
625
|
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
604
626
|
* HTTP types to construct.
|
|
@@ -609,7 +631,8 @@ export declare function createInProcessFetch(generate: (request: InProcessReques
|
|
|
609
631
|
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
610
632
|
* is just that adapter wired into a `Client`.
|
|
611
633
|
*
|
|
612
|
-
* Streaming is refused rather than faked
|
|
634
|
+
* Streaming is refused rather than faked for a plain `generate`; with `generateStream`
|
|
635
|
+
* it streams for real — see {@link createInProcessFetch}.
|
|
613
636
|
*/
|
|
614
637
|
export declare function createInProcessClient(opts: InProcessOptions): Client;
|
|
615
638
|
/** Stateful multi-turn conversation: each send() continues the same transcript (memory). */
|
|
@@ -625,3 +648,4 @@ export declare class Conversation {
|
|
|
625
648
|
/** Reset the conversation memory. */
|
|
626
649
|
reset(): void;
|
|
627
650
|
}
|
|
651
|
+
export {};
|
package/dist/client.js
CHANGED
|
@@ -1132,6 +1132,13 @@ export class Client {
|
|
|
1132
1132
|
if (payload === "[DONE]")
|
|
1133
1133
|
break;
|
|
1134
1134
|
const j = safeParse(payload);
|
|
1135
|
+
// A non-text event from an in-process streaming generate rides in-band so it keeps
|
|
1136
|
+
// its order and arrives as it happens; it goes straight to the consumer and is never
|
|
1137
|
+
// executed. Honoured only on the in-process sentinel URL, never from a real server.
|
|
1138
|
+
if (j && j[IN_PROCESS_SIDE_KEY] && this.opts.baseUrl === IN_PROCESS_BASE_URL) {
|
|
1139
|
+
yield j[IN_PROCESS_SIDE_KEY];
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1135
1142
|
if (j?.usage)
|
|
1136
1143
|
addUsage(usage, j.usage, "openai");
|
|
1137
1144
|
const choice = j?.choices?.[0];
|
|
@@ -1336,6 +1343,9 @@ export function createClient(opts) {
|
|
|
1336
1343
|
* it must be syntactically valid. `.invalid` is reserved by RFC 2606 precisely so a
|
|
1337
1344
|
* name can never resolve. */
|
|
1338
1345
|
const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
1346
|
+
/** The in-band SSE key that carries a non-text stream event from an in-process
|
|
1347
|
+
* streaming generate to the consumer (honoured only on {@link IN_PROCESS_BASE_URL}). */
|
|
1348
|
+
const IN_PROCESS_SIDE_KEY = "toolnexus_event";
|
|
1339
1349
|
/**
|
|
1340
1350
|
* Build a `fetch`-shaped adapter over a semantic `generate` function (ADR 0030):
|
|
1341
1351
|
* the reusable piece of {@link createInProcessClient} — assembles the wire request,
|
|
@@ -1344,14 +1354,19 @@ const IN_PROCESS_BASE_URL = "http://in-process.invalid/v1";
|
|
|
1344
1354
|
* or the agent runtime's `inProcess` option) goes through this ONE function, so there
|
|
1345
1355
|
* is no second copy of the request/response assembly to drift.
|
|
1346
1356
|
*
|
|
1347
|
-
* Streaming is refused rather than faked — see the thrown
|
|
1357
|
+
* Streaming with only a plain `generate` is refused rather than faked — see the thrown
|
|
1358
|
+
* error. With `generateStream` it streams for real: each text event is written to the
|
|
1359
|
+
* SSE body the moment it is emitted.
|
|
1348
1360
|
*/
|
|
1349
|
-
export function createInProcessFetch(generate) {
|
|
1350
|
-
if (typeof generate !== "function") {
|
|
1351
|
-
throw new Error("toolnexus: createInProcessFetch requires a `generate` function");
|
|
1361
|
+
export function createInProcessFetch(generate, generateStream) {
|
|
1362
|
+
if (typeof generate !== "function" && typeof generateStream !== "function") {
|
|
1363
|
+
throw new Error("toolnexus: createInProcessFetch requires a `generate` (or `generateStream`) function");
|
|
1352
1364
|
}
|
|
1353
1365
|
const inProcessFetch = async (_url, init) => {
|
|
1354
1366
|
const body = JSON.parse(String(init?.body ?? "{}"));
|
|
1367
|
+
const request = { messages: body.messages ?? [], tools: body.tools, model: body.model, body };
|
|
1368
|
+
if (body.stream && generateStream)
|
|
1369
|
+
return inProcessStream(generateStream, request);
|
|
1355
1370
|
// A generate() returns a whole answer, so a stream would be one chunk pretending
|
|
1356
1371
|
// to be many. Content and delta-count assertions cannot tell that from a real
|
|
1357
1372
|
// stream, so refuse instead of degrading silently.
|
|
@@ -1359,24 +1374,10 @@ export function createInProcessFetch(generate) {
|
|
|
1359
1374
|
throw new Error("toolnexus: createInProcessClient does not support streaming — `generate` returns a " +
|
|
1360
1375
|
"complete answer. Use the non-streaming run, or supply a `fetch`/transport that streams.");
|
|
1361
1376
|
}
|
|
1362
|
-
const answer = await generate({
|
|
1363
|
-
messages: body.messages ?? [],
|
|
1364
|
-
tools: body.tools,
|
|
1365
|
-
model: body.model,
|
|
1366
|
-
body,
|
|
1367
|
-
});
|
|
1377
|
+
const answer = generate ? await generate(request) : await generateStream(request, () => { });
|
|
1368
1378
|
const message = { role: "assistant" };
|
|
1369
1379
|
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
1370
|
-
message.tool_calls = answer.toolCalls
|
|
1371
|
-
id: c.id ?? `call_${i}`,
|
|
1372
|
-
type: "function",
|
|
1373
|
-
function: {
|
|
1374
|
-
name: c.name,
|
|
1375
|
-
// A string is already encoded; anything else is encoded here so a model
|
|
1376
|
-
// author never has to think about the wire.
|
|
1377
|
-
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1378
|
-
},
|
|
1379
|
-
}));
|
|
1380
|
+
message.tool_calls = wireToolCalls(answer.toolCalls, false);
|
|
1380
1381
|
}
|
|
1381
1382
|
else {
|
|
1382
1383
|
message.content = answer.content ?? "";
|
|
@@ -1395,6 +1396,80 @@ export function createInProcessFetch(generate) {
|
|
|
1395
1396
|
};
|
|
1396
1397
|
return inProcessFetch;
|
|
1397
1398
|
}
|
|
1399
|
+
function wireToolCalls(calls, indexed) {
|
|
1400
|
+
return calls.map((c, i) => ({
|
|
1401
|
+
...(indexed ? { index: i } : {}),
|
|
1402
|
+
id: c.id ?? `call_${i}`,
|
|
1403
|
+
type: "function",
|
|
1404
|
+
function: {
|
|
1405
|
+
name: c.name,
|
|
1406
|
+
// A string is already encoded; anything else is encoded here so a model
|
|
1407
|
+
// author never has to think about the wire.
|
|
1408
|
+
arguments: typeof c.arguments === "string" ? c.arguments : JSON.stringify(c.arguments ?? {}),
|
|
1409
|
+
},
|
|
1410
|
+
}));
|
|
1411
|
+
}
|
|
1412
|
+
/** Runs a streaming generate and answers with an OpenAI SSE body written AS EVENTS ARRIVE:
|
|
1413
|
+
* text → a content delta; any other event → an in-band side chunk for the consumer; then
|
|
1414
|
+
* (tool calls as one delta | the content as one delta when no text was emitted), the
|
|
1415
|
+
* finish_reason chunk, usage, and [DONE]. */
|
|
1416
|
+
function inProcessStream(generateStream, request) {
|
|
1417
|
+
const enc = new TextEncoder();
|
|
1418
|
+
let open = true;
|
|
1419
|
+
const sse = new ReadableStream({
|
|
1420
|
+
start(controller) {
|
|
1421
|
+
const chunk = (v) => {
|
|
1422
|
+
if (open)
|
|
1423
|
+
controller.enqueue(enc.encode(`data: ${JSON.stringify(v)}\n\n`));
|
|
1424
|
+
};
|
|
1425
|
+
let sawText = false;
|
|
1426
|
+
void (async () => {
|
|
1427
|
+
try {
|
|
1428
|
+
const answer = await generateStream(request, (ev) => {
|
|
1429
|
+
if (!ev || typeof ev !== "object")
|
|
1430
|
+
return;
|
|
1431
|
+
if (ev.type === "text") {
|
|
1432
|
+
if (ev.delta) {
|
|
1433
|
+
sawText = true;
|
|
1434
|
+
chunk({ choices: [{ index: 0, delta: { content: ev.delta } }] });
|
|
1435
|
+
}
|
|
1436
|
+
return;
|
|
1437
|
+
}
|
|
1438
|
+
chunk({ [IN_PROCESS_SIDE_KEY]: ev });
|
|
1439
|
+
});
|
|
1440
|
+
let finish = "stop";
|
|
1441
|
+
if (answer.toolCalls && answer.toolCalls.length > 0) {
|
|
1442
|
+
chunk({ choices: [{ index: 0, delta: { tool_calls: wireToolCalls(answer.toolCalls, true) } }] });
|
|
1443
|
+
finish = "tool_calls";
|
|
1444
|
+
}
|
|
1445
|
+
else if (!sawText && answer.content) {
|
|
1446
|
+
chunk({ choices: [{ index: 0, delta: { content: answer.content } }] });
|
|
1447
|
+
}
|
|
1448
|
+
chunk({ choices: [{ index: 0, delta: {}, finish_reason: finish }] });
|
|
1449
|
+
const u = answer.usage;
|
|
1450
|
+
if (u) {
|
|
1451
|
+
const p = u.promptTokens ?? 0, c = u.completionTokens ?? 0;
|
|
1452
|
+
chunk({ choices: [], usage: { prompt_tokens: p, completion_tokens: c, total_tokens: u.totalTokens || p + c } });
|
|
1453
|
+
}
|
|
1454
|
+
if (open)
|
|
1455
|
+
controller.enqueue(enc.encode("data: [DONE]\n\n"));
|
|
1456
|
+
open = false;
|
|
1457
|
+
controller.close();
|
|
1458
|
+
}
|
|
1459
|
+
catch (e) {
|
|
1460
|
+
if (open) {
|
|
1461
|
+
open = false;
|
|
1462
|
+
controller.error(e);
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
})();
|
|
1466
|
+
},
|
|
1467
|
+
cancel() {
|
|
1468
|
+
open = false;
|
|
1469
|
+
},
|
|
1470
|
+
});
|
|
1471
|
+
return new Response(sse, { status: 200, headers: { "content-type": "text/event-stream" } });
|
|
1472
|
+
}
|
|
1398
1473
|
/**
|
|
1399
1474
|
* A client backed by a model running IN THIS PROCESS — no server, no socket, and no
|
|
1400
1475
|
* HTTP types to construct.
|
|
@@ -1405,11 +1480,15 @@ export function createInProcessFetch(generate) {
|
|
|
1405
1480
|
* The `fetch`-shaped adapter itself is {@link createInProcessFetch} — this function
|
|
1406
1481
|
* is just that adapter wired into a `Client`.
|
|
1407
1482
|
*
|
|
1408
|
-
* Streaming is refused rather than faked
|
|
1483
|
+
* Streaming is refused rather than faked for a plain `generate`; with `generateStream`
|
|
1484
|
+
* it streams for real — see {@link createInProcessFetch}.
|
|
1409
1485
|
*/
|
|
1410
1486
|
export function createInProcessClient(opts) {
|
|
1411
|
-
const { generate, ...rest } = opts;
|
|
1412
|
-
|
|
1487
|
+
const { generate, generateStream, ...rest } = opts;
|
|
1488
|
+
if (typeof generate !== "function" && typeof generateStream !== "function") {
|
|
1489
|
+
throw new Error("toolnexus: createInProcessClient requires a `generate` (or `generateStream`) function");
|
|
1490
|
+
}
|
|
1491
|
+
const inProcessFetch = createInProcessFetch(generate, generateStream);
|
|
1413
1492
|
return new Client({
|
|
1414
1493
|
// Retries default to ZERO here, unlike a network client. There is no wire, so
|
|
1415
1494
|
// there is no transient failure to ride out: whatever `generate` throws will
|
package/dist/index.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export * from "./a2a.js";
|
|
|
10
10
|
export * from "./acp.js";
|
|
11
11
|
export * from "./serve.js";
|
|
12
12
|
export * from "./mcpserve.js";
|
|
13
|
+
export * from "./mcphandler.js";
|
|
14
|
+
export * from "./mcpserverauth.js";
|
|
13
15
|
export * from "./adapters.js";
|
|
14
16
|
export * from "./translate.js";
|
|
15
17
|
export * from "./toolkit.js";
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ export * from "./a2a.js";
|
|
|
10
10
|
export * from "./acp.js";
|
|
11
11
|
export * from "./serve.js";
|
|
12
12
|
export * from "./mcpserve.js";
|
|
13
|
+
export * from "./mcphandler.js";
|
|
14
|
+
export * from "./mcpserverauth.js";
|
|
13
15
|
export * from "./adapters.js";
|
|
14
16
|
export * from "./translate.js";
|
|
15
17
|
export * from "./toolkit.js";
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ElicitRequest, ElicitResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
-
import { type JSONSchema, type Tool, type ToolHints, type McpStatus, type Request, type Answer } from "./types.js";
|
|
2
|
+
import { type JSONSchema, type Tool, type ToolHints, type ToolResult, type McpStatus, type Request, type Answer } from "./types.js";
|
|
3
3
|
import { type ContentPart } from "./content.js";
|
|
4
|
+
import type { SkillDef } from "./skill.js";
|
|
5
|
+
import { type AuthConfig } from "./mcp2026/source.js";
|
|
6
|
+
import { type TokenStore } from "./mcp2026/auth.js";
|
|
7
|
+
export { Token, MemoryTokenStore, type TokenStore } from "./mcp2026/auth.js";
|
|
8
|
+
export type { AuthConfig } from "./mcp2026/source.js";
|
|
4
9
|
/** Map an MCP `elicitation/create` request onto a §10 `Request`. */
|
|
5
10
|
export declare function elicitationToRequest(params: ElicitRequest["params"]): Request;
|
|
6
11
|
/** Map a resolved §10 `Answer` back onto an MCP `ElicitResult`. ok→accept; declined→decline; else→cancel. */
|
|
@@ -18,6 +23,10 @@ export interface LocalServer {
|
|
|
18
23
|
* builtins/skills filter (nil/empty ⇒ all; ≥1 true ⇒ allowlist; only-false ⇒ drop-list; unknown
|
|
19
24
|
* ⇒ ignore+warn). Applied to the listed defs before sanitize/prefix. */
|
|
20
25
|
tools?: Record<string, boolean>;
|
|
26
|
+
/** §2.8 lazy tool search: expose `<server>_search_tools` + `<server>_call_tool` instead of every tool. */
|
|
27
|
+
toolSearch?: boolean;
|
|
28
|
+
/** §2.8 what this server is for (shown by search_tools). */
|
|
29
|
+
description?: string;
|
|
21
30
|
}
|
|
22
31
|
export interface RemoteServer {
|
|
23
32
|
type?: "remote";
|
|
@@ -28,6 +37,11 @@ export interface RemoteServer {
|
|
|
28
37
|
timeout?: number;
|
|
29
38
|
/** §2 Gap 7. Per-server tool allowlist (see LocalServer.tools). */
|
|
30
39
|
tools?: Record<string, boolean>;
|
|
40
|
+
/** §2.8 lazy tool search (see LocalServer.toolSearch). */
|
|
41
|
+
toolSearch?: boolean;
|
|
42
|
+
description?: string;
|
|
43
|
+
/** §2A OAuth 2.1 client settings. Absent ⇒ a 401 still triggers the flow (CIMD if the AS supports it). */
|
|
44
|
+
auth?: AuthConfig;
|
|
31
45
|
}
|
|
32
46
|
export type ServerConfig = LocalServer | RemoteServer;
|
|
33
47
|
export type McpConfig = Record<string, ServerConfig>;
|
|
@@ -46,6 +60,8 @@ export declare function expandEnvHeaders(headers?: Record<string, string>): Reco
|
|
|
46
60
|
* takes it there.
|
|
47
61
|
*/
|
|
48
62
|
export declare function mcpContentToParts(content: unknown): ContentPart[] | undefined;
|
|
63
|
+
/** §2 `tools/call` result → ToolResult (shared by the 2026 path and the frozen fallback). */
|
|
64
|
+
export declare function mapCallToolResult(result: any, server: string): ToolResult;
|
|
49
65
|
export interface McpSource {
|
|
50
66
|
tools: Tool[];
|
|
51
67
|
status: Record<string, McpStatus>;
|
|
@@ -56,6 +72,8 @@ export interface McpSource {
|
|
|
56
72
|
* source is closed.
|
|
57
73
|
*/
|
|
58
74
|
resourceTools(): Tool[];
|
|
75
|
+
/** §2.7 skills served by 2026 servers (skills extension); the toolkit adds them to the `skill` tool. */
|
|
76
|
+
skills: SkillDef[];
|
|
59
77
|
}
|
|
60
78
|
/** One server's outcome, reported by `loadMcp`'s `onServer` the moment that server finishes. */
|
|
61
79
|
export interface McpServerResult {
|
|
@@ -75,12 +93,21 @@ export declare function mcpHints(a: {
|
|
|
75
93
|
destructiveHint?: boolean;
|
|
76
94
|
openWorldHint?: boolean;
|
|
77
95
|
} | undefined): ToolHints | undefined;
|
|
96
|
+
/** Options shared by loadMcp / listMcpTools for the 2026 path and OAuth (§2A.1a). */
|
|
97
|
+
export interface McpLoadOptions {
|
|
98
|
+
waitFor?: (request: Request) => Promise<Answer>;
|
|
99
|
+
signal?: AbortSignal;
|
|
100
|
+
/** Where OAuth tokens live (default: in memory, per load). */
|
|
101
|
+
tokenStore?: TokenStore;
|
|
102
|
+
/** How `auto` OAuth mode opens the authorization URL (default: the OS opener). */
|
|
103
|
+
openUrl?: (url: string) => unknown | Promise<unknown>;
|
|
104
|
+
/** Final exposed names to drop; on a tool-search server they are also hidden from search/call. */
|
|
105
|
+
disableTools?: string[];
|
|
106
|
+
}
|
|
78
107
|
/** Connect to every enabled server, list + convert tools. Failures are isolated.
|
|
79
108
|
* §2 Gap 3: an optional `signal` bounds each connect/list by the server timeout and aborts the
|
|
80
109
|
* whole load promptly on parent cancellation. */
|
|
81
|
-
export declare function loadMcp(input: string | object, opts?: {
|
|
82
|
-
waitFor?: (request: Request) => Promise<Answer>;
|
|
83
|
-
signal?: AbortSignal;
|
|
110
|
+
export declare function loadMcp(input: string | object, opts?: McpLoadOptions & {
|
|
84
111
|
/**
|
|
85
112
|
* Called once per server the moment it is connected, failed or found disabled (SPEC §4C),
|
|
86
113
|
* so a host can register a fast server's tools while a slow one is still connecting.
|
|
@@ -105,6 +132,4 @@ export interface McpInventory {
|
|
|
105
132
|
* §2 Gap 6. Connect to every enabled server, list tool definitions, and DISCONNECT before
|
|
106
133
|
* returning — no toolkit, no execute wiring, nothing left running. Failure isolation as in loadMcp.
|
|
107
134
|
*/
|
|
108
|
-
export declare function listMcpTools(input: string | object, opts?:
|
|
109
|
-
signal?: AbortSignal;
|
|
110
|
-
}): Promise<McpInventory>;
|
|
135
|
+
export declare function listMcpTools(input: string | object, opts?: McpLoadOptions): Promise<McpInventory>;
|
package/dist/mcp.js
CHANGED
|
@@ -11,6 +11,9 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
|
11
11
|
import { ElicitRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
12
12
|
import { sanitize } from "./types.js";
|
|
13
13
|
import { summarizePart } from "./content.js";
|
|
14
|
+
import { probe, newOAuth, defaultTokenStore } from "./mcp2026/source.js";
|
|
15
|
+
import { PendingError } from "./mcp2026/auth.js";
|
|
16
|
+
export { Token, MemoryTokenStore } from "./mcp2026/auth.js";
|
|
14
17
|
// ---------------------------------------------------------------------------
|
|
15
18
|
// MCP elicitation bridge (§10). A server can ask US for input mid-`tools/call`
|
|
16
19
|
// (a reverse-request). We map it onto the one `waitFor`: form mode → kind:"input",
|
|
@@ -224,6 +227,22 @@ async function paginateTools(client, timeout) {
|
|
|
224
227
|
}
|
|
225
228
|
throw new Error(`MCP list exceeded ${MAX_LIST_PAGES} pages`);
|
|
226
229
|
}
|
|
230
|
+
/** §2 `tools/call` result → ToolResult (shared by the 2026 path and the frozen fallback). */
|
|
231
|
+
export function mapCallToolResult(result, server) {
|
|
232
|
+
// Parts are collected BEFORE any branch returns: the isError and structuredContent
|
|
233
|
+
// short-circuits both read `content[]` on the way past, so neither reintroduces the
|
|
234
|
+
// silent drop this mapping exists to remove (§2).
|
|
235
|
+
const parts = mcpContentToParts(result.content);
|
|
236
|
+
const withParts = (r) => (parts ? { ...r, parts } : r);
|
|
237
|
+
if (result.isError) {
|
|
238
|
+
return withParts({ output: formatToolErrorContent(result.content), isError: true, metadata: { server } });
|
|
239
|
+
}
|
|
240
|
+
if (result.structuredContent !== undefined && result.structuredContent !== null) {
|
|
241
|
+
return withParts({ output: JSON.stringify(result.structuredContent), isError: false, metadata: { server } });
|
|
242
|
+
}
|
|
243
|
+
const text = joinTextContent(result.content);
|
|
244
|
+
return withParts({ output: text || describeMcpParts(parts), isError: false, metadata: { server } });
|
|
245
|
+
}
|
|
227
246
|
function convertTool(server, def, client, timeout) {
|
|
228
247
|
const inputSchema = {
|
|
229
248
|
...def.inputSchema,
|
|
@@ -244,19 +263,7 @@ function convertTool(server, def, client, timeout) {
|
|
|
244
263
|
// `resetTimeoutOnProgress` never fires (add-mcp-progress-timeout). Notifications for a
|
|
245
264
|
// token that matches no in-flight request are dropped by the SDK, so they never reset.
|
|
246
265
|
{ onprogress: () => { }, resetTimeoutOnProgress: true, signal: ctx?.signal, timeout: ctx?.timeout ?? timeout });
|
|
247
|
-
|
|
248
|
-
// short-circuits both read `content[]` on the way past, so neither reintroduces the
|
|
249
|
-
// silent drop this mapping exists to remove (§2).
|
|
250
|
-
const parts = mcpContentToParts(result.content);
|
|
251
|
-
const withParts = (r) => (parts ? { ...r, parts } : r);
|
|
252
|
-
if (result.isError) {
|
|
253
|
-
return withParts({ output: formatToolErrorContent(result.content), isError: true, metadata: { server } });
|
|
254
|
-
}
|
|
255
|
-
if (result.structuredContent !== undefined && result.structuredContent !== null) {
|
|
256
|
-
return withParts({ output: JSON.stringify(result.structuredContent), isError: false, metadata: { server } });
|
|
257
|
-
}
|
|
258
|
-
const text = joinTextContent(result.content);
|
|
259
|
-
return withParts({ output: text || describeMcpParts(parts), isError: false, metadata: { server } });
|
|
266
|
+
return mapCallToolResult(result, server);
|
|
260
267
|
}
|
|
261
268
|
catch (e) {
|
|
262
269
|
return { output: e instanceof Error ? e.message : String(e), isError: true, metadata: { server } };
|
|
@@ -276,6 +283,15 @@ export function mcpHints(a) {
|
|
|
276
283
|
h.destructive = false;
|
|
277
284
|
return h;
|
|
278
285
|
}
|
|
286
|
+
function sdkConn(client, timeout) {
|
|
287
|
+
return {
|
|
288
|
+
listResources: async (signal) => (await client.listResources({}, { timeout, signal })).resources ?? [],
|
|
289
|
+
readResource: async (uri, signal) => (await client.readResource({ uri }, { timeout, signal })).contents ?? [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function modernConn(s) {
|
|
293
|
+
return { listResources: (signal) => s.listResources(signal), readResource: (uri, signal) => s.readResource(uri, signal) };
|
|
294
|
+
}
|
|
279
295
|
function resourceTools(servers) {
|
|
280
296
|
const ro = { readOnly: true, destructive: false, openWorld: false };
|
|
281
297
|
const names = () => [...servers.keys()].sort();
|
|
@@ -312,8 +328,7 @@ function resourceTools(servers) {
|
|
|
312
328
|
for (const name of targets) {
|
|
313
329
|
const c = conn(name);
|
|
314
330
|
try {
|
|
315
|
-
const
|
|
316
|
-
for (const r of res.resources ?? []) {
|
|
331
|
+
for (const r of await c.listResources(ctx?.signal)) {
|
|
317
332
|
let line = `${name} ${r.uri}`;
|
|
318
333
|
if (r.name)
|
|
319
334
|
line += ` — ${r.name}`;
|
|
@@ -361,9 +376,8 @@ function resourceTools(servers) {
|
|
|
361
376
|
if (!uri)
|
|
362
377
|
return { output: "read_mcp_resource: uri is required", isError: true };
|
|
363
378
|
try {
|
|
364
|
-
const res = await c.client.readResource({ uri }, { timeout: c.timeout, signal: ctx?.signal });
|
|
365
379
|
const parts = [];
|
|
366
|
-
for (const rc of
|
|
380
|
+
for (const rc of await c.readResource(uri, ctx?.signal)) {
|
|
367
381
|
if (typeof rc.text === "string")
|
|
368
382
|
parts.push(rc.text);
|
|
369
383
|
else if (typeof rc.blob === "string")
|
|
@@ -390,16 +404,17 @@ function newMcpClient(waitFor) {
|
|
|
390
404
|
return client;
|
|
391
405
|
}
|
|
392
406
|
/** Connect an existing client's transport (remote streamable-HTTP with SSE fallback, or local stdio). */
|
|
393
|
-
async function connectTransport(client, cfg) {
|
|
407
|
+
async function connectTransport(client, cfg, auth) {
|
|
394
408
|
if (isRemote(cfg)) {
|
|
395
409
|
const url = new URL(cfg.url);
|
|
396
410
|
const expanded = expandEnvHeaders(cfg.headers);
|
|
397
411
|
const requestInit = expanded ? { headers: expanded } : undefined;
|
|
412
|
+
const fetch = auth ? bearerFetch(cfg.url, auth) : undefined;
|
|
398
413
|
try {
|
|
399
|
-
await client.connect(new StreamableHTTPClientTransport(url, { requestInit }));
|
|
414
|
+
await client.connect(new StreamableHTTPClientTransport(url, { requestInit, ...(fetch ? { fetch } : {}) }));
|
|
400
415
|
}
|
|
401
416
|
catch {
|
|
402
|
-
await client.connect(new SSEClientTransport(url, { requestInit }));
|
|
417
|
+
await client.connect(new SSEClientTransport(url, { requestInit, ...(fetch ? { fetch } : {}) }));
|
|
403
418
|
}
|
|
404
419
|
}
|
|
405
420
|
else {
|
|
@@ -414,6 +429,66 @@ async function connectTransport(client, cfg) {
|
|
|
414
429
|
}));
|
|
415
430
|
}
|
|
416
431
|
}
|
|
432
|
+
/**
|
|
433
|
+
* §2A.1a frozen fallback + OAuth: the SDK transport's fetch attaches the current bearer token
|
|
434
|
+
* (refreshing as needed); a 401/403 challenge runs the same OAuth flow once, then the request is
|
|
435
|
+
* re-sent. A pause is answered through waitFor, else the request fails ("authorization pending").
|
|
436
|
+
*/
|
|
437
|
+
function bearerFetch(resource, a) {
|
|
438
|
+
return async (input, init) => {
|
|
439
|
+
for (let attempt = 0;; attempt++) {
|
|
440
|
+
const headers = new Headers(init?.headers);
|
|
441
|
+
const tok = await a.oauth.bearer(resource);
|
|
442
|
+
if (tok)
|
|
443
|
+
headers.set("Authorization", "Bearer " + tok);
|
|
444
|
+
const res = await globalThis.fetch(input, { ...init, headers });
|
|
445
|
+
const www = res.headers.get("www-authenticate") ?? "";
|
|
446
|
+
if (attempt > 0 || (res.status !== 401 && res.status !== 403) || !www)
|
|
447
|
+
return res;
|
|
448
|
+
try {
|
|
449
|
+
await a.oauth.challenge(resource, res.status, www, tok, async () => ({}));
|
|
450
|
+
}
|
|
451
|
+
catch (e) {
|
|
452
|
+
if (!(e instanceof PendingError))
|
|
453
|
+
throw e;
|
|
454
|
+
if (!a.waitFor)
|
|
455
|
+
throw new Error("authorization pending");
|
|
456
|
+
const ans = await a.waitFor({ ...e.pending });
|
|
457
|
+
if (!ans?.ok)
|
|
458
|
+
throw new Error(`authorization denied: ${ans?.reason ?? "cancelled"}`);
|
|
459
|
+
const cb = ans.data?.callbackUrl;
|
|
460
|
+
if (typeof cb !== "string")
|
|
461
|
+
throw new Error("authorization answer carries no data.callbackUrl");
|
|
462
|
+
await a.oauth.resume(e.pending.id, cb);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Discover first (§2.2): a 2026 server is used directly; an old one (§2.2a) gets the frozen SDK
|
|
469
|
+
* path, with the OAuth bearer attached for remote servers. Throws ⇒ the server failed.
|
|
470
|
+
*/
|
|
471
|
+
async function connectServer(name, cfg, timeout, opts, store, sdkClient, onSdk) {
|
|
472
|
+
const o = { waitFor: opts.waitFor, tokenStore: store, openUrl: opts.openUrl, signal: opts.signal, disableTools: opts.disableTools };
|
|
473
|
+
const remote = isRemote(cfg);
|
|
474
|
+
const oauth = remote ? newOAuth(cfg.auth, o, store) : undefined;
|
|
475
|
+
const p = await probe({
|
|
476
|
+
name, timeout, tools: cfg.tools, toolSearch: cfg.toolSearch === true, description: cfg.description,
|
|
477
|
+
remote: remote ? { url: cfg.url, headers: expandEnvHeaders(cfg.headers), oauth: oauth } : undefined,
|
|
478
|
+
local: remote ? undefined : {
|
|
479
|
+
argv: cfg.command,
|
|
480
|
+
env: { ...process.env, ...(cfg.environment ?? cfg.env ?? {}) },
|
|
481
|
+
cwd: cfg.cwd,
|
|
482
|
+
},
|
|
483
|
+
}, o);
|
|
484
|
+
if (p.kind === "modern")
|
|
485
|
+
return p;
|
|
486
|
+
const client = sdkClient();
|
|
487
|
+
onSdk(client);
|
|
488
|
+
await raceTimeout(connectTransport(client, cfg, oauth ? { oauth, waitFor: opts.waitFor } : undefined), timeout, opts.signal);
|
|
489
|
+
const defs = await raceTimeout(paginateTools(client, timeout), timeout, opts.signal);
|
|
490
|
+
return { kind: "sdk", client, defs };
|
|
491
|
+
}
|
|
417
492
|
/** Connect to every enabled server, list + convert tools. Failures are isolated.
|
|
418
493
|
* §2 Gap 3: an optional `signal` bounds each connect/list by the server timeout and aborts the
|
|
419
494
|
* whole load promptly on parent cancellation. */
|
|
@@ -423,6 +498,8 @@ export async function loadMcp(input, opts) {
|
|
|
423
498
|
const status = {};
|
|
424
499
|
const clients = [];
|
|
425
500
|
const servers = new Map();
|
|
501
|
+
const skills = [];
|
|
502
|
+
const store = defaultTokenStore({ tokenStore: opts?.tokenStore });
|
|
426
503
|
const waitFor = opts?.waitFor;
|
|
427
504
|
const signal = opts?.signal;
|
|
428
505
|
const onServer = opts?.onServer;
|
|
@@ -447,23 +524,31 @@ export async function loadMcp(input, opts) {
|
|
|
447
524
|
return;
|
|
448
525
|
}
|
|
449
526
|
const timeout = cfg.timeout ?? DEFAULT_TIMEOUT;
|
|
450
|
-
//
|
|
451
|
-
// (its stdio child is killed by close() — never orphaned).
|
|
452
|
-
|
|
527
|
+
// The fallback SDK client is created before its connect so a timed-out/aborted connect
|
|
528
|
+
// still has a closeable handle (its stdio child is killed by close() — never orphaned).
|
|
529
|
+
let client;
|
|
453
530
|
try {
|
|
454
|
-
await
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
531
|
+
const c = await connectServer(name, cfg, timeout, opts ?? {}, store, () => newMcpClient(waitFor), (cl) => (client = cl));
|
|
532
|
+
let converted;
|
|
533
|
+
if (c.kind === "modern") {
|
|
534
|
+
converted = c.server.tools();
|
|
535
|
+
skills.push(...c.server.skills);
|
|
536
|
+
clients.push(c.server);
|
|
537
|
+
servers.set(name, modernConn(c.server));
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
const kept = applyToolsFilter(name, c.defs, cfg.tools);
|
|
541
|
+
converted = kept.map((def) => convertTool(name, def, c.client, timeout));
|
|
542
|
+
clients.push(c.client);
|
|
543
|
+
servers.set(name, sdkConn(c.client, timeout));
|
|
544
|
+
}
|
|
458
545
|
tools.push(...converted);
|
|
459
|
-
clients.push(client);
|
|
460
|
-
servers.set(name, { client, timeout });
|
|
461
546
|
status[name] = "connected";
|
|
462
547
|
report({ name, status: "connected", tools: converted });
|
|
463
548
|
}
|
|
464
549
|
catch (e) {
|
|
465
550
|
status[name] = "failed";
|
|
466
|
-
await client
|
|
551
|
+
await client?.close().catch(() => { });
|
|
467
552
|
// Parent cancellation/deadline aborts the whole load (§2 Gap 3, A1); a per-server timeout
|
|
468
553
|
// within budget isolates just this server.
|
|
469
554
|
if (signal?.aborted)
|
|
@@ -493,6 +578,7 @@ export async function loadMcp(input, opts) {
|
|
|
493
578
|
await Promise.all(clients.map((c) => c.close().catch(() => { })));
|
|
494
579
|
},
|
|
495
580
|
resourceTools: () => resourceTools(servers),
|
|
581
|
+
skills,
|
|
496
582
|
};
|
|
497
583
|
}
|
|
498
584
|
/**
|
|
@@ -504,17 +590,20 @@ export async function listMcpTools(input, opts) {
|
|
|
504
590
|
const toolsByServer = {};
|
|
505
591
|
const status = {};
|
|
506
592
|
const signal = opts?.signal;
|
|
593
|
+
const store = defaultTokenStore({ tokenStore: opts?.tokenStore });
|
|
507
594
|
await Promise.all(Object.entries(config).map(async ([name, cfg]) => {
|
|
508
595
|
if (!isEnabled(cfg)) {
|
|
509
596
|
status[name] = "disabled";
|
|
510
597
|
return;
|
|
511
598
|
}
|
|
512
599
|
const timeout = cfg.timeout ?? DEFAULT_TIMEOUT;
|
|
513
|
-
//
|
|
514
|
-
|
|
600
|
+
// The fallback client is created before its connect so an aborted/timed-out connect is still closed.
|
|
601
|
+
let client;
|
|
515
602
|
try {
|
|
516
|
-
await
|
|
517
|
-
const defs =
|
|
603
|
+
const c = await connectServer(name, cfg, timeout, { ...opts, waitFor: opts?.waitFor }, store, () => newMcpClient(), (cl) => (client = cl));
|
|
604
|
+
const defs = c.kind === "modern" ? c.server.rawDefs() : c.defs;
|
|
605
|
+
if (c.kind === "modern")
|
|
606
|
+
await c.server.close().catch(() => { });
|
|
518
607
|
toolsByServer[name] = defs.map((d) => ({
|
|
519
608
|
name: d.name,
|
|
520
609
|
description: d.description ?? "",
|
|
@@ -529,13 +618,13 @@ export async function listMcpTools(input, opts) {
|
|
|
529
618
|
catch (e) {
|
|
530
619
|
status[name] = "failed";
|
|
531
620
|
if (signal?.aborted) {
|
|
532
|
-
await client
|
|
621
|
+
await client?.close().catch(() => { });
|
|
533
622
|
throw e;
|
|
534
623
|
}
|
|
535
624
|
console.warn(`[toolnexus] MCP server "${name}" failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
536
625
|
}
|
|
537
626
|
finally {
|
|
538
|
-
await client
|
|
627
|
+
await client?.close().catch(() => { });
|
|
539
628
|
}
|
|
540
629
|
}));
|
|
541
630
|
return { tools: toolsByServer, status };
|