@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,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `toTools()` adapter - bridges MCP tool descriptors into the
|
|
3
|
+
* Graphorin tool registry.
|
|
4
|
+
*
|
|
5
|
+
* The adapter orchestrates three extracted concerns (F-MCP-001):
|
|
6
|
+
*
|
|
7
|
+
* 1. Filters / namespaces the `listTools()` output, then resolves the
|
|
8
|
+
* per-server effective `defer_loading` flag - see
|
|
9
|
+
* {@link import('./defer-loading.js').resolveDeferLoading}.
|
|
10
|
+
* 2. Resolves the per-server inbound prompt-injection policy and strips
|
|
11
|
+
* imperative payloads from each description - see
|
|
12
|
+
* {@link import('./inbound-filters.js')}.
|
|
13
|
+
* 3. Converts each MCP tool into a strongly-typed Graphorin `Tool` whose
|
|
14
|
+
* `execute(...)` calls back into {@link MCPClient.callTool} and adapts
|
|
15
|
+
* the result - see {@link import('./adapt-result.js').adaptCallResult}.
|
|
16
|
+
*
|
|
17
|
+
* The trust class is pinned to `'mcp-derived'` so the agent runtime's
|
|
18
|
+
* per-step preamble fires regardless of the body-level policy.
|
|
19
|
+
*
|
|
20
|
+
* @packageDocumentation
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { ContentChunk, InboundSanitizationPolicy, Tool, ToolReturn } from '@graphorin/core';
|
|
24
|
+
import { buildJsonSchemaValidator, type JsonSchemaLike } from '../registry/json-schema.js';
|
|
25
|
+
import type { ServerIdentity } from '../transport/types.js';
|
|
26
|
+
import { adaptCallResult } from './adapt-result.js';
|
|
27
|
+
import {
|
|
28
|
+
_resetDeferLoadingDedupForTesting,
|
|
29
|
+
DEFAULT_DEFER_LOADING_THRESHOLD,
|
|
30
|
+
resolveDeferLoading,
|
|
31
|
+
} from './defer-loading.js';
|
|
32
|
+
import {
|
|
33
|
+
_resetInboundFiltersDedupForTesting,
|
|
34
|
+
resolveInboundPolicy,
|
|
35
|
+
sanitizeDescription,
|
|
36
|
+
sanitizeSchemaAnnotations,
|
|
37
|
+
warnOnPassthroughOverride,
|
|
38
|
+
} from './inbound-filters.js';
|
|
39
|
+
import { computeToolDefinitionHash } from './pinning.js';
|
|
40
|
+
import type { MCPClient, MCPToolDefinition, MCPToToolsOptions } from './types.js';
|
|
41
|
+
|
|
42
|
+
// Re-exported for backward compatibility: callers (and tests) import
|
|
43
|
+
// these symbols directly from `./to-tools.js` and via the client barrel.
|
|
44
|
+
export { adaptCallResult } from './adapt-result.js';
|
|
45
|
+
export { DEFAULT_DEFER_LOADING_THRESHOLD } from './defer-loading.js';
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reset every process-scoped dedup set owned by the adapter modules.
|
|
49
|
+
* Used by tests.
|
|
50
|
+
*
|
|
51
|
+
* @experimental
|
|
52
|
+
*/
|
|
53
|
+
export function _resetMcpAdapterDedupForTesting(): void {
|
|
54
|
+
_resetDeferLoadingDedupForTesting();
|
|
55
|
+
_resetInboundFiltersDedupForTesting();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Result returned by {@link adaptMCPTools}. */
|
|
59
|
+
export interface AdaptedToolsResult {
|
|
60
|
+
readonly tools: ReadonlyArray<Tool>;
|
|
61
|
+
/** MC-6: sha256 definition fingerprint per MCP tool name. */
|
|
62
|
+
readonly fingerprints: ReadonlyMap<string, string>;
|
|
63
|
+
readonly autoDeferralFired: boolean;
|
|
64
|
+
readonly resolvedDeferLoading: boolean;
|
|
65
|
+
readonly resolvedInboundSanitization: InboundSanitizationPolicy;
|
|
66
|
+
readonly toolCount: number;
|
|
67
|
+
readonly deferralThreshold: number;
|
|
68
|
+
/**
|
|
69
|
+
* W-105: tool names the operator downgraded below the sink classes
|
|
70
|
+
* via `sideEffectClassByTool` (`'read-only'` / `'pure'`). Each such
|
|
71
|
+
* tool leaves EVERY sink check - the dataflow gate, the Rule-of-Two
|
|
72
|
+
* writer forbid, the read-only capability gate - so operator audits
|
|
73
|
+
* should review this list. Empty when no override downgrades.
|
|
74
|
+
*/
|
|
75
|
+
readonly downgradedTools: ReadonlyArray<string>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Build the {@link Tool} array for the supplied MCP tool catalogue.
|
|
80
|
+
*
|
|
81
|
+
* @stable
|
|
82
|
+
*/
|
|
83
|
+
export function adaptMCPTools(args: {
|
|
84
|
+
readonly client: MCPClient;
|
|
85
|
+
readonly serverIdentity: ServerIdentity;
|
|
86
|
+
readonly catalogue: ReadonlyArray<MCPToolDefinition>;
|
|
87
|
+
readonly options?: MCPToToolsOptions;
|
|
88
|
+
readonly logger?: (
|
|
89
|
+
level: 'debug' | 'info' | 'warn' | 'error',
|
|
90
|
+
message: string,
|
|
91
|
+
fields?: Record<string, unknown>,
|
|
92
|
+
) => void;
|
|
93
|
+
}): AdaptedToolsResult {
|
|
94
|
+
const opts = args.options ?? {};
|
|
95
|
+
const fingerprints = new Map<string, string>();
|
|
96
|
+
const filter = opts.filter;
|
|
97
|
+
const namespace = (opts.namespace ?? '').trim();
|
|
98
|
+
const filtered = filter === undefined ? args.catalogue : args.catalogue.filter((t) => filter(t));
|
|
99
|
+
const total = filtered.length;
|
|
100
|
+
const threshold = opts.deferLoadingThreshold ?? DEFAULT_DEFER_LOADING_THRESHOLD;
|
|
101
|
+
|
|
102
|
+
const { autoDeferralFired, resolvedDeferLoading } = resolveDeferLoading({
|
|
103
|
+
serverIdentity: args.serverIdentity,
|
|
104
|
+
toolNames: filtered.map((t) => t.name),
|
|
105
|
+
explicitDefer: opts.defer_loading,
|
|
106
|
+
threshold,
|
|
107
|
+
...(args.logger === undefined ? {} : { logger: args.logger }),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const resolvedInbound = resolveInboundPolicy(opts.inboundSanitization);
|
|
111
|
+
warnOnPassthroughOverride({
|
|
112
|
+
resolvedInbound,
|
|
113
|
+
serverIdentity: args.serverIdentity,
|
|
114
|
+
...(args.logger === undefined ? {} : { logger: args.logger }),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const tools: Tool[] = [];
|
|
118
|
+
const downgradedTools: string[] = [];
|
|
119
|
+
for (const definition of filtered) {
|
|
120
|
+
const namespacedName =
|
|
121
|
+
namespace.length === 0 ? definition.name : `${namespace}.${definition.name}`;
|
|
122
|
+
const sideEffectClass = opts.sideEffectClassByTool?.[namespacedName] ?? 'external-stateful';
|
|
123
|
+
// W-105: a downgrade below the sink classes is an explicit operator
|
|
124
|
+
// trust decision with wide consequences - the tool leaves every sink
|
|
125
|
+
// check (dataflow gate, Rule-of-Two writer forbid, read-only
|
|
126
|
+
// capability gate). One WARN per tool at adaptation time + a result
|
|
127
|
+
// field so audits can pick it up; the server's own readOnlyHint is
|
|
128
|
+
// deliberately never trusted for this.
|
|
129
|
+
if (sideEffectClass === 'read-only' || sideEffectClass === 'pure') {
|
|
130
|
+
downgradedTools.push(namespacedName);
|
|
131
|
+
args.logger?.(
|
|
132
|
+
'warn',
|
|
133
|
+
`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)`,
|
|
134
|
+
{
|
|
135
|
+
server: args.serverIdentity.id,
|
|
136
|
+
tool: namespacedName,
|
|
137
|
+
sideEffectClass,
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const preferredModel = opts.preferredModelByTool?.[namespacedName];
|
|
142
|
+
// W-018: strip imperative payloads from schema ANNOTATIONS
|
|
143
|
+
// (description / title / $comment / string examples at any depth -
|
|
144
|
+
// the Invariant Labs tool-poisoning hiding place) before the
|
|
145
|
+
// schema reaches the validator, whose `toJSON()` is what the
|
|
146
|
+
// provider wire and `tool_search` re-expose.
|
|
147
|
+
const sanitizedInput = sanitizeSchemaAnnotations({
|
|
148
|
+
schema: definition.inputSchema as JsonSchemaLike,
|
|
149
|
+
inboundSanitization: resolvedInbound,
|
|
150
|
+
toolName: namespacedName,
|
|
151
|
+
serverIdentity: args.serverIdentity,
|
|
152
|
+
});
|
|
153
|
+
const sanitizedOutput =
|
|
154
|
+
definition.outputSchema === undefined
|
|
155
|
+
? undefined
|
|
156
|
+
: sanitizeSchemaAnnotations({
|
|
157
|
+
schema: definition.outputSchema as JsonSchemaLike,
|
|
158
|
+
inboundSanitization: resolvedInbound,
|
|
159
|
+
toolName: namespacedName,
|
|
160
|
+
serverIdentity: args.serverIdentity,
|
|
161
|
+
});
|
|
162
|
+
const inputValidator = buildJsonSchemaValidator(sanitizedInput.schema);
|
|
163
|
+
const outputValidator =
|
|
164
|
+
sanitizedOutput === undefined ? undefined : buildJsonSchemaValidator(sanitizedOutput.schema);
|
|
165
|
+
// TOFU invariant (MC-6): the fingerprint hashes the RAW definition,
|
|
166
|
+
// never the sanitized copy. Hashing redacted bytes would invalidate
|
|
167
|
+
// the pins of existing deployments AND collapse two
|
|
168
|
+
// differently-poisoned schemas into one redacted hash, hiding drift.
|
|
169
|
+
// `sanitizeSchemaAnnotations` returns a clone and never mutates
|
|
170
|
+
// `definition`, so the order here is belt-and-braces.
|
|
171
|
+
const definitionHash = computeToolDefinitionHash(definition);
|
|
172
|
+
fingerprints.set(definition.name, definitionHash);
|
|
173
|
+
tools.push(
|
|
174
|
+
buildAdaptedTool({
|
|
175
|
+
client: args.client,
|
|
176
|
+
serverIdentity: args.serverIdentity,
|
|
177
|
+
definitionHash,
|
|
178
|
+
...(args.logger !== undefined ? { logger: args.logger } : {}),
|
|
179
|
+
mcpToolName: definition.name,
|
|
180
|
+
graphorinToolName: namespacedName,
|
|
181
|
+
description:
|
|
182
|
+
definition.description.length === 0 ? `${definition.name} (MCP)` : definition.description,
|
|
183
|
+
inputSchema: inputValidator,
|
|
184
|
+
outputSchema: outputValidator,
|
|
185
|
+
defer_loading: resolvedDeferLoading,
|
|
186
|
+
inboundSanitization: resolvedInbound,
|
|
187
|
+
sideEffectClass,
|
|
188
|
+
...(opts.maxResultTokens === undefined ? {} : { maxResultTokens: opts.maxResultTokens }),
|
|
189
|
+
...(opts.truncationStrategy === undefined
|
|
190
|
+
? {}
|
|
191
|
+
: { truncationStrategy: opts.truncationStrategy }),
|
|
192
|
+
...(opts.callTimeoutMs === undefined ? {} : { callTimeoutMs: opts.callTimeoutMs }),
|
|
193
|
+
...(preferredModel === undefined ? {} : { preferredModel }),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return Object.freeze({
|
|
199
|
+
tools: Object.freeze(tools),
|
|
200
|
+
fingerprints,
|
|
201
|
+
autoDeferralFired,
|
|
202
|
+
resolvedDeferLoading,
|
|
203
|
+
resolvedInboundSanitization: resolvedInbound,
|
|
204
|
+
toolCount: total,
|
|
205
|
+
deferralThreshold: threshold,
|
|
206
|
+
downgradedTools: Object.freeze(downgradedTools),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
interface BuildAdaptedToolArgs {
|
|
211
|
+
readonly client: MCPClient;
|
|
212
|
+
readonly serverIdentity: ServerIdentity;
|
|
213
|
+
readonly mcpToolName: string;
|
|
214
|
+
readonly graphorinToolName: string;
|
|
215
|
+
readonly description: string;
|
|
216
|
+
readonly inputSchema: import('@graphorin/core').ZodLikeSchema<unknown>;
|
|
217
|
+
readonly outputSchema?: import('@graphorin/core').ZodLikeSchema<unknown> | undefined;
|
|
218
|
+
readonly defer_loading: boolean;
|
|
219
|
+
readonly inboundSanitization: InboundSanitizationPolicy;
|
|
220
|
+
readonly sideEffectClass: import('@graphorin/core').SideEffectClass;
|
|
221
|
+
readonly maxResultTokens?: number;
|
|
222
|
+
readonly truncationStrategy?: import('@graphorin/core').TruncationStrategy;
|
|
223
|
+
/** Per-call timeout forwarded to `client.callTool` (MC-3/MC-5). */
|
|
224
|
+
readonly callTimeoutMs?: number;
|
|
225
|
+
/** MC-6: sha256 fingerprint of the producing MCP definition. */
|
|
226
|
+
readonly definitionHash: string;
|
|
227
|
+
readonly logger?: (
|
|
228
|
+
level: 'debug' | 'info' | 'warn' | 'error',
|
|
229
|
+
message: string,
|
|
230
|
+
fields?: Record<string, unknown>,
|
|
231
|
+
) => void;
|
|
232
|
+
readonly preferredModel?:
|
|
233
|
+
| import('@graphorin/core').ModelHint
|
|
234
|
+
| import('@graphorin/core').ModelSpec;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function buildAdaptedTool(args: BuildAdaptedToolArgs): Tool<unknown, unknown, unknown> {
|
|
238
|
+
const sanitizedDescription = sanitizeDescription({
|
|
239
|
+
description: args.description,
|
|
240
|
+
inboundSanitization: args.inboundSanitization,
|
|
241
|
+
toolName: args.graphorinToolName,
|
|
242
|
+
serverIdentity: args.serverIdentity,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const tool = {
|
|
246
|
+
name: args.graphorinToolName,
|
|
247
|
+
description: sanitizedDescription,
|
|
248
|
+
inputSchema: args.inputSchema,
|
|
249
|
+
...(args.outputSchema === undefined ? {} : { outputSchema: args.outputSchema }),
|
|
250
|
+
defer_loading: args.defer_loading,
|
|
251
|
+
inboundSanitization: args.inboundSanitization,
|
|
252
|
+
sideEffectClass: args.sideEffectClass,
|
|
253
|
+
sandboxPolicy: 'sandboxed' as const,
|
|
254
|
+
...(args.maxResultTokens === undefined ? {} : { maxResultTokens: args.maxResultTokens }),
|
|
255
|
+
...(args.truncationStrategy === undefined
|
|
256
|
+
? {}
|
|
257
|
+
: { truncationStrategy: args.truncationStrategy }),
|
|
258
|
+
...(args.preferredModel === undefined ? {} : { preferredModel: args.preferredModel }),
|
|
259
|
+
async execute(
|
|
260
|
+
input: unknown,
|
|
261
|
+
ctx?: import('@graphorin/core').ToolExecutionContext<unknown>,
|
|
262
|
+
): Promise<ToolReturn<unknown>> {
|
|
263
|
+
// MC-5: the agent's per-call AbortSignal reaches the wire - an
|
|
264
|
+
// aborted run sends `notifications/cancelled` instead of orphaning
|
|
265
|
+
// the JSON-RPC request on the server. MC-3: the per-server call
|
|
266
|
+
// timeout rides along.
|
|
267
|
+
const result = await args.client.callTool(args.mcpToolName, input, {
|
|
268
|
+
...(ctx?.signal !== undefined ? { signal: ctx.signal } : {}),
|
|
269
|
+
...(args.callTimeoutMs !== undefined ? { timeoutMs: args.callTimeoutMs } : {}),
|
|
270
|
+
});
|
|
271
|
+
return adaptCallResult({
|
|
272
|
+
result,
|
|
273
|
+
outputSchema: args.outputSchema,
|
|
274
|
+
serverIdentity: args.serverIdentity,
|
|
275
|
+
toolName: args.graphorinToolName,
|
|
276
|
+
inboundSanitization: args.inboundSanitization,
|
|
277
|
+
...(args.logger !== undefined ? { logger: args.logger } : {}),
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
} satisfies Tool<unknown, unknown, unknown>;
|
|
281
|
+
// MC-7: pre-stamp the provenance so the agent's inferToolSource -
|
|
282
|
+
// which honours an existing stamp - classifies tools passed via
|
|
283
|
+
// `config.tools` as 'mcp-derived' (untrusted for the WI-12 dataflow
|
|
284
|
+
// policy) instead of first-party. Zero operator boilerplate.
|
|
285
|
+
return Object.assign(tool, {
|
|
286
|
+
__source: { kind: 'mcp', serverIdentity: args.serverIdentity.id } as const,
|
|
287
|
+
// MC-6: operators persist this fingerprint to pin the approved
|
|
288
|
+
// definition (`toTools({ pinnedFingerprints })`).
|
|
289
|
+
__definitionHash: args.definitionHash,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Unused export kept for ergonomic test access. */
|
|
294
|
+
export type AdaptedToolChunkBuffer = ReadonlyArray<ContentChunk>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory that materialises a `@modelcontextprotocol/sdk` `Transport`
|
|
3
|
+
* instance from a {@link MCPTransportConfig}.
|
|
4
|
+
*
|
|
5
|
+
* The factory is small on purpose: every MCP-spec evolution (new
|
|
6
|
+
* transport, transport-specific option, transport-level header
|
|
7
|
+
* convention) lands here so the higher-level {@link createMCPClient}
|
|
8
|
+
* stays untouched.
|
|
9
|
+
*
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
|
|
14
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
15
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
16
|
+
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
17
|
+
|
|
18
|
+
import type { MCPTransportConfig } from '../transport/types.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Outcome of the transport build, including the SDK transport
|
|
22
|
+
* instance and the helper hooks the client needs to forward to the
|
|
23
|
+
* SDK.
|
|
24
|
+
*/
|
|
25
|
+
export interface BuiltTransport {
|
|
26
|
+
readonly transport: Transport;
|
|
27
|
+
readonly disposeBeforeStart?: () => Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Live source of the outbound `Authorization` header for the HTTP
|
|
32
|
+
* transports. The bridge's `OAuthAuthorizationProvider` satisfies this
|
|
33
|
+
* structurally (its `resolveHeader()` already returns the full
|
|
34
|
+
* `Bearer <token>` value); a static `bearerToken` is wrapped into a
|
|
35
|
+
* constant resolver by {@link createMCPClient}.
|
|
36
|
+
*
|
|
37
|
+
* @internal
|
|
38
|
+
*/
|
|
39
|
+
export interface TransportAuthSource {
|
|
40
|
+
/** Resolve the full `Authorization` header value (e.g. `Bearer abc`). */
|
|
41
|
+
resolveHeader(): Promise<string> | string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Wrap a `fetch` implementation so the live `Authorization` header is
|
|
46
|
+
* re-resolved and injected on **every** outgoing request. The wrapper
|
|
47
|
+
* always overwrites a caller-supplied `Authorization` header so the
|
|
48
|
+
* provider stays the single source of truth (refresh-ahead from
|
|
49
|
+
* {@link createOAuthAuthorizationProvider} fires per request).
|
|
50
|
+
*/
|
|
51
|
+
function authWrappedFetch(baseFetch: typeof fetch, auth: TransportAuthSource): typeof fetch {
|
|
52
|
+
return (async (input: Parameters<typeof fetch>[0], init?: RequestInit): Promise<Response> => {
|
|
53
|
+
const header = await auth.resolveHeader();
|
|
54
|
+
const headers = new Headers(init?.headers);
|
|
55
|
+
headers.set('Authorization', header);
|
|
56
|
+
return baseFetch(input, { ...init, headers });
|
|
57
|
+
}) as typeof fetch;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Build a SDK `Transport` from a {@link MCPTransportConfig}.
|
|
62
|
+
*
|
|
63
|
+
* When an `auth` source is supplied (HTTP transports only) the factory
|
|
64
|
+
* installs a per-request fetch-wrapper that re-resolves the
|
|
65
|
+
* `Authorization` header on every outgoing call - this is the seam that
|
|
66
|
+
* makes a long-lived agent session survive OAuth token expiry without
|
|
67
|
+
* re-creating the client.
|
|
68
|
+
*
|
|
69
|
+
* @stable
|
|
70
|
+
*/
|
|
71
|
+
export function buildTransport(
|
|
72
|
+
config: MCPTransportConfig,
|
|
73
|
+
options?: { readonly auth?: TransportAuthSource },
|
|
74
|
+
): BuiltTransport {
|
|
75
|
+
switch (config.kind) {
|
|
76
|
+
case 'stdio': {
|
|
77
|
+
const stdio: Transport = new StdioClientTransport({
|
|
78
|
+
command: config.command,
|
|
79
|
+
...(config.args === undefined ? {} : { args: [...config.args] }),
|
|
80
|
+
...(config.env === undefined ? {} : { env: { ...config.env } }),
|
|
81
|
+
...(config.cwd === undefined ? {} : { cwd: config.cwd }),
|
|
82
|
+
...(config.stderr === undefined ? {} : { stderr: config.stderr }),
|
|
83
|
+
});
|
|
84
|
+
return Object.freeze({ transport: stdio });
|
|
85
|
+
}
|
|
86
|
+
case 'streamable-http': {
|
|
87
|
+
const url = typeof config.url === 'string' ? new URL(config.url) : config.url;
|
|
88
|
+
const headers = { ...(config.headers ?? {}) };
|
|
89
|
+
const baseFetch = config.fetch ?? fetch;
|
|
90
|
+
const fetchImpl =
|
|
91
|
+
options?.auth === undefined ? config.fetch : authWrappedFetch(baseFetch, options.auth);
|
|
92
|
+
// The SDK class's `sessionId: string | undefined` getter is
|
|
93
|
+
// structurally narrower than `Transport.sessionId?: string` in
|
|
94
|
+
// strict mode; route through `unknown` to keep the public
|
|
95
|
+
// surface clean.
|
|
96
|
+
const sdkTransport = new StreamableHTTPClientTransport(url, {
|
|
97
|
+
...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),
|
|
98
|
+
requestInit: { headers },
|
|
99
|
+
...(config.sessionId === undefined ? {} : { sessionId: config.sessionId }),
|
|
100
|
+
});
|
|
101
|
+
const transport = sdkTransport as unknown as Transport;
|
|
102
|
+
return Object.freeze({ transport });
|
|
103
|
+
}
|
|
104
|
+
case 'sse': {
|
|
105
|
+
const url = typeof config.url === 'string' ? new URL(config.url) : config.url;
|
|
106
|
+
const headers = { ...(config.headers ?? {}) };
|
|
107
|
+
const baseFetch = config.fetch ?? fetch;
|
|
108
|
+
const fetchImpl =
|
|
109
|
+
options?.auth === undefined ? config.fetch : authWrappedFetch(baseFetch, options.auth);
|
|
110
|
+
const transport: Transport = new SSEClientTransport(url, {
|
|
111
|
+
...(fetchImpl === undefined ? {} : { fetch: fetchImpl }),
|
|
112
|
+
requestInit: { headers },
|
|
113
|
+
});
|
|
114
|
+
return Object.freeze({ transport });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|