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
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stateless MCP 2026-07-28 client (SPEC §2.2–§2.7): Streamable HTTP or stdio, one API.
|
|
3
|
+
* Never sends `initialize`; no session; never re-sends a request except after a pre-dispatch
|
|
4
|
+
* 401/403 auth rejection (§2.6).
|
|
5
|
+
*/
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { defaultHttp, encodeHeader, HttpStatusError, META_CLIENT_CAPS, META_CLIENT_INFO, META_VERSION, NotJsonRpcError, PROTOCOL_VERSION, RpcError, DEFAULT_TIMEOUT_MS, } from "./protocol.js";
|
|
8
|
+
import { parseSkillURI } from "./skills.js";
|
|
9
|
+
export class Mcp2026Client {
|
|
10
|
+
url;
|
|
11
|
+
info = { name: "toolnexus", version: "0.1.0" };
|
|
12
|
+
caps = { elicitation: { form: {} } };
|
|
13
|
+
http = defaultHttp;
|
|
14
|
+
headers;
|
|
15
|
+
auth;
|
|
16
|
+
timeoutMs;
|
|
17
|
+
transport;
|
|
18
|
+
nextId = 0;
|
|
19
|
+
constructor(url, o = {}) {
|
|
20
|
+
this.url = url;
|
|
21
|
+
this.headers = o.headers ?? {};
|
|
22
|
+
this.auth = o.auth;
|
|
23
|
+
this.timeoutMs = o.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
24
|
+
this.transport = o.transport;
|
|
25
|
+
}
|
|
26
|
+
/** Sends one request; params gets the per-request _meta merged in. Throws RpcError on a JSON-RPC error. */
|
|
27
|
+
async call(method, params = {}, o = {}) {
|
|
28
|
+
params = { ...params, _meta: { ...(params._meta ?? {}), [META_VERSION]: PROTOCOL_VERSION, [META_CLIENT_CAPS]: this.caps, [META_CLIENT_INFO]: this.info } };
|
|
29
|
+
const id = ++this.nextId;
|
|
30
|
+
const body = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
31
|
+
const timeoutMs = o.timeoutMs ?? this.timeoutMs;
|
|
32
|
+
const msg = this.transport ? await this.transport.roundTrip(id, body, timeoutMs, o.signal) : await this.httpRoundTrip(method, params, body, timeoutMs, o.signal);
|
|
33
|
+
if (msg.error != null) {
|
|
34
|
+
const e = msg.error;
|
|
35
|
+
const err = new RpcError(Number(e.code), String(e.message ?? ""), e.data);
|
|
36
|
+
if (msg.__status)
|
|
37
|
+
err.httpStatus = msg.__status;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const r = msg.result;
|
|
41
|
+
if (!r || typeof r !== "object" || Array.isArray(r))
|
|
42
|
+
throw new NotJsonRpcError("response has neither result nor error");
|
|
43
|
+
if (!("resultType" in r))
|
|
44
|
+
r.resultType = "complete";
|
|
45
|
+
if (r.resultType !== "complete" && r.resultType !== "input_required")
|
|
46
|
+
throw new Error(`unsupported resultType "${String(r.resultType)}"`);
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
async httpRoundTrip(method, params, body, timeoutMs, signal) {
|
|
50
|
+
let resp;
|
|
51
|
+
for (let attempt = 0;; attempt++) {
|
|
52
|
+
const sent = this.auth ? await this.auth.bearer(this.url) : "";
|
|
53
|
+
resp = await this.post(method, params, body, sent, timeoutMs, signal); // a network error is never retried
|
|
54
|
+
const www = resp.headers["www-authenticate"] ?? "";
|
|
55
|
+
if (!this.auth || (resp.status !== 401 && resp.status !== 403) || !www)
|
|
56
|
+
break;
|
|
57
|
+
if (attempt >= 3)
|
|
58
|
+
throw new Error(`authorization failed: HTTP ${resp.status} after ${attempt + 1} attempts`);
|
|
59
|
+
await this.auth.challenge(this.url, resp.status, www, sent, () => this.call(method, params, { timeoutMs, signal }));
|
|
60
|
+
}
|
|
61
|
+
let msg;
|
|
62
|
+
if ((resp.headers["content-type"] ?? "").startsWith("text/event-stream")) {
|
|
63
|
+
// The last data: payload carrying a result or error is the final response.
|
|
64
|
+
for (const line of resp.body.split(/\r?\n/)) {
|
|
65
|
+
if (!line.startsWith("data:"))
|
|
66
|
+
continue;
|
|
67
|
+
try {
|
|
68
|
+
const m = JSON.parse(line.slice(5));
|
|
69
|
+
if (m && (m.result !== undefined || m.error !== undefined))
|
|
70
|
+
msg = m;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* not JSON: ignore */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
try {
|
|
79
|
+
msg = JSON.parse(resp.body);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
msg = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const ok = resp.status >= 200 && resp.status < 300;
|
|
86
|
+
const isMsg = !!msg && typeof msg === "object" && (msg.result != null || (msg.error != null && typeof msg.error === "object" && typeof msg.error.code === "number"));
|
|
87
|
+
if (!isMsg) {
|
|
88
|
+
if (!ok)
|
|
89
|
+
throw new HttpStatusError(resp.status);
|
|
90
|
+
throw new NotJsonRpcError(`HTTP ${resp.status}: response is not a JSON-RPC response`);
|
|
91
|
+
}
|
|
92
|
+
if (!ok && msg.error == null)
|
|
93
|
+
throw new HttpStatusError(resp.status);
|
|
94
|
+
return { ...msg, __status: ok ? undefined : resp.status };
|
|
95
|
+
}
|
|
96
|
+
post(method, params, body, bearer, timeoutMs, signal) {
|
|
97
|
+
const h = {
|
|
98
|
+
...this.headers,
|
|
99
|
+
"Content-Type": "application/json",
|
|
100
|
+
Accept: "application/json, text/event-stream",
|
|
101
|
+
"MCP-Protocol-Version": PROTOCOL_VERSION,
|
|
102
|
+
"Mcp-Method": method,
|
|
103
|
+
};
|
|
104
|
+
if (method === "tools/call" || method === "prompts/get")
|
|
105
|
+
h["Mcp-Name"] = encodeHeader(String(params.name ?? ""));
|
|
106
|
+
if (method === "resources/read")
|
|
107
|
+
h["Mcp-Name"] = encodeHeader(String(params.uri ?? ""));
|
|
108
|
+
if (bearer)
|
|
109
|
+
h.Authorization = "Bearer " + bearer;
|
|
110
|
+
return this.http({ method: "POST", url: this.url, headers: h, body, timeoutMs, signal });
|
|
111
|
+
}
|
|
112
|
+
/** tools/list (one page — no nextCursor on the 2026 path, §2.9). Returns the tools and the ttlMs hint. */
|
|
113
|
+
async listTools(o = {}) {
|
|
114
|
+
const r = await this.call("tools/list", {}, o);
|
|
115
|
+
const tools = (Array.isArray(r.tools) ? r.tools : []).filter((t) => t && typeof t === "object" && typeof t.name === "string");
|
|
116
|
+
return { tools, ttlMs: typeof r.ttlMs === "number" ? r.ttlMs : undefined };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One remote skill (§2.7) as a data skill: skills/get, then resources/read of its SKILL.md (sha256
|
|
120
|
+
* verified). Every resource URI must be inside the skill. Returns the raw SKILL.md text and the
|
|
121
|
+
* other resources' relative paths.
|
|
122
|
+
*/
|
|
123
|
+
async fetchSkill(uri, o = {}) {
|
|
124
|
+
const { name } = parseSkillURI(uri);
|
|
125
|
+
const r = await this.call("skills/get", { uri }, o);
|
|
126
|
+
const res = Array.isArray(r.skill?.resources) ? r.skill.resources : [];
|
|
127
|
+
const others = [];
|
|
128
|
+
let md;
|
|
129
|
+
for (const m of res) {
|
|
130
|
+
const fu = String(m?.uri ?? "");
|
|
131
|
+
let rel;
|
|
132
|
+
try {
|
|
133
|
+
const p = parseSkillURI(fu);
|
|
134
|
+
if (p.name !== name)
|
|
135
|
+
throw new Error();
|
|
136
|
+
rel = p.rel;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
throw new Error(`refusing skill resource "${fu}": outside skill ${name}`);
|
|
140
|
+
}
|
|
141
|
+
if (rel === "SKILL.md")
|
|
142
|
+
md = m;
|
|
143
|
+
else
|
|
144
|
+
others.push(rel);
|
|
145
|
+
}
|
|
146
|
+
const mdUri = md ? String(md.uri) : uri;
|
|
147
|
+
const rr = await this.call("resources/read", { uri: mdUri }, o);
|
|
148
|
+
const cs = Array.isArray(rr.contents) ? rr.contents : [];
|
|
149
|
+
if (cs.length === 0)
|
|
150
|
+
throw new Error(`${mdUri}: empty resources/read`);
|
|
151
|
+
const text = typeof cs[0]?.text === "string" ? cs[0].text : "";
|
|
152
|
+
const d = typeof md?.digest === "string" ? md.digest : "";
|
|
153
|
+
if (d && d !== "sha256:" + createHash("sha256").update(text).digest("hex"))
|
|
154
|
+
throw new Error(`${mdUri}: digest mismatch`);
|
|
155
|
+
return { name, markdown: text, resources: others.sort() };
|
|
156
|
+
}
|
|
157
|
+
async close() {
|
|
158
|
+
await this.transport?.close();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export declare const PROTOCOL_VERSION = "2026-07-28";
|
|
2
|
+
export declare const META_VERSION = "io.modelcontextprotocol/protocolVersion";
|
|
3
|
+
export declare const META_CLIENT_CAPS = "io.modelcontextprotocol/clientCapabilities";
|
|
4
|
+
export declare const META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo";
|
|
5
|
+
export declare const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
|
|
6
|
+
export declare const SKILLS_EXT = "io.modelcontextprotocol/skills";
|
|
7
|
+
export declare const CODE_METHOD_NOT_FOUND = -32601;
|
|
8
|
+
export declare const CODE_INVALID_PARAMS = -32602;
|
|
9
|
+
export declare const CODE_HEADER_MISMATCH = -32020;
|
|
10
|
+
export declare const CODE_UNSUPPORTED_PROTOCOL = -32022;
|
|
11
|
+
/** A JSON object. */
|
|
12
|
+
export type Obj = Record<string, any>;
|
|
13
|
+
/** A JSON-RPC error returned by the server. `httpStatus` is set when it came over HTTP. */
|
|
14
|
+
export declare class RpcError extends Error {
|
|
15
|
+
code: number;
|
|
16
|
+
data?: unknown;
|
|
17
|
+
httpStatus?: number;
|
|
18
|
+
constructor(code: number, message: string, data?: unknown);
|
|
19
|
+
}
|
|
20
|
+
/** A non-2xx HTTP reply that carried no JSON-RPC message. */
|
|
21
|
+
export declare class HttpStatusError extends Error {
|
|
22
|
+
status: number;
|
|
23
|
+
constructor(status: number, message?: string);
|
|
24
|
+
}
|
|
25
|
+
/** A 2xx reply that is not a JSON-RPC response for the request (§2.2a: falls back on discover). */
|
|
26
|
+
export declare class NotJsonRpcError extends Error {
|
|
27
|
+
}
|
|
28
|
+
export declare const DEFAULT_TIMEOUT_MS = 30000;
|
|
29
|
+
export interface HttpRequest {
|
|
30
|
+
method: string;
|
|
31
|
+
url: string;
|
|
32
|
+
headers?: Record<string, string>;
|
|
33
|
+
body?: string;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
export interface HttpResponse {
|
|
38
|
+
status: number;
|
|
39
|
+
headers: Record<string, string>;
|
|
40
|
+
body: string;
|
|
41
|
+
}
|
|
42
|
+
/** The seam for every outbound HTTP call. */
|
|
43
|
+
export type HttpFn = (r: HttpRequest) => Promise<HttpResponse>;
|
|
44
|
+
export declare function trustDevCert(pem: string): void;
|
|
45
|
+
/** node:http(s) request; reads the whole body (capped at 16 MiB). Never follows redirects. */
|
|
46
|
+
export declare const defaultHttp: HttpFn;
|
|
47
|
+
export declare function getJSON(httpFn: HttpFn, url: string): Promise<Obj>;
|
|
48
|
+
/** A small async mutex (one login / one refresh at a time). */
|
|
49
|
+
export declare class Mutex {
|
|
50
|
+
private tail;
|
|
51
|
+
lock<T>(f: () => Promise<T>): Promise<T>;
|
|
52
|
+
}
|
|
53
|
+
/** Plain value if printable ASCII without edge whitespace, else the =?base64?…?= sentinel (§2.3). */
|
|
54
|
+
export declare function encodeHeader(v: string): string;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP 2026-07-28 wire constants + a tiny node:http(s) layer (SPEC §2.1, §2.3). No MCP SDK.
|
|
3
|
+
* node:http instead of fetch so a per-request trust store is possible (dev authorization servers).
|
|
4
|
+
*/
|
|
5
|
+
import http from "node:http";
|
|
6
|
+
import https from "node:https";
|
|
7
|
+
import tls from "node:tls";
|
|
8
|
+
export const PROTOCOL_VERSION = "2026-07-28";
|
|
9
|
+
export const META_VERSION = "io.modelcontextprotocol/protocolVersion";
|
|
10
|
+
export const META_CLIENT_CAPS = "io.modelcontextprotocol/clientCapabilities";
|
|
11
|
+
export const META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo";
|
|
12
|
+
export const META_SERVER_INFO = "io.modelcontextprotocol/serverInfo";
|
|
13
|
+
export const SKILLS_EXT = "io.modelcontextprotocol/skills";
|
|
14
|
+
export const CODE_METHOD_NOT_FOUND = -32601;
|
|
15
|
+
export const CODE_INVALID_PARAMS = -32602;
|
|
16
|
+
export const CODE_HEADER_MISMATCH = -32020;
|
|
17
|
+
export const CODE_UNSUPPORTED_PROTOCOL = -32022;
|
|
18
|
+
/** A JSON-RPC error returned by the server. `httpStatus` is set when it came over HTTP. */
|
|
19
|
+
export class RpcError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
data;
|
|
22
|
+
httpStatus;
|
|
23
|
+
constructor(code, message, data) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.code = code;
|
|
26
|
+
if (data !== undefined)
|
|
27
|
+
this.data = data;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/** A non-2xx HTTP reply that carried no JSON-RPC message. */
|
|
31
|
+
export class HttpStatusError extends Error {
|
|
32
|
+
status;
|
|
33
|
+
constructor(status, message) {
|
|
34
|
+
super(message ?? `HTTP ${status}`);
|
|
35
|
+
this.status = status;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** A 2xx reply that is not a JSON-RPC response for the request (§2.2a: falls back on discover). */
|
|
39
|
+
export class NotJsonRpcError extends Error {
|
|
40
|
+
}
|
|
41
|
+
export const DEFAULT_TIMEOUT_MS = 30_000;
|
|
42
|
+
const MAX_RESPONSE_BYTES = 16 << 20;
|
|
43
|
+
// Test/dev only: extra CA certificates trusted by defaultHttp (a dev authorization server's
|
|
44
|
+
// self-signed certificate). Not exported from the package index.
|
|
45
|
+
const extraCAs = [];
|
|
46
|
+
export function trustDevCert(pem) {
|
|
47
|
+
extraCAs.push(pem);
|
|
48
|
+
}
|
|
49
|
+
/** node:http(s) request; reads the whole body (capped at 16 MiB). Never follows redirects. */
|
|
50
|
+
export const defaultHttp = (r) => new Promise((resolve, reject) => {
|
|
51
|
+
const u = new URL(r.url);
|
|
52
|
+
const opts = { method: r.method, headers: r.headers ?? {}, agent: false };
|
|
53
|
+
if (u.protocol === "https:" && extraCAs.length)
|
|
54
|
+
opts.ca = [...tls.rootCertificates, ...extraCAs];
|
|
55
|
+
if (r.signal?.aborted)
|
|
56
|
+
return reject(r.signal.reason ?? new Error("aborted"));
|
|
57
|
+
const req = (u.protocol === "https:" ? https : http).request(u, opts, (res) => {
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let n = 0;
|
|
60
|
+
res.on("data", (c) => {
|
|
61
|
+
n += c.length;
|
|
62
|
+
if (n > MAX_RESPONSE_BYTES) {
|
|
63
|
+
req.destroy(new Error("response too large"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
chunks.push(c);
|
|
67
|
+
});
|
|
68
|
+
res.on("end", () => {
|
|
69
|
+
const headers = {};
|
|
70
|
+
for (const [k, v] of Object.entries(res.headers))
|
|
71
|
+
if (v !== undefined)
|
|
72
|
+
headers[k] = Array.isArray(v) ? v.join(", ") : v;
|
|
73
|
+
resolve({ status: res.statusCode ?? 0, headers, body: Buffer.concat(chunks).toString("utf8") });
|
|
74
|
+
});
|
|
75
|
+
res.on("error", reject);
|
|
76
|
+
});
|
|
77
|
+
const onAbort = () => req.destroy(r.signal?.reason instanceof Error ? r.signal.reason : new Error("aborted"));
|
|
78
|
+
r.signal?.addEventListener("abort", onAbort, { once: true });
|
|
79
|
+
req.on("close", () => r.signal?.removeEventListener("abort", onAbort));
|
|
80
|
+
req.setTimeout(r.timeoutMs ?? DEFAULT_TIMEOUT_MS, () => req.destroy(new Error(`request to ${u.host} timed out`)));
|
|
81
|
+
req.on("error", reject);
|
|
82
|
+
req.end(r.body);
|
|
83
|
+
});
|
|
84
|
+
export async function getJSON(httpFn, url) {
|
|
85
|
+
const res = await httpFn({ method: "GET", url, headers: { Accept: "application/json" } });
|
|
86
|
+
if (res.status !== 200)
|
|
87
|
+
throw new Error(`GET ${url}: HTTP ${res.status}`);
|
|
88
|
+
const v = JSON.parse(res.body);
|
|
89
|
+
if (!v || typeof v !== "object" || Array.isArray(v))
|
|
90
|
+
throw new Error(`GET ${url}: not a JSON object`);
|
|
91
|
+
return v;
|
|
92
|
+
}
|
|
93
|
+
/** A small async mutex (one login / one refresh at a time). */
|
|
94
|
+
export class Mutex {
|
|
95
|
+
tail = Promise.resolve();
|
|
96
|
+
async lock(f) {
|
|
97
|
+
let release;
|
|
98
|
+
const prev = this.tail;
|
|
99
|
+
this.tail = new Promise((r) => (release = r));
|
|
100
|
+
await prev;
|
|
101
|
+
try {
|
|
102
|
+
return await f();
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
release();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Plain value if printable ASCII without edge whitespace, else the =?base64?…?= sentinel (§2.3). */
|
|
110
|
+
export function encodeHeader(v) {
|
|
111
|
+
const safe = v === v.trim() && !(v.startsWith("=?base64?") && v.endsWith("?=")) && /^[\x20-\x7e]*$/.test(v);
|
|
112
|
+
return safe ? v : "=?base64?" + Buffer.from(v, "utf8").toString("base64") + "?=";
|
|
113
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Skill URI safety (python-sdk #3685): a skill:// URI is percent-decoded ONCE, then rejected if it
|
|
2
|
+
// still contains '%' (double encoding), a backslash, NUL, an absolute/empty path, or a "."/".."
|
|
3
|
+
// segment. Both the server (before lookup) and the client (before writing files) use it.
|
|
4
|
+
/** Splits skill://<name>/<path> into a validated name and slash-separated relative path. Throws on anything unsafe. */
|
|
5
|
+
export function parseSkillURI(uri) {
|
|
6
|
+
if (typeof uri !== "string" || !uri.startsWith("skill://"))
|
|
7
|
+
throw new Error("not a skill:// URI");
|
|
8
|
+
let dec;
|
|
9
|
+
try {
|
|
10
|
+
dec = decodeURIComponent(uri.slice("skill://".length));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new Error("bad percent-encoding");
|
|
14
|
+
}
|
|
15
|
+
if (/[%\\\x00]/.test(dec))
|
|
16
|
+
throw new Error("skill URI has encoded, backslash or NUL characters");
|
|
17
|
+
const segs = dec.split("/");
|
|
18
|
+
if (segs.length < 2)
|
|
19
|
+
throw new Error("skill URI needs skill://<name>/<path>");
|
|
20
|
+
for (const s of segs) {
|
|
21
|
+
if (s === "" || s === "." || s === ".." || s.includes(":"))
|
|
22
|
+
throw new Error("skill URI path escapes the skill (empty, '.', '..' or ':' segment)");
|
|
23
|
+
}
|
|
24
|
+
return { name: segs[0], rel: segs.slice(1).join("/") };
|
|
25
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Mcp2026Client } from "./client.js";
|
|
2
|
+
import { type Obj } from "./protocol.js";
|
|
3
|
+
import { OAuth, type TokenStore } from "./auth.js";
|
|
4
|
+
import type { Answer, Request, Tool, ToolContext, ToolResult } from "../types.js";
|
|
5
|
+
import type { SkillDef } from "../skill.js";
|
|
6
|
+
export type WaitFor = (request: Request) => Promise<Answer>;
|
|
7
|
+
/** Per-server `auth` (§2A.1; keys camelCase as in mcp.json). */
|
|
8
|
+
export interface AuthConfig {
|
|
9
|
+
clientId?: string;
|
|
10
|
+
clientSecret?: string;
|
|
11
|
+
tokenAuthMethod?: "client_secret_basic" | "client_secret_post";
|
|
12
|
+
clientIds?: Record<string, string>;
|
|
13
|
+
clientMetadataUrl?: string;
|
|
14
|
+
redirectUri?: string;
|
|
15
|
+
mode?: "auto" | "pause";
|
|
16
|
+
omitRefreshResource?: boolean;
|
|
17
|
+
allowHttp?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface Load2026Options {
|
|
20
|
+
waitFor?: WaitFor;
|
|
21
|
+
tokenStore?: TokenStore;
|
|
22
|
+
openUrl?: (url: string) => unknown | Promise<unknown>;
|
|
23
|
+
signal?: AbortSignal;
|
|
24
|
+
/** Final exposed names to drop (Toolkit `disableTools`); applied before tool search too. */
|
|
25
|
+
disableTools?: string[];
|
|
26
|
+
}
|
|
27
|
+
/** The OAuth engine for one remote server (§2A.1). */
|
|
28
|
+
export declare function newOAuth(auth: AuthConfig | undefined, o: Load2026Options, store: TokenStore): OAuth;
|
|
29
|
+
export declare function defaultTokenStore(o: Load2026Options): TokenStore;
|
|
30
|
+
/** What the probe decided. `fallback` ⇒ take the frozen SDK path; anything thrown ⇒ status failed. */
|
|
31
|
+
export type Probe = {
|
|
32
|
+
kind: "modern";
|
|
33
|
+
server: ModernServer;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "fallback";
|
|
36
|
+
};
|
|
37
|
+
export interface ProbeTarget {
|
|
38
|
+
name: string;
|
|
39
|
+
timeout: number;
|
|
40
|
+
tools?: Record<string, boolean>;
|
|
41
|
+
toolSearch?: boolean;
|
|
42
|
+
description?: string;
|
|
43
|
+
remote?: {
|
|
44
|
+
url: string;
|
|
45
|
+
headers?: Record<string, string>;
|
|
46
|
+
oauth: OAuth;
|
|
47
|
+
};
|
|
48
|
+
local?: {
|
|
49
|
+
argv: string[];
|
|
50
|
+
env: NodeJS.ProcessEnv;
|
|
51
|
+
cwd?: string;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** Discover first (§2.2). Resolves `modern` (tools listed) or `fallback`; throws on a load failure. */
|
|
55
|
+
export declare function probe(t: ProbeTarget, o: Load2026Options): Promise<Probe>;
|
|
56
|
+
export declare class ModernServer {
|
|
57
|
+
private t;
|
|
58
|
+
private o;
|
|
59
|
+
readonly client: Mcp2026Client;
|
|
60
|
+
readonly discover: Obj;
|
|
61
|
+
defs: Obj[];
|
|
62
|
+
skills: SkillDef[];
|
|
63
|
+
private all;
|
|
64
|
+
private cacheUntil;
|
|
65
|
+
private pend;
|
|
66
|
+
constructor(t: ProbeTarget, client: Mcp2026Client, disc: Obj, o: Load2026Options);
|
|
67
|
+
private get oauth();
|
|
68
|
+
private req;
|
|
69
|
+
load(): Promise<void>;
|
|
70
|
+
private refreshTools;
|
|
71
|
+
private loadSkills;
|
|
72
|
+
/** The toolkit tools for this server: every (filtered) server tool, or the two tool-search tools. */
|
|
73
|
+
tools(): Tool[];
|
|
74
|
+
private convert;
|
|
75
|
+
/** tools/call with MRTR rounds (§2.5) and OAuth pauses (§2A.1 10); never re-sends after a failure (§2.6). */
|
|
76
|
+
run(name: string, args: Obj, ctx?: ToolContext): Promise<ToolResult>;
|
|
77
|
+
private suspend;
|
|
78
|
+
private drive;
|
|
79
|
+
private toRequest;
|
|
80
|
+
/** resources/list for list_mcp_resources (§4C). */
|
|
81
|
+
listResources(signal?: AbortSignal): Promise<Obj[]>;
|
|
82
|
+
readResource(uri: string, signal?: AbortSignal): Promise<Obj[]>;
|
|
83
|
+
/** Raw (unfiltered) tool defs, for listMcpTools. */
|
|
84
|
+
rawDefs(): Obj[];
|
|
85
|
+
close(): Promise<void>;
|
|
86
|
+
}
|