toolnexus 0.23.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 +139 -1
- package/dist/acp.d.ts +37 -6
- package/dist/acp.js +239 -52
- package/dist/agents/loop.js +4 -1
- package/dist/agents/runtime.js +4 -1
- package/dist/client.d.ts +55 -9
- package/dist/client.js +154 -41
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -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/media.d.ts +126 -0
- package/dist/media.js +540 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +44 -0
- package/dist/toolconcurrency.d.ts +16 -0
- package/dist/toolconcurrency.js +59 -0
- package/dist/toolkit.d.ts +23 -1
- package/dist/toolkit.js +26 -4
- package/dist/types.d.ts +26 -1
- package/package.json +5 -3
- package/src/acp.ts +258 -55
- package/src/agents/loop.ts +4 -1
- package/src/agents/runtime.ts +4 -1
- package/src/client.ts +196 -48
- package/src/index.ts +4 -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/media.ts +604 -0
- package/src/skill.ts +50 -0
- package/src/toolconcurrency.ts +67 -0
- package/src/toolkit.ts +35 -5
- package/src/types.ts +27 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stdio transport for the 2026 path (§2.3): one JSON-RPC message per line, responses demultiplexed
|
|
3
|
+
* by id (concurrent calls allowed), stderr passed through and never parsed, a dead child fails every
|
|
4
|
+
* pending call instead of hanging. No headers, no auth.
|
|
5
|
+
*/
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { createInterface } from "node:readline";
|
|
8
|
+
/** The child exited / closed stdout before answering. */
|
|
9
|
+
export class StdioClosedError extends Error {
|
|
10
|
+
}
|
|
11
|
+
export class StdioTransport {
|
|
12
|
+
pending = new Map();
|
|
13
|
+
err;
|
|
14
|
+
done;
|
|
15
|
+
child;
|
|
16
|
+
constructor(child) {
|
|
17
|
+
this.child = child;
|
|
18
|
+
const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
19
|
+
rl.on("line", (line) => {
|
|
20
|
+
let m;
|
|
21
|
+
try {
|
|
22
|
+
m = JSON.parse(line);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return; // noise
|
|
26
|
+
}
|
|
27
|
+
if (!m || typeof m !== "object" || m.method !== undefined || m.id === undefined || m.id === null)
|
|
28
|
+
return; // not a response
|
|
29
|
+
const key = JSON.stringify(m.id);
|
|
30
|
+
const f = this.pending.get(key);
|
|
31
|
+
this.pending.delete(key);
|
|
32
|
+
f?.(m);
|
|
33
|
+
});
|
|
34
|
+
this.done = new Promise((r) => rl.once("close", () => {
|
|
35
|
+
this.err ??= new StdioClosedError("stdio server closed its stdout");
|
|
36
|
+
for (const f of this.pending.values())
|
|
37
|
+
f({ __closed: true });
|
|
38
|
+
this.pending.clear();
|
|
39
|
+
r();
|
|
40
|
+
}));
|
|
41
|
+
child.stdin?.on("error", () => { }); // EPIPE after the server died surfaces as a closed stdout
|
|
42
|
+
}
|
|
43
|
+
/** Writes one line and waits for the response with the same id. Never retried. */
|
|
44
|
+
roundTrip(id, body, timeoutMs, signal) {
|
|
45
|
+
if (this.err)
|
|
46
|
+
return Promise.reject(this.err);
|
|
47
|
+
const key = JSON.stringify(id);
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
signal?.removeEventListener("abort", onAbort);
|
|
52
|
+
this.pending.delete(key);
|
|
53
|
+
};
|
|
54
|
+
const onAbort = () => {
|
|
55
|
+
cleanup();
|
|
56
|
+
reject(signal?.reason ?? new Error("aborted"));
|
|
57
|
+
};
|
|
58
|
+
const timer = setTimeout(() => {
|
|
59
|
+
cleanup();
|
|
60
|
+
reject(new Error("stdio request timed out"));
|
|
61
|
+
}, timeoutMs);
|
|
62
|
+
if (signal) {
|
|
63
|
+
if (signal.aborted)
|
|
64
|
+
return onAbort();
|
|
65
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
}
|
|
67
|
+
this.pending.set(key, (m) => {
|
|
68
|
+
cleanup();
|
|
69
|
+
if (m.__closed)
|
|
70
|
+
return reject(this.err);
|
|
71
|
+
if (m.result == null && m.error == null)
|
|
72
|
+
return reject(new Error("response has neither result nor error"));
|
|
73
|
+
resolve(m);
|
|
74
|
+
});
|
|
75
|
+
this.child.stdin?.write(body + "\n");
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Close stdin, then SIGKILL after 2 s if the child is still alive. */
|
|
79
|
+
async close() {
|
|
80
|
+
if (this.child.exitCode === null && this.child.signalCode === null) {
|
|
81
|
+
this.child.stdin?.end();
|
|
82
|
+
const exited = new Promise((r) => this.child.once("exit", () => r()));
|
|
83
|
+
const t = setTimeout(() => this.child.kill("SIGKILL"), 2000);
|
|
84
|
+
await exited;
|
|
85
|
+
clearTimeout(t);
|
|
86
|
+
}
|
|
87
|
+
this.child.stdout?.destroy();
|
|
88
|
+
await this.done;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Spawns argv (no shell) and returns its transport; rejects if the spawn itself fails. */
|
|
92
|
+
export function spawnStdio(argv, env, cwd) {
|
|
93
|
+
const [cmd, ...args] = argv;
|
|
94
|
+
const child = spawn(cmd, args, { env, cwd, stdio: ["pipe", "pipe", "inherit"] });
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
child.once("error", reject);
|
|
97
|
+
child.once("spawn", () => resolve(new StdioTransport(child)));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Obj } from "./protocol.js";
|
|
2
|
+
/** BM25 (k1 1.2, b 0.75) over name tokens twice + description tokens; best first, ties keep list order. */
|
|
3
|
+
export declare function rank(tools: Obj[], query: string, limit: number): Obj[];
|
|
4
|
+
/** Lowercases and splits on non-alphanumerics and camelCase boundaries. */
|
|
5
|
+
export declare function tokens(s: string): string[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** BM25 (k1 1.2, b 0.75) over name tokens twice + description tokens; best first, ties keep list order. */
|
|
2
|
+
export function rank(tools, query, limit) {
|
|
3
|
+
const docs = tools.map((t) => {
|
|
4
|
+
const nt = tokens(String(t.name ?? ""));
|
|
5
|
+
return [...nt, ...nt, ...tokens(String(t.description ?? ""))];
|
|
6
|
+
});
|
|
7
|
+
const df = new Map();
|
|
8
|
+
let total = 0;
|
|
9
|
+
for (const d of docs) {
|
|
10
|
+
total += d.length;
|
|
11
|
+
for (const w of new Set(d))
|
|
12
|
+
df.set(w, (df.get(w) ?? 0) + 1);
|
|
13
|
+
}
|
|
14
|
+
const avg = total / Math.max(1, tools.length);
|
|
15
|
+
const qs = tokens(query);
|
|
16
|
+
const hits = [];
|
|
17
|
+
docs.forEach((d, i) => {
|
|
18
|
+
const tf = new Map();
|
|
19
|
+
for (const w of d)
|
|
20
|
+
tf.set(w, (tf.get(w) ?? 0) + 1);
|
|
21
|
+
let s = 0;
|
|
22
|
+
for (const q of qs) {
|
|
23
|
+
const f = tf.get(q) ?? 0;
|
|
24
|
+
if (f === 0)
|
|
25
|
+
continue;
|
|
26
|
+
const n = df.get(q) ?? 0;
|
|
27
|
+
const idf = Math.log(1 + (tools.length - n + 0.5) / (n + 0.5));
|
|
28
|
+
s += (idf * f * 2.2) / (f + 1.2 * (0.25 + (0.75 * d.length) / avg));
|
|
29
|
+
}
|
|
30
|
+
if (s > 0)
|
|
31
|
+
hits.push({ i, score: s });
|
|
32
|
+
});
|
|
33
|
+
hits.sort((a, b) => b.score - a.score); // stable
|
|
34
|
+
return hits.slice(0, Math.max(0, limit)).map((h) => tools[h.i]);
|
|
35
|
+
}
|
|
36
|
+
/** Lowercases and splits on non-alphanumerics and camelCase boundaries. */
|
|
37
|
+
export function tokens(s) {
|
|
38
|
+
const out = [];
|
|
39
|
+
let cur = "";
|
|
40
|
+
let prevLower = false;
|
|
41
|
+
for (const r of s) {
|
|
42
|
+
if (/\p{Lu}/u.test(r) && prevLower) {
|
|
43
|
+
if (cur)
|
|
44
|
+
out.push(cur.toLowerCase());
|
|
45
|
+
cur = r;
|
|
46
|
+
}
|
|
47
|
+
else if (/[\p{L}\p{N}]/u.test(r))
|
|
48
|
+
cur += r;
|
|
49
|
+
else if (cur) {
|
|
50
|
+
out.push(cur.toLowerCase());
|
|
51
|
+
cur = "";
|
|
52
|
+
}
|
|
53
|
+
prevLower = /\p{Ll}/u.test(r);
|
|
54
|
+
}
|
|
55
|
+
if (cur)
|
|
56
|
+
out.push(cur.toLowerCase());
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { Tool, ToolContext, ToolResult, ToolSource } from "./types.js";
|
|
3
|
+
import type { McpSkill } from "./skill.js";
|
|
4
|
+
/** The MCP 2026-07-28 protocol version: an opt-in stateless wire (SPEC §7C.2), never a default. */
|
|
5
|
+
export declare const MCP_PROTOCOL_2026 = "2026-07-28";
|
|
6
|
+
/** The SEP-2640 skills extension id advertised by `server/discover` when `listSkills` is set. */
|
|
7
|
+
export declare const MCP_SKILLS_EXTENSION = "io.modelcontextprotocol/skills";
|
|
8
|
+
/**
|
|
9
|
+
* The MCP protocol versions whose wire this handler implements (ADR 0039 §versions).
|
|
10
|
+
* 2026-07-28 is opt-in only (list it in `protocolVersions`): the stateless wire of SPEC §7C.2.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MCP_HANDLER_IMPLEMENTED_VERSIONS: readonly string[];
|
|
13
|
+
export type McpDecisionKind = "allow" | "deny" | "confirm";
|
|
14
|
+
/**
|
|
15
|
+
* What `beforeCall` returns. `undefined` / `{}` allows. deny ⇒ `result` when set, else isError
|
|
16
|
+
* `message` (default "denied"). confirm ⇒ elicitation when the client can, else `result` when
|
|
17
|
+
* set, else isError "confirmation required: <message>".
|
|
18
|
+
*/
|
|
19
|
+
export interface McpDecision {
|
|
20
|
+
kind?: McpDecisionKind;
|
|
21
|
+
message?: string;
|
|
22
|
+
result?: ToolResult;
|
|
23
|
+
}
|
|
24
|
+
export interface McpServerInfo {
|
|
25
|
+
name?: string;
|
|
26
|
+
/** serverInfo.title; empty ⇒ omitted. */
|
|
27
|
+
title?: string;
|
|
28
|
+
version?: string;
|
|
29
|
+
instructions?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface McpResource {
|
|
32
|
+
uri: string;
|
|
33
|
+
name: string;
|
|
34
|
+
title?: string;
|
|
35
|
+
description?: string;
|
|
36
|
+
mimeType?: string;
|
|
37
|
+
}
|
|
38
|
+
/** One entry of resources/read: set `text`, or `blob` (base64). */
|
|
39
|
+
export interface McpResourceContent {
|
|
40
|
+
uri: string;
|
|
41
|
+
mimeType?: string;
|
|
42
|
+
text?: string;
|
|
43
|
+
blob?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface McpPromptArgument {
|
|
46
|
+
name: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
required?: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface McpPrompt {
|
|
51
|
+
name: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
arguments?: McpPromptArgument[];
|
|
55
|
+
}
|
|
56
|
+
export interface McpPromptMessage {
|
|
57
|
+
role: "user" | "assistant";
|
|
58
|
+
text: string;
|
|
59
|
+
}
|
|
60
|
+
export interface McpPromptResult {
|
|
61
|
+
description?: string;
|
|
62
|
+
messages: McpPromptMessage[];
|
|
63
|
+
}
|
|
64
|
+
/** The inbound request a hook or tool is serving. Read it with the `*From(ctx)` helpers. */
|
|
65
|
+
export interface McpContext {
|
|
66
|
+
/** What `authenticate` returned (undefined without one). */
|
|
67
|
+
identity?: unknown;
|
|
68
|
+
request: IncomingMessage;
|
|
69
|
+
/** Mcp-Session-Id ("" when stateless / none). */
|
|
70
|
+
sessionId: string;
|
|
71
|
+
/** The JSON-RPC request's `params._meta`, when present. */
|
|
72
|
+
meta?: Record<string, unknown>;
|
|
73
|
+
/** Aborted when the HTTP request goes away. */
|
|
74
|
+
signal: AbortSignal;
|
|
75
|
+
}
|
|
76
|
+
type Awaitable<T> = T | Promise<T>;
|
|
77
|
+
/** Issues and checks Mcp-Session-Id values. Default: an in-memory set. */
|
|
78
|
+
export interface McpSessionIds {
|
|
79
|
+
generate(ctx: McpContext): Awaitable<string>;
|
|
80
|
+
/** false ⇒ HTTP 404. */
|
|
81
|
+
validate(ctx: McpContext, id: string): Awaitable<boolean>;
|
|
82
|
+
terminate(ctx: McpContext, id: string): Awaitable<void>;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* An `authenticate` failure with an explicit HTTP status and challenge (`McpServerAuth` throws
|
|
86
|
+
* it; any hook may). On any wire the handler answers `status` (default 401) with
|
|
87
|
+
* `WWW-Authenticate` = `challenge` (a non-empty `wwwAuthenticate` result wins) and the body
|
|
88
|
+
* `{"error":"unauthorized"}`. `reason` is never sent on the wire.
|
|
89
|
+
*/
|
|
90
|
+
export declare class McpAuthError extends Error {
|
|
91
|
+
readonly status: number;
|
|
92
|
+
readonly challenge: string;
|
|
93
|
+
readonly reason: string;
|
|
94
|
+
constructor(status: number, challenge: string, reason?: string);
|
|
95
|
+
}
|
|
96
|
+
/** Surfaced once per inbound tools/call. */
|
|
97
|
+
export interface McpCallEvent {
|
|
98
|
+
name: string;
|
|
99
|
+
source: ToolSource;
|
|
100
|
+
ms: number;
|
|
101
|
+
isError: boolean;
|
|
102
|
+
/** The execute() failure, when it threw. */
|
|
103
|
+
err?: unknown;
|
|
104
|
+
/** Size in bytes of the JSON CallToolResult sent back. */
|
|
105
|
+
resultBytes: number;
|
|
106
|
+
decision: McpDecisionKind;
|
|
107
|
+
identity: unknown;
|
|
108
|
+
sessionId: string;
|
|
109
|
+
}
|
|
110
|
+
/** Configures `toolkit.mcpHandler`. Every field is optional. */
|
|
111
|
+
export interface McpHandlerConfig {
|
|
112
|
+
name?: string;
|
|
113
|
+
title?: string;
|
|
114
|
+
version?: string;
|
|
115
|
+
instructions?: string;
|
|
116
|
+
/** Resolved at initialize; wins over the static fields. */
|
|
117
|
+
serverInfo?: (ctx: McpContext) => Awaitable<McpServerInfo>;
|
|
118
|
+
/** Accepted versions, newest first; unimplemented entries dropped. Default ["2025-06-18"]. */
|
|
119
|
+
protocolVersions?: string[];
|
|
120
|
+
/** Every HTTP request, before JSON-RPC parsing. Throw / reject ⇒ HTTP 401. */
|
|
121
|
+
authenticate?: (req: IncomingMessage) => Awaitable<unknown>;
|
|
122
|
+
/** WWW-Authenticate header for the 401 (when non-empty). */
|
|
123
|
+
wwwAuthenticate?: (req: IncomingMessage, err: unknown) => string | undefined;
|
|
124
|
+
/** Per request, for tools/list AND the tools/call lookup. Omit ⇒ the toolkit's tools. */
|
|
125
|
+
listTools?: (ctx: McpContext) => Awaitable<Tool[]>;
|
|
126
|
+
/** Gate per tools/call. A throw is a deny with its message. */
|
|
127
|
+
beforeCall?: (ctx: McpContext, tool: Tool, args: Record<string, unknown>) => Awaitable<McpDecision | void>;
|
|
128
|
+
/** Elicitation round-trip bound in ms. Default 5 minutes. */
|
|
129
|
+
confirmTimeout?: number;
|
|
130
|
+
/** Fires once per tools/call; its throws are isolated. */
|
|
131
|
+
onCall?: (ev: McpCallEvent) => void;
|
|
132
|
+
listResources?: (ctx: McpContext) => Awaitable<McpResource[]>;
|
|
133
|
+
readResource?: (ctx: McpContext, uri: string) => Awaitable<McpResourceContent[]>;
|
|
134
|
+
listPrompts?: (ctx: McpContext) => Awaitable<McpPrompt[]>;
|
|
135
|
+
getPrompt?: (ctx: McpContext, name: string, args: Record<string, string>) => Awaitable<McpPromptResult>;
|
|
136
|
+
/**
|
|
137
|
+
* §7C.2: skills published through the SEP-2640 skills extension on the 2026-07-28 wire
|
|
138
|
+
* (skills/list, skills/get, resources/read of skill:// URIs). Omit ⇒ not advertised.
|
|
139
|
+
* `loadMcpSkills(dir)` builds the list from a skills directory.
|
|
140
|
+
*/
|
|
141
|
+
listSkills?: (ctx: McpContext) => Awaitable<McpSkill[]>;
|
|
142
|
+
sessionIds?: McpSessionIds;
|
|
143
|
+
/** No Mcp-Session-Id issued or checked; elicitation never used. */
|
|
144
|
+
stateless?: boolean;
|
|
145
|
+
/** Stateless only: echoed as Mcp-Session-Id on initialize, never validated. */
|
|
146
|
+
statelessSessionId?: string;
|
|
147
|
+
/** POST body cap in bytes. Default 4 MiB. */
|
|
148
|
+
maxBodyBytes?: number;
|
|
149
|
+
}
|
|
150
|
+
/** The embeddable handler: a Node request listener plus `notifyToolsChanged()`. */
|
|
151
|
+
export interface McpHandler {
|
|
152
|
+
(req: IncomingMessage, res: ServerResponse): void;
|
|
153
|
+
/** Sends notifications/tools/list_changed on every open GET stream. */
|
|
154
|
+
notifyToolsChanged(): void;
|
|
155
|
+
}
|
|
156
|
+
type AnyCtx = McpContext | ToolContext | undefined | null;
|
|
157
|
+
/** The value `authenticate` produced for this request (hook ctx or tool ctx). */
|
|
158
|
+
export declare const identityFrom: (ctx: AnyCtx) => unknown;
|
|
159
|
+
/** The inbound IncomingMessage being served. */
|
|
160
|
+
export declare const requestFrom: (ctx: AnyCtx) => IncomingMessage | undefined;
|
|
161
|
+
/** The request's Mcp-Session-Id ("" if none). */
|
|
162
|
+
export declare const mcpSessionIdFrom: (ctx: AnyCtx) => string;
|
|
163
|
+
/** The JSON-RPC request's `params._meta` (undefined when absent). */
|
|
164
|
+
export declare const mcpRequestMetaFrom: (ctx: AnyCtx) => Record<string, unknown> | undefined;
|
|
165
|
+
/** The tools/list entry: name, description, inputSchema, outputSchema?, annotations?. */
|
|
166
|
+
export declare function mcpWireTool(t: Tool): Record<string, unknown>;
|
|
167
|
+
/** Builds the handler (`toolkit.mcpHandler(cfg)` calls this with the toolkit's tools). */
|
|
168
|
+
export declare function createMcpHandler(toolkitTools: () => Tool[], cfg?: McpHandlerConfig): McpHandler;
|
|
169
|
+
export {};
|