@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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { loadConfig } from "../../runtime/config/config.js";
|
|
4
|
+
import { ExternalMcpCredentialStore, externalMcpCredentialIdentity, } from "../../runtime/config/external-mcp-auth-store.js";
|
|
5
|
+
import { ExternalMcpConfigRegistry, } from "../../runtime/config/external-mcp-registry.js";
|
|
6
|
+
export function resolveExternalMcpScope(request, dependencies = {}) {
|
|
7
|
+
if (request.global && request.projectRoot) {
|
|
8
|
+
throw new Error("--global and --project cannot be used together.");
|
|
9
|
+
}
|
|
10
|
+
const env = dependencies.env ?? process.env;
|
|
11
|
+
const config = loadConfig(env);
|
|
12
|
+
const cwd = resolve(dependencies.cwd ?? process.cwd());
|
|
13
|
+
const registry = new ExternalMcpConfigRegistry({
|
|
14
|
+
configDir: config.configDir,
|
|
15
|
+
legacyServers: config.mcpServers,
|
|
16
|
+
});
|
|
17
|
+
const store = new ExternalMcpCredentialStore({ configDir: config.configDir });
|
|
18
|
+
if (request.global) {
|
|
19
|
+
return {
|
|
20
|
+
mode: "global",
|
|
21
|
+
projectRoot: cwd,
|
|
22
|
+
projectSelection: "global",
|
|
23
|
+
config,
|
|
24
|
+
registry,
|
|
25
|
+
snapshot: registry.resolveGlobal(),
|
|
26
|
+
store,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const selected = selectProjectRoot(request.projectRoot, cwd, env);
|
|
30
|
+
return {
|
|
31
|
+
mode: "project",
|
|
32
|
+
projectRoot: selected.root,
|
|
33
|
+
projectSelection: selected.selection,
|
|
34
|
+
config,
|
|
35
|
+
registry,
|
|
36
|
+
snapshot: registry.resolve(selected.root),
|
|
37
|
+
store,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function inspectExternalMcpStatus(scope) {
|
|
41
|
+
let credentialStore = "missing";
|
|
42
|
+
let credentialStoreMessage;
|
|
43
|
+
try {
|
|
44
|
+
credentialStore = scope.store.inspect().exists ? "ok" : "missing";
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
credentialStore = "invalid";
|
|
48
|
+
credentialStoreMessage = boundedStatusMessage(error);
|
|
49
|
+
}
|
|
50
|
+
const effective = Object.entries(scope.snapshot.servers).map(([name, config]) => {
|
|
51
|
+
const source = scope.snapshot.origins[name];
|
|
52
|
+
return serverStatus(scope, name, source, config, credentialStore === "invalid");
|
|
53
|
+
});
|
|
54
|
+
const disabled = Object.entries(scope.snapshot.masked).map(([name, source]) => ({
|
|
55
|
+
name,
|
|
56
|
+
source,
|
|
57
|
+
enabled: false,
|
|
58
|
+
}));
|
|
59
|
+
const servers = [...effective, ...disabled].sort((left, right) => left.name.localeCompare(right.name));
|
|
60
|
+
return {
|
|
61
|
+
scope,
|
|
62
|
+
servers,
|
|
63
|
+
configIssues: scope.snapshot.sources.filter((source) => source.state === "invalid").length
|
|
64
|
+
+ (credentialStore === "invalid" ? 1 : 0),
|
|
65
|
+
credentialStore,
|
|
66
|
+
...(credentialStoreMessage ? { credentialStoreMessage } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function findExternalMcpServerStatus(status, name) {
|
|
70
|
+
return status.servers.find((server) => server.name === name);
|
|
71
|
+
}
|
|
72
|
+
export function formatExternalMcpList(status) {
|
|
73
|
+
const lines = ["External MCP", ""];
|
|
74
|
+
if (status.scope.mode === "global") {
|
|
75
|
+
lines.push("Scope: global");
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
lines.push("Scope: project", `Project: ${status.scope.projectRoot}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push("", "Config:");
|
|
81
|
+
for (const source of status.scope.snapshot.sources) {
|
|
82
|
+
lines.push(` ${source.source.padEnd(7)} ${source.path} · ${formatConfigSourceState(source, status)}`);
|
|
83
|
+
}
|
|
84
|
+
if (status.credentialStore === "invalid") {
|
|
85
|
+
lines.push(` auth ${status.scope.store.filePath} · invalid`);
|
|
86
|
+
if (status.credentialStoreMessage)
|
|
87
|
+
lines.push(` ${status.credentialStoreMessage}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push(` auth ${status.scope.store.filePath} · ${status.credentialStore}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push("", "Servers:");
|
|
93
|
+
if (status.servers.length === 0) {
|
|
94
|
+
lines.push(" none");
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
for (const server of status.servers) {
|
|
98
|
+
lines.push(` ${server.name}`);
|
|
99
|
+
lines.push(` source: ${server.source}`);
|
|
100
|
+
if (!server.enabled) {
|
|
101
|
+
lines.push(" status: disabled");
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
lines.push(` transport: ${server.transport}`);
|
|
105
|
+
lines.push(` auth: ${formatAuth(server)}`);
|
|
106
|
+
lines.push(" status: configured");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (status.scope.snapshot.diagnostics.length > 0) {
|
|
110
|
+
lines.push("", "Config issues:");
|
|
111
|
+
for (const diagnostic of status.scope.snapshot.diagnostics) {
|
|
112
|
+
const source = status.scope.snapshot.sources.find((candidate) => candidate.source === diagnostic.source);
|
|
113
|
+
lines.push(` ${diagnostic.source}: ${diagnostic.message}`);
|
|
114
|
+
if (source?.usingLastKnownGood) {
|
|
115
|
+
lines.push(" Using this process's last-known-good configuration.");
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
lines.push(" Existing ForgeRelay runtimes that loaded a valid version may continue using last-known-good configuration.");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return lines.join("\n");
|
|
123
|
+
}
|
|
124
|
+
export function formatExternalMcpDoctor(status) {
|
|
125
|
+
const enabled = status.servers.filter((server) => server.enabled);
|
|
126
|
+
const disabled = status.servers.length - enabled.length;
|
|
127
|
+
const authenticated = enabled.filter((server) => server.authState === "authenticated").length;
|
|
128
|
+
const authRequired = enabled.filter((server) => server.authState === "auth-required").length;
|
|
129
|
+
const reauthRequired = enabled.filter((server) => server.authState === "reauth-required").length;
|
|
130
|
+
const sourceSummary = status.scope.snapshot.sources
|
|
131
|
+
.map((source) => `${source.source}=${compactSourceState(source, status)}`)
|
|
132
|
+
.join(" · ");
|
|
133
|
+
return [
|
|
134
|
+
"External MCP:",
|
|
135
|
+
` Scope: ${status.scope.mode}${status.scope.mode === "project" ? ` (${status.scope.projectRoot})` : ""}`,
|
|
136
|
+
` Config: ${sourceSummary}`,
|
|
137
|
+
` Effective servers: ${enabled.length}`,
|
|
138
|
+
` Disabled: ${disabled}`,
|
|
139
|
+
` OAuth authenticated: ${authenticated}`,
|
|
140
|
+
` Auth required: ${authRequired}`,
|
|
141
|
+
` Reauthorization required: ${reauthRequired}`,
|
|
142
|
+
` Credential store: ${status.credentialStore}`,
|
|
143
|
+
` Config issues: ${status.configIssues}`,
|
|
144
|
+
" Hot reload: active",
|
|
145
|
+
" Active checks: not run (use `forgerelay mcp test <server>`)",
|
|
146
|
+
].join("\n");
|
|
147
|
+
}
|
|
148
|
+
export function formatAuth(status) {
|
|
149
|
+
switch (status.authKind) {
|
|
150
|
+
case "oauth":
|
|
151
|
+
if (status.authState === "authenticated")
|
|
152
|
+
return "oauth · authenticated";
|
|
153
|
+
if (status.authState === "auth-required")
|
|
154
|
+
return "oauth · auth required";
|
|
155
|
+
if (status.authState === "reauth-required")
|
|
156
|
+
return "oauth · reauthorization required";
|
|
157
|
+
return "oauth · not authenticated";
|
|
158
|
+
case "static":
|
|
159
|
+
return "static · configured";
|
|
160
|
+
case "config-managed":
|
|
161
|
+
return "config-managed";
|
|
162
|
+
case "none-detected":
|
|
163
|
+
default:
|
|
164
|
+
return "none detected";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function serverStatus(scope, name, source, config, credentialStoreInvalid) {
|
|
168
|
+
if (config.transport === "stdio") {
|
|
169
|
+
return {
|
|
170
|
+
name,
|
|
171
|
+
source,
|
|
172
|
+
enabled: true,
|
|
173
|
+
transport: config.transport,
|
|
174
|
+
authKind: "config-managed",
|
|
175
|
+
authState: "configured",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (hasStaticAuthorizationHeader(config.headers)) {
|
|
179
|
+
return {
|
|
180
|
+
name,
|
|
181
|
+
source,
|
|
182
|
+
enabled: true,
|
|
183
|
+
transport: config.transport,
|
|
184
|
+
authKind: "static",
|
|
185
|
+
authState: "configured",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (credentialStoreInvalid) {
|
|
189
|
+
return {
|
|
190
|
+
name,
|
|
191
|
+
source,
|
|
192
|
+
enabled: true,
|
|
193
|
+
transport: config.transport,
|
|
194
|
+
authKind: config.oauth ? "oauth" : "none-detected",
|
|
195
|
+
authState: "not-detected",
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const identity = externalMcpCredentialIdentity(source, name, scope.projectRoot);
|
|
199
|
+
let record;
|
|
200
|
+
try {
|
|
201
|
+
record = scope.store.read(identity);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
record = undefined;
|
|
205
|
+
}
|
|
206
|
+
const bindingMismatch = record ? !credentialBindingMatches(record, config.url) : false;
|
|
207
|
+
const authState = bindingMismatch
|
|
208
|
+
? "reauth-required"
|
|
209
|
+
: record?.reauthorization?.reason === "authorization_required" && !record.tokens?.access_token
|
|
210
|
+
? "auth-required"
|
|
211
|
+
: record?.reauthorization
|
|
212
|
+
? "reauth-required"
|
|
213
|
+
: record?.tokens?.access_token
|
|
214
|
+
? "authenticated"
|
|
215
|
+
: "not-detected";
|
|
216
|
+
const authKind = record || config.oauth ? "oauth" : "none-detected";
|
|
217
|
+
return {
|
|
218
|
+
name,
|
|
219
|
+
source,
|
|
220
|
+
enabled: true,
|
|
221
|
+
transport: config.transport,
|
|
222
|
+
authKind,
|
|
223
|
+
authState,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function credentialBindingMatches(record, serverUrl) {
|
|
227
|
+
try {
|
|
228
|
+
if (new URL(record.serverUrl).toString() !== new URL(serverUrl).toString())
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const expectedIssuer = record.discoveryState?.authorizationServerMetadata?.issuer
|
|
235
|
+
?? record.authorizationServerUrl
|
|
236
|
+
?? record.discoveryState?.authorizationServerUrl;
|
|
237
|
+
if (!expectedIssuer)
|
|
238
|
+
return true;
|
|
239
|
+
for (const candidate of [record.tokens?.issuer, record.clientInformation?.issuer]) {
|
|
240
|
+
if (candidate && candidate !== expectedIssuer)
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
function selectProjectRoot(explicit, cwd, env) {
|
|
246
|
+
if (explicit)
|
|
247
|
+
return { root: resolve(explicit), selection: "explicit" };
|
|
248
|
+
if (env.FORGERELAY_WORKSPACE_ROOT) {
|
|
249
|
+
return { root: resolve(env.FORGERELAY_WORKSPACE_ROOT), selection: "workspace-env" };
|
|
250
|
+
}
|
|
251
|
+
const ancestor = findNearestProjectConfig(cwd);
|
|
252
|
+
if (ancestor)
|
|
253
|
+
return { root: ancestor, selection: "ancestor-config" };
|
|
254
|
+
return { root: cwd, selection: "cwd" };
|
|
255
|
+
}
|
|
256
|
+
function findNearestProjectConfig(start) {
|
|
257
|
+
let current = resolve(start);
|
|
258
|
+
while (true) {
|
|
259
|
+
if (existsSync(join(current, ".forgerelay", "mcp.json")))
|
|
260
|
+
return current;
|
|
261
|
+
const parent = dirname(current);
|
|
262
|
+
if (parent === current)
|
|
263
|
+
return undefined;
|
|
264
|
+
current = parent;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function hasStaticAuthorizationHeader(headers) {
|
|
268
|
+
return Object.keys(headers ?? {}).some((name) => name.toLowerCase() === "authorization");
|
|
269
|
+
}
|
|
270
|
+
function formatConfigSourceState(source, status) {
|
|
271
|
+
if (source.source === "legacy") {
|
|
272
|
+
const legacyCount = status.servers.filter((server) => server.enabled && server.source === "legacy").length;
|
|
273
|
+
return legacyCount > 0 ? `compatibility · ${legacyCount} server${legacyCount === 1 ? "" : "s"}` : "compatibility · none";
|
|
274
|
+
}
|
|
275
|
+
if (source.state === "valid")
|
|
276
|
+
return "ok";
|
|
277
|
+
if (source.state === "missing")
|
|
278
|
+
return "missing";
|
|
279
|
+
return source.usingLastKnownGood ? "invalid · using last-known-good" : "invalid";
|
|
280
|
+
}
|
|
281
|
+
function compactSourceState(source, status) {
|
|
282
|
+
if (source.source === "legacy") {
|
|
283
|
+
return status.servers.some((server) => server.enabled && server.source === "legacy") ? "compat" : "none";
|
|
284
|
+
}
|
|
285
|
+
if (source.state === "valid")
|
|
286
|
+
return "ok";
|
|
287
|
+
if (source.state === "missing")
|
|
288
|
+
return "missing";
|
|
289
|
+
return source.usingLastKnownGood ? "invalid/lkg" : "invalid";
|
|
290
|
+
}
|
|
291
|
+
function boundedStatusMessage(error) {
|
|
292
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
293
|
+
const normalized = raw.replace(/\s+/g, " ").trim() || "External MCP status unavailable.";
|
|
294
|
+
return normalized.length <= 320 ? normalized : `${normalized.slice(0, 317)}...`;
|
|
295
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -12,6 +12,8 @@ import { loadConfig } from "./runtime/config/config.js";
|
|
|
12
12
|
import { acquireRuntimeLease } from "./runtime/state/runtime-lease.js";
|
|
13
13
|
import { runInit } from "./cli/init.js";
|
|
14
14
|
import { runMaintenanceCommand } from "./cli/maintenance.js";
|
|
15
|
+
import { runExternalMcpCommand } from "./cli/mcp/external-mcp.js";
|
|
16
|
+
import { formatExternalMcpDoctor, inspectExternalMcpStatus, resolveExternalMcpScope, } from "./cli/mcp/status.js";
|
|
15
17
|
import { runHooksCommand } from "./mcp/hooks/hook-cli.js";
|
|
16
18
|
import { executeSubagentSession } from "./subagents/sessions/execution.js";
|
|
17
19
|
import { SubagentDeliveryMailbox } from "./subagents/sessions/delivery-mailbox.js";
|
|
@@ -63,6 +65,9 @@ async function main(argv) {
|
|
|
63
65
|
case "auth":
|
|
64
66
|
await runAuthCommand(args);
|
|
65
67
|
return;
|
|
68
|
+
case "mcp":
|
|
69
|
+
await runExternalMcpCommand(args);
|
|
70
|
+
return;
|
|
66
71
|
case "maintenance":
|
|
67
72
|
runMaintenanceCommand(args);
|
|
68
73
|
return;
|
|
@@ -77,7 +82,7 @@ async function main(argv) {
|
|
|
77
82
|
function normalizeCommand(command) {
|
|
78
83
|
if (!command || command === "serve" || command === "start")
|
|
79
84
|
return "serve";
|
|
80
|
-
if (command === "init" || command === "doctor" || command === "config" || command === "hooks" || command === "agents" || command === "auth" || command === "maintenance")
|
|
85
|
+
if (command === "init" || command === "doctor" || command === "config" || command === "hooks" || command === "agents" || command === "auth" || command === "mcp" || command === "maintenance")
|
|
81
86
|
return command;
|
|
82
87
|
if (command === "help" || command === "--help" || command === "-h")
|
|
83
88
|
return "help";
|
|
@@ -379,6 +384,10 @@ async function runDoctor() {
|
|
|
379
384
|
console.log(`Agent-managed Language Server install: ${config.allowAgentLanguageServerInstall ? "enabled" : "disabled"}`);
|
|
380
385
|
console.log(`Allowed roots: ${config.allowedRoots.join(", ")}`);
|
|
381
386
|
console.log(`Allowed hosts: ${config.allowedHosts.join(", ")}`);
|
|
387
|
+
console.log(formatExternalMcpDoctor(inspectExternalMcpStatus(resolveExternalMcpScope({}, {
|
|
388
|
+
env: process.env,
|
|
389
|
+
cwd: process.cwd(),
|
|
390
|
+
}))));
|
|
382
391
|
}
|
|
383
392
|
catch (error) {
|
|
384
393
|
console.log(`Config status: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -430,6 +439,10 @@ function printHelp() {
|
|
|
430
439
|
" forgerelay auth test <alias>",
|
|
431
440
|
" forgerelay auth rename <old-alias> <new-alias>",
|
|
432
441
|
" forgerelay auth remove <alias>",
|
|
442
|
+
" forgerelay mcp list [--project <path>|--global]",
|
|
443
|
+
" forgerelay mcp test <server> [--project <path>|--global]",
|
|
444
|
+
" forgerelay mcp auth <server> [--project <path>|--global]",
|
|
445
|
+
" forgerelay mcp logout <server> [--project <path>|--global]",
|
|
433
446
|
" forgerelay maintenance inspect [--json]",
|
|
434
447
|
" forgerelay -v, --version Print the installed version",
|
|
435
448
|
"",
|
|
@@ -2,8 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
-
import { Client } from "@modelcontextprotocol/
|
|
6
|
-
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
5
|
+
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
|
7
6
|
import { ActivityAuditStore } from "../../activity/history/audit-store.js";
|
|
8
7
|
import { BashOutputStore } from "../../activity/history/bash-output-store.js";
|
|
9
8
|
import { HostTurnStore } from "../../activity/history/host-turn-store.js";
|