@graphorin/mcp 0.6.1 → 0.7.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/CHANGELOG.md +43 -0
- package/README.md +64 -3
- package/dist/client/adapt-result.d.ts +9 -1
- package/dist/client/adapt-result.d.ts.map +1 -1
- package/dist/client/adapt-result.js +28 -10
- package/dist/client/adapt-result.js.map +1 -1
- package/dist/client/client-handlers.js +7 -1
- package/dist/client/client-handlers.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +45 -70
- package/dist/client/client.js.map +1 -1
- package/dist/client/inbound-filters.js +101 -1
- package/dist/client/inbound-filters.js.map +1 -1
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.js +2 -1
- package/dist/client/managed.d.ts +35 -0
- package/dist/client/managed.d.ts.map +1 -0
- package/dist/client/managed.js +136 -0
- package/dist/client/managed.js.map +1 -0
- package/dist/client/mcp-resource-reader.js +1 -1
- package/dist/client/mcp-resource-reader.js.map +1 -1
- package/dist/client/to-tools-run.js +119 -0
- package/dist/client/to-tools-run.js.map +1 -0
- package/dist/client/to-tools.d.ts +8 -0
- package/dist/client/to-tools.d.ts.map +1 -1
- package/dist/client/to-tools.js +27 -4
- package/dist/client/to-tools.js.map +1 -1
- package/dist/client/types.d.ts +12 -3
- package/dist/client/types.d.ts.map +1 -1
- package/dist/errors/index.d.ts +3 -3
- package/dist/errors/index.js +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/helpers/identity.d.ts +11 -0
- package/dist/helpers/identity.d.ts.map +1 -1
- package/dist/helpers/identity.js +13 -2
- package/dist/helpers/identity.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/index.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/dist/registry/json-schema.js +26 -8
- package/dist/registry/json-schema.js.map +1 -1
- package/dist/transport/types.d.ts +9 -0
- package/package.json +13 -12
- package/src/client/adapt-result.ts +302 -0
- package/src/client/client-handlers.ts +215 -0
- package/src/client/client.ts +725 -0
- package/src/client/defer-loading.ts +108 -0
- package/src/client/inbound-filters.ts +246 -0
- package/src/client/index.ts +42 -0
- package/src/client/managed.ts +222 -0
- package/src/client/mcp-resource-reader.ts +183 -0
- package/src/client/pinning.ts +48 -0
- package/src/client/to-tools-run.ts +178 -0
- package/src/client/to-tools.ts +294 -0
- package/src/client/transport-factory.ts +117 -0
- package/src/client/types.ts +422 -0
- package/src/errors/index.ts +170 -0
- package/src/helpers/identity.ts +128 -0
- package/src/helpers/index.ts +8 -0
- package/src/helpers/validate-config.ts +95 -0
- package/src/index.ts +49 -0
- package/src/oauth/bridge.ts +143 -0
- package/src/oauth/index.ts +18 -0
- package/src/oauth/library.ts +61 -0
- package/src/registry/json-schema.ts +402 -0
- package/src/transport/index.ts +15 -0
- package/src/transport/types.ts +108 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { runToTools } from "./to-tools-run.js";
|
|
2
|
+
import { createMCPClient } from "./client.js";
|
|
3
|
+
import { incrementCounter } from "@graphorin/tools/audit";
|
|
4
|
+
|
|
5
|
+
//#region src/client/managed.ts
|
|
6
|
+
/**
|
|
7
|
+
* Open a managed (auto-reconnecting) MCP client. See the module doc for
|
|
8
|
+
* the exact semantics. `close()` is terminal: it stops any in-progress
|
|
9
|
+
* backoff and no further reconnects happen.
|
|
10
|
+
*
|
|
11
|
+
* @stable
|
|
12
|
+
*/
|
|
13
|
+
async function createManagedMCPClient(options) {
|
|
14
|
+
const { reconnect, _clientFactory, ...clientOptions } = options;
|
|
15
|
+
const factory = _clientFactory ?? createMCPClient;
|
|
16
|
+
const maxAttempts = reconnect?.maxAttempts ?? 5;
|
|
17
|
+
const initialDelayMs = reconnect?.initialDelayMs ?? 500;
|
|
18
|
+
const maxDelayMs = reconnect?.maxDelayMs ?? 3e4;
|
|
19
|
+
let closed = false;
|
|
20
|
+
let reconnecting = null;
|
|
21
|
+
let toolsEverListed = false;
|
|
22
|
+
let lastToToolsOpts;
|
|
23
|
+
const fingerprintRef = { current: void 0 };
|
|
24
|
+
const innerOptions = {
|
|
25
|
+
...clientOptions,
|
|
26
|
+
onTransportClose: (info) => {
|
|
27
|
+
if (closed) return;
|
|
28
|
+
reconnectLoop(info);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
let current = await factory(innerOptions);
|
|
32
|
+
async function reconnectLoop(info) {
|
|
33
|
+
if (closed || reconnecting !== null) return;
|
|
34
|
+
const task = (async () => {
|
|
35
|
+
const dead = current;
|
|
36
|
+
let delay = initialDelayMs;
|
|
37
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
38
|
+
if (closed) return;
|
|
39
|
+
incrementCounter("mcp.reconnect.attempt.total", { server: info.server });
|
|
40
|
+
try {
|
|
41
|
+
const next = await factory(innerOptions);
|
|
42
|
+
if (closed) {
|
|
43
|
+
next.close().catch(() => {});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
current = next;
|
|
47
|
+
incrementCounter("mcp.reconnect.success.total", { server: info.server });
|
|
48
|
+
options.logger?.("info", "mcp.reconnect.success: inner client rebuilt", {
|
|
49
|
+
server: info.server,
|
|
50
|
+
attempt
|
|
51
|
+
});
|
|
52
|
+
dead.close().catch(() => {});
|
|
53
|
+
if (toolsEverListed) try {
|
|
54
|
+
await managedToTools(lastToToolsOpts);
|
|
55
|
+
} catch (cause) {
|
|
56
|
+
options.logger?.("warn", "mcp.reconnect.catalogue-rescreen-failed: post-reconnect toTools() rejected (pin mismatch or listing failure) - the previously adapted tools remain registered; investigate before trusting this server", {
|
|
57
|
+
server: info.server,
|
|
58
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return;
|
|
62
|
+
} catch (cause) {
|
|
63
|
+
if (attempt >= maxAttempts) {
|
|
64
|
+
incrementCounter("mcp.reconnect.gave-up.total", { server: info.server });
|
|
65
|
+
options.logger?.("error", "mcp.reconnect.gave-up: reconnect attempts exhausted", {
|
|
66
|
+
server: info.server,
|
|
67
|
+
attempts: attempt,
|
|
68
|
+
error: cause instanceof Error ? cause.message : String(cause)
|
|
69
|
+
});
|
|
70
|
+
options.onTransportClose?.(info);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
await sleep(Math.min(delay, maxDelayMs) * (.5 + Math.random() * .5));
|
|
74
|
+
delay = Math.min(delay * 2, maxDelayMs);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
})().finally(() => {
|
|
78
|
+
reconnecting = null;
|
|
79
|
+
});
|
|
80
|
+
reconnecting = task;
|
|
81
|
+
await task;
|
|
82
|
+
}
|
|
83
|
+
async function managedToTools(toolsOpts) {
|
|
84
|
+
toolsEverListed = true;
|
|
85
|
+
lastToToolsOpts = toolsOpts;
|
|
86
|
+
return runToTools({
|
|
87
|
+
client: managed,
|
|
88
|
+
fingerprintRef,
|
|
89
|
+
...options.logger === void 0 ? {} : { logger: options.logger },
|
|
90
|
+
...toolsOpts === void 0 ? {} : { toolsOpts }
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const managed = {
|
|
94
|
+
get id() {
|
|
95
|
+
return current.id;
|
|
96
|
+
},
|
|
97
|
+
get serverInfo() {
|
|
98
|
+
return current.serverInfo;
|
|
99
|
+
},
|
|
100
|
+
get serverIdentity() {
|
|
101
|
+
return current.serverIdentity;
|
|
102
|
+
},
|
|
103
|
+
get collisionStrategy() {
|
|
104
|
+
return current.collisionStrategy;
|
|
105
|
+
},
|
|
106
|
+
get priority() {
|
|
107
|
+
return current.priority;
|
|
108
|
+
},
|
|
109
|
+
get sessionIdPresent() {
|
|
110
|
+
return current.sessionIdPresent;
|
|
111
|
+
},
|
|
112
|
+
get resumable() {
|
|
113
|
+
return current.resumable;
|
|
114
|
+
},
|
|
115
|
+
listTools: (opts) => current.listTools(opts),
|
|
116
|
+
listResources: (opts) => current.listResources(opts),
|
|
117
|
+
listPrompts: (opts) => current.listPrompts(opts),
|
|
118
|
+
callTool: (name, args, opts) => current.callTool(name, args, opts),
|
|
119
|
+
readResource: (uri, opts) => current.readResource(uri, opts),
|
|
120
|
+
readResourceContents: (uri, opts) => current.readResourceContents(uri, opts),
|
|
121
|
+
getPrompt: (name, args, opts) => current.getPrompt(name, args, opts),
|
|
122
|
+
toTools: managedToTools,
|
|
123
|
+
close: async () => {
|
|
124
|
+
closed = true;
|
|
125
|
+
await current.close();
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
return managed;
|
|
129
|
+
}
|
|
130
|
+
function sleep(ms) {
|
|
131
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
//#endregion
|
|
135
|
+
export { createManagedMCPClient };
|
|
136
|
+
//# sourceMappingURL=managed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"managed.js","names":["reconnecting: Promise<void> | null","lastToToolsOpts: MCPToToolsOptions | undefined","fingerprintRef: ToolFingerprintRef","innerOptions: CreateMCPClientOptions"],"sources":["../../src/client/managed.ts"],"sourcesContent":["/**\n * W-080: opt-in managed MCP client with automatic reconnection.\n *\n * `createMCPClient` connections are deliberately one-shot: transient\n * Streamable-HTTP hiccups are healed by the SDK itself (Last-Event-ID),\n * but a dead stdio child or a lost HTTP session kills the client for\n * good, and every `Tool` adapted from it closes over the corpse. The\n * managed wrapper fixes the long-running-agent story:\n *\n * - it holds the current inner client in a mutable ref and implements\n * {@link MCPClient} by delegation, so ALL consumers (including every\n * adapted `Tool.execute`, which the wrapper's `toTools()` binds to the\n * WRAPPER, not the inner client) transparently follow a swap;\n * - on transport close it rebuilds the inner client with exponential\n * backoff + jitter (`mcp.reconnect.attempt/success/gave-up.total`\n * counters), then re-runs `toTools()` with the last-used options so\n * the pin comparison (TOFU store / explicit pins) re-screens the\n * post-reconnect catalogue - a rug-pull across a reconnect is caught;\n * - it NEVER retries an in-flight call: a call that died with the\n * transport stays failed (retry policy belongs to the executor /\n * model), only the CONNECTION heals;\n * - the operator's `onTransportClose` fires once, on FINAL failure\n * (reconnect exhausted) - intermediate closes are the wrapper's job.\n *\n * The plain `createMCPClient` contract is unchanged; this wrapper is a\n * separate, additive entry point.\n *\n * @packageDocumentation\n */\n\nimport type { Tool } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { createMCPClient } from './client.js';\nimport { runToTools, type ToolFingerprintRef } from './to-tools-run.js';\nimport type { CreateMCPClientOptions, MCPClient, MCPToToolsOptions } from './types.js';\n\n/** Reconnection tuning for {@link createManagedMCPClient}. @stable */\nexport interface ManagedReconnectOptions {\n /** Attempts per outage before giving up. Default `5`. */\n readonly maxAttempts?: number;\n /** First backoff delay (doubles per attempt, jittered). Default `500` ms. */\n readonly initialDelayMs?: number;\n /** Backoff ceiling. Default `30_000` ms. */\n readonly maxDelayMs?: number;\n}\n\n/** Options for {@link createManagedMCPClient}. @stable */\nexport type CreateManagedMCPClientOptions = CreateMCPClientOptions & {\n readonly reconnect?: ManagedReconnectOptions;\n /**\n * Client factory seam - tests inject fake inner clients; production\n * uses {@link createMCPClient}.\n *\n * @internal\n */\n readonly _clientFactory?: (options: CreateMCPClientOptions) => Promise<MCPClient>;\n};\n\n/**\n * Open a managed (auto-reconnecting) MCP client. See the module doc for\n * the exact semantics. `close()` is terminal: it stops any in-progress\n * backoff and no further reconnects happen.\n *\n * @stable\n */\nexport async function createManagedMCPClient(\n options: CreateManagedMCPClientOptions,\n): Promise<MCPClient> {\n const { reconnect, _clientFactory, ...clientOptions } = options;\n const factory = _clientFactory ?? createMCPClient;\n const maxAttempts = reconnect?.maxAttempts ?? 5;\n const initialDelayMs = reconnect?.initialDelayMs ?? 500;\n const maxDelayMs = reconnect?.maxDelayMs ?? 30_000;\n\n let closed = false;\n let reconnecting: Promise<void> | null = null;\n let toolsEverListed = false;\n let lastToToolsOpts: MCPToToolsOptions | undefined;\n // Drift tracking spans reconnects on purpose: a definition swapped\n // behind a reconnect still diffs against the pre-outage snapshot.\n const fingerprintRef: ToolFingerprintRef = { current: undefined };\n\n const innerOptions: CreateMCPClientOptions = {\n ...clientOptions,\n // The wrapper owns transport-close handling; the operator callback\n // fires once, on final (gave-up) failure - see the module doc.\n onTransportClose: (info) => {\n if (closed) return;\n void reconnectLoop(info);\n },\n };\n\n let current = await factory(innerOptions);\n\n async function reconnectLoop(info: { readonly server: string }): Promise<void> {\n if (closed || reconnecting !== null) return;\n const task = (async () => {\n const dead = current;\n let delay = initialDelayMs;\n for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {\n if (closed) return;\n incrementCounter('mcp.reconnect.attempt.total', { server: info.server });\n try {\n const next = await factory(innerOptions);\n if (closed) {\n // close() raced the rebuild - do not resurrect.\n void next.close().catch(() => {});\n return;\n }\n current = next;\n incrementCounter('mcp.reconnect.success.total', { server: info.server });\n options.logger?.('info', 'mcp.reconnect.success: inner client rebuilt', {\n server: info.server,\n attempt,\n });\n // Best-effort close of the dead client (idempotent by contract).\n void dead.close().catch(() => {});\n // Re-screen the post-reconnect catalogue with the SAME options\n // the operator last used, so pin comparison + drift diff run\n // against it. A pin rejection here must not kill the reconnect\n // (the connection is healthy) - it is logged and counted by\n // the pipeline itself.\n if (toolsEverListed) {\n try {\n await managedToTools(lastToToolsOpts);\n } catch (cause) {\n options.logger?.(\n 'warn',\n 'mcp.reconnect.catalogue-rescreen-failed: post-reconnect toTools() rejected (pin mismatch or listing failure) - the previously adapted tools remain registered; investigate before trusting this server',\n {\n server: info.server,\n error: cause instanceof Error ? cause.message : String(cause),\n },\n );\n }\n }\n return;\n } catch (cause) {\n if (attempt >= maxAttempts) {\n incrementCounter('mcp.reconnect.gave-up.total', { server: info.server });\n options.logger?.('error', 'mcp.reconnect.gave-up: reconnect attempts exhausted', {\n server: info.server,\n attempts: attempt,\n error: cause instanceof Error ? cause.message : String(cause),\n });\n options.onTransportClose?.(info);\n return;\n }\n // Full jitter on an exponential ladder, capped at maxDelayMs.\n const jittered = Math.min(delay, maxDelayMs) * (0.5 + Math.random() * 0.5);\n await sleep(jittered);\n delay = Math.min(delay * 2, maxDelayMs);\n }\n }\n })().finally(() => {\n reconnecting = null;\n });\n reconnecting = task;\n await task;\n }\n\n async function managedToTools(toolsOpts?: MCPToToolsOptions): Promise<ReadonlyArray<Tool>> {\n toolsEverListed = true;\n lastToToolsOpts = toolsOpts;\n return runToTools({\n // THE point of the wrapper: adapted tools close over `managed`,\n // so their `execute` transparently follows an inner-client swap.\n client: managed,\n fingerprintRef,\n ...(options.logger === undefined ? {} : { logger: options.logger }),\n ...(toolsOpts === undefined ? {} : { toolsOpts }),\n });\n }\n\n // Data fields delegate through getters so a swap is instantly visible;\n // the cast bridges `priority?: number` vs an always-present getter\n // returning `number | undefined` (exactOptionalPropertyTypes).\n const managed = {\n get id() {\n return current.id;\n },\n get serverInfo() {\n return current.serverInfo;\n },\n get serverIdentity() {\n return current.serverIdentity;\n },\n get collisionStrategy() {\n return current.collisionStrategy;\n },\n get priority() {\n return current.priority;\n },\n get sessionIdPresent() {\n return current.sessionIdPresent;\n },\n get resumable() {\n return current.resumable;\n },\n listTools: (opts?: { signal?: AbortSignal }) => current.listTools(opts),\n listResources: (opts?: { signal?: AbortSignal }) => current.listResources(opts),\n listPrompts: (opts?: { signal?: AbortSignal }) => current.listPrompts(opts),\n callTool: (name: string, args: unknown, opts?: { signal?: AbortSignal; timeoutMs?: number }) =>\n current.callTool(name, args, opts),\n readResource: (uri: string, opts?: { signal?: AbortSignal }) => current.readResource(uri, opts),\n readResourceContents: (uri: string, opts?: { signal?: AbortSignal }) =>\n current.readResourceContents(uri, opts),\n getPrompt: (name: string, args?: unknown, opts?: { signal?: AbortSignal }) =>\n current.getPrompt(name, args, opts),\n toTools: managedToTools,\n close: async (): Promise<void> => {\n closed = true;\n await current.close();\n },\n } as MCPClient;\n\n return managed;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n"],"mappings":"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"}
|
|
@@ -40,7 +40,7 @@ function createMcpResourceReader(opts) {
|
|
|
40
40
|
let uri = handle;
|
|
41
41
|
let candidates = clients;
|
|
42
42
|
if (scoped !== null) {
|
|
43
|
-
const serverId = scoped[1] ?? "";
|
|
43
|
+
const serverId = decodeURIComponent(scoped[1] ?? "");
|
|
44
44
|
uri = scoped[2] ?? "";
|
|
45
45
|
candidates = clients.filter((c) => c.serverIdentity.id === serverId);
|
|
46
46
|
if (candidates.length === 0) throw new Error(`createMcpResourceReader: no configured client matches the handle's server '${serverId}' - a handle only resolves against its originating server.`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-resource-reader.js","names":["lastError: unknown","content: MCPResourceContent","end"],"sources":["../../src/client/mcp-resource-reader.ts"],"sourcesContent":["/**\n * {@link createMcpResourceReader} - resolve MCP `resource_link` handles\n * on demand (WI-13 / P2-2, ties to WI-10 result handles).\n *\n * The `toTools()` adapter surfaces an MCP `resource_link` as a preview +\n * the resource `uri` (the {@link import('@graphorin/tools/result').ResultReader}\n * handle) instead of inlining the body. This reader resolves that handle\n * via {@link MCPClient.readResource} when the model later calls the\n * built-in `read_result` tool - so a large MCP resource never inflates\n * context until the model actually asks for it.\n *\n * Compose it with the agent's spill-file reader (the agent tries each\n * configured reader in order): the spill reader claims\n * `graphorin-spill:` handles, this reader resolves the rest. With more\n * than one MCP client, the reader tries each until one server resolves\n * the URI.\n *\n * Network note (R4): this reader performs no I/O until `read(...)` is\n * called, and then only over a connection the operator already opened.\n *\n * @packageDocumentation\n */\n\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { ResultReader, ResultReadOutcome, ResultReadRange } from '@graphorin/tools/result';\nimport type { MCPClient, MCPResourceContent } from './types.js';\n\n/** Configuration for {@link createMcpResourceReader}. */\nexport interface McpResourceReaderOptions {\n /** Clients consulted (in order) to resolve a resource URI. */\n readonly clients: ReadonlyArray<MCPClient>;\n /** Default `maxBytes` when `read(...)` is called without one. Default `65536`. */\n readonly defaultMaxBytes?: number;\n /**\n * mcp-skills-06: allow a BARE (unscoped) resource URI to be tried\n * against every configured client. Default `false` - handles minted\n * by the adapter are scoped (`mcp:<serverId>:<uri>`) and resolve\n * ONLY against their originating server, so a malicious server's\n * link (or a prompt-injected model) cannot fetch a resource from a\n * different, more-trusted server (the cross-server confused-deputy\n * hop). Enable only when you accept that risk for legacy handles.\n */\n readonly allowCrossServer?: boolean;\n}\n\n/** Scoped-handle grammar minted by the tool adapter: `mcp:<serverId>:<uri>`. */\nconst SCOPED_HANDLE = /^mcp:([^:]+):(.+)$/;\n\n/**\n * Build a {@link ResultReader} that resolves MCP resource URIs through\n * one or more connected {@link MCPClient}s.\n *\n * @stable\n */\nexport function createMcpResourceReader(opts: McpResourceReaderOptions): ResultReader {\n const clients = [...opts.clients];\n const defaultMaxBytes = opts.defaultMaxBytes ?? 65_536;\n return {\n async read(handle, range): Promise<ResultReadOutcome> {\n if (clients.length === 0) {\n throw new Error(\n 'createMcpResourceReader: no MCP clients configured to resolve resource handles.',\n );\n }\n // mcp-skills-06: a scoped handle (`mcp:<serverId>:<uri>`) resolves\n // ONLY against its originating server. The try-every-client loop\n // survives solely for bare URIs behind the explicit\n // `allowCrossServer` opt-in.\n const scoped = SCOPED_HANDLE.exec(handle);\n let uri = handle;\n let candidates = clients;\n if (scoped !== null) {\n const serverId = scoped[1] ?? '';\n uri = scoped[2] ?? '';\n candidates = clients.filter((c) => c.serverIdentity.id === serverId);\n if (candidates.length === 0) {\n throw new Error(\n `createMcpResourceReader: no configured client matches the handle's server ` +\n `'${serverId}' - a handle only resolves against its originating server.`,\n );\n }\n } else if (opts.allowCrossServer !== true) {\n throw new Error(\n `createMcpResourceReader: refusing to resolve the unscoped resource URI ` +\n `${JSON.stringify(handle)} against every configured server (cross-server ` +\n `confused-deputy risk). Use the scoped handle from the tool result ` +\n `('mcp:<serverId>:<uri>'), or opt in with allowCrossServer: true.`,\n );\n }\n let lastError: unknown;\n for (const client of candidates) {\n let content: MCPResourceContent;\n try {\n content = await client.readResource(uri);\n } catch (err) {\n lastError = err;\n continue;\n }\n incrementCounter('mcp.resource-link.resolved.total', {\n server: client.serverIdentity.id,\n });\n return sliceResource(content, range, defaultMaxBytes);\n }\n const suffix = lastError instanceof Error ? ` Last error: ${lastError.message}` : '';\n throw new Error(\n `createMcpResourceReader: no configured MCP server resolved resource ${JSON.stringify(uri)}.${suffix}`,\n );\n },\n };\n}\n\n/**\n * Raw resource bytes (MC-10): text resources as UTF-8, blob resources\n * DECODED from base64 - slicing/totalBytes operate on real payload\n * bytes, never on the ~33%-inflated base64 string (whose arbitrary\n * cuts also break base64 quads).\n */\nfunction resourceBytes(content: MCPResourceContent): Buffer {\n if (content.text !== undefined) return Buffer.from(content.text, 'utf8');\n if (content.blob !== undefined) return Buffer.from(content.blob, 'base64');\n return Buffer.alloc(0);\n}\n\n/**\n * Apply a {@link ResultReadRange} to the fully-fetched resource body,\n * mirroring the spill-file reader's slicing semantics (line mode wins;\n * `maxBytes` caps the returned slice) so `read_result` pages uniformly.\n */\nfunction sliceResource(\n content: MCPResourceContent,\n range: ResultReadRange | undefined,\n defaultMaxBytes: number,\n): ResultReadOutcome {\n const buf = resourceBytes(content);\n const full = buf.toString('utf8');\n const totalBytes = buf.byteLength;\n const cap = Math.max(0, range?.maxBytes ?? defaultMaxBytes);\n\n if (range?.startLine !== undefined || range?.endLine !== undefined) {\n const lines = full.split('\\n');\n const start = Math.max(1, range.startLine ?? 1);\n const end = Math.min(lines.length, range.endLine ?? lines.length);\n const selected = start <= end ? lines.slice(start - 1, end).join('\\n') : '';\n const capped = Buffer.byteLength(selected, 'utf8') > cap;\n const out = capBytes(selected, cap);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition - the\n // executor re-applies inbound sanitization + dataflow provenance\n // by this class when read_result relays it.\n producerTrustClass: 'mcp-derived' as const,\n content: out,\n bytes: Buffer.byteLength(out, 'utf8'),\n totalBytes,\n eof: end >= lines.length && !capped,\n });\n }\n\n const offset = clamp(range?.offset ?? 0, 0, totalBytes);\n const requested = range?.length ?? totalBytes - offset;\n const rawEnd = clamp(offset + Math.max(0, requested), offset, totalBytes);\n const end = Math.min(rawEnd, offset + cap);\n const slice = buf.subarray(offset, end);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition.\n producerTrustClass: 'mcp-derived' as const,\n content: slice.toString('utf8'),\n bytes: slice.byteLength,\n totalBytes,\n eof: end >= totalBytes,\n });\n}\n\nfunction clamp(value: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, value));\n}\n\n/** Truncate `s` to at most `maxBytes` UTF-8 bytes (tolerating a split trailing char). */\nfunction capBytes(s: string, maxBytes: number): string {\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n return Buffer.from(s, 'utf8').subarray(0, maxBytes).toString('utf8');\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-resource-reader.js","names":["lastError: unknown","content: MCPResourceContent","end"],"sources":["../../src/client/mcp-resource-reader.ts"],"sourcesContent":["/**\n * {@link createMcpResourceReader} - resolve MCP `resource_link` handles\n * on demand (WI-13 / P2-2, ties to WI-10 result handles).\n *\n * The `toTools()` adapter surfaces an MCP `resource_link` as a preview +\n * the resource `uri` (the {@link import('@graphorin/tools/result').ResultReader}\n * handle) instead of inlining the body. This reader resolves that handle\n * via {@link MCPClient.readResource} when the model later calls the\n * built-in `read_result` tool - so a large MCP resource never inflates\n * context until the model actually asks for it.\n *\n * Compose it with the agent's spill-file reader (the agent tries each\n * configured reader in order): the spill reader claims\n * `graphorin-spill:` handles, this reader resolves the rest. With more\n * than one MCP client, the reader tries each until one server resolves\n * the URI.\n *\n * Network note (R4): this reader performs no I/O until `read(...)` is\n * called, and then only over a connection the operator already opened.\n *\n * @packageDocumentation\n */\n\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport type { ResultReader, ResultReadOutcome, ResultReadRange } from '@graphorin/tools/result';\nimport type { MCPClient, MCPResourceContent } from './types.js';\n\n/** Configuration for {@link createMcpResourceReader}. */\nexport interface McpResourceReaderOptions {\n /** Clients consulted (in order) to resolve a resource URI. */\n readonly clients: ReadonlyArray<MCPClient>;\n /** Default `maxBytes` when `read(...)` is called without one. Default `65536`. */\n readonly defaultMaxBytes?: number;\n /**\n * mcp-skills-06: allow a BARE (unscoped) resource URI to be tried\n * against every configured client. Default `false` - handles minted\n * by the adapter are scoped (`mcp:<serverId>:<uri>`) and resolve\n * ONLY against their originating server, so a malicious server's\n * link (or a prompt-injected model) cannot fetch a resource from a\n * different, more-trusted server (the cross-server confused-deputy\n * hop). Enable only when you accept that risk for legacy handles.\n */\n readonly allowCrossServer?: boolean;\n}\n\n/** Scoped-handle grammar minted by the tool adapter: `mcp:<serverId>:<uri>`. */\nconst SCOPED_HANDLE = /^mcp:([^:]+):(.+)$/;\n\n/**\n * Build a {@link ResultReader} that resolves MCP resource URIs through\n * one or more connected {@link MCPClient}s.\n *\n * @stable\n */\nexport function createMcpResourceReader(opts: McpResourceReaderOptions): ResultReader {\n const clients = [...opts.clients];\n const defaultMaxBytes = opts.defaultMaxBytes ?? 65_536;\n return {\n async read(handle, range): Promise<ResultReadOutcome> {\n if (clients.length === 0) {\n throw new Error(\n 'createMcpResourceReader: no MCP clients configured to resolve resource handles.',\n );\n }\n // mcp-skills-06: a scoped handle (`mcp:<serverId>:<uri>`) resolves\n // ONLY against its originating server. The try-every-client loop\n // survives solely for bare URIs behind the explicit\n // `allowCrossServer` opt-in.\n const scoped = SCOPED_HANDLE.exec(handle);\n let uri = handle;\n let candidates = clients;\n if (scoped !== null) {\n // W-140: the id segment is percent-encoded at mint time (ids\n // carry ':' since W-016 made ports part of the identity).\n const serverId = decodeURIComponent(scoped[1] ?? '');\n uri = scoped[2] ?? '';\n candidates = clients.filter((c) => c.serverIdentity.id === serverId);\n if (candidates.length === 0) {\n throw new Error(\n `createMcpResourceReader: no configured client matches the handle's server ` +\n `'${serverId}' - a handle only resolves against its originating server.`,\n );\n }\n } else if (opts.allowCrossServer !== true) {\n throw new Error(\n `createMcpResourceReader: refusing to resolve the unscoped resource URI ` +\n `${JSON.stringify(handle)} against every configured server (cross-server ` +\n `confused-deputy risk). Use the scoped handle from the tool result ` +\n `('mcp:<serverId>:<uri>'), or opt in with allowCrossServer: true.`,\n );\n }\n let lastError: unknown;\n for (const client of candidates) {\n let content: MCPResourceContent;\n try {\n content = await client.readResource(uri);\n } catch (err) {\n lastError = err;\n continue;\n }\n incrementCounter('mcp.resource-link.resolved.total', {\n server: client.serverIdentity.id,\n });\n return sliceResource(content, range, defaultMaxBytes);\n }\n const suffix = lastError instanceof Error ? ` Last error: ${lastError.message}` : '';\n throw new Error(\n `createMcpResourceReader: no configured MCP server resolved resource ${JSON.stringify(uri)}.${suffix}`,\n );\n },\n };\n}\n\n/**\n * Raw resource bytes (MC-10): text resources as UTF-8, blob resources\n * DECODED from base64 - slicing/totalBytes operate on real payload\n * bytes, never on the ~33%-inflated base64 string (whose arbitrary\n * cuts also break base64 quads).\n */\nfunction resourceBytes(content: MCPResourceContent): Buffer {\n if (content.text !== undefined) return Buffer.from(content.text, 'utf8');\n if (content.blob !== undefined) return Buffer.from(content.blob, 'base64');\n return Buffer.alloc(0);\n}\n\n/**\n * Apply a {@link ResultReadRange} to the fully-fetched resource body,\n * mirroring the spill-file reader's slicing semantics (line mode wins;\n * `maxBytes` caps the returned slice) so `read_result` pages uniformly.\n */\nfunction sliceResource(\n content: MCPResourceContent,\n range: ResultReadRange | undefined,\n defaultMaxBytes: number,\n): ResultReadOutcome {\n const buf = resourceBytes(content);\n const full = buf.toString('utf8');\n const totalBytes = buf.byteLength;\n const cap = Math.max(0, range?.maxBytes ?? defaultMaxBytes);\n\n if (range?.startLine !== undefined || range?.endLine !== undefined) {\n const lines = full.split('\\n');\n const start = Math.max(1, range.startLine ?? 1);\n const end = Math.min(lines.length, range.endLine ?? lines.length);\n const selected = start <= end ? lines.slice(start - 1, end).join('\\n') : '';\n const capped = Buffer.byteLength(selected, 'utf8') > cap;\n const out = capBytes(selected, cap);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition - the\n // executor re-applies inbound sanitization + dataflow provenance\n // by this class when read_result relays it.\n producerTrustClass: 'mcp-derived' as const,\n content: out,\n bytes: Buffer.byteLength(out, 'utf8'),\n totalBytes,\n eof: end >= lines.length && !capped,\n });\n }\n\n const offset = clamp(range?.offset ?? 0, 0, totalBytes);\n const requested = range?.length ?? totalBytes - offset;\n const rawEnd = clamp(offset + Math.max(0, requested), offset, totalBytes);\n const end = Math.min(rawEnd, offset + cap);\n const slice = buf.subarray(offset, end);\n return Object.freeze({\n // TL-6: MCP resource content is mcp-derived by definition.\n producerTrustClass: 'mcp-derived' as const,\n content: slice.toString('utf8'),\n bytes: slice.byteLength,\n totalBytes,\n eof: end >= totalBytes,\n });\n}\n\nfunction clamp(value: number, lo: number, hi: number): number {\n return Math.min(hi, Math.max(lo, value));\n}\n\n/** Truncate `s` to at most `maxBytes` UTF-8 bytes (tolerating a split trailing char). */\nfunction capBytes(s: string, maxBytes: number): string {\n if (Buffer.byteLength(s, 'utf8') <= maxBytes) return s;\n return Buffer.from(s, 'utf8').subarray(0, maxBytes).toString('utf8');\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { MCPToolPinningError } from "../errors/index.js";
|
|
2
|
+
import { adaptMCPTools } from "./to-tools.js";
|
|
3
|
+
import { incrementCounter } from "@graphorin/tools/audit";
|
|
4
|
+
|
|
5
|
+
//#region src/client/to-tools-run.ts
|
|
6
|
+
/**
|
|
7
|
+
* Run the list -> adapt -> drift -> pin pipeline against `args.client`.
|
|
8
|
+
*
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
async function runToTools(args) {
|
|
12
|
+
const { client, fingerprintRef, logger, toolsOpts } = args;
|
|
13
|
+
const serverIdentity = client.serverIdentity;
|
|
14
|
+
const adapted = adaptMCPTools({
|
|
15
|
+
client,
|
|
16
|
+
serverIdentity,
|
|
17
|
+
catalogue: await client.listTools(),
|
|
18
|
+
...toolsOpts === void 0 ? {} : { options: toolsOpts },
|
|
19
|
+
...logger === void 0 ? {} : { logger }
|
|
20
|
+
});
|
|
21
|
+
if (fingerprintRef.current !== void 0) for (const [name, hash] of adapted.fingerprints) {
|
|
22
|
+
const previous = fingerprintRef.current.get(name);
|
|
23
|
+
if (previous !== void 0 && previous !== hash) {
|
|
24
|
+
incrementCounter("mcp.tools.changed.total", {
|
|
25
|
+
server: serverIdentity.id,
|
|
26
|
+
tool: name
|
|
27
|
+
});
|
|
28
|
+
logger?.("warn", "mcp.tools.changed: definition drifted between snapshots", {
|
|
29
|
+
server: serverIdentity.id,
|
|
30
|
+
tool: name,
|
|
31
|
+
previous,
|
|
32
|
+
current: hash
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
fingerprintRef.current = adapted.fingerprints;
|
|
37
|
+
let pins = toolsOpts?.pinnedFingerprints;
|
|
38
|
+
let mismatchAction = toolsOpts?.onPinMismatch ?? "warn";
|
|
39
|
+
let pinsFromStore = false;
|
|
40
|
+
const pinStore = toolsOpts?.pinStore;
|
|
41
|
+
if (pins === void 0 && pinStore !== void 0) {
|
|
42
|
+
const stored = await pinStore.get(serverIdentity.id);
|
|
43
|
+
if (stored === void 0) {
|
|
44
|
+
const recorded = {};
|
|
45
|
+
for (const [name, hash] of adapted.fingerprints) recorded[name] = hash;
|
|
46
|
+
await pinStore.set(serverIdentity.id, recorded);
|
|
47
|
+
incrementCounter("mcp.tools.pins-recorded.total", { server: serverIdentity.id });
|
|
48
|
+
logger?.("info", "mcp.tools.pins-recorded: first-use fingerprints stored", {
|
|
49
|
+
server: serverIdentity.id,
|
|
50
|
+
tools: Object.keys(recorded).length
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
pins = stored;
|
|
54
|
+
pinsFromStore = true;
|
|
55
|
+
mismatchAction = toolsOpts?.onPinMismatch ?? "reject";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (pins !== void 0) {
|
|
59
|
+
for (const [name, pinned] of Object.entries(pins)) {
|
|
60
|
+
const current = adapted.fingerprints.get(name);
|
|
61
|
+
if (current !== void 0 && current !== pinned) {
|
|
62
|
+
if (mismatchAction === "reject") throw new MCPToolPinningError(`MCP tool '${name}' no longer matches its pinned definition fingerprint - the server changed the definition behind an approved name.`, { metadata: {
|
|
63
|
+
server: serverIdentity.id,
|
|
64
|
+
tool: name
|
|
65
|
+
} });
|
|
66
|
+
incrementCounter("mcp.tools.pin-mismatch.total", {
|
|
67
|
+
server: serverIdentity.id,
|
|
68
|
+
tool: name
|
|
69
|
+
});
|
|
70
|
+
logger?.("warn", "mcp.tools.pin-mismatch: pinned fingerprint diverged", {
|
|
71
|
+
server: serverIdentity.id,
|
|
72
|
+
tool: name
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const pinnedNames = new Set(Object.keys(pins));
|
|
77
|
+
for (const name of pinsFromStore ? adapted.fingerprints.keys() : []) {
|
|
78
|
+
if (pinnedNames.has(name)) continue;
|
|
79
|
+
if (mismatchAction === "reject") throw new MCPToolPinningError(`MCP server added tool '${name}' after its catalogue was pinned - a post-approval addition is rejected until the operator re-pins (onPinMismatch: 'accept-and-update').`, { metadata: {
|
|
80
|
+
server: serverIdentity.id,
|
|
81
|
+
tool: name
|
|
82
|
+
} });
|
|
83
|
+
incrementCounter("mcp.tools.pin-added.total", {
|
|
84
|
+
server: serverIdentity.id,
|
|
85
|
+
tool: name
|
|
86
|
+
});
|
|
87
|
+
logger?.("warn", "mcp.tools.pin-added: tool added after the catalogue was pinned", {
|
|
88
|
+
server: serverIdentity.id,
|
|
89
|
+
tool: name
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
for (const name of pinsFromStore ? pinnedNames : []) {
|
|
93
|
+
if (adapted.fingerprints.has(name)) continue;
|
|
94
|
+
incrementCounter("mcp.tools.pin-removed.total", {
|
|
95
|
+
server: serverIdentity.id,
|
|
96
|
+
tool: name
|
|
97
|
+
});
|
|
98
|
+
logger?.("info", "mcp.tools.pin-removed: pinned tool disappeared from the catalogue", {
|
|
99
|
+
server: serverIdentity.id,
|
|
100
|
+
tool: name
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
if (mismatchAction === "accept-and-update" && pinsFromStore && pinStore !== void 0) {
|
|
104
|
+
const refreshed = {};
|
|
105
|
+
for (const [name, hash] of adapted.fingerprints) refreshed[name] = hash;
|
|
106
|
+
await pinStore.set(serverIdentity.id, refreshed);
|
|
107
|
+
incrementCounter("mcp.tools.pins-updated.total", { server: serverIdentity.id });
|
|
108
|
+
logger?.("info", "mcp.tools.pins-updated: operator accepted the current catalogue", {
|
|
109
|
+
server: serverIdentity.id,
|
|
110
|
+
tools: Object.keys(refreshed).length
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return adapted.tools;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
//#endregion
|
|
118
|
+
export { runToTools };
|
|
119
|
+
//# sourceMappingURL=to-tools-run.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-tools-run.js","names":["recorded: Record<string, string>","refreshed: Record<string, string>"],"sources":["../../src/client/to-tools-run.ts"],"sourcesContent":["/**\n * W-080: the full `toTools()` pipeline (list -> adapt -> drift diff ->\n * pin comparison / TOFU store legs) extracted from `client.ts` and\n * parameterized by an {@link MCPClient}. Two callers:\n *\n * - the plain client's own `toTools()` (client = itself, the pre-W-080\n * behaviour byte-for-byte), and\n * - the managed wrapper's `toTools()` (client = THE WRAPPER), so every\n * adapted `Tool.execute` closes over the wrapper and keeps working\n * after the wrapper swaps its inner client on reconnect - no\n * re-registration required.\n *\n * @packageDocumentation\n */\n\nimport type { Tool } from '@graphorin/core';\nimport { incrementCounter } from '@graphorin/tools/audit';\nimport { MCPToolPinningError } from '../errors/index.js';\nimport { adaptMCPTools } from './to-tools.js';\nimport type { CreateMCPClientOptions, MCPClient, MCPToToolsOptions } from './types.js';\n\n/** Mutable cross-snapshot fingerprint cell (MC-6 drift tracking). */\nexport interface ToolFingerprintRef {\n current: ReadonlyMap<string, string> | undefined;\n}\n\n/** Arguments for {@link runToTools}. @internal */\nexport interface RunToToolsArgs {\n /** The client the adapted tools CLOSE OVER (wrapper for managed). */\n readonly client: MCPClient;\n /**\n * Cross-snapshot fingerprint cell. Owned by the caller so drift\n * tracking spans the caller's lifetime (for the managed wrapper that\n * includes reconnects - a rug-pull across a reconnect still diffs).\n */\n readonly fingerprintRef: ToolFingerprintRef;\n readonly logger?: CreateMCPClientOptions['logger'];\n readonly toolsOpts?: MCPToToolsOptions;\n}\n\n/**\n * Run the list -> adapt -> drift -> pin pipeline against `args.client`.\n *\n * @internal\n */\nexport async function runToTools(args: RunToToolsArgs): Promise<ReadonlyArray<Tool>> {\n const { client, fingerprintRef, logger, toolsOpts } = args;\n const serverIdentity = client.serverIdentity;\n const catalogue = await client.listTools();\n const adapted = adaptMCPTools({\n client,\n serverIdentity,\n catalogue,\n ...(toolsOpts === undefined ? {} : { options: toolsOpts }),\n ...(logger === undefined ? {} : { logger }),\n });\n // MC-6: cross-snapshot drift - a definition changing behind an\n // already-seen name within this client's lifetime is audited.\n if (fingerprintRef.current !== undefined) {\n for (const [name, hash] of adapted.fingerprints) {\n const previous = fingerprintRef.current.get(name);\n if (previous !== undefined && previous !== hash) {\n incrementCounter('mcp.tools.changed.total', {\n server: serverIdentity.id,\n tool: name,\n });\n logger?.('warn', 'mcp.tools.changed: definition drifted between snapshots', {\n server: serverIdentity.id,\n tool: name,\n previous,\n current: hash,\n });\n }\n }\n }\n fingerprintRef.current = adapted.fingerprints;\n // MC-6: operator pins from a previously approved snapshot - the\n // rug-pull (approve-then-swap across restarts) posture. C6 extends it\n // with durable trust-on-first-use via `pinStore`: the first snapshot\n // is RECORDED, later snapshots are COMPARED, and a store-backed\n // mismatch defaults to 'reject' (a persisted first approval is an\n // explicit trust decision).\n let pins = toolsOpts?.pinnedFingerprints;\n let mismatchAction = toolsOpts?.onPinMismatch ?? 'warn';\n // W-079: the added/removed lifecycle legs only apply to STORE pins -\n // a store snapshot covers the full catalogue by construction, while\n // explicit pinnedFingerprints may deliberately pin a subset.\n let pinsFromStore = false;\n const pinStore = toolsOpts?.pinStore;\n if (pins === undefined && pinStore !== undefined) {\n const stored = await pinStore.get(serverIdentity.id);\n if (stored === undefined) {\n const recorded: Record<string, string> = {};\n for (const [name, hash] of adapted.fingerprints) recorded[name] = hash;\n await pinStore.set(serverIdentity.id, recorded);\n incrementCounter('mcp.tools.pins-recorded.total', { server: serverIdentity.id });\n logger?.('info', 'mcp.tools.pins-recorded: first-use fingerprints stored', {\n server: serverIdentity.id,\n tools: Object.keys(recorded).length,\n });\n } else {\n pins = stored;\n pinsFromStore = true;\n mismatchAction = toolsOpts?.onPinMismatch ?? 'reject';\n }\n }\n if (pins !== undefined) {\n for (const [name, pinned] of Object.entries(pins)) {\n const current = adapted.fingerprints.get(name);\n if (current !== undefined && current !== pinned) {\n if (mismatchAction === 'reject') {\n throw new MCPToolPinningError(\n `MCP tool '${name}' no longer matches its pinned definition fingerprint - the server changed the definition behind an approved name.`,\n { metadata: { server: serverIdentity.id, tool: name } },\n );\n }\n incrementCounter('mcp.tools.pin-mismatch.total', {\n server: serverIdentity.id,\n tool: name,\n });\n logger?.('warn', 'mcp.tools.pin-mismatch: pinned fingerprint diverged', {\n server: serverIdentity.id,\n tool: name,\n });\n }\n }\n // W-079: the comparison loop above only sees names that were\n // pinned. A server that passed its first-use recording can later\n // ADD a poisoned tool (or rename one) - without this leg it would\n // enter the catalogue with no counter and no rejection.\n const pinnedNames = new Set(Object.keys(pins));\n for (const name of pinsFromStore ? adapted.fingerprints.keys() : []) {\n if (pinnedNames.has(name)) continue;\n if (mismatchAction === 'reject') {\n throw new MCPToolPinningError(\n `MCP server added tool '${name}' after its catalogue was pinned - a post-approval addition is rejected until the operator re-pins (onPinMismatch: 'accept-and-update').`,\n { metadata: { server: serverIdentity.id, tool: name } },\n );\n }\n incrementCounter('mcp.tools.pin-added.total', {\n server: serverIdentity.id,\n tool: name,\n });\n logger?.('warn', 'mcp.tools.pin-added: tool added after the catalogue was pinned', {\n server: serverIdentity.id,\n tool: name,\n });\n }\n // W-079: removals are not an injection by themselves, but they can\n // hide a rename (remove + add) - keep them observable.\n for (const name of pinsFromStore ? pinnedNames : []) {\n if (adapted.fingerprints.has(name)) continue;\n incrementCounter('mcp.tools.pin-removed.total', {\n server: serverIdentity.id,\n tool: name,\n });\n logger?.('info', 'mcp.tools.pin-removed: pinned tool disappeared from the catalogue', {\n server: serverIdentity.id,\n tool: name,\n });\n }\n // W-079: the explicit operator path to accept a changed catalogue -\n // overwrite the store with the CURRENT snapshot so subsequent\n // toTools() calls are clean. Explicit pinnedFingerprints stay\n // read-only (they are config, not a store).\n if (mismatchAction === 'accept-and-update' && pinsFromStore && pinStore !== undefined) {\n const refreshed: Record<string, string> = {};\n for (const [name, hash] of adapted.fingerprints) refreshed[name] = hash;\n await pinStore.set(serverIdentity.id, refreshed);\n incrementCounter('mcp.tools.pins-updated.total', { server: serverIdentity.id });\n logger?.('info', 'mcp.tools.pins-updated: operator accepted the current catalogue', {\n server: serverIdentity.id,\n tools: Object.keys(refreshed).length,\n });\n }\n }\n return adapted.tools;\n}\n"],"mappings":"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"}
|
|
@@ -23,6 +23,14 @@ interface AdaptedToolsResult {
|
|
|
23
23
|
readonly resolvedInboundSanitization: InboundSanitizationPolicy;
|
|
24
24
|
readonly toolCount: number;
|
|
25
25
|
readonly deferralThreshold: number;
|
|
26
|
+
/**
|
|
27
|
+
* W-105: tool names the operator downgraded below the sink classes
|
|
28
|
+
* via `sideEffectClassByTool` (`'read-only'` / `'pure'`). Each such
|
|
29
|
+
* tool leaves EVERY sink check - the dataflow gate, the Rule-of-Two
|
|
30
|
+
* writer forbid, the read-only capability gate - so operator audits
|
|
31
|
+
* should review this list. Empty when no override downgrades.
|
|
32
|
+
*/
|
|
33
|
+
readonly downgradedTools: ReadonlyArray<string>;
|
|
26
34
|
}
|
|
27
35
|
/**
|
|
28
36
|
* Build the {@link Tool} array for the supplied MCP tool catalogue.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-tools.d.ts","names":[],"sources":["../../src/client/to-tools.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"to-tools.d.ts","names":[],"sources":["../../src/client/to-tools.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;iBAoDgB,+BAAA,CAAA;;UAMC,kBAAA;kBACC,cAAc;;yBAEP;;;wCAGe;;;;;;;;;;4BAUZ;;;;;;;iBAQZ,aAAA;mBACG;2BACQ;sBACL,cAAc;qBACf;2FAIR;IAET"}
|
package/dist/client/to-tools.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildJsonSchemaValidator } from "../registry/json-schema.js";
|
|
2
2
|
import { adaptCallResult } from "./adapt-result.js";
|
|
3
3
|
import { DEFAULT_DEFER_LOADING_THRESHOLD, _resetDeferLoadingDedupForTesting, resolveDeferLoading } from "./defer-loading.js";
|
|
4
|
-
import { _resetInboundFiltersDedupForTesting, resolveInboundPolicy, sanitizeDescription, warnOnPassthroughOverride } from "./inbound-filters.js";
|
|
4
|
+
import { _resetInboundFiltersDedupForTesting, resolveInboundPolicy, sanitizeDescription, sanitizeSchemaAnnotations, warnOnPassthroughOverride } from "./inbound-filters.js";
|
|
5
5
|
import { computeToolDefinitionHash } from "./pinning.js";
|
|
6
6
|
|
|
7
7
|
//#region src/client/to-tools.ts
|
|
@@ -42,12 +42,33 @@ function adaptMCPTools(args) {
|
|
|
42
42
|
...args.logger === void 0 ? {} : { logger: args.logger }
|
|
43
43
|
});
|
|
44
44
|
const tools = [];
|
|
45
|
+
const downgradedTools = [];
|
|
45
46
|
for (const definition of filtered) {
|
|
46
47
|
const namespacedName = namespace.length === 0 ? definition.name : `${namespace}.${definition.name}`;
|
|
47
48
|
const sideEffectClass = opts.sideEffectClassByTool?.[namespacedName] ?? "external-stateful";
|
|
49
|
+
if (sideEffectClass === "read-only" || sideEffectClass === "pure") {
|
|
50
|
+
downgradedTools.push(namespacedName);
|
|
51
|
+
args.logger?.("warn", `mcp.tools.side-effect-downgraded: operator override classifies '${namespacedName}' as '${sideEffectClass}' - the tool leaves every sink check (dataflow gate, Rule-of-Two writer forbid, read-only capability gate)`, {
|
|
52
|
+
server: args.serverIdentity.id,
|
|
53
|
+
tool: namespacedName,
|
|
54
|
+
sideEffectClass
|
|
55
|
+
});
|
|
56
|
+
}
|
|
48
57
|
const preferredModel = opts.preferredModelByTool?.[namespacedName];
|
|
49
|
-
const
|
|
50
|
-
|
|
58
|
+
const sanitizedInput = sanitizeSchemaAnnotations({
|
|
59
|
+
schema: definition.inputSchema,
|
|
60
|
+
inboundSanitization: resolvedInbound,
|
|
61
|
+
toolName: namespacedName,
|
|
62
|
+
serverIdentity: args.serverIdentity
|
|
63
|
+
});
|
|
64
|
+
const sanitizedOutput = definition.outputSchema === void 0 ? void 0 : sanitizeSchemaAnnotations({
|
|
65
|
+
schema: definition.outputSchema,
|
|
66
|
+
inboundSanitization: resolvedInbound,
|
|
67
|
+
toolName: namespacedName,
|
|
68
|
+
serverIdentity: args.serverIdentity
|
|
69
|
+
});
|
|
70
|
+
const inputValidator = buildJsonSchemaValidator(sanitizedInput.schema);
|
|
71
|
+
const outputValidator = sanitizedOutput === void 0 ? void 0 : buildJsonSchemaValidator(sanitizedOutput.schema);
|
|
51
72
|
const definitionHash = computeToolDefinitionHash(definition);
|
|
52
73
|
fingerprints.set(definition.name, definitionHash);
|
|
53
74
|
tools.push(buildAdaptedTool({
|
|
@@ -76,7 +97,8 @@ function adaptMCPTools(args) {
|
|
|
76
97
|
resolvedDeferLoading,
|
|
77
98
|
resolvedInboundSanitization: resolvedInbound,
|
|
78
99
|
toolCount: total,
|
|
79
|
-
deferralThreshold: threshold
|
|
100
|
+
deferralThreshold: threshold,
|
|
101
|
+
downgradedTools: Object.freeze(downgradedTools)
|
|
80
102
|
});
|
|
81
103
|
}
|
|
82
104
|
function buildAdaptedTool(args) {
|
|
@@ -107,6 +129,7 @@ function buildAdaptedTool(args) {
|
|
|
107
129
|
outputSchema: args.outputSchema,
|
|
108
130
|
serverIdentity: args.serverIdentity,
|
|
109
131
|
toolName: args.graphorinToolName,
|
|
132
|
+
inboundSanitization: args.inboundSanitization,
|
|
110
133
|
...args.logger !== void 0 ? { logger: args.logger } : {}
|
|
111
134
|
});
|
|
112
135
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"to-tools.js","names":["tools: Tool[]"],"sources":["../../src/client/to-tools.ts"],"sourcesContent":["/**\n * `toTools()` adapter - bridges MCP tool descriptors into the\n * Graphorin tool registry.\n *\n * The adapter orchestrates three extracted concerns (F-MCP-001):\n *\n * 1. Filters / namespaces the `listTools()` output, then resolves the\n * per-server effective `defer_loading` flag - see\n * {@link import('./defer-loading.js').resolveDeferLoading}.\n * 2. Resolves the per-server inbound prompt-injection policy and strips\n * imperative payloads from each description - see\n * {@link import('./inbound-filters.js')}.\n * 3. Converts each MCP tool into a strongly-typed Graphorin `Tool` whose\n * `execute(...)` calls back into {@link MCPClient.callTool} and adapts\n * the result - see {@link import('./adapt-result.js').adaptCallResult}.\n *\n * The trust class is pinned to `'mcp-derived'` so the agent runtime's\n * per-step preamble fires regardless of the body-level policy.\n *\n * @packageDocumentation\n */\n\nimport type { ContentChunk, InboundSanitizationPolicy, Tool, ToolReturn } from '@graphorin/core';\nimport { buildJsonSchemaValidator, type JsonSchemaLike } from '../registry/json-schema.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport { adaptCallResult } from './adapt-result.js';\nimport {\n _resetDeferLoadingDedupForTesting,\n DEFAULT_DEFER_LOADING_THRESHOLD,\n resolveDeferLoading,\n} from './defer-loading.js';\nimport {\n _resetInboundFiltersDedupForTesting,\n resolveInboundPolicy,\n sanitizeDescription,\n warnOnPassthroughOverride,\n} from './inbound-filters.js';\nimport { computeToolDefinitionHash } from './pinning.js';\nimport type { MCPClient, MCPToolDefinition, MCPToToolsOptions } from './types.js';\n\n// Re-exported for backward compatibility: callers (and tests) import\n// these symbols directly from `./to-tools.js` and via the client barrel.\nexport { adaptCallResult } from './adapt-result.js';\nexport { DEFAULT_DEFER_LOADING_THRESHOLD } from './defer-loading.js';\n\n/**\n * Reset every process-scoped dedup set owned by the adapter modules.\n * Used by tests.\n *\n * @experimental\n */\nexport function _resetMcpAdapterDedupForTesting(): void {\n _resetDeferLoadingDedupForTesting();\n _resetInboundFiltersDedupForTesting();\n}\n\n/** Result returned by {@link adaptMCPTools}. */\nexport interface AdaptedToolsResult {\n readonly tools: ReadonlyArray<Tool>;\n /** MC-6: sha256 definition fingerprint per MCP tool name. */\n readonly fingerprints: ReadonlyMap<string, string>;\n readonly autoDeferralFired: boolean;\n readonly resolvedDeferLoading: boolean;\n readonly resolvedInboundSanitization: InboundSanitizationPolicy;\n readonly toolCount: number;\n readonly deferralThreshold: number;\n}\n\n/**\n * Build the {@link Tool} array for the supplied MCP tool catalogue.\n *\n * @stable\n */\nexport function adaptMCPTools(args: {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly catalogue: ReadonlyArray<MCPToolDefinition>;\n readonly options?: MCPToToolsOptions;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}): AdaptedToolsResult {\n const opts = args.options ?? {};\n const fingerprints = new Map<string, string>();\n const filter = opts.filter;\n const namespace = (opts.namespace ?? '').trim();\n const filtered = filter === undefined ? args.catalogue : args.catalogue.filter((t) => filter(t));\n const total = filtered.length;\n const threshold = opts.deferLoadingThreshold ?? DEFAULT_DEFER_LOADING_THRESHOLD;\n\n const { autoDeferralFired, resolvedDeferLoading } = resolveDeferLoading({\n serverIdentity: args.serverIdentity,\n toolNames: filtered.map((t) => t.name),\n explicitDefer: opts.defer_loading,\n threshold,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const resolvedInbound = resolveInboundPolicy(opts.inboundSanitization);\n warnOnPassthroughOverride({\n resolvedInbound,\n serverIdentity: args.serverIdentity,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const tools: Tool[] = [];\n for (const definition of filtered) {\n const namespacedName =\n namespace.length === 0 ? definition.name : `${namespace}.${definition.name}`;\n const sideEffectClass = opts.sideEffectClassByTool?.[namespacedName] ?? 'external-stateful';\n const preferredModel = opts.preferredModelByTool?.[namespacedName];\n const inputValidator = buildJsonSchemaValidator(definition.inputSchema as JsonSchemaLike);\n const outputValidator =\n definition.outputSchema === undefined\n ? undefined\n : buildJsonSchemaValidator(definition.outputSchema as JsonSchemaLike);\n const definitionHash = computeToolDefinitionHash(definition);\n fingerprints.set(definition.name, definitionHash);\n tools.push(\n buildAdaptedTool({\n client: args.client,\n serverIdentity: args.serverIdentity,\n definitionHash,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n mcpToolName: definition.name,\n graphorinToolName: namespacedName,\n description:\n definition.description.length === 0 ? `${definition.name} (MCP)` : definition.description,\n inputSchema: inputValidator,\n outputSchema: outputValidator,\n defer_loading: resolvedDeferLoading,\n inboundSanitization: resolvedInbound,\n sideEffectClass,\n ...(opts.maxResultTokens === undefined ? {} : { maxResultTokens: opts.maxResultTokens }),\n ...(opts.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: opts.truncationStrategy }),\n ...(opts.callTimeoutMs === undefined ? {} : { callTimeoutMs: opts.callTimeoutMs }),\n ...(preferredModel === undefined ? {} : { preferredModel }),\n }),\n );\n }\n\n return Object.freeze({\n tools: Object.freeze(tools),\n fingerprints,\n autoDeferralFired,\n resolvedDeferLoading,\n resolvedInboundSanitization: resolvedInbound,\n toolCount: total,\n deferralThreshold: threshold,\n });\n}\n\ninterface BuildAdaptedToolArgs {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly mcpToolName: string;\n readonly graphorinToolName: string;\n readonly description: string;\n readonly inputSchema: import('@graphorin/core').ZodLikeSchema<unknown>;\n readonly outputSchema?: import('@graphorin/core').ZodLikeSchema<unknown> | undefined;\n readonly defer_loading: boolean;\n readonly inboundSanitization: InboundSanitizationPolicy;\n readonly sideEffectClass: import('@graphorin/core').SideEffectClass;\n readonly maxResultTokens?: number;\n readonly truncationStrategy?: import('@graphorin/core').TruncationStrategy;\n /** Per-call timeout forwarded to `client.callTool` (MC-3/MC-5). */\n readonly callTimeoutMs?: number;\n /** MC-6: sha256 fingerprint of the producing MCP definition. */\n readonly definitionHash: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n readonly preferredModel?:\n | import('@graphorin/core').ModelHint\n | import('@graphorin/core').ModelSpec;\n}\n\nfunction buildAdaptedTool(args: BuildAdaptedToolArgs): Tool<unknown, unknown, unknown> {\n const sanitizedDescription = sanitizeDescription({\n description: args.description,\n inboundSanitization: args.inboundSanitization,\n toolName: args.graphorinToolName,\n serverIdentity: args.serverIdentity,\n });\n\n const tool = {\n name: args.graphorinToolName,\n description: sanitizedDescription,\n inputSchema: args.inputSchema,\n ...(args.outputSchema === undefined ? {} : { outputSchema: args.outputSchema }),\n defer_loading: args.defer_loading,\n inboundSanitization: args.inboundSanitization,\n sideEffectClass: args.sideEffectClass,\n sandboxPolicy: 'sandboxed' as const,\n ...(args.maxResultTokens === undefined ? {} : { maxResultTokens: args.maxResultTokens }),\n ...(args.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: args.truncationStrategy }),\n ...(args.preferredModel === undefined ? {} : { preferredModel: args.preferredModel }),\n async execute(\n input: unknown,\n ctx?: import('@graphorin/core').ToolExecutionContext<unknown>,\n ): Promise<ToolReturn<unknown>> {\n // MC-5: the agent's per-call AbortSignal reaches the wire - an\n // aborted run sends `notifications/cancelled` instead of orphaning\n // the JSON-RPC request on the server. MC-3: the per-server call\n // timeout rides along.\n const result = await args.client.callTool(args.mcpToolName, input, {\n ...(ctx?.signal !== undefined ? { signal: ctx.signal } : {}),\n ...(args.callTimeoutMs !== undefined ? { timeoutMs: args.callTimeoutMs } : {}),\n });\n return adaptCallResult({\n result,\n outputSchema: args.outputSchema,\n serverIdentity: args.serverIdentity,\n toolName: args.graphorinToolName,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n });\n },\n } satisfies Tool<unknown, unknown, unknown>;\n // MC-7: pre-stamp the provenance so the agent's inferToolSource -\n // which honours an existing stamp - classifies tools passed via\n // `config.tools` as 'mcp-derived' (untrusted for the WI-12 dataflow\n // policy) instead of first-party. Zero operator boilerplate.\n return Object.assign(tool, {\n __source: { kind: 'mcp', serverIdentity: args.serverIdentity.id } as const,\n // MC-6: operators persist this fingerprint to pin the approved\n // definition (`toTools({ pinnedFingerprints })`).\n __definitionHash: args.definitionHash,\n });\n}\n\n/** Unused export kept for ergonomic test access. */\nexport type AdaptedToolChunkBuffer = ReadonlyArray<ContentChunk>;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"to-tools.js","names":["tools: Tool[]","downgradedTools: string[]"],"sources":["../../src/client/to-tools.ts"],"sourcesContent":["/**\n * `toTools()` adapter - bridges MCP tool descriptors into the\n * Graphorin tool registry.\n *\n * The adapter orchestrates three extracted concerns (F-MCP-001):\n *\n * 1. Filters / namespaces the `listTools()` output, then resolves the\n * per-server effective `defer_loading` flag - see\n * {@link import('./defer-loading.js').resolveDeferLoading}.\n * 2. Resolves the per-server inbound prompt-injection policy and strips\n * imperative payloads from each description - see\n * {@link import('./inbound-filters.js')}.\n * 3. Converts each MCP tool into a strongly-typed Graphorin `Tool` whose\n * `execute(...)` calls back into {@link MCPClient.callTool} and adapts\n * the result - see {@link import('./adapt-result.js').adaptCallResult}.\n *\n * The trust class is pinned to `'mcp-derived'` so the agent runtime's\n * per-step preamble fires regardless of the body-level policy.\n *\n * @packageDocumentation\n */\n\nimport type { ContentChunk, InboundSanitizationPolicy, Tool, ToolReturn } from '@graphorin/core';\nimport { buildJsonSchemaValidator, type JsonSchemaLike } from '../registry/json-schema.js';\nimport type { ServerIdentity } from '../transport/types.js';\nimport { adaptCallResult } from './adapt-result.js';\nimport {\n _resetDeferLoadingDedupForTesting,\n DEFAULT_DEFER_LOADING_THRESHOLD,\n resolveDeferLoading,\n} from './defer-loading.js';\nimport {\n _resetInboundFiltersDedupForTesting,\n resolveInboundPolicy,\n sanitizeDescription,\n sanitizeSchemaAnnotations,\n warnOnPassthroughOverride,\n} from './inbound-filters.js';\nimport { computeToolDefinitionHash } from './pinning.js';\nimport type { MCPClient, MCPToolDefinition, MCPToToolsOptions } from './types.js';\n\n// Re-exported for backward compatibility: callers (and tests) import\n// these symbols directly from `./to-tools.js` and via the client barrel.\nexport { adaptCallResult } from './adapt-result.js';\nexport { DEFAULT_DEFER_LOADING_THRESHOLD } from './defer-loading.js';\n\n/**\n * Reset every process-scoped dedup set owned by the adapter modules.\n * Used by tests.\n *\n * @experimental\n */\nexport function _resetMcpAdapterDedupForTesting(): void {\n _resetDeferLoadingDedupForTesting();\n _resetInboundFiltersDedupForTesting();\n}\n\n/** Result returned by {@link adaptMCPTools}. */\nexport interface AdaptedToolsResult {\n readonly tools: ReadonlyArray<Tool>;\n /** MC-6: sha256 definition fingerprint per MCP tool name. */\n readonly fingerprints: ReadonlyMap<string, string>;\n readonly autoDeferralFired: boolean;\n readonly resolvedDeferLoading: boolean;\n readonly resolvedInboundSanitization: InboundSanitizationPolicy;\n readonly toolCount: number;\n readonly deferralThreshold: number;\n /**\n * W-105: tool names the operator downgraded below the sink classes\n * via `sideEffectClassByTool` (`'read-only'` / `'pure'`). Each such\n * tool leaves EVERY sink check - the dataflow gate, the Rule-of-Two\n * writer forbid, the read-only capability gate - so operator audits\n * should review this list. Empty when no override downgrades.\n */\n readonly downgradedTools: ReadonlyArray<string>;\n}\n\n/**\n * Build the {@link Tool} array for the supplied MCP tool catalogue.\n *\n * @stable\n */\nexport function adaptMCPTools(args: {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly catalogue: ReadonlyArray<MCPToolDefinition>;\n readonly options?: MCPToToolsOptions;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n}): AdaptedToolsResult {\n const opts = args.options ?? {};\n const fingerprints = new Map<string, string>();\n const filter = opts.filter;\n const namespace = (opts.namespace ?? '').trim();\n const filtered = filter === undefined ? args.catalogue : args.catalogue.filter((t) => filter(t));\n const total = filtered.length;\n const threshold = opts.deferLoadingThreshold ?? DEFAULT_DEFER_LOADING_THRESHOLD;\n\n const { autoDeferralFired, resolvedDeferLoading } = resolveDeferLoading({\n serverIdentity: args.serverIdentity,\n toolNames: filtered.map((t) => t.name),\n explicitDefer: opts.defer_loading,\n threshold,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const resolvedInbound = resolveInboundPolicy(opts.inboundSanitization);\n warnOnPassthroughOverride({\n resolvedInbound,\n serverIdentity: args.serverIdentity,\n ...(args.logger === undefined ? {} : { logger: args.logger }),\n });\n\n const tools: Tool[] = [];\n const downgradedTools: string[] = [];\n for (const definition of filtered) {\n const namespacedName =\n namespace.length === 0 ? definition.name : `${namespace}.${definition.name}`;\n const sideEffectClass = opts.sideEffectClassByTool?.[namespacedName] ?? 'external-stateful';\n // W-105: a downgrade below the sink classes is an explicit operator\n // trust decision with wide consequences - the tool leaves every sink\n // check (dataflow gate, Rule-of-Two writer forbid, read-only\n // capability gate). One WARN per tool at adaptation time + a result\n // field so audits can pick it up; the server's own readOnlyHint is\n // deliberately never trusted for this.\n if (sideEffectClass === 'read-only' || sideEffectClass === 'pure') {\n downgradedTools.push(namespacedName);\n args.logger?.(\n 'warn',\n `mcp.tools.side-effect-downgraded: operator override classifies '${namespacedName}' as '${sideEffectClass}' - the tool leaves every sink check (dataflow gate, Rule-of-Two writer forbid, read-only capability gate)`,\n {\n server: args.serverIdentity.id,\n tool: namespacedName,\n sideEffectClass,\n },\n );\n }\n const preferredModel = opts.preferredModelByTool?.[namespacedName];\n // W-018: strip imperative payloads from schema ANNOTATIONS\n // (description / title / $comment / string examples at any depth -\n // the Invariant Labs tool-poisoning hiding place) before the\n // schema reaches the validator, whose `toJSON()` is what the\n // provider wire and `tool_search` re-expose.\n const sanitizedInput = sanitizeSchemaAnnotations({\n schema: definition.inputSchema as JsonSchemaLike,\n inboundSanitization: resolvedInbound,\n toolName: namespacedName,\n serverIdentity: args.serverIdentity,\n });\n const sanitizedOutput =\n definition.outputSchema === undefined\n ? undefined\n : sanitizeSchemaAnnotations({\n schema: definition.outputSchema as JsonSchemaLike,\n inboundSanitization: resolvedInbound,\n toolName: namespacedName,\n serverIdentity: args.serverIdentity,\n });\n const inputValidator = buildJsonSchemaValidator(sanitizedInput.schema);\n const outputValidator =\n sanitizedOutput === undefined ? undefined : buildJsonSchemaValidator(sanitizedOutput.schema);\n // TOFU invariant (MC-6): the fingerprint hashes the RAW definition,\n // never the sanitized copy. Hashing redacted bytes would invalidate\n // the pins of existing deployments AND collapse two\n // differently-poisoned schemas into one redacted hash, hiding drift.\n // `sanitizeSchemaAnnotations` returns a clone and never mutates\n // `definition`, so the order here is belt-and-braces.\n const definitionHash = computeToolDefinitionHash(definition);\n fingerprints.set(definition.name, definitionHash);\n tools.push(\n buildAdaptedTool({\n client: args.client,\n serverIdentity: args.serverIdentity,\n definitionHash,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n mcpToolName: definition.name,\n graphorinToolName: namespacedName,\n description:\n definition.description.length === 0 ? `${definition.name} (MCP)` : definition.description,\n inputSchema: inputValidator,\n outputSchema: outputValidator,\n defer_loading: resolvedDeferLoading,\n inboundSanitization: resolvedInbound,\n sideEffectClass,\n ...(opts.maxResultTokens === undefined ? {} : { maxResultTokens: opts.maxResultTokens }),\n ...(opts.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: opts.truncationStrategy }),\n ...(opts.callTimeoutMs === undefined ? {} : { callTimeoutMs: opts.callTimeoutMs }),\n ...(preferredModel === undefined ? {} : { preferredModel }),\n }),\n );\n }\n\n return Object.freeze({\n tools: Object.freeze(tools),\n fingerprints,\n autoDeferralFired,\n resolvedDeferLoading,\n resolvedInboundSanitization: resolvedInbound,\n toolCount: total,\n deferralThreshold: threshold,\n downgradedTools: Object.freeze(downgradedTools),\n });\n}\n\ninterface BuildAdaptedToolArgs {\n readonly client: MCPClient;\n readonly serverIdentity: ServerIdentity;\n readonly mcpToolName: string;\n readonly graphorinToolName: string;\n readonly description: string;\n readonly inputSchema: import('@graphorin/core').ZodLikeSchema<unknown>;\n readonly outputSchema?: import('@graphorin/core').ZodLikeSchema<unknown> | undefined;\n readonly defer_loading: boolean;\n readonly inboundSanitization: InboundSanitizationPolicy;\n readonly sideEffectClass: import('@graphorin/core').SideEffectClass;\n readonly maxResultTokens?: number;\n readonly truncationStrategy?: import('@graphorin/core').TruncationStrategy;\n /** Per-call timeout forwarded to `client.callTool` (MC-3/MC-5). */\n readonly callTimeoutMs?: number;\n /** MC-6: sha256 fingerprint of the producing MCP definition. */\n readonly definitionHash: string;\n readonly logger?: (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n fields?: Record<string, unknown>,\n ) => void;\n readonly preferredModel?:\n | import('@graphorin/core').ModelHint\n | import('@graphorin/core').ModelSpec;\n}\n\nfunction buildAdaptedTool(args: BuildAdaptedToolArgs): Tool<unknown, unknown, unknown> {\n const sanitizedDescription = sanitizeDescription({\n description: args.description,\n inboundSanitization: args.inboundSanitization,\n toolName: args.graphorinToolName,\n serverIdentity: args.serverIdentity,\n });\n\n const tool = {\n name: args.graphorinToolName,\n description: sanitizedDescription,\n inputSchema: args.inputSchema,\n ...(args.outputSchema === undefined ? {} : { outputSchema: args.outputSchema }),\n defer_loading: args.defer_loading,\n inboundSanitization: args.inboundSanitization,\n sideEffectClass: args.sideEffectClass,\n sandboxPolicy: 'sandboxed' as const,\n ...(args.maxResultTokens === undefined ? {} : { maxResultTokens: args.maxResultTokens }),\n ...(args.truncationStrategy === undefined\n ? {}\n : { truncationStrategy: args.truncationStrategy }),\n ...(args.preferredModel === undefined ? {} : { preferredModel: args.preferredModel }),\n async execute(\n input: unknown,\n ctx?: import('@graphorin/core').ToolExecutionContext<unknown>,\n ): Promise<ToolReturn<unknown>> {\n // MC-5: the agent's per-call AbortSignal reaches the wire - an\n // aborted run sends `notifications/cancelled` instead of orphaning\n // the JSON-RPC request on the server. MC-3: the per-server call\n // timeout rides along.\n const result = await args.client.callTool(args.mcpToolName, input, {\n ...(ctx?.signal !== undefined ? { signal: ctx.signal } : {}),\n ...(args.callTimeoutMs !== undefined ? { timeoutMs: args.callTimeoutMs } : {}),\n });\n return adaptCallResult({\n result,\n outputSchema: args.outputSchema,\n serverIdentity: args.serverIdentity,\n toolName: args.graphorinToolName,\n inboundSanitization: args.inboundSanitization,\n ...(args.logger !== undefined ? { logger: args.logger } : {}),\n });\n },\n } satisfies Tool<unknown, unknown, unknown>;\n // MC-7: pre-stamp the provenance so the agent's inferToolSource -\n // which honours an existing stamp - classifies tools passed via\n // `config.tools` as 'mcp-derived' (untrusted for the WI-12 dataflow\n // policy) instead of first-party. Zero operator boilerplate.\n return Object.assign(tool, {\n __source: { kind: 'mcp', serverIdentity: args.serverIdentity.id } as const,\n // MC-6: operators persist this fingerprint to pin the approved\n // definition (`toTools({ pinnedFingerprints })`).\n __definitionHash: args.definitionHash,\n });\n}\n\n/** Unused export kept for ergonomic test access. */\nexport type AdaptedToolChunkBuffer = ReadonlyArray<ContentChunk>;\n"],"mappings":"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"}
|
package/dist/client/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MCPTransportConfig, ServerIdentity } from "../transport/types.js";
|
|
2
2
|
import { OAuthAuthorizationProvider } from "../oauth/bridge.js";
|
|
3
|
-
import { CollisionStrategy } from "@graphorin/tools/registry";
|
|
4
3
|
import { InboundSanitizationPolicy, ModelHint, ModelSpec, SideEffectClass, Tool, TruncationStrategy } from "@graphorin/core";
|
|
4
|
+
import { CollisionStrategy } from "@graphorin/tools/registry";
|
|
5
5
|
|
|
6
6
|
//#region src/client/types.d.ts
|
|
7
7
|
|
|
@@ -75,7 +75,10 @@ interface CreateMCPClientOptions {
|
|
|
75
75
|
* resume). Without it a disconnect is observable only as
|
|
76
76
|
* `MCPProtocolError`s on subsequent calls. The client does NOT
|
|
77
77
|
* auto-reconnect - rebuild it via `createMCPClient(...)` (and re-run
|
|
78
|
-
* `toTools()` for the drift diff) when this fires
|
|
78
|
+
* `toTools()` for the drift diff) when this fires, or use the W-080
|
|
79
|
+
* `createManagedMCPClient(...)` wrapper, which does exactly that
|
|
80
|
+
* automatically (there the operator callback fires only when the
|
|
81
|
+
* wrapper's reconnect attempts are exhausted).
|
|
79
82
|
*/
|
|
80
83
|
readonly onTransportClose?: (info: {
|
|
81
84
|
readonly server: string;
|
|
@@ -207,8 +210,14 @@ interface MCPToToolsOptions {
|
|
|
207
210
|
* `mcp.tools.pin-mismatch.total` and continues; `'reject'` (the
|
|
208
211
|
* default WHEN a `pinStore` is wired - a persisted first approval is
|
|
209
212
|
* an explicit trust decision) throws `MCPToolPinningError`.
|
|
213
|
+
*
|
|
214
|
+
* W-079: `'accept-and-update'` is the documented operator path to
|
|
215
|
+
* ACCEPT a legitimate catalogue change: after the comparison (and its
|
|
216
|
+
* counters/logs) the store is overwritten with the current snapshot
|
|
217
|
+
* (`mcp.tools.pins-updated.total`), so subsequent calls are clean -
|
|
218
|
+
* an explicit re-trust decision, never a silent default.
|
|
210
219
|
*/
|
|
211
|
-
readonly onPinMismatch?: 'warn' | 'reject';
|
|
220
|
+
readonly onPinMismatch?: 'warn' | 'reject' | 'accept-and-update';
|
|
212
221
|
/**
|
|
213
222
|
* C6: durable trust-on-first-use pin storage. On the first `toTools()`
|
|
214
223
|
* the current definition fingerprints are RECORDED
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/client/types.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../src/client/types.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Typed error union for the `@graphorin/mcp` package.
|
|
4
4
|
*
|
|
5
5
|
* Every error carries a stable lowercase {@link MCPErrorKind}
|
|
6
|
-
* discriminator, an actionable {@link
|
|
6
|
+
* discriminator, an actionable {@link GraphorinMCPError.hint} field where
|
|
7
7
|
* applicable, and an optional structured `metadata` bag the audit
|
|
8
8
|
* emitter persists alongside the standard `runId` / `sessionId`
|
|
9
9
|
* context.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* @stable
|
|
19
19
|
*/
|
|
20
20
|
type MCPErrorKind = 'connection-failed' | 'protocol-error' | 'auth-error' | 'tool-not-found' | 'tool-execution' | 'pin-mismatch' | 'call-timeout' | 'cancelled' | 'invalid-config' | 'session-lost' | 'transport-not-supported' | 'transport-resumable-not-supported';
|
|
21
|
-
/** Common metadata bag attached to every {@link
|
|
21
|
+
/** Common metadata bag attached to every {@link GraphorinMCPError}. */
|
|
22
22
|
interface MCPErrorMetadata {
|
|
23
23
|
readonly server?: string;
|
|
24
24
|
readonly tool?: string;
|
|
@@ -60,7 +60,7 @@ declare class MCPProtocolError extends GraphorinMCPError {
|
|
|
60
60
|
declare class MCPAuthError extends GraphorinMCPError {
|
|
61
61
|
readonly kind: "auth-error";
|
|
62
62
|
}
|
|
63
|
-
/** Raised when
|
|
63
|
+
/** Raised when `MCPClient.callTool` is invoked for an unknown tool. */
|
|
64
64
|
declare class MCPToolNotFoundError extends GraphorinMCPError {
|
|
65
65
|
readonly kind: "tool-not-found";
|
|
66
66
|
}
|
package/dist/errors/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var MCPProtocolError = class extends GraphorinMCPError {
|
|
|
31
31
|
var MCPAuthError = class extends GraphorinMCPError {
|
|
32
32
|
kind = "auth-error";
|
|
33
33
|
};
|
|
34
|
-
/** Raised when
|
|
34
|
+
/** Raised when `MCPClient.callTool` is invoked for an unknown tool. */
|
|
35
35
|
var MCPToolNotFoundError = class extends GraphorinMCPError {
|
|
36
36
|
kind = "tool-not-found";
|
|
37
37
|
};
|