@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.
Files changed (150) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +4 -2
  3. package/dist/activity/runtime/mcp-query-tools.js +16 -10
  4. package/dist/cli/mcp/external-mcp.js +481 -0
  5. package/dist/cli/mcp/status.js +295 -0
  6. package/dist/cli.js +14 -1
  7. package/dist/lsp/test-support/server-fixture.js +1 -2
  8. package/dist/mcp/oauth/auth-protocol.js +375 -0
  9. package/dist/mcp/oauth/oauth-provider.js +11 -12
  10. package/dist/mcp/oauth/oauth-store.js +2 -2
  11. package/dist/mcp/oauth/router.js +2 -6
  12. package/dist/mcp/operations/batch/executor.js +1 -1
  13. package/dist/mcp/operations/external-mcp/external-mcp-oauth.js +387 -0
  14. package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +23 -6
  15. package/dist/mcp/operations/external-mcp/external-mcp.js +178 -23
  16. package/dist/mcp/operations/native-bulk-mutations.js +1 -1
  17. package/dist/mcp/process/tools.js +19 -15
  18. package/dist/mcp/request-context.js +18 -0
  19. package/dist/mcp/request-meta.js +28 -3
  20. package/dist/mcp/server/core/capabilities.js +1 -4
  21. package/dist/mcp/server/operations/runtime/filesystem-tools.js +35 -28
  22. package/dist/mcp/server/operations/runtime/operation-runtime.js +7 -7
  23. package/dist/mcp/server/transport/http-server.js +41 -20
  24. package/dist/mcp/server/workspace/runtime/workspace-open.js +13 -9
  25. package/dist/mcp/server/workspace/runtime/workspace-tools.js +29 -25
  26. package/dist/runtime/config/external-mcp-auth-store.js +225 -0
  27. package/dist/runtime/config/external-mcp-config.js +76 -22
  28. package/dist/runtime/config/external-mcp-registry.js +165 -0
  29. package/dist/runtime/testing/server-fixture.js +4 -4
  30. package/dist/server.js +15 -9
  31. package/dist/ui/.vite/manifest.json +297 -298
  32. package/dist/ui/activity-panel-app.html +1 -1
  33. package/dist/ui/assets/activity-panel-app-Bw7ZFX2g.js +114 -0
  34. package/dist/ui/assets/angular-html-DeUNP12X.js +1 -0
  35. package/dist/ui/assets/{angular-ts-BearSZDb.js → angular-ts-B3v8FKi_.js} +1 -1
  36. package/dist/ui/assets/{apl-CYQtr-y6.js → apl-Bs3iQm4P.js} +1 -1
  37. package/dist/ui/assets/{astro-DDzqKIjK.js → astro-D6GHecBY.js} +1 -1
  38. package/dist/ui/assets/{blade-DkwEyHvQ.js → blade-BMNbVOHv.js} +1 -1
  39. package/dist/ui/assets/c-BquuA6bv.js +1 -0
  40. package/dist/ui/assets/{cobol-xBn5JxHE.js → cobol-BuZ6kfps.js} +1 -1
  41. package/dist/ui/assets/{coffee-BaXXvoJw.js → coffee-DgC5uBzL.js} +1 -1
  42. package/dist/ui/assets/cpp-BdvWqCaB.js +1 -0
  43. package/dist/ui/assets/{crystal-DjOpXVU1.js → crystal-0Q5qBvNI.js} +1 -1
  44. package/dist/ui/assets/css-Be_T8idh.js +1 -0
  45. package/dist/ui/assets/{edge-DNaRizoR.js → edge-DaGLdgh_.js} +1 -1
  46. package/dist/ui/assets/{elixir-CwBMwZmy.js → elixir-CMgiFIzB.js} +1 -1
  47. package/dist/ui/assets/{elm-gOUD2CW3.js → elm-B-ufMlMC.js} +1 -1
  48. package/dist/ui/assets/{erb-BcR-fjUp.js → erb-CaGiOZM-.js} +1 -1
  49. package/dist/ui/assets/{git-rebase-BAQPKHF6.js → git-rebase-CpwUWCzK.js} +1 -1
  50. package/dist/ui/assets/{glimmer-js-P_wOfSz_.js → glimmer-js-BRxccvnE.js} +1 -1
  51. package/dist/ui/assets/{glimmer-ts-B44aY1fZ.js → glimmer-ts-CgQbPzlT.js} +1 -1
  52. package/dist/ui/assets/glsl-D7JmHFr0.js +1 -0
  53. package/dist/ui/assets/graphql-C8n0eUBK.js +1 -0
  54. package/dist/ui/assets/{hack-ddU2k0nt.js → hack-lVN7KVQv.js} +1 -1
  55. package/dist/ui/assets/haml-h18lG0pS.js +1 -0
  56. package/dist/ui/assets/{handlebars-BpzaUpmz.js → handlebars-lRFjRPfj.js} +1 -1
  57. package/dist/ui/assets/heavy-payload-Cqx9MOkH.js +290 -0
  58. package/dist/ui/assets/html-DtILqTE1.js +1 -0
  59. package/dist/ui/assets/{html-derivative-B2beVdmA.js → html-derivative-CSTSmPup.js} +1 -1
  60. package/dist/ui/assets/{http-iEskZy8U.js → http-DNI8fsbY.js} +1 -1
  61. package/dist/ui/assets/{hurl-BuUsLSKv.js → hurl-DgRi60JG.js} +1 -1
  62. package/dist/ui/assets/java-CW9pZ5bh.js +1 -0
  63. package/dist/ui/assets/javascript-Cvg2UrWL.js +1 -0
  64. package/dist/ui/assets/{jinja-CRpkWwrH.js → jinja-CZw_S1FB.js} +1 -1
  65. package/dist/ui/assets/{jison-BYfSnuHj.js → jison-C56u_FZv.js} +1 -1
  66. package/dist/ui/assets/json-DW2UTv1q.js +1 -0
  67. package/dist/ui/assets/jsx-Zj5gobdD.js +1 -0
  68. package/dist/ui/assets/{julia-B8Rxy7vD.js → julia-Cvcrre-L.js} +1 -1
  69. package/dist/ui/assets/{just-DzhIWpFP.js → just-8CZ2u6Tj.js} +1 -1
  70. package/dist/ui/assets/{latex-CUNjUUL-.js → latex-D289AXWG.js} +1 -1
  71. package/dist/ui/assets/{liquid-hUg7nC61.js → liquid-CFEX3SkZ.js} +1 -1
  72. package/dist/ui/assets/lua-BKpJ0ybO.js +1 -0
  73. package/dist/ui/assets/{marko-CQmIkwMF.js → marko-D9maZ0a6.js} +1 -1
  74. package/dist/ui/assets/{mdc-CB-6dHYa.js → mdc-rM9l0qWR.js} +1 -1
  75. package/dist/ui/assets/{nginx-CQORo_Bt.js → nginx-C9w6p2C9.js} +1 -1
  76. package/dist/ui/assets/{nim-BNwBNzdn.js → nim-BRePHk0J.js} +1 -1
  77. package/dist/ui/assets/{perl-Cnz14fAJ.js → perl-CskTmiPQ.js} +1 -1
  78. package/dist/ui/assets/{php-YywDo63b.js → php-DCU_FvNI.js} +1 -1
  79. package/dist/ui/assets/{pug-BwbJp4cM.js → pug-XLaVxTvv.js} +1 -1
  80. package/dist/ui/assets/{qml-D2DY6dMH.js → qml-CaONKSl-.js} +1 -1
  81. package/dist/ui/assets/r-BCVR0ZE3.js +1 -0
  82. package/dist/ui/assets/{razor-pMTG7k41.js → razor-DDnGXxnU.js} +1 -1
  83. package/dist/ui/assets/regexp-DWGTYYVx.js +1 -0
  84. package/dist/ui/assets/{rst-CO_ndDm4.js → rst-Dz3GWvQm.js} +1 -1
  85. package/dist/ui/assets/{ruby-xzRn9gTh.js → ruby-DC3jGtMY.js} +1 -1
  86. package/dist/ui/assets/{sas-CBdtFKLA.js → sas-7mNpvsjZ.js} +1 -1
  87. package/dist/ui/assets/scss-uBsoCJVy.js +1 -0
  88. package/dist/ui/assets/shellscript-BCN9N_Jf.js +1 -0
  89. package/dist/ui/assets/{shellsession-BaZnm5rZ.js → shellsession-Hsw46VXV.js} +1 -1
  90. package/dist/ui/assets/{soy-IouG3xdB.js → soy-D6KV69HU.js} +1 -1
  91. package/dist/ui/assets/sql-CVzVQkJh.js +1 -0
  92. package/dist/ui/assets/{stata-dgbkIZHP.js → stata-BwUjXNhI.js} +1 -1
  93. package/dist/ui/assets/{surrealql-CBROnw_p.js → surrealql-CgloWqRa.js} +1 -1
  94. package/dist/ui/assets/{svelte-6r6BpX4-.js → svelte-DcC3n6IC.js} +1 -1
  95. package/dist/ui/assets/{templ-BKD_PsbL.js → templ-DOFfnWTJ.js} +1 -1
  96. package/dist/ui/assets/{tex-CKY1bwHb.js → tex-ln89VPaX.js} +1 -1
  97. package/dist/ui/assets/{ts-tags-BiYP5N_C.js → ts-tags-CWMzCstH.js} +1 -1
  98. package/dist/ui/assets/tsx-DgzfiGeK.js +1 -0
  99. package/dist/ui/assets/{twig-BTe1oPFW.js → twig-Bi2itZL9.js} +1 -1
  100. package/dist/ui/assets/typescript-CQ24f4zW.js +1 -0
  101. package/dist/ui/assets/{vue-CT0IiySh.js → vue-BS8jE5u6.js} +1 -1
  102. package/dist/ui/assets/{vue-html-B-tXcbat.js → vue-html-o38xqhyB.js} +1 -1
  103. package/dist/ui/assets/{vue-vine-C5ZyDCrs.js → vue-vine-D2NvKv50.js} +1 -1
  104. package/dist/ui/assets/xml-_44rx9_2.js +1 -0
  105. package/dist/ui/assets/{xsl-DvDSndNI.js → xsl-B5QWQPoi.js} +1 -1
  106. package/dist/ui/assets/yaml-CKL2pEWe.js +1 -0
  107. package/dist/workspaces/relay/auth/remote-auth.js +3 -5
  108. package/dist/workspaces/relay/tests/test-support.js +7 -4
  109. package/dist/workspaces/relay/workspace-relay.js +14 -8
  110. package/docs/configuration.md +154 -4
  111. package/docs/debugging.md +8 -5
  112. package/docs/roadmap.md +64 -3
  113. package/docs/security.md +18 -0
  114. package/package.json +11 -4
  115. package/scripts/debug/accept/bootstrap.mjs +2 -0
  116. package/scripts/debug/accept/harness.mjs +9 -2
  117. package/scripts/debug/accept/modern-http.mjs +169 -0
  118. package/scripts/debug/accept/support.mjs +10 -6
  119. package/scripts/debug/accept.mjs +1 -0
  120. package/scripts/debug/runtime.mjs +4 -1
  121. package/scripts/debug/runtime.test.mjs +9 -0
  122. package/scripts/release/parity-sandbox.mjs +35 -0
  123. package/scripts/release/release-gate.test.mjs +42 -1
  124. package/scripts/release-parity.mjs +3 -22
  125. package/scripts/release-proof.mjs +24 -4
  126. package/scripts/release-proof.test.mjs +12 -4
  127. package/dist/ui/assets/activity-panel-app-XlqSanXT.js +0 -115
  128. package/dist/ui/assets/angular-html-tB9EchbM.js +0 -1
  129. package/dist/ui/assets/c-Dv-N5hjn.js +0 -1
  130. package/dist/ui/assets/cpp-C3unXX-3.js +0 -1
  131. package/dist/ui/assets/css-8Xr4kNNH.js +0 -1
  132. package/dist/ui/assets/glsl-BvCQg9MI.js +0 -1
  133. package/dist/ui/assets/graphql-C9S90gn_.js +0 -1
  134. package/dist/ui/assets/haml-Bb1w9rMG.js +0 -1
  135. package/dist/ui/assets/heavy-payload-CJKqi8Ac.js +0 -290
  136. package/dist/ui/assets/html-dUOs5pA9.js +0 -1
  137. package/dist/ui/assets/java-BFZCrORY.js +0 -1
  138. package/dist/ui/assets/javascript-DaIQ0Zqj.js +0 -1
  139. package/dist/ui/assets/json-BQC_Yp48.js +0 -1
  140. package/dist/ui/assets/jsx-Dd-CJRf9.js +0 -1
  141. package/dist/ui/assets/lua-BoZTiL2I.js +0 -1
  142. package/dist/ui/assets/r-B5nYsy-M.js +0 -1
  143. package/dist/ui/assets/regexp-oP0lbks_.js +0 -1
  144. package/dist/ui/assets/scss-CEqgKQNf.js +0 -1
  145. package/dist/ui/assets/shellscript-DzJJiWQC.js +0 -1
  146. package/dist/ui/assets/sql-DC2rdTzV.js +0 -1
  147. package/dist/ui/assets/tsx-D2OMiPNF.js +0 -1
  148. package/dist/ui/assets/typescript-BGIABksB.js +0 -1
  149. package/dist/ui/assets/xml-CfeBNlMP.js +0 -1
  150. 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/sdk/client/index.js";
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";