@atrib/agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +333 -0
- package/dist/adapters/cloudflare-agent.d.ts +46 -0
- package/dist/adapters/cloudflare-agent.d.ts.map +1 -0
- package/dist/adapters/cloudflare-agent.js +124 -0
- package/dist/adapters/cloudflare-agent.js.map +1 -0
- package/dist/adapters/langchain-mcp.d.ts +172 -0
- package/dist/adapters/langchain-mcp.d.ts.map +1 -0
- package/dist/adapters/langchain-mcp.js +145 -0
- package/dist/adapters/langchain-mcp.js.map +1 -0
- package/dist/adapters/mcp-client.d.ts +94 -0
- package/dist/adapters/mcp-client.d.ts.map +1 -0
- package/dist/adapters/mcp-client.js +91 -0
- package/dist/adapters/mcp-client.js.map +1 -0
- package/dist/adapters/vercel-ai-sdk-mcp.d.ts +129 -0
- package/dist/adapters/vercel-ai-sdk-mcp.d.ts.map +1 -0
- package/dist/adapters/vercel-ai-sdk-mcp.js +115 -0
- package/dist/adapters/vercel-ai-sdk-mcp.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware.d.ts +53 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +268 -0
- package/dist/middleware.js.map +1 -0
- package/dist/policy.d.ts +28 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +196 -0
- package/dist/policy.js.map +1 -0
- package/dist/session.d.ts +50 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +141 -0
- package/dist/session.js.map +1 -0
- package/dist/transaction.d.ts +52 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +123 -0
- package/dist/transaction.js.map +1 -0
- package/package.json +50 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Adapter: attribute MCP tool calls flowing through a Cloudflare Agent.
|
|
4
|
+
*
|
|
5
|
+
* Cloudflare's `agents` package exposes two MCP integration surfaces:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Server-side `McpAgent`**. you build an MCP server inside a Worker
|
|
8
|
+
* by extending `McpAgent` and defining tools on `this.server`. Because
|
|
9
|
+
* `this.server` is a real `McpServer` from `@modelcontextprotocol/sdk`,
|
|
10
|
+
* you wrap it directly with `atrib()` from `@atrib/mcp`. no helper
|
|
11
|
+
* needed. See `packages/integration/examples/cloudflare-agents/` for
|
|
12
|
+
* the runnable McpAgent example.
|
|
13
|
+
*
|
|
14
|
+
* 2. **Client-side `Agent.addMcpServer`**. your `Agent` (or `AIChatAgent`)
|
|
15
|
+
* connects to one or more upstream MCP servers via `this.addMcpServer(name, url)`.
|
|
16
|
+
* Cloudflare's `MCPClientManager` constructs an `@modelcontextprotocol/sdk`
|
|
17
|
+
* Client per upstream and stores it on `agent.mcp.mcpConnections[name].client`.
|
|
18
|
+
* Tool invocations flow through `MCPClientManager.callTool()` which
|
|
19
|
+
* delegates straight to `mcpConnections[serverId].client.callTool(...)`
|
|
20
|
+
* (verified against `agents@0.9.0` `dist/client-BwgM3cRz.js` line 1444).
|
|
21
|
+
*
|
|
22
|
+
* This file is the helper for surface (2). It walks `agent.mcp.mcpConnections`
|
|
23
|
+
* after the agent has finished registering its upstream MCP servers and
|
|
24
|
+
* replaces each connection's `client` field with one wrapped by `wrapMcpClient`.
|
|
25
|
+
* Subsequent tool calls go through atrib's interceptor lifecycle.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
*
|
|
29
|
+
* import { Agent } from 'agents'
|
|
30
|
+
* import { atrib, attributeCloudflareAgentMcp } from '@atrib/agent'
|
|
31
|
+
*
|
|
32
|
+
* class WeatherAgent extends Agent<Env> {
|
|
33
|
+
* interceptor = atrib({
|
|
34
|
+
* creatorKey: this.env.ATRIB_PRIVATE_KEY,
|
|
35
|
+
* merchantDomain: 'https://merchant.example.com',
|
|
36
|
+
* serverUrls: ['https://weather-mcp.example.com'],
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
39
|
+
* async onStart() {
|
|
40
|
+
* await this.addMcpServer('weather', 'https://weather-mcp.example.com/mcp', {
|
|
41
|
+
* transport: { type: 'streamable-http' },
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // After all addMcpServer() calls complete, attribute the connections.
|
|
45
|
+
* attributeCloudflareAgentMcp(this, { interceptor: this.interceptor })
|
|
46
|
+
* }
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* If you call `addMcpServer` again later (e.g. in a message handler or after
|
|
50
|
+
* an OAuth flow completes), call `attributeCloudflareAgentMcp` again. The
|
|
51
|
+
* helper is idempotent. connections that are already wrapped are skipped.
|
|
52
|
+
*
|
|
53
|
+
* Per spec §5.8 (degradation contract), if any single connection fails to wrap
|
|
54
|
+
* (missing `client` field, unexpected shape), the helper logs a warning with
|
|
55
|
+
* the `atrib:` prefix and skips it without throwing. The agent's tool calls
|
|
56
|
+
* continue to work. they just won't be attributed for that connection.
|
|
57
|
+
*/
|
|
58
|
+
import { wrapMcpClient } from './mcp-client.js';
|
|
59
|
+
/** Runtime check that an unknown value structurally matches MinimalMcpClient. */
|
|
60
|
+
function isMinimalMcpClient(v) {
|
|
61
|
+
return (v != null &&
|
|
62
|
+
typeof v === 'object' &&
|
|
63
|
+
typeof v.callTool === 'function');
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Marker symbol set on a wrapped client to make repeated calls to
|
|
67
|
+
* `attributeCloudflareAgentMcp` idempotent. The Proxy returned by
|
|
68
|
+
* `wrapMcpClient` carries this property; the unwrapped Client does not.
|
|
69
|
+
*/
|
|
70
|
+
const ATRIB_WRAPPED = Symbol.for('atrib.cloudflare.wrapped');
|
|
71
|
+
/**
|
|
72
|
+
* Wrap every currently-connected MCP client on a Cloudflare Agent with atrib
|
|
73
|
+
* attribution. Returns the number of connections wrapped (excluding ones that
|
|
74
|
+
* were already wrapped). Idempotent. safe to call multiple times.
|
|
75
|
+
*
|
|
76
|
+
* Call this in `onStart()` after your `addMcpServer()` calls. If you add more
|
|
77
|
+
* MCP servers later (in a message handler, after OAuth, etc.), call again.
|
|
78
|
+
*/
|
|
79
|
+
export function attributeCloudflareAgentMcp(agent, options) {
|
|
80
|
+
const connections = agent.mcp?.mcpConnections;
|
|
81
|
+
if (!connections || typeof connections !== 'object') {
|
|
82
|
+
console.warn('atrib: attributeCloudflareAgentMcp called on an agent with no mcp.mcpConnections. ' +
|
|
83
|
+
"the Cloudflare 'agents' package shape may have changed. Skipping.");
|
|
84
|
+
return 0;
|
|
85
|
+
}
|
|
86
|
+
let wrapped = 0;
|
|
87
|
+
for (const [name, conn] of Object.entries(connections)) {
|
|
88
|
+
try {
|
|
89
|
+
if (!conn || !isMinimalMcpClient(conn.client)) {
|
|
90
|
+
console.warn(`atrib: connection '${name}' has no client field, skipping`);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Skip already-wrapped clients (idempotency)
|
|
94
|
+
if (conn.client[ATRIB_WRAPPED] === true) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
// Derive serverUrl: explicit override > connection.url.origin > undefined
|
|
98
|
+
let serverUrl = options.serverUrls?.[name];
|
|
99
|
+
if (!serverUrl && conn.url) {
|
|
100
|
+
try {
|
|
101
|
+
const u = conn.url instanceof URL ? conn.url : new URL(conn.url);
|
|
102
|
+
serverUrl = u.origin;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// URL parse failed; let wrapMcpClient fall back to no serverUrl
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const wrappedClient = wrapMcpClient(conn.client, options.interceptor, serverUrl ? { serverUrl } : {});
|
|
109
|
+
wrappedClient[ATRIB_WRAPPED] = true;
|
|
110
|
+
// Replace the client field in place. MCPClientManager.callTool reads
|
|
111
|
+
// mcpConnections[serverId].client at invocation time, so subsequent
|
|
112
|
+
// tool calls will go through the wrapped client.
|
|
113
|
+
conn.client = wrappedClient;
|
|
114
|
+
wrapped++;
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
// §5.8 degradation contract: never let a single bad connection break
|
|
118
|
+
// the whole agent. Log and continue.
|
|
119
|
+
console.warn(`atrib: failed to wrap MCP connection '${name}', skipping:`, err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return wrapped;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=cloudflare-agent.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cloudflare-agent.js","sourceRoot":"","sources":["../../src/adapters/cloudflare-agent.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAEH,OAAO,EAAE,aAAa,EAAyB,MAAM,iBAAiB,CAAA;AAGtE,iFAAiF;AACjF,SAAS,kBAAkB,CAAC,CAAU;IACpC,OAAO,CACL,CAAC,IAAI,IAAI;QACT,OAAO,CAAC,KAAK,QAAQ;QACrB,OAAQ,CAA4B,CAAC,QAAQ,KAAK,UAAU,CAC7D,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,0BAA0B,CAAC,CAAA;AA2C5D;;;;;;;GAOG;AACH,MAAM,UAAU,2BAA2B,CACzC,KAA0B,EAC1B,OAA2C;IAE3C,MAAM,WAAW,GAAG,KAAK,CAAC,GAAG,EAAE,cAAc,CAAA;IAC7C,IAAI,CAAC,WAAW,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpD,OAAO,CAAC,IAAI,CACV,oFAAoF;YAClF,mEAAmE,CACtE,CAAA;QACD,OAAO,CAAC,CAAA;IACV,CAAC;IAED,IAAI,OAAO,GAAG,CAAC,CAAA;IAEf,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,CAAC;YACH,IAAI,CAAC,IAAI,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC9C,OAAO,CAAC,IAAI,CAAC,sBAAsB,IAAI,iCAAiC,CAAC,CAAA;gBACzE,SAAQ;YACV,CAAC;YAED,6CAA6C;YAC7C,IAAK,IAAI,CAAC,MAA6C,CAAC,aAAa,CAAC,KAAK,IAAI,EAAE,CAAC;gBAChF,SAAQ;YACV,CAAC;YAED,0EAA0E;YAC1E,IAAI,SAAS,GAAuB,OAAO,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAA;YAC9D,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC3B,IAAI,CAAC;oBACH,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,YAAY,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;oBAChE,SAAS,GAAG,CAAC,CAAC,MAAM,CAAA;gBACtB,CAAC;gBAAC,MAAM,CAAC;oBACP,gEAAgE;gBAClE,CAAC;YACH,CAAC;YAED,MAAM,aAAa,GAAG,aAAa,CACjC,IAAI,CAAC,MAAM,EACX,OAAO,CAAC,WAAW,EACnB,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAC/B,CAMA;YAAC,aAAoD,CAAC,aAAa,CAAC,GAAG,IAAI,CAAA;YAE5E,qEAAqE;YACrE,oEAAoE;YACpE,iDAAiD;YACjD,IAAI,CAAC,MAAM,GAAG,aAAa,CAAA;YAC3B,OAAO,EAAE,CAAA;QACX,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,qCAAqC;YACrC,OAAO,CAAC,IAAI,CAAC,yCAAyC,IAAI,cAAc,EAAE,GAAG,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAA;AAChB,CAAC"}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: attribute MCP tool calls flowing through a LangChain JS MCP client.
|
|
3
|
+
*
|
|
4
|
+
* LangChain exposes MCP integration via the `@langchain/mcp-adapters` package
|
|
5
|
+
* (verified against `@langchain/mcp-adapters@1.1.3`). Two APIs are supported:
|
|
6
|
+
*
|
|
7
|
+
* 1. **High-level: `new MultiServerMCPClient({ mcpServers: {...} }).getTools()`**
|
|
8
|
+
* . config-driven, the client owns its internal
|
|
9
|
+
* `@modelcontextprotocol/sdk` Client instances behind `#private` fields.
|
|
10
|
+
* Users never see the Client reference directly; there is no public
|
|
11
|
+
* setter to inject a `wrapMcpClient`-style Proxy replacement. The
|
|
12
|
+
* `getClient(serverName)` getter returns the internal Client, so we
|
|
13
|
+
* can reach it. but we cannot substitute it.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Low-level: `loadMcpTools(serverName, rawClient)`**. accepts a raw
|
|
16
|
+
* `@modelcontextprotocol/sdk` Client directly (verified at
|
|
17
|
+
* `dist/tools.d.ts:28`). For this path, users can construct their own
|
|
18
|
+
* Client, wrap with the existing `wrapMcpClient` helper, and pass it
|
|
19
|
+
* in. No new code required. see the example in
|
|
20
|
+
* `packages/integration/examples/langchain-js/` for the pattern.
|
|
21
|
+
*
|
|
22
|
+
* This adapter handles the high-level `MultiServerMCPClient` path. It walks
|
|
23
|
+
* the multi-client's configured servers, calls `getClient()` on each to reach
|
|
24
|
+
* the internal Client, and monkey-patches `callTool` in place. Structurally
|
|
25
|
+
* the same pattern as `attributeVercelAiSdkMcp` (see D023), with one
|
|
26
|
+
* additional concern documented below.
|
|
27
|
+
*
|
|
28
|
+
* ## Per-call fork propagation
|
|
29
|
+
*
|
|
30
|
+
* LangChain's internal `_callTool` function (verified at `dist/tools.js:384`)
|
|
31
|
+
* supports per-call header changes via `client.fork(headers)`. this creates
|
|
32
|
+
* a new Client instance with different HTTP headers and calls `callTool` on
|
|
33
|
+
* the forked instance, not the original. A naive monkey-patch on the original
|
|
34
|
+
* Client would silently lose attribution for any LangChain user whose tools
|
|
35
|
+
* set per-call headers (the dominant pattern for per-user authentication).
|
|
36
|
+
*
|
|
37
|
+
* The patch therefore ALSO wraps `fork()` when present, so the forked Client
|
|
38
|
+
* is recursively patched before being returned to `_callTool`. This means
|
|
39
|
+
* every forked instance also flows through the atrib interceptor. no silent
|
|
40
|
+
* attribution drop for header-changing tools.
|
|
41
|
+
*
|
|
42
|
+
* ## Idempotency
|
|
43
|
+
*
|
|
44
|
+
* Each patched Client is marked with `Symbol.for('atrib.langchain-mcp.patched')`.
|
|
45
|
+
* Repeat calls to the helper on the same multi-client, and repeat `fork()`
|
|
46
|
+
* calls on already-patched Clients, are no-ops.
|
|
47
|
+
*
|
|
48
|
+
* ## Order sensitivity
|
|
49
|
+
*
|
|
50
|
+
* The helper is SAFE to call AFTER `multiClient.getTools()` because LangChain's
|
|
51
|
+
* tool `func` captures `client` by closure but dereferences `client.callTool`
|
|
52
|
+
* at invocation time (not at tool construction time. see `dist/tools.js:391`).
|
|
53
|
+
* Patching the client after tools are built still takes effect on the next
|
|
54
|
+
* tool invocation.
|
|
55
|
+
*
|
|
56
|
+
* The helper is also safe to call BEFORE `getTools()`, but in that case
|
|
57
|
+
* `multiClient.getClient(serverName)` will lazily initialize the connection
|
|
58
|
+
* if it hasn't happened yet. For predictable startup, most users will call
|
|
59
|
+
* `await multiClient.initializeConnections()` first, then
|
|
60
|
+
* `await attributeLangchainMcp(multiClient, { interceptor })`.
|
|
61
|
+
*/
|
|
62
|
+
import type { ToolCallInterceptor } from '../middleware.js';
|
|
63
|
+
/**
|
|
64
|
+
* Minimal structural type for a LangChain-extended MCP Client (from
|
|
65
|
+
* `@langchain/mcp-adapters`'s `connection.ts`). This is the MCP SDK Client
|
|
66
|
+
* with an added `fork()` method for per-call header changes. We don't import
|
|
67
|
+
* from `@langchain/mcp-adapters` to avoid a hard dependency on the LangChain
|
|
68
|
+
* ecosystem from `@atrib/agent`.
|
|
69
|
+
*
|
|
70
|
+
* The `callTool` signature mirrors `@modelcontextprotocol/sdk`'s
|
|
71
|
+
* `Client.callTool(params, resultSchema?, options?)`.
|
|
72
|
+
*/
|
|
73
|
+
export interface LangchainMcpClientLike {
|
|
74
|
+
callTool(params: {
|
|
75
|
+
name: string;
|
|
76
|
+
arguments?: Record<string, unknown> | undefined;
|
|
77
|
+
_meta?: Record<string, unknown> | undefined;
|
|
78
|
+
}, resultSchema?: unknown, options?: {
|
|
79
|
+
signal?: AbortSignal | undefined;
|
|
80
|
+
timeout?: number | undefined;
|
|
81
|
+
onprogress?: ((progress: unknown) => void) | undefined;
|
|
82
|
+
}): Promise<unknown>;
|
|
83
|
+
fork?(headers: Record<string, string>): Promise<LangchainMcpClientLike>;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Minimal structural type for a LangChain `MultiServerMCPClient`. Mirrors the
|
|
87
|
+
* subset of the public surface we depend on (verified against
|
|
88
|
+
* `@langchain/mcp-adapters@1.1.3` `dist/client.d.ts`).
|
|
89
|
+
*
|
|
90
|
+
* The `config` getter returns a cloned `ClientConfig` whose `mcpServers`
|
|
91
|
+
* field enumerates every configured server by name. this is how we
|
|
92
|
+
* discover which servers to patch without requiring the caller to list
|
|
93
|
+
* them explicitly.
|
|
94
|
+
*/
|
|
95
|
+
export interface LangchainMultiServerMcpClientLike {
|
|
96
|
+
readonly config: {
|
|
97
|
+
mcpServers?: Record<string, unknown> | undefined;
|
|
98
|
+
};
|
|
99
|
+
getClient(serverName: string, options?: unknown): Promise<LangchainMcpClientLike | undefined>;
|
|
100
|
+
}
|
|
101
|
+
/** Options for `attributeLangchainMcp`. */
|
|
102
|
+
export interface AttributeLangchainMcpOptions {
|
|
103
|
+
/** The atrib interceptor that observes tool calls on this client. */
|
|
104
|
+
interceptor: ToolCallInterceptor;
|
|
105
|
+
/**
|
|
106
|
+
* Optional per-server canonical URL map. Used by the interceptor for
|
|
107
|
+
* content_id derivation when emitting Path 2 transaction records (§5.4.5).
|
|
108
|
+
* Keys are the server names as they appear in the multi-client's
|
|
109
|
+
* `mcpServers` config; values are canonical URLs (e.g.
|
|
110
|
+
* `'https://search.example.com'`).
|
|
111
|
+
*/
|
|
112
|
+
serverUrls?: Record<string, string>;
|
|
113
|
+
/**
|
|
114
|
+
* Optional list of server names to patch. If omitted, the helper patches
|
|
115
|
+
* every server in the multi-client's `config.mcpServers`. Provide this
|
|
116
|
+
* when you want to selectively attribute only a subset of configured
|
|
117
|
+
* servers (rare. usually you want all of them).
|
|
118
|
+
*/
|
|
119
|
+
servers?: string[];
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Patch a LangChain `MultiServerMCPClient` so every outbound `tools/call`
|
|
123
|
+
* flows through atrib's interceptor lifecycle.
|
|
124
|
+
*
|
|
125
|
+
* Walks the multi-client's configured servers, calls `getClient(serverName)`
|
|
126
|
+
* on each to reach the internal `@modelcontextprotocol/sdk` Client, and
|
|
127
|
+
* monkey-patches `callTool` (and `fork` when present) in place.
|
|
128
|
+
*
|
|
129
|
+
* Idempotent: calling the helper twice on the same multi-client, or calling
|
|
130
|
+
* it on a multi-client with some already-patched servers, is safe. Repeat
|
|
131
|
+
* invocations on already-patched clients are no-ops.
|
|
132
|
+
*
|
|
133
|
+
* Order independent: this helper can be called BEFORE or AFTER
|
|
134
|
+
* `multiClient.getTools()` because LangChain's tool `func` dereferences
|
|
135
|
+
* `client.callTool` at invocation time, not at build time.
|
|
136
|
+
*
|
|
137
|
+
* Returns the number of clients that were newly patched on this call
|
|
138
|
+
* (excludes clients that were already patched from a previous call).
|
|
139
|
+
*
|
|
140
|
+
* Usage:
|
|
141
|
+
*
|
|
142
|
+
* import { MultiServerMCPClient } from '@langchain/mcp-adapters'
|
|
143
|
+
* import { ChatAnthropic } from '@langchain/anthropic'
|
|
144
|
+
* import { createReactAgent } from '@langchain/langgraph/prebuilt'
|
|
145
|
+
* import { atrib, attributeLangchainMcp } from '@atrib/agent'
|
|
146
|
+
*
|
|
147
|
+
* const interceptor = atrib({
|
|
148
|
+
* creatorKey: process.env.ATRIB_PRIVATE_KEY!,
|
|
149
|
+
* merchantDomain: 'https://merchant.example.com',
|
|
150
|
+
* serverUrls: ['https://search.example.com'],
|
|
151
|
+
* })
|
|
152
|
+
*
|
|
153
|
+
* const multi = new MultiServerMCPClient({
|
|
154
|
+
* mcpServers: {
|
|
155
|
+
* search: { transport: 'http', url: 'https://search.example.com/mcp' },
|
|
156
|
+
* },
|
|
157
|
+
* })
|
|
158
|
+
*
|
|
159
|
+
* await multi.initializeConnections()
|
|
160
|
+
* await attributeLangchainMcp(multi, {
|
|
161
|
+
* interceptor,
|
|
162
|
+
* serverUrls: { search: 'https://search.example.com' },
|
|
163
|
+
* })
|
|
164
|
+
*
|
|
165
|
+
* const tools = await multi.getTools()
|
|
166
|
+
* const agent = createReactAgent({
|
|
167
|
+
* llm: new ChatAnthropic({ model: 'claude-sonnet-4-6' }),
|
|
168
|
+
* tools,
|
|
169
|
+
* })
|
|
170
|
+
*/
|
|
171
|
+
export declare function attributeLangchainMcp(multiClient: LangchainMultiServerMcpClientLike, options: AttributeLangchainMcpOptions): Promise<number>;
|
|
172
|
+
//# sourceMappingURL=langchain-mcp.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"langchain-mcp.d.ts","sourceRoot":"","sources":["../../src/adapters/langchain-mcp.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAO3D;;;;;;;;;GASG;AACH,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CACN,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;QAC/C,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;KAC5C,EACD,YAAY,CAAC,EAAE,OAAO,EACtB,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;QAChC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;QAC5B,UAAU,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC,GAAG,SAAS,CAAA;KACvD,GACA,OAAO,CAAC,OAAO,CAAC,CAAA;IACnB,IAAI,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAA;CACxE;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,MAAM,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAA;KAAE,CAAA;IACrE,SAAS,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,sBAAsB,GAAG,SAAS,CAAC,CAAA;CAC9F;AAED,2CAA2C;AAC3C,MAAM,WAAW,4BAA4B;IAC3C,qEAAqE;IACrE,WAAW,EAAE,mBAAmB,CAAA;IAEhC;;;;;;OAMG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEnC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,wBAAsB,qBAAqB,CACzC,WAAW,EAAE,iCAAiC,EAC9C,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,MAAM,CAAC,CA2BjB"}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Marker symbol set on a patched client to make repeated patches idempotent.
|
|
4
|
+
*/
|
|
5
|
+
const ATRIB_PATCHED = Symbol.for('atrib.langchain-mcp.patched');
|
|
6
|
+
/**
|
|
7
|
+
* Patch a LangChain `MultiServerMCPClient` so every outbound `tools/call`
|
|
8
|
+
* flows through atrib's interceptor lifecycle.
|
|
9
|
+
*
|
|
10
|
+
* Walks the multi-client's configured servers, calls `getClient(serverName)`
|
|
11
|
+
* on each to reach the internal `@modelcontextprotocol/sdk` Client, and
|
|
12
|
+
* monkey-patches `callTool` (and `fork` when present) in place.
|
|
13
|
+
*
|
|
14
|
+
* Idempotent: calling the helper twice on the same multi-client, or calling
|
|
15
|
+
* it on a multi-client with some already-patched servers, is safe. Repeat
|
|
16
|
+
* invocations on already-patched clients are no-ops.
|
|
17
|
+
*
|
|
18
|
+
* Order independent: this helper can be called BEFORE or AFTER
|
|
19
|
+
* `multiClient.getTools()` because LangChain's tool `func` dereferences
|
|
20
|
+
* `client.callTool` at invocation time, not at build time.
|
|
21
|
+
*
|
|
22
|
+
* Returns the number of clients that were newly patched on this call
|
|
23
|
+
* (excludes clients that were already patched from a previous call).
|
|
24
|
+
*
|
|
25
|
+
* Usage:
|
|
26
|
+
*
|
|
27
|
+
* import { MultiServerMCPClient } from '@langchain/mcp-adapters'
|
|
28
|
+
* import { ChatAnthropic } from '@langchain/anthropic'
|
|
29
|
+
* import { createReactAgent } from '@langchain/langgraph/prebuilt'
|
|
30
|
+
* import { atrib, attributeLangchainMcp } from '@atrib/agent'
|
|
31
|
+
*
|
|
32
|
+
* const interceptor = atrib({
|
|
33
|
+
* creatorKey: process.env.ATRIB_PRIVATE_KEY!,
|
|
34
|
+
* merchantDomain: 'https://merchant.example.com',
|
|
35
|
+
* serverUrls: ['https://search.example.com'],
|
|
36
|
+
* })
|
|
37
|
+
*
|
|
38
|
+
* const multi = new MultiServerMCPClient({
|
|
39
|
+
* mcpServers: {
|
|
40
|
+
* search: { transport: 'http', url: 'https://search.example.com/mcp' },
|
|
41
|
+
* },
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* await multi.initializeConnections()
|
|
45
|
+
* await attributeLangchainMcp(multi, {
|
|
46
|
+
* interceptor,
|
|
47
|
+
* serverUrls: { search: 'https://search.example.com' },
|
|
48
|
+
* })
|
|
49
|
+
*
|
|
50
|
+
* const tools = await multi.getTools()
|
|
51
|
+
* const agent = createReactAgent({
|
|
52
|
+
* llm: new ChatAnthropic({ model: 'claude-sonnet-4-6' }),
|
|
53
|
+
* tools,
|
|
54
|
+
* })
|
|
55
|
+
*/
|
|
56
|
+
export async function attributeLangchainMcp(multiClient, options) {
|
|
57
|
+
const { interceptor, serverUrls = {}, servers } = options;
|
|
58
|
+
const configuredServers = servers ?? Object.keys(multiClient.config.mcpServers ?? {});
|
|
59
|
+
let newlyPatched = 0;
|
|
60
|
+
for (const serverName of configuredServers) {
|
|
61
|
+
let client;
|
|
62
|
+
try {
|
|
63
|
+
client = await multiClient.getClient(serverName);
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.warn(`atrib: langchain-mcp could not resolve client for server '${serverName}', skipping`, err);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (!client) {
|
|
70
|
+
console.warn(`atrib: langchain-mcp server '${serverName}' has no client (not initialized?), skipping`);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (patchClient(client, interceptor, serverUrls[serverName])) {
|
|
74
|
+
newlyPatched++;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return newlyPatched;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Patch a single LangChain MCP Client in place. Returns true if the client
|
|
81
|
+
* was newly patched, false if it was already patched from a previous call.
|
|
82
|
+
*
|
|
83
|
+
* Recursive on `fork()`: when the patched `fork` is invoked, the returned
|
|
84
|
+
* forked client is itself passed through `patchClient` before being handed
|
|
85
|
+
* back to LangChain's `_callTool`. This ensures that per-call-header
|
|
86
|
+
* workflows (common for per-user authentication) don't silently lose
|
|
87
|
+
* attribution.
|
|
88
|
+
*/
|
|
89
|
+
function patchClient(client, interceptor, serverUrl) {
|
|
90
|
+
if (client[ATRIB_PATCHED] === true) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const originalCallTool = client.callTool.bind(client);
|
|
94
|
+
const patchedCallTool = async (params, resultSchema, opts) => {
|
|
95
|
+
const toolName = params.name;
|
|
96
|
+
const existingMeta = (params._meta ?? {});
|
|
97
|
+
// §5.4.3: Build outbound _meta via the interceptor. The interceptor
|
|
98
|
+
// returns a record that includes any existing _meta keys plus atrib's
|
|
99
|
+
// own (atrib token, traceparent, tracestate, baggage, X-Atrib-Chain).
|
|
100
|
+
let outboundMeta;
|
|
101
|
+
try {
|
|
102
|
+
outboundMeta = await interceptor.onBeforeToolCall(toolName, existingMeta);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
// §5.8 degradation: pass through with the original params on failure
|
|
106
|
+
console.warn('atrib: langchain-mcp onBeforeToolCall failed, passing through', err);
|
|
107
|
+
outboundMeta = undefined;
|
|
108
|
+
}
|
|
109
|
+
// Construct new params rather than mutating. caller's reference is preserved.
|
|
110
|
+
const forwardedParams = outboundMeta !== undefined ? { ...params, _meta: outboundMeta } : params;
|
|
111
|
+
const result = await originalCallTool(forwardedParams, resultSchema, opts);
|
|
112
|
+
// §5.4.4: Update session state from the response _meta.
|
|
113
|
+
try {
|
|
114
|
+
const responseMeta = (result?._meta ?? {});
|
|
115
|
+
const responseOptions = {
|
|
116
|
+
...(serverUrl !== undefined ? { serverUrl } : {}),
|
|
117
|
+
isError: result?.isError === true,
|
|
118
|
+
};
|
|
119
|
+
interceptor.onAfterToolResponse(toolName, result, responseMeta, responseOptions);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.warn('atrib: langchain-mcp onAfterToolResponse failed, passing through', err);
|
|
123
|
+
}
|
|
124
|
+
return result;
|
|
125
|
+
};
|
|
126
|
+
client.callTool = patchedCallTool;
|
|
127
|
+
// Fork propagation: LangChain's _callTool creates a new Client via fork()
|
|
128
|
+
// when per-call header changes are requested (dist/tools.js:384). The
|
|
129
|
+
// forked client needs its own callTool patch, or it silently bypasses
|
|
130
|
+
// atrib. We wrap fork so the returned forked client is patched recursively
|
|
131
|
+
// before being handed back to the caller.
|
|
132
|
+
if (typeof client.fork === 'function') {
|
|
133
|
+
const originalFork = client.fork.bind(client);
|
|
134
|
+
const patchedFork = async (headers) => {
|
|
135
|
+
const forkedClient = await originalFork(headers);
|
|
136
|
+
patchClient(forkedClient, interceptor, serverUrl);
|
|
137
|
+
return forkedClient;
|
|
138
|
+
};
|
|
139
|
+
client.fork = patchedFork;
|
|
140
|
+
}
|
|
141
|
+
;
|
|
142
|
+
client[ATRIB_PATCHED] = true;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=langchain-mcp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"langchain-mcp.js","sourceRoot":"","sources":["../../src/adapters/langchain-mcp.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adapter: wrap a raw `@modelcontextprotocol/sdk` Client with atrib attribution.
|
|
3
|
+
*
|
|
4
|
+
* The MCP TypeScript SDK exposes a `Client` class with a `callTool(params, ...)`
|
|
5
|
+
* method. This adapter returns a thin proxy around such a client whose
|
|
6
|
+
* `callTool` is wrapped to:
|
|
7
|
+
*
|
|
8
|
+
* 1. Call `interceptor.onBeforeToolCall(toolName, existingMeta)` to get the
|
|
9
|
+
* attribution `_meta` block (traceparent, atrib token, baggage, etc.)
|
|
10
|
+
* 2. Forward the request through the underlying client with the new
|
|
11
|
+
* `_meta` merged into `params._meta`
|
|
12
|
+
* 3. Call `interceptor.onAfterToolResponse(toolName, result, result._meta, ...)`
|
|
13
|
+
* so the interceptor can update its session state, detect transactions,
|
|
14
|
+
* and emit Path 2 records when the tool server has no @atrib/mcp
|
|
15
|
+
*
|
|
16
|
+
* The adapter is intentionally narrow. it only proxies `callTool`. All other
|
|
17
|
+
* client methods (`listTools`, `close`, `connect`, `request`, etc.) are
|
|
18
|
+
* forwarded unchanged. This means a developer can drop the adapter into any
|
|
19
|
+
* existing code that uses the MCP SDK Client directly without changing
|
|
20
|
+
* anything else.
|
|
21
|
+
*
|
|
22
|
+
* If the MCP SDK ever changes the `callTool` signature, the adapter will
|
|
23
|
+
* fail loudly because TypeScript will complain about the proxied parameters
|
|
24
|
+
* shape. see the `MinimalMcpClient` interface below.
|
|
25
|
+
*/
|
|
26
|
+
import type { ToolCallInterceptor } from '../middleware.js';
|
|
27
|
+
/**
|
|
28
|
+
* The minimal subset of `@modelcontextprotocol/sdk/client/index.js` Client
|
|
29
|
+
* that this adapter depends on. We do not import the SDK type directly to
|
|
30
|
+
* avoid making `@modelcontextprotocol/sdk` a hard dependency of `@atrib/agent`
|
|
31
|
+
*. the SDK is only needed if you actually use this adapter, not for the
|
|
32
|
+
* core interceptor API.
|
|
33
|
+
*/
|
|
34
|
+
export interface MinimalMcpClient {
|
|
35
|
+
callTool(params: {
|
|
36
|
+
name: string;
|
|
37
|
+
arguments?: Record<string, unknown>;
|
|
38
|
+
_meta?: Record<string, unknown>;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}, resultSchema?: unknown, options?: unknown): Promise<{
|
|
41
|
+
content?: unknown;
|
|
42
|
+
_meta?: Record<string, unknown>;
|
|
43
|
+
isError?: boolean;
|
|
44
|
+
[key: string]: unknown;
|
|
45
|
+
}>;
|
|
46
|
+
}
|
|
47
|
+
/** Options for `wrapMcpClient`. */
|
|
48
|
+
export interface WrapMcpClientOptions {
|
|
49
|
+
/**
|
|
50
|
+
* The canonical URL of the MCP server this client connects to. Used by
|
|
51
|
+
* the interceptor for content_id derivation when emitting Path 2
|
|
52
|
+
* transaction records (the agent fallback path described in spec §5.4.5).
|
|
53
|
+
* If omitted, the interceptor will fall back to the tool's MCP server
|
|
54
|
+
* URL only when it can be derived elsewhere. for many transports
|
|
55
|
+
* (especially stdio) it cannot, and content_id values for transactions
|
|
56
|
+
* will be less specific.
|
|
57
|
+
*/
|
|
58
|
+
serverUrl?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wrap a raw MCP Client so every `callTool` invocation participates in
|
|
62
|
+
* atrib attribution.
|
|
63
|
+
*
|
|
64
|
+
* Returns a Proxy over the client. All non-`callTool` methods are forwarded
|
|
65
|
+
* unchanged via the prototype, so the wrapped client is API-compatible with
|
|
66
|
+
* the original.
|
|
67
|
+
*
|
|
68
|
+
* Usage:
|
|
69
|
+
*
|
|
70
|
+
* import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
71
|
+
* import { atrib, wrapMcpClient } from '@atrib/agent'
|
|
72
|
+
*
|
|
73
|
+
* const interceptor = atrib({
|
|
74
|
+
* creatorKey: process.env.ATRIB_PRIVATE_KEY,
|
|
75
|
+
* merchantDomain: 'https://merchant.example.com',
|
|
76
|
+
* serverUrls: ['https://my-tool.example'],
|
|
77
|
+
* })
|
|
78
|
+
*
|
|
79
|
+
* const rawClient = new Client({ name: 'my-agent', version: '1.0.0' })
|
|
80
|
+
* await rawClient.connect(transport)
|
|
81
|
+
*
|
|
82
|
+
* const client = wrapMcpClient(rawClient, interceptor, {
|
|
83
|
+
* serverUrl: 'https://my-tool.example',
|
|
84
|
+
* })
|
|
85
|
+
*
|
|
86
|
+
* // Use `client` exactly like the raw MCP Client.
|
|
87
|
+
* const result = await client.callTool({ name: 'search', arguments: { q: 'foo' } })
|
|
88
|
+
*
|
|
89
|
+
* // On shutdown:
|
|
90
|
+
* await interceptor.flush()
|
|
91
|
+
* await rawClient.close()
|
|
92
|
+
*/
|
|
93
|
+
export declare function wrapMcpClient<C extends MinimalMcpClient>(client: C, interceptor: ToolCallInterceptor, options?: WrapMcpClientOptions): C;
|
|
94
|
+
//# sourceMappingURL=mcp-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-client.d.ts","sourceRoot":"","sources":["../../src/adapters/mcp-client.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAA;AAE3D;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CACN,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACnC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC/B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB,EACD,YAAY,CAAC,EAAE,OAAO,EACtB,OAAO,CAAC,EAAE,OAAO,GAChB,OAAO,CAAC;QACT,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QAC/B,OAAO,CAAC,EAAE,OAAO,CAAA;QACjB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KACvB,CAAC,CAAA;CACH;AAED,mCAAmC;AACnC,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,gBAAgB,EACtD,MAAM,EAAE,CAAC,EACT,WAAW,EAAE,mBAAmB,EAChC,OAAO,GAAE,oBAAyB,GACjC,CAAC,CAiEH"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a raw MCP Client so every `callTool` invocation participates in
|
|
4
|
+
* atrib attribution.
|
|
5
|
+
*
|
|
6
|
+
* Returns a Proxy over the client. All non-`callTool` methods are forwarded
|
|
7
|
+
* unchanged via the prototype, so the wrapped client is API-compatible with
|
|
8
|
+
* the original.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
*
|
|
12
|
+
* import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
13
|
+
* import { atrib, wrapMcpClient } from '@atrib/agent'
|
|
14
|
+
*
|
|
15
|
+
* const interceptor = atrib({
|
|
16
|
+
* creatorKey: process.env.ATRIB_PRIVATE_KEY,
|
|
17
|
+
* merchantDomain: 'https://merchant.example.com',
|
|
18
|
+
* serverUrls: ['https://my-tool.example'],
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* const rawClient = new Client({ name: 'my-agent', version: '1.0.0' })
|
|
22
|
+
* await rawClient.connect(transport)
|
|
23
|
+
*
|
|
24
|
+
* const client = wrapMcpClient(rawClient, interceptor, {
|
|
25
|
+
* serverUrl: 'https://my-tool.example',
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* // Use `client` exactly like the raw MCP Client.
|
|
29
|
+
* const result = await client.callTool({ name: 'search', arguments: { q: 'foo' } })
|
|
30
|
+
*
|
|
31
|
+
* // On shutdown:
|
|
32
|
+
* await interceptor.flush()
|
|
33
|
+
* await rawClient.close()
|
|
34
|
+
*/
|
|
35
|
+
export function wrapMcpClient(client, interceptor, options = {}) {
|
|
36
|
+
const { serverUrl } = options;
|
|
37
|
+
const wrappedCallTool = async (params, resultSchema, requestOptions) => {
|
|
38
|
+
const toolName = params.name;
|
|
39
|
+
// §5.4.3: Build outbound _meta. The interceptor returns a record that
|
|
40
|
+
// includes any existing _meta keys from the caller plus atrib's own
|
|
41
|
+
// (atrib token, traceparent, tracestate, baggage, X-Atrib-Chain).
|
|
42
|
+
let outboundMeta;
|
|
43
|
+
try {
|
|
44
|
+
const existingMeta = (params._meta ?? {});
|
|
45
|
+
outboundMeta = await interceptor.onBeforeToolCall(toolName, existingMeta);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// §5.8 degradation contract: never let attribution failures break the
|
|
49
|
+
// primary tool call. Fall through to the original params unchanged.
|
|
50
|
+
console.warn('atrib: wrapMcpClient onBeforeToolCall failed, passing through', err);
|
|
51
|
+
outboundMeta = undefined;
|
|
52
|
+
}
|
|
53
|
+
const forwardedParams = outboundMeta !== undefined ? { ...params, _meta: outboundMeta } : params;
|
|
54
|
+
// Forward to the underlying client with the merged _meta.
|
|
55
|
+
const result = await client.callTool(forwardedParams, resultSchema, requestOptions);
|
|
56
|
+
// §5.4.4: Update session state from the response _meta. Also runs Path 1/2
|
|
57
|
+
// transaction detection if the response shape matches a known protocol.
|
|
58
|
+
try {
|
|
59
|
+
const responseMeta = (result?._meta ?? {});
|
|
60
|
+
// exactOptionalPropertyTypes: only include serverUrl when it's set,
|
|
61
|
+
// because the interceptor's option type doesn't accept undefined.
|
|
62
|
+
const responseOptions = {
|
|
63
|
+
...(serverUrl !== undefined ? { serverUrl } : {}),
|
|
64
|
+
isError: result?.isError === true,
|
|
65
|
+
};
|
|
66
|
+
interceptor.onAfterToolResponse(toolName, result, responseMeta, responseOptions);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
console.warn('atrib: wrapMcpClient onAfterToolResponse failed, passing through', err);
|
|
70
|
+
}
|
|
71
|
+
return result;
|
|
72
|
+
};
|
|
73
|
+
// Proxy the client so all other methods (listTools, close, connect, etc.)
|
|
74
|
+
// pass through unchanged. We can't simply spread the client because the
|
|
75
|
+
// SDK's Client class methods rely on `this` being the original instance.
|
|
76
|
+
return new Proxy(client, {
|
|
77
|
+
get(target, prop, receiver) {
|
|
78
|
+
if (prop === 'callTool') {
|
|
79
|
+
return wrappedCallTool;
|
|
80
|
+
}
|
|
81
|
+
const value = Reflect.get(target, prop, receiver);
|
|
82
|
+
// If the property is a function, bind it to the original target so
|
|
83
|
+
// method calls like `client.listTools()` still work correctly.
|
|
84
|
+
if (typeof value === 'function') {
|
|
85
|
+
return value.bind(target);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=mcp-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mcp-client.js","sourceRoot":"","sources":["../../src/adapters/mcp-client.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAqEtC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAS,EACT,WAAgC,EAChC,UAAgC,EAAE;IAElC,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAA;IAE7B,MAAM,eAAe,GAAiC,KAAK,EACzD,MAAM,EACN,YAAY,EACZ,cAAc,EACd,EAAE;QACF,MAAM,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAA;QAE5B,sEAAsE;QACtE,oEAAoE;QACpE,kEAAkE;QAClE,IAAI,YAAiD,CAAA;QACrD,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,CAAC,MAAM,CAAC,KAAK,IAAI,EAAE,CAA4B,CAAA;YACpE,YAAY,GAAG,MAAM,WAAW,CAAC,gBAAgB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAA;QAC3E,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,sEAAsE;YACtE,oEAAoE;YACpE,OAAO,CAAC,IAAI,CAAC,+DAA+D,EAAE,GAAG,CAAC,CAAA;YAClF,YAAY,GAAG,SAAS,CAAA;QAC1B,CAAC;QAED,MAAM,eAAe,GACnB,YAAY,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAC,CAAC,MAAM,CAAA;QAE1E,0DAA0D;QAC1D,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,QAAQ,CAAC,eAAe,EAAE,YAAY,EAAE,cAAc,CAAC,CAAA;QAEnF,2EAA2E;QAC3E,wEAAwE;QACxE,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,CAAC,MAAM,EAAE,KAAK,IAAI,EAAE,CAA4B,CAAA;YACrE,oEAAoE;YACpE,kEAAkE;YAClE,MAAM,eAAe,GAAG;gBACtB,GAAG,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBACjD,OAAO,EAAG,MAAkC,EAAE,OAAO,KAAK,IAAI;aAC/D,CAAA;YACD,WAAW,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,eAAe,CAAC,CAAA;QAClF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,IAAI,CAAC,kEAAkE,EAAE,GAAG,CAAC,CAAA;QACvF,CAAC;QAED,OAAO,MAAM,CAAA;IACf,CAAC,CAAA;IAED,0EAA0E;IAC1E,wEAAwE;IACxE,yEAAyE;IACzE,OAAO,IAAI,KAAK,CAAC,MAAM,EAAE;QACvB,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ;YACxB,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;gBACxB,OAAO,eAAe,CAAA;YACxB,CAAC;YACD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAA;YACjD,mEAAmE;YACnE,+DAA+D;YAC/D,IAAI,OAAO,KAAK,KAAK,UAAU,EAAE,CAAC;gBAChC,OAAQ,KAAkB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YACzC,CAAC;YACD,OAAO,KAAK,CAAA;QACd,CAAC;KACF,CAAC,CAAA;AACJ,CAAC"}
|