@akira-tl/forgerelay 1.1.0 → 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 +19 -0
- package/README.md +4 -2
- 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/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/external-mcp/external-mcp-oauth.js +387 -0
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +23 -6
- package/dist/mcp/operations/external-mcp/external-mcp.js +178 -23
- 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/capabilities.js +1 -4
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +35 -28
- package/dist/mcp/server/operations/runtime/operation-runtime.js +7 -7
- package/dist/mcp/server/transport/http-server.js +41 -20
- 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/external-mcp-auth-store.js +225 -0
- package/dist/runtime/config/external-mcp-config.js +76 -22
- package/dist/runtime/config/external-mcp-registry.js +165 -0
- package/dist/runtime/testing/server-fixture.js +4 -4
- package/dist/server.js +15 -9
- 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/tests/test-support.js +7 -4
- package/dist/workspaces/relay/workspace-relay.js +14 -8
- package/docs/configuration.md +154 -4
- package/docs/debugging.md +8 -5
- package/docs/roadmap.md +64 -3
- package/docs/security.md +18 -0
- package/package.json +11 -4
- package/scripts/debug/accept/bootstrap.mjs +2 -0
- package/scripts/debug/accept/harness.mjs +9 -2
- package/scripts/debug/accept/modern-http.mjs +169 -0
- package/scripts/debug/accept/support.mjs +10 -6
- package/scripts/debug/accept.mjs +1 -0
- 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
|
@@ -1,45 +1,48 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { CallToolResultSchema } from "@modelcontextprotocol/
|
|
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";
|
|
5
6
|
import { claimMediaBytes, createMediaBudget, isSupportedImageMimeType, strictBase64ByteLength, } from "../media-content.js";
|
|
6
7
|
import { ExternalMcpTransformError, } from "../../hooks/external-mcp-transform.js";
|
|
8
|
+
import { ExternalMcpOAuthError, createExternalMcpRuntimeAuth, externalMcpAuthError, markExternalMcpAuthorizationRequired, markExternalMcpReauthorization, } from "./external-mcp-oauth.js";
|
|
7
9
|
const MAX_DISCOVERED_TOOLS = 100;
|
|
8
10
|
const MAX_TOOL_DESCRIPTION_CHARS = 2_000;
|
|
9
11
|
const MAX_TOOL_SCHEMA_BYTES = 64 * 1024;
|
|
10
12
|
const MAX_TOOL_DISCOVERY_PAGES = 16;
|
|
13
|
+
const LEGACY_NEGOTIATION_TTL_MS = 5 * 60 * 1_000;
|
|
11
14
|
export class ExternalMcpError extends Error {
|
|
12
15
|
code;
|
|
13
|
-
|
|
16
|
+
detail;
|
|
17
|
+
constructor(code, message, detail) {
|
|
14
18
|
super(message);
|
|
15
19
|
this.code = code;
|
|
20
|
+
this.detail = detail;
|
|
16
21
|
this.name = "ExternalMcpError";
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
export class ExternalMcpGateway {
|
|
20
|
-
servers;
|
|
21
25
|
mediaMaxBytes;
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
credentialStore;
|
|
27
|
+
negotiationCache = new Map();
|
|
28
|
+
constructor(mediaMaxBytes, credentialStore) {
|
|
24
29
|
this.mediaMaxBytes = mediaMaxBytes;
|
|
30
|
+
this.credentialStore = credentialStore;
|
|
25
31
|
}
|
|
26
|
-
|
|
27
|
-
return Object.keys(this.servers).length > 0;
|
|
28
|
-
}
|
|
29
|
-
async run(input, signal, transforms) {
|
|
32
|
+
async run(servers, input, signal, transforms, authContext) {
|
|
30
33
|
signal?.throwIfAborted();
|
|
31
34
|
switch (input.operation) {
|
|
32
35
|
case "servers":
|
|
33
36
|
return {
|
|
34
37
|
value: {
|
|
35
38
|
operation: "servers",
|
|
36
|
-
servers: Object.entries(
|
|
39
|
+
servers: Object.entries(servers)
|
|
37
40
|
.sort(([left], [right]) => left.localeCompare(right))
|
|
38
41
|
.map(([name, server]) => ({ name, transport: server.transport })),
|
|
39
42
|
},
|
|
40
43
|
};
|
|
41
44
|
case "tools":
|
|
42
|
-
return this.withClient(input.server, signal, async (client) => {
|
|
45
|
+
return this.withClient(servers, input.server, signal, authContext, async (client) => {
|
|
43
46
|
const discovery = await discoverTools(client, signal);
|
|
44
47
|
return {
|
|
45
48
|
value: {
|
|
@@ -51,7 +54,7 @@ export class ExternalMcpGateway {
|
|
|
51
54
|
};
|
|
52
55
|
});
|
|
53
56
|
case "call":
|
|
54
|
-
return this.withClient(input.server, signal, async (client) => {
|
|
57
|
+
return this.withClient(servers, input.server, signal, authContext, async (client) => {
|
|
55
58
|
await assertRegisteredTool(client, input.server, input.tool, signal);
|
|
56
59
|
const appliedTransforms = [];
|
|
57
60
|
let callArguments = input.arguments ?? {};
|
|
@@ -60,7 +63,7 @@ export class ExternalMcpGateway {
|
|
|
60
63
|
callArguments = transformed.value;
|
|
61
64
|
appliedTransforms.push(...transformed.transforms);
|
|
62
65
|
}
|
|
63
|
-
let result = await client.callTool({ name: input.tool, arguments: callArguments },
|
|
66
|
+
let result = await client.callTool({ name: input.tool, arguments: callArguments }, { signal });
|
|
64
67
|
assertCallToolResult(input.server, input.tool, result);
|
|
65
68
|
if (result.isError) {
|
|
66
69
|
throw new ExternalMcpError("tool_failed", `External MCP ${input.server} tool ${input.tool} returned an upstream tool error.`);
|
|
@@ -91,31 +94,146 @@ export class ExternalMcpGateway {
|
|
|
91
94
|
});
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
|
-
async
|
|
95
|
-
|
|
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];
|
|
96
120
|
if (!config)
|
|
97
121
|
throw new ExternalMcpError("unknown_server", `Unknown configured external MCP server: ${name}.`);
|
|
98
|
-
const
|
|
99
|
-
const
|
|
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);
|
|
100
127
|
try {
|
|
101
128
|
signal?.throwIfAborted();
|
|
102
|
-
await client.connect(transport);
|
|
129
|
+
await client.connect(transport, prior ? { prior } : undefined);
|
|
130
|
+
this.rememberNegotiation(name, fingerprint, client);
|
|
103
131
|
signal?.throwIfAborted();
|
|
104
132
|
return await operation(client);
|
|
105
133
|
}
|
|
106
134
|
catch (error) {
|
|
107
135
|
if (error instanceof ExternalMcpError || error instanceof ExternalMcpTransformError)
|
|
108
136
|
throw error;
|
|
137
|
+
this.invalidateNegotiation(name, fingerprint);
|
|
109
138
|
if (signal?.aborted)
|
|
110
139
|
signal.throwIfAborted();
|
|
111
|
-
|
|
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));
|
|
112
159
|
}
|
|
113
160
|
finally {
|
|
114
161
|
await client.close().catch(() => undefined);
|
|
115
162
|
}
|
|
116
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)));
|
|
117
235
|
}
|
|
118
|
-
function createTransport(config) {
|
|
236
|
+
function createTransport(config, authProvider) {
|
|
119
237
|
if (config.transport === "stdio") {
|
|
120
238
|
return new StdioClientTransport({
|
|
121
239
|
command: config.command,
|
|
@@ -129,8 +247,45 @@ function createTransport(config) {
|
|
|
129
247
|
}
|
|
130
248
|
return new StreamableHTTPClientTransport(new URL(config.url), {
|
|
131
249
|
...(config.headers ? { requestInit: { headers: config.headers } } : {}),
|
|
250
|
+
...(authProvider ? { authProvider } : {}),
|
|
251
|
+
onInsufficientScope: "throw",
|
|
132
252
|
});
|
|
133
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
|
+
}
|
|
134
289
|
async function discoverTools(client, signal) {
|
|
135
290
|
const tools = [];
|
|
136
291
|
let cursor;
|
|
@@ -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
|
}
|
|
@@ -45,7 +45,6 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
|
|
|
45
45
|
directory: "host-integration/external-mcp",
|
|
46
46
|
description: "User-configured external MCP tool discovery and explicit forwarding through capability.",
|
|
47
47
|
whenToRead: "Read before discovering or calling a configured external MCP server.",
|
|
48
|
-
enabled: (config) => Object.keys(config.mcpServers).length > 0,
|
|
49
48
|
},
|
|
50
49
|
{
|
|
51
50
|
name: "workspace-tasks",
|
|
@@ -117,9 +116,7 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
|
|
|
117
116
|
if (config.subagents) {
|
|
118
117
|
capabilities.push("subagent.session");
|
|
119
118
|
}
|
|
120
|
-
|
|
121
|
-
capabilities.push("mcp.external");
|
|
122
|
-
}
|
|
119
|
+
capabilities.push("mcp.external");
|
|
123
120
|
if (config.artifactsEnabled && context.artifactDownloadSupported) {
|
|
124
121
|
capabilities.push("artifact.native-download");
|
|
125
122
|
}
|