@akira-tl/forgerelay 1.0.1 → 1.1.1
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 +36 -0
- package/README.md +4 -0
- package/capabilities/host-integration/external-mcp/GUIDE.md +29 -0
- package/capabilities/lifecycle-hooks/GUIDE.md +13 -3
- package/dist/activity/runtime/mcp-query-tools.js +16 -10
- package/dist/cli/mcp/external-mcp.js +481 -0
- package/dist/cli/mcp/status.js +295 -0
- package/dist/cli.js +14 -1
- package/dist/lsp/test-support/server-fixture.js +1 -2
- package/dist/mcp/filesystem/filesystem-tools.js +7 -3
- package/dist/mcp/hooks/command-runner.js +12 -7
- package/dist/mcp/hooks/external-mcp-transform.js +224 -0
- package/dist/mcp/hooks/hook-cli.js +3 -0
- package/dist/mcp/hooks/hooks.js +49 -3
- package/dist/mcp/oauth/auth-protocol.js +375 -0
- package/dist/mcp/oauth/oauth-provider.js +11 -12
- package/dist/mcp/oauth/oauth-store.js +2 -2
- package/dist/mcp/oauth/router.js +2 -6
- package/dist/mcp/operations/batch/executor.js +1 -1
- package/dist/mcp/operations/bulk-read.js +7 -6
- package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +59 -0
- package/dist/mcp/operations/external-mcp/external-mcp.js +382 -0
- package/dist/mcp/operations/media-content.js +28 -0
- package/dist/mcp/operations/native-bulk-mutations.js +1 -1
- package/dist/mcp/process/tools.js +19 -15
- package/dist/mcp/request-context.js +18 -0
- package/dist/mcp/request-meta.js +28 -3
- package/dist/mcp/server/core/activity-support.js +2 -2
- package/dist/mcp/server/core/capabilities/external-mcp.js +31 -0
- package/dist/mcp/server/core/capabilities.js +7 -0
- package/dist/mcp/server/core/capability-registry.js +2 -0
- package/dist/mcp/server/core/tool-support.js +29 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +91 -48
- package/dist/mcp/server/operations/runtime/operation-runtime.js +18 -11
- package/dist/mcp/server/transport/http-server.js +42 -21
- package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +29 -25
- package/dist/runtime/config/config.js +4 -0
- package/dist/runtime/config/external-mcp-auth-store.js +225 -0
- package/dist/runtime/config/external-mcp-config.js +146 -0
- package/dist/runtime/config/external-mcp-registry.js +165 -0
- package/dist/runtime/testing/server-fixture.js +10 -5
- package/dist/server.js +19 -10
- package/dist/subagents/sessions/mcp/audit.js +81 -0
- package/dist/ui/.vite/manifest.json +297 -298
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
- package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
- package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
- package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
- package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
- package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
- package/dist/ui/assets/c-BquuA6bv.js +1 -0
- package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
- package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
- package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
- package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
- package/dist/ui/assets/css-Be_T8idh.js +1 -0
- package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
- package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
- package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
- package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
- package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
- package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
- package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
- package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
- package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
- package/dist/ui/assets/haml-h18lG0pS.js +1 -0
- package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
- package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
- package/dist/ui/assets/html-DtILqTE1.js +1 -0
- package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
- package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
- package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
- package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
- package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
- package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
- package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
- package/dist/ui/assets/json-DW2UTv1q.js +1 -0
- package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
- package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
- package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
- package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
- package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
- package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
- package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
- package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
- package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
- package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
- package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
- package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
- package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
- package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
- package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
- package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
- package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
- package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
- package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
- package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
- package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
- package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
- package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
- package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
- package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
- package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
- package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
- package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
- package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
- package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
- package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
- package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
- package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
- package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
- package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
- package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
- package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
- package/dist/ui/assets/xml-_44rx9_2.js +1 -0
- package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
- package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
- package/dist/workspaces/relay/auth/remote-auth.js +3 -5
- package/dist/workspaces/relay/result-support.js +19 -0
- package/dist/workspaces/relay/tests/test-support.js +10 -4
- package/dist/workspaces/relay/workspace-relay.js +20 -12
- package/docs/configuration.md +200 -2
- package/docs/debugging.md +8 -5
- package/docs/roadmap.md +64 -3
- package/docs/security.md +18 -0
- package/package.json +12 -4
- package/scripts/debug/accept/bootstrap.mjs +5 -0
- package/scripts/debug/accept/harness.mjs +10 -3
- package/scripts/debug/accept/media.mjs +335 -0
- package/scripts/debug/accept/modern-http.mjs +169 -0
- package/scripts/debug/accept/support.mjs +10 -6
- package/scripts/debug/accept.mjs +12 -1
- package/scripts/debug/relay-accept/support.mjs +40 -0
- package/scripts/debug/relay-accept.mjs +8 -9
- package/scripts/debug/runtime.mjs +4 -1
- package/scripts/debug/runtime.test.mjs +9 -0
- package/scripts/release/parity-sandbox.mjs +35 -0
- package/scripts/release/release-gate.test.mjs +42 -1
- package/scripts/release-parity.mjs +3 -22
- package/scripts/release-proof.mjs +24 -4
- package/scripts/release-proof.test.mjs +12 -4
- package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
- package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
- package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
- package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
- package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
- package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
- package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
- package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
- package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
- package/dist/ui/assets/html-dUOs5pA9.js +0 -1
- package/dist/ui/assets/java-BFZCrORY.js +0 -1
- package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
- package/dist/ui/assets/json-BQC_Yp48.js +0 -1
- package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
- package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
- package/dist/ui/assets/r-B5nYsy-M.js +0 -1
- package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
- package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
- package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
- package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
- package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
- package/dist/ui/assets/typescript-BGIABksB.js +0 -1
- package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
- package/dist/ui/assets/yaml-umbj2r1g.js +0 -1
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AuthorizationServerMismatchError, Client, InsufficientScopeError, SdkHttpError, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
|
|
3
|
+
import { StdioClientTransport, getDefaultEnvironment, } from "@modelcontextprotocol/client/stdio";
|
|
4
|
+
import { CallToolResultSchema } from "@modelcontextprotocol/core";
|
|
5
|
+
import { externalMcpCredentialIdentity, } from "../../../runtime/config/external-mcp-auth-store.js";
|
|
6
|
+
import { claimMediaBytes, createMediaBudget, isSupportedImageMimeType, strictBase64ByteLength, } from "../media-content.js";
|
|
7
|
+
import { ExternalMcpTransformError, } from "../../hooks/external-mcp-transform.js";
|
|
8
|
+
import { ExternalMcpOAuthError, createExternalMcpRuntimeAuth, externalMcpAuthError, markExternalMcpAuthorizationRequired, markExternalMcpReauthorization, } from "./external-mcp-oauth.js";
|
|
9
|
+
const MAX_DISCOVERED_TOOLS = 100;
|
|
10
|
+
const MAX_TOOL_DESCRIPTION_CHARS = 2_000;
|
|
11
|
+
const MAX_TOOL_SCHEMA_BYTES = 64 * 1024;
|
|
12
|
+
const MAX_TOOL_DISCOVERY_PAGES = 16;
|
|
13
|
+
const LEGACY_NEGOTIATION_TTL_MS = 5 * 60 * 1_000;
|
|
14
|
+
export class ExternalMcpError extends Error {
|
|
15
|
+
code;
|
|
16
|
+
detail;
|
|
17
|
+
constructor(code, message, detail) {
|
|
18
|
+
super(message);
|
|
19
|
+
this.code = code;
|
|
20
|
+
this.detail = detail;
|
|
21
|
+
this.name = "ExternalMcpError";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ExternalMcpGateway {
|
|
25
|
+
mediaMaxBytes;
|
|
26
|
+
credentialStore;
|
|
27
|
+
negotiationCache = new Map();
|
|
28
|
+
constructor(mediaMaxBytes, credentialStore) {
|
|
29
|
+
this.mediaMaxBytes = mediaMaxBytes;
|
|
30
|
+
this.credentialStore = credentialStore;
|
|
31
|
+
}
|
|
32
|
+
async run(servers, input, signal, transforms, authContext) {
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
switch (input.operation) {
|
|
35
|
+
case "servers":
|
|
36
|
+
return {
|
|
37
|
+
value: {
|
|
38
|
+
operation: "servers",
|
|
39
|
+
servers: Object.entries(servers)
|
|
40
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
41
|
+
.map(([name, server]) => ({ name, transport: server.transport })),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
case "tools":
|
|
45
|
+
return this.withClient(servers, input.server, signal, authContext, async (client) => {
|
|
46
|
+
const discovery = await discoverTools(client, signal);
|
|
47
|
+
return {
|
|
48
|
+
value: {
|
|
49
|
+
operation: "tools",
|
|
50
|
+
server: input.server,
|
|
51
|
+
tools: discovery.tools.map(summarizeTool),
|
|
52
|
+
...(discovery.truncated ? { truncated: true } : {}),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
case "call":
|
|
57
|
+
return this.withClient(servers, input.server, signal, authContext, async (client) => {
|
|
58
|
+
await assertRegisteredTool(client, input.server, input.tool, signal);
|
|
59
|
+
const appliedTransforms = [];
|
|
60
|
+
let callArguments = input.arguments ?? {};
|
|
61
|
+
if (transforms?.request) {
|
|
62
|
+
const transformed = await transforms.request(input.server, input.tool, callArguments);
|
|
63
|
+
callArguments = transformed.value;
|
|
64
|
+
appliedTransforms.push(...transformed.transforms);
|
|
65
|
+
}
|
|
66
|
+
let result = await client.callTool({ name: input.tool, arguments: callArguments }, { signal });
|
|
67
|
+
assertCallToolResult(input.server, input.tool, result);
|
|
68
|
+
if (result.isError) {
|
|
69
|
+
throw new ExternalMcpError("tool_failed", `External MCP ${input.server} tool ${input.tool} returned an upstream tool error.`);
|
|
70
|
+
}
|
|
71
|
+
if (transforms?.result) {
|
|
72
|
+
const transformed = await transforms.result(input.server, input.tool, result);
|
|
73
|
+
result = transformed.value;
|
|
74
|
+
appliedTransforms.push(...transformed.transforms);
|
|
75
|
+
assertCallToolResult(input.server, input.tool, result);
|
|
76
|
+
if (result.isError) {
|
|
77
|
+
throw new ExternalMcpError("tool_failed", `External MCP ${input.server} tool ${input.tool} returned an error after result transformation.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const projection = projectExternalMcpContent(input.server, input.tool, result, this.mediaMaxBytes);
|
|
81
|
+
return {
|
|
82
|
+
content: result.content,
|
|
83
|
+
value: {
|
|
84
|
+
operation: "call",
|
|
85
|
+
server: input.server,
|
|
86
|
+
tool: input.tool,
|
|
87
|
+
content: projection.content,
|
|
88
|
+
...(!projection.hasMedia && isRecord(result.structuredContent)
|
|
89
|
+
? { structuredContent: result.structuredContent }
|
|
90
|
+
: {}),
|
|
91
|
+
...(appliedTransforms.length > 0 ? { transforms: appliedTransforms } : {}),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async probe(servers, name, signal, authContext) {
|
|
98
|
+
return this.withClient(servers, name, signal, authContext, async (client) => {
|
|
99
|
+
let discovery;
|
|
100
|
+
try {
|
|
101
|
+
discovery = await discoverTools(client, signal);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (isExternalMcpAuthTransportError(error))
|
|
105
|
+
throw error;
|
|
106
|
+
throw new ExternalMcpError("tool_discovery_failed", `External MCP ${name} tool discovery failed.`, externalMcpFailureDetail(error));
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
protocolEra: client.getProtocolEra() ?? "unknown",
|
|
110
|
+
...(client.getNegotiatedProtocolVersion()
|
|
111
|
+
? { protocolVersion: client.getNegotiatedProtocolVersion() }
|
|
112
|
+
: {}),
|
|
113
|
+
toolCount: discovery.tools.length,
|
|
114
|
+
truncated: discovery.truncated,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async withClient(servers, name, signal, authContext, operation) {
|
|
119
|
+
const config = servers[name];
|
|
120
|
+
if (!config)
|
|
121
|
+
throw new ExternalMcpError("unknown_server", `Unknown configured external MCP server: ${name}.`);
|
|
122
|
+
const fingerprint = externalMcpServerFingerprint(config);
|
|
123
|
+
const runtimeAuth = this.resolveRuntimeAuth(name, config, authContext);
|
|
124
|
+
const client = new Client({ name: "forgerelay-external-mcp", version: "1.0.0" }, { versionNegotiation: { mode: "auto" } });
|
|
125
|
+
const transport = createTransport(config, runtimeAuth?.authProvider);
|
|
126
|
+
const prior = this.cachedPrior(name, fingerprint);
|
|
127
|
+
try {
|
|
128
|
+
signal?.throwIfAborted();
|
|
129
|
+
await client.connect(transport, prior ? { prior } : undefined);
|
|
130
|
+
this.rememberNegotiation(name, fingerprint, client);
|
|
131
|
+
signal?.throwIfAborted();
|
|
132
|
+
return await operation(client);
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
if (error instanceof ExternalMcpError || error instanceof ExternalMcpTransformError)
|
|
136
|
+
throw error;
|
|
137
|
+
this.invalidateNegotiation(name, fingerprint);
|
|
138
|
+
if (signal?.aborted)
|
|
139
|
+
signal.throwIfAborted();
|
|
140
|
+
if (error instanceof ExternalMcpOAuthError) {
|
|
141
|
+
throw new ExternalMcpError(error.code, error.message);
|
|
142
|
+
}
|
|
143
|
+
const authError = externalMcpAuthError(name, error, runtimeAuth);
|
|
144
|
+
if (authError) {
|
|
145
|
+
if (this.credentialStore) {
|
|
146
|
+
if (authError.code === "auth_required") {
|
|
147
|
+
await markExternalMcpAuthorizationRequired(this.credentialStore, runtimeAuth).catch(() => undefined);
|
|
148
|
+
}
|
|
149
|
+
else if (error instanceof InsufficientScopeError) {
|
|
150
|
+
await markExternalMcpReauthorization(this.credentialStore, runtimeAuth, "insufficient_scope", { ...(error.requiredScope ? { scope: error.requiredScope } : {}) }).catch(() => undefined);
|
|
151
|
+
}
|
|
152
|
+
else if (runtimeAuth?.bindingMismatch || error instanceof AuthorizationServerMismatchError) {
|
|
153
|
+
await markExternalMcpReauthorization(this.credentialStore, runtimeAuth, "binding_changed").catch(() => undefined);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
throw new ExternalMcpError(authError.code, authError.message);
|
|
157
|
+
}
|
|
158
|
+
throw new ExternalMcpError("transport_failed", `External MCP ${name} request failed.`, externalMcpFailureDetail(error));
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
await client.close().catch(() => undefined);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
resolveRuntimeAuth(name, config, context) {
|
|
165
|
+
if (!this.credentialStore || !context || config.transport !== "streamable-http")
|
|
166
|
+
return undefined;
|
|
167
|
+
if (hasStaticAuthorizationHeader(config.headers))
|
|
168
|
+
return undefined;
|
|
169
|
+
const source = context.origins[name];
|
|
170
|
+
if (!source)
|
|
171
|
+
return undefined;
|
|
172
|
+
const identity = externalMcpCredentialIdentity(source, name, context.workspaceRoot);
|
|
173
|
+
return createExternalMcpRuntimeAuth(this.credentialStore, identity, config.url);
|
|
174
|
+
}
|
|
175
|
+
cachedPrior(name, fingerprint) {
|
|
176
|
+
const cached = this.negotiationCache.get(name);
|
|
177
|
+
if (!cached || cached.fingerprint !== fingerprint) {
|
|
178
|
+
if (cached)
|
|
179
|
+
this.negotiationCache.delete(name);
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
if (cached.prior.kind === "legacy" &&
|
|
183
|
+
Date.now() - cached.cachedAt >= LEGACY_NEGOTIATION_TTL_MS) {
|
|
184
|
+
this.negotiationCache.delete(name);
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
return cached.prior;
|
|
188
|
+
}
|
|
189
|
+
rememberNegotiation(name, fingerprint, client) {
|
|
190
|
+
const era = client.getProtocolEra();
|
|
191
|
+
if (era === "legacy") {
|
|
192
|
+
this.negotiationCache.set(name, {
|
|
193
|
+
fingerprint,
|
|
194
|
+
prior: { kind: "legacy" },
|
|
195
|
+
cachedAt: Date.now(),
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (era !== "modern")
|
|
200
|
+
return;
|
|
201
|
+
const discover = client.getDiscoverResult();
|
|
202
|
+
if (!discover)
|
|
203
|
+
return;
|
|
204
|
+
this.negotiationCache.set(name, {
|
|
205
|
+
fingerprint,
|
|
206
|
+
prior: { kind: "modern", discover },
|
|
207
|
+
cachedAt: Date.now(),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
invalidateNegotiation(name, fingerprint) {
|
|
211
|
+
const cached = this.negotiationCache.get(name);
|
|
212
|
+
if (cached?.fingerprint === fingerprint)
|
|
213
|
+
this.negotiationCache.delete(name);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function externalMcpServerFingerprint(config) {
|
|
217
|
+
const normalized = config.transport === "stdio"
|
|
218
|
+
? {
|
|
219
|
+
transport: config.transport,
|
|
220
|
+
command: config.command,
|
|
221
|
+
args: config.args ?? [],
|
|
222
|
+
cwd: config.cwd ?? null,
|
|
223
|
+
env: sortedStringRecord(config.env),
|
|
224
|
+
}
|
|
225
|
+
: {
|
|
226
|
+
transport: config.transport,
|
|
227
|
+
url: config.url,
|
|
228
|
+
headers: sortedStringRecord(config.headers),
|
|
229
|
+
oauth: config.oauth ?? null,
|
|
230
|
+
};
|
|
231
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
232
|
+
}
|
|
233
|
+
function sortedStringRecord(value) {
|
|
234
|
+
return Object.fromEntries(Object.entries(value ?? {}).sort(([left], [right]) => left.localeCompare(right)));
|
|
235
|
+
}
|
|
236
|
+
function createTransport(config, authProvider) {
|
|
237
|
+
if (config.transport === "stdio") {
|
|
238
|
+
return new StdioClientTransport({
|
|
239
|
+
command: config.command,
|
|
240
|
+
...(config.args ? { args: config.args } : {}),
|
|
241
|
+
...(config.cwd ? { cwd: config.cwd } : {}),
|
|
242
|
+
...(config.env
|
|
243
|
+
? { env: { ...getDefaultEnvironment(), ...config.env } }
|
|
244
|
+
: {}),
|
|
245
|
+
stderr: "ignore",
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
249
|
+
...(config.headers ? { requestInit: { headers: config.headers } } : {}),
|
|
250
|
+
...(authProvider ? { authProvider } : {}),
|
|
251
|
+
onInsufficientScope: "throw",
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
function hasStaticAuthorizationHeader(headers) {
|
|
255
|
+
return Object.keys(headers ?? {}).some((name) => name.toLowerCase() === "authorization");
|
|
256
|
+
}
|
|
257
|
+
function isExternalMcpAuthTransportError(error) {
|
|
258
|
+
return error instanceof ExternalMcpOAuthError
|
|
259
|
+
|| error instanceof InsufficientScopeError
|
|
260
|
+
|| error instanceof AuthorizationServerMismatchError
|
|
261
|
+
|| error instanceof UnauthorizedError
|
|
262
|
+
|| (error instanceof SdkHttpError && error.status === 401);
|
|
263
|
+
}
|
|
264
|
+
function externalMcpFailureDetail(error) {
|
|
265
|
+
if (error instanceof SdkHttpError)
|
|
266
|
+
return `HTTP ${error.status}`;
|
|
267
|
+
const code = nestedErrorCode(error);
|
|
268
|
+
if (code && code !== "ERA_NEGOTIATION_FAILED")
|
|
269
|
+
return code;
|
|
270
|
+
if (error instanceof Error && /tim(?:e|ed)\s*out|timeout/i.test(error.message))
|
|
271
|
+
return "timeout";
|
|
272
|
+
return undefined;
|
|
273
|
+
}
|
|
274
|
+
function nestedErrorCode(error, depth = 0) {
|
|
275
|
+
if (depth > 4 || typeof error !== "object" || error === null)
|
|
276
|
+
return undefined;
|
|
277
|
+
if ("cause" in error) {
|
|
278
|
+
const nested = nestedErrorCode(error.cause, depth + 1);
|
|
279
|
+
if (nested)
|
|
280
|
+
return nested;
|
|
281
|
+
}
|
|
282
|
+
if ("code" in error) {
|
|
283
|
+
const code = error.code;
|
|
284
|
+
if (typeof code === "string" && /^[A-Z][A-Z0-9_]{1,39}$/.test(code))
|
|
285
|
+
return code;
|
|
286
|
+
}
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
async function discoverTools(client, signal) {
|
|
290
|
+
const tools = [];
|
|
291
|
+
let cursor;
|
|
292
|
+
for (let pageIndex = 0; pageIndex < MAX_TOOL_DISCOVERY_PAGES; pageIndex += 1) {
|
|
293
|
+
signal?.throwIfAborted();
|
|
294
|
+
const page = await client.listTools(cursor ? { cursor } : undefined, { signal });
|
|
295
|
+
const remaining = MAX_DISCOVERED_TOOLS - tools.length;
|
|
296
|
+
tools.push(...page.tools.slice(0, remaining));
|
|
297
|
+
if (page.tools.length > remaining)
|
|
298
|
+
return { tools, truncated: true };
|
|
299
|
+
cursor = page.nextCursor;
|
|
300
|
+
if (!cursor)
|
|
301
|
+
return { tools, truncated: false };
|
|
302
|
+
if (tools.length >= MAX_DISCOVERED_TOOLS)
|
|
303
|
+
return { tools, truncated: true };
|
|
304
|
+
}
|
|
305
|
+
return { tools, truncated: cursor !== undefined };
|
|
306
|
+
}
|
|
307
|
+
async function assertRegisteredTool(client, server, tool, signal) {
|
|
308
|
+
let cursor;
|
|
309
|
+
for (let pageIndex = 0; pageIndex < MAX_TOOL_DISCOVERY_PAGES; pageIndex += 1) {
|
|
310
|
+
signal?.throwIfAborted();
|
|
311
|
+
const page = await client.listTools(cursor ? { cursor } : undefined, { signal });
|
|
312
|
+
if (page.tools.some((candidate) => candidate.name === tool))
|
|
313
|
+
return;
|
|
314
|
+
cursor = page.nextCursor;
|
|
315
|
+
if (!cursor)
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
throw new ExternalMcpError("unknown_tool", `External MCP ${server} does not advertise tool ${tool}.`);
|
|
319
|
+
}
|
|
320
|
+
function summarizeTool(tool) {
|
|
321
|
+
const serialized = safeJson(tool.inputSchema);
|
|
322
|
+
const schemaTruncated = Buffer.byteLength(serialized, "utf8") > MAX_TOOL_SCHEMA_BYTES;
|
|
323
|
+
return {
|
|
324
|
+
name: tool.name,
|
|
325
|
+
...(tool.description
|
|
326
|
+
? { description: tool.description.slice(0, MAX_TOOL_DESCRIPTION_CHARS) }
|
|
327
|
+
: {}),
|
|
328
|
+
inputSchema: schemaTruncated ? { type: "object" } : tool.inputSchema,
|
|
329
|
+
...(schemaTruncated ? { schemaTruncated: true } : {}),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function projectExternalMcpContent(server, tool, result, mediaMaxBytes) {
|
|
333
|
+
const budget = createMediaBudget(mediaMaxBytes);
|
|
334
|
+
const content = [];
|
|
335
|
+
let hasMedia = false;
|
|
336
|
+
for (const entry of result.content ?? []) {
|
|
337
|
+
if (entry.type === "image") {
|
|
338
|
+
hasMedia = true;
|
|
339
|
+
if (!isSupportedImageMimeType(entry.mimeType)) {
|
|
340
|
+
throw new ExternalMcpError("media_unsupported", `External MCP ${server} tool ${tool} returned unsupported image MIME type ${entry.mimeType}.`);
|
|
341
|
+
}
|
|
342
|
+
const bytes = strictBase64ByteLength(entry.data);
|
|
343
|
+
if (bytes === undefined) {
|
|
344
|
+
throw new ExternalMcpError("media_malformed", `External MCP ${server} tool ${tool} returned malformed base64 image content.`);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
claimMediaBytes(budget, bytes);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
throw new ExternalMcpError("media_too_large", `External MCP ${server} tool ${tool} returned media exceeding the configured ${mediaMaxBytes}-byte aggregate limit.`);
|
|
351
|
+
}
|
|
352
|
+
content.push({ type: "image", mimeType: entry.mimeType, bytes });
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
if (entry.type === "audio") {
|
|
356
|
+
throw new ExternalMcpError("media_unsupported", `External MCP ${server} tool ${tool} returned unsupported audio media content.`);
|
|
357
|
+
}
|
|
358
|
+
if (entry.type === "resource" && "blob" in entry.resource && typeof entry.resource.blob === "string") {
|
|
359
|
+
throw new ExternalMcpError("media_unsupported", `External MCP ${server} tool ${tool} returned unsupported binary resource content.`);
|
|
360
|
+
}
|
|
361
|
+
content.push(entry);
|
|
362
|
+
}
|
|
363
|
+
return { content, hasMedia };
|
|
364
|
+
}
|
|
365
|
+
function assertCallToolResult(server, tool, value) {
|
|
366
|
+
if (!isRecord(value)
|
|
367
|
+
|| !Array.isArray(value.content)
|
|
368
|
+
|| !CallToolResultSchema.safeParse(value).success) {
|
|
369
|
+
throw new ExternalMcpError("unsupported_result", `External MCP ${server} tool ${tool} returned an unsupported result shape.`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function safeJson(value) {
|
|
373
|
+
try {
|
|
374
|
+
return JSON.stringify(value) ?? "";
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return "";
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
function isRecord(value) {
|
|
381
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
382
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const DEFAULT_MEDIA_MAX_BYTES = 20 * 1024 * 1024;
|
|
2
|
+
export const SUPPORTED_IMAGE_MIME_TYPES = [
|
|
3
|
+
"image/png",
|
|
4
|
+
"image/jpeg",
|
|
5
|
+
"image/gif",
|
|
6
|
+
"image/webp",
|
|
7
|
+
];
|
|
8
|
+
export function createMediaBudget(maxBytes) {
|
|
9
|
+
return { maxBytes, remainingBytes: maxBytes };
|
|
10
|
+
}
|
|
11
|
+
export function isSupportedImageMimeType(value) {
|
|
12
|
+
return SUPPORTED_IMAGE_MIME_TYPES.includes(value);
|
|
13
|
+
}
|
|
14
|
+
export function strictBase64ByteLength(value) {
|
|
15
|
+
if (value.length === 0 || value.length % 4 !== 0)
|
|
16
|
+
return undefined;
|
|
17
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return Buffer.byteLength(value, "base64");
|
|
21
|
+
}
|
|
22
|
+
export function claimMediaBytes(budget, bytes) {
|
|
23
|
+
if (bytes > budget.remainingBytes) {
|
|
24
|
+
throw new Error(`Media content exceeds the configured per-result limit of ${budget.maxBytes} bytes ` +
|
|
25
|
+
`(${bytes} bytes requested, ${budget.remainingBytes} bytes remaining).`);
|
|
26
|
+
}
|
|
27
|
+
budget.remainingBytes -= bytes;
|
|
28
|
+
}
|
|
@@ -60,7 +60,7 @@ export class NativeBulkMutationExecutor {
|
|
|
60
60
|
return this.dependencies.lifecycle.run({
|
|
61
61
|
tool,
|
|
62
62
|
workspace: workspaceSnapshot(workspace),
|
|
63
|
-
conversationScopeId: openAiConversationScopeId(context.requestMeta),
|
|
63
|
+
conversationScopeId: context.conversationScopeId ?? openAiConversationScopeId(context.requestMeta),
|
|
64
64
|
request,
|
|
65
65
|
operation,
|
|
66
66
|
outcome,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
|
+
import { mcpHandlerRequestContext } from "../request-context.js";
|
|
3
4
|
import { runToolWithHooks } from "../hooks/hooks.js";
|
|
4
5
|
import { commandPreview, logEvent, workspaceLogLabel } from "../../runtime/logging/logger.js";
|
|
5
6
|
import { resolveProcessId } from "./process-sessions.js";
|
|
@@ -50,9 +51,10 @@ function registerBashTool(options) {
|
|
|
50
51
|
_meta: {},
|
|
51
52
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
52
53
|
}, async ({ workspaceId, member, action = "run", command, processId, outputId, input, interrupt, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens, }, extra) => {
|
|
54
|
+
const requestContext = mcpHandlerRequestContext(extra);
|
|
53
55
|
const target = routing.resolve(workspaceId, member);
|
|
54
56
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
55
|
-
const executionContext = await routing.prepare(target,
|
|
57
|
+
const executionContext = await routing.prepare(target, requestContext);
|
|
56
58
|
if (routing.isRemote(executionWorkspaceId)) {
|
|
57
59
|
const response = await routing.bashRemote(executionWorkspaceId, {
|
|
58
60
|
action,
|
|
@@ -68,9 +70,9 @@ function registerBashTool(options) {
|
|
|
68
70
|
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
69
71
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
70
72
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
71
|
-
}, routing.hostScopeIdFor(
|
|
73
|
+
}, routing.hostScopeIdFor(requestContext));
|
|
72
74
|
return action === "run"
|
|
73
|
-
? routing.presentSemantic(response, target, routing.hostScopeIdFor(
|
|
75
|
+
? routing.presentSemantic(response, target, routing.hostScopeIdFor(requestContext))
|
|
74
76
|
: routing.present(response, target);
|
|
75
77
|
}
|
|
76
78
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
@@ -91,7 +93,7 @@ function registerBashTool(options) {
|
|
|
91
93
|
yieldTimeMs,
|
|
92
94
|
timeoutMs,
|
|
93
95
|
maxOutputTokens,
|
|
94
|
-
}, executionContext), target, routing.hostScopeIdFor(
|
|
96
|
+
}, executionContext), target, routing.hostScopeIdFor(requestContext));
|
|
95
97
|
}
|
|
96
98
|
if (action === "output") {
|
|
97
99
|
if (!outputId)
|
|
@@ -102,7 +104,7 @@ function registerBashTool(options) {
|
|
|
102
104
|
throw new Error("bash action=output accepts only workspaceId and outputId.");
|
|
103
105
|
}
|
|
104
106
|
return runToolWithHooks(hooks, {
|
|
105
|
-
signal:
|
|
107
|
+
signal: requestContext.signal,
|
|
106
108
|
tool: toolNames.shell,
|
|
107
109
|
invocation: workspaceHookInvocation(workspace),
|
|
108
110
|
payload: { action, outputId },
|
|
@@ -120,7 +122,7 @@ function registerBashTool(options) {
|
|
|
120
122
|
throw new Error("bash action=process cannot combine interrupt with input.");
|
|
121
123
|
}
|
|
122
124
|
return runToolWithHooks(hooks, {
|
|
123
|
-
signal:
|
|
125
|
+
signal: requestContext.signal,
|
|
124
126
|
tool: toolNames.shell,
|
|
125
127
|
invocation: workspaceHookInvocation(workspace),
|
|
126
128
|
payload: {
|
|
@@ -142,7 +144,7 @@ function registerBashTool(options) {
|
|
|
142
144
|
rows,
|
|
143
145
|
yieldTimeMs,
|
|
144
146
|
maxOutputTokens,
|
|
145
|
-
signal:
|
|
147
|
+
signal: requestContext.signal,
|
|
146
148
|
});
|
|
147
149
|
logProcessToolCall(config, workspace, {
|
|
148
150
|
tool: toolNames.shell,
|
|
@@ -190,8 +192,9 @@ function registerCodexProcessTools(options) {
|
|
|
190
192
|
_meta: {},
|
|
191
193
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
192
194
|
}, async ({ workspaceId, member, cmd, tty, columns, rows, workingDirectory, yieldTimeMs, timeoutMs, maxOutputTokens }, extra) => {
|
|
195
|
+
const requestContext = mcpHandlerRequestContext(extra);
|
|
193
196
|
const target = routing.resolve(workspaceId, member);
|
|
194
|
-
const context = await routing.prepare(target,
|
|
197
|
+
const context = await routing.prepare(target, requestContext);
|
|
195
198
|
if (routing.isRemote(target.executionWorkspaceId)) {
|
|
196
199
|
return routing.presentSemantic(await routing.execCommandRemote(target.executionWorkspaceId, {
|
|
197
200
|
cmd,
|
|
@@ -202,7 +205,7 @@ function registerCodexProcessTools(options) {
|
|
|
202
205
|
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
203
206
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
204
207
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
205
|
-
}, routing.hostScopeIdFor(
|
|
208
|
+
}, routing.hostScopeIdFor(requestContext)), target, routing.hostScopeIdFor(requestContext));
|
|
206
209
|
}
|
|
207
210
|
return routing.presentSemantic(await shellRun({
|
|
208
211
|
workspaceId: target.executionWorkspaceId,
|
|
@@ -215,7 +218,7 @@ function registerCodexProcessTools(options) {
|
|
|
215
218
|
yieldTimeMs,
|
|
216
219
|
timeoutMs,
|
|
217
220
|
maxOutputTokens,
|
|
218
|
-
}, context), target, routing.hostScopeIdFor(
|
|
221
|
+
}, context), target, routing.hostScopeIdFor(requestContext));
|
|
219
222
|
});
|
|
220
223
|
registerAppTool(server, "write_stdin", {
|
|
221
224
|
title: "Write to process",
|
|
@@ -236,8 +239,9 @@ function registerCodexProcessTools(options) {
|
|
|
236
239
|
_meta: {},
|
|
237
240
|
annotations: SHELL_TOOL_ANNOTATIONS,
|
|
238
241
|
}, async ({ workspaceId, member, processId, sessionId, outputId, chars, columns, rows, yieldTimeMs, maxOutputTokens }, extra) => {
|
|
242
|
+
const requestContext = mcpHandlerRequestContext(extra);
|
|
239
243
|
const target = routing.resolve(workspaceId, member);
|
|
240
|
-
await routing.prepare(target,
|
|
244
|
+
await routing.prepare(target, requestContext);
|
|
241
245
|
if (routing.isRemote(target.executionWorkspaceId)) {
|
|
242
246
|
return routing.present(await routing.writeStdinRemote(target.executionWorkspaceId, {
|
|
243
247
|
...(processId !== undefined ? { processId } : {}),
|
|
@@ -248,7 +252,7 @@ function registerCodexProcessTools(options) {
|
|
|
248
252
|
...(rows !== undefined ? { rows } : {}),
|
|
249
253
|
...(yieldTimeMs !== undefined ? { yieldTimeMs } : {}),
|
|
250
254
|
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
251
|
-
}, routing.hostScopeIdFor(
|
|
255
|
+
}, routing.hostScopeIdFor(requestContext)), target);
|
|
252
256
|
}
|
|
253
257
|
const executionWorkspaceId = target.executionWorkspaceId;
|
|
254
258
|
const workspace = workspaces.getWorkspace(executionWorkspaceId);
|
|
@@ -258,7 +262,7 @@ function registerCodexProcessTools(options) {
|
|
|
258
262
|
throw new Error("write_stdin outputId lookup cannot be combined with process control fields.");
|
|
259
263
|
}
|
|
260
264
|
return runToolWithHooks(hooks, {
|
|
261
|
-
signal:
|
|
265
|
+
signal: requestContext.signal,
|
|
262
266
|
tool: "write_stdin",
|
|
263
267
|
invocation: workspaceHookInvocation(workspace),
|
|
264
268
|
payload: { outputId },
|
|
@@ -267,7 +271,7 @@ function registerCodexProcessTools(options) {
|
|
|
267
271
|
}
|
|
268
272
|
const resolvedProcessId = resolveProcessId(processId, sessionId);
|
|
269
273
|
return runToolWithHooks(hooks, {
|
|
270
|
-
signal:
|
|
274
|
+
signal: requestContext.signal,
|
|
271
275
|
tool: "write_stdin",
|
|
272
276
|
invocation: workspaceHookInvocation(workspace),
|
|
273
277
|
payload: {
|
|
@@ -286,7 +290,7 @@ function registerCodexProcessTools(options) {
|
|
|
286
290
|
rows,
|
|
287
291
|
yieldTimeMs,
|
|
288
292
|
maxOutputTokens,
|
|
289
|
-
signal:
|
|
293
|
+
signal: requestContext.signal,
|
|
290
294
|
});
|
|
291
295
|
logProcessToolCall(config, workspace, {
|
|
292
296
|
tool: "write_stdin",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function mcpHandlerRequestContext(extra) {
|
|
2
|
+
if ("mcpReq" in extra) {
|
|
3
|
+
return {
|
|
4
|
+
requestMeta: extra.mcpReq._meta,
|
|
5
|
+
signal: extra.mcpReq.signal,
|
|
6
|
+
transportSessionId: extra.sessionId,
|
|
7
|
+
requestId: extra.mcpReq.id,
|
|
8
|
+
protocolEra: extra.mcpReq.envelope === undefined ? "legacy" : "modern",
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
requestMeta: extra._meta,
|
|
13
|
+
signal: extra.signal,
|
|
14
|
+
transportSessionId: extra.sessionId,
|
|
15
|
+
requestId: extra.requestId,
|
|
16
|
+
protocolEra: "legacy",
|
|
17
|
+
};
|
|
18
|
+
}
|
package/dist/mcp/request-meta.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export const FORGERELAY_CONVERSATION_META_KEY = "dev.forgerelay/conversation";
|
|
1
2
|
function metadataString(meta, key) {
|
|
2
3
|
if (typeof meta !== "object" || meta === null)
|
|
3
4
|
return undefined;
|
|
@@ -7,7 +8,31 @@ function metadataString(meta, key) {
|
|
|
7
8
|
export function openAiConversationScopeId(meta) {
|
|
8
9
|
return metadataString(meta, "openai/session");
|
|
9
10
|
}
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
export function forgeRelayConversationScopeId(meta) {
|
|
12
|
+
const value = metadataString(meta, FORGERELAY_CONVERSATION_META_KEY);
|
|
13
|
+
return value ? `forgerelay-conversation:${value}` : undefined;
|
|
14
|
+
}
|
|
15
|
+
function stableHostConversationScopeId(context) {
|
|
16
|
+
return openAiConversationScopeId(context.requestMeta)
|
|
17
|
+
?? forgeRelayConversationScopeId(context.requestMeta);
|
|
18
|
+
}
|
|
19
|
+
function modernRequestScopeId(context, connectionScopeId) {
|
|
20
|
+
return `mcp-request:${connectionScopeId}:${String(context.requestId ?? "unknown")}`;
|
|
21
|
+
}
|
|
22
|
+
export function hostConversationScopeId(context, connectionScopeId) {
|
|
23
|
+
const stableScope = stableHostConversationScopeId(context);
|
|
24
|
+
if (stableScope)
|
|
25
|
+
return stableScope;
|
|
26
|
+
if (context.protocolEra === "modern")
|
|
27
|
+
return modernRequestScopeId(context, connectionScopeId);
|
|
28
|
+
const transportSessionId = context.transportSessionId ?? context.sessionId;
|
|
29
|
+
return transportSessionId ? `mcp-session:${transportSessionId}` : connectionScopeId;
|
|
30
|
+
}
|
|
31
|
+
export function hostContextDeliveryScopeId(context, connectionScopeId) {
|
|
32
|
+
const stableScope = stableHostConversationScopeId(context);
|
|
33
|
+
if (stableScope)
|
|
34
|
+
return stableScope;
|
|
35
|
+
return context.protocolEra === "modern"
|
|
36
|
+
? modernRequestScopeId(context, connectionScopeId)
|
|
37
|
+
: undefined;
|
|
13
38
|
}
|
|
@@ -65,6 +65,6 @@ export function runActivityTool(lifecycle, workspace, conversationScopeId, tool,
|
|
|
65
65
|
...relation,
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
-
export function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}) {
|
|
69
|
-
return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation);
|
|
68
|
+
export function runActivityToolWithHooks(lifecycle, hooks, workspace, conversationScopeId, request, hookOptions, relation = {}, auditResult) {
|
|
69
|
+
return runActivityTool(lifecycle, workspace, conversationScopeId, hookOptions.tool, request, () => runToolWithHooks(hooks, hookOptions), standardActivityOutcome, relation, auditResult);
|
|
70
70
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const externalMcpInputSchema = z.discriminatedUnion("operation", [
|
|
3
|
+
z.object({ operation: z.literal("servers") }).strict(),
|
|
4
|
+
z.object({
|
|
5
|
+
operation: z.literal("tools"),
|
|
6
|
+
server: z.string().min(1),
|
|
7
|
+
}).strict(),
|
|
8
|
+
z.object({
|
|
9
|
+
operation: z.literal("call"),
|
|
10
|
+
server: z.string().min(1),
|
|
11
|
+
tool: z.string().min(1),
|
|
12
|
+
arguments: z.record(z.string(), z.unknown()).optional(),
|
|
13
|
+
}).strict(),
|
|
14
|
+
]);
|
|
15
|
+
export function externalMcpCapabilityDefinitions(dependency) {
|
|
16
|
+
if (!dependency)
|
|
17
|
+
return [];
|
|
18
|
+
return [{
|
|
19
|
+
name: "mcp.external",
|
|
20
|
+
description: "Discover and call tools from user-configured external MCP servers through the ForgeRelay Capability gateway.",
|
|
21
|
+
guideName: "external-mcp",
|
|
22
|
+
readGuideBeforeFirstUse: true,
|
|
23
|
+
batchPolicy: "unsupported",
|
|
24
|
+
inputSchema: externalMcpInputSchema,
|
|
25
|
+
availability: () => ({
|
|
26
|
+
available: dependency.available,
|
|
27
|
+
reason: dependency.unavailableReason,
|
|
28
|
+
}),
|
|
29
|
+
run: async (input, context, options) => dependency.run(input, context, options),
|
|
30
|
+
}];
|
|
31
|
+
}
|