@stigmer/cli 3.12.9 → 3.14.0-dev.20260910084630

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 (141) hide show
  1. package/auth/login.d.ts.map +1 -1
  2. package/auth/login.js +30 -5
  3. package/auth/login.js.map +1 -1
  4. package/auth/token.d.ts +10 -5
  5. package/auth/token.d.ts.map +1 -1
  6. package/auth/token.js +30 -21
  7. package/auth/token.js.map +1 -1
  8. package/commands/apikey/index.d.ts.map +1 -1
  9. package/commands/apikey/index.js +11 -5
  10. package/commands/apikey/index.js.map +1 -1
  11. package/commands/auth/index.d.ts.map +1 -1
  12. package/commands/auth/index.js +19 -8
  13. package/commands/auth/index.js.map +1 -1
  14. package/commands/config/backend.d.ts.map +1 -1
  15. package/commands/config/backend.js +134 -12
  16. package/commands/config/backend.js.map +1 -1
  17. package/commands/connect.d.ts.map +1 -1
  18. package/commands/connect.js +6 -3
  19. package/commands/connect.js.map +1 -1
  20. package/commands/share.d.ts.map +1 -1
  21. package/commands/share.js +8 -6
  22. package/commands/share.js.map +1 -1
  23. package/commands/up.d.ts.map +1 -1
  24. package/commands/up.js +22 -6
  25. package/commands/up.js.map +1 -1
  26. package/config/config.d.ts +56 -7
  27. package/config/config.d.ts.map +1 -1
  28. package/config/config.js +115 -16
  29. package/config/config.js.map +1 -1
  30. package/config/index.d.ts +1 -1
  31. package/config/index.d.ts.map +1 -1
  32. package/config/index.js +1 -1
  33. package/config/index.js.map +1 -1
  34. package/config/keys.d.ts +1 -1
  35. package/config/keys.d.ts.map +1 -1
  36. package/config/keys.js +29 -6
  37. package/config/keys.js.map +1 -1
  38. package/config/resolve.d.ts +32 -21
  39. package/config/resolve.d.ts.map +1 -1
  40. package/config/resolve.js +80 -39
  41. package/config/resolve.js.map +1 -1
  42. package/local/constants.d.ts +8 -3
  43. package/local/constants.d.ts.map +1 -1
  44. package/local/constants.js +8 -3
  45. package/local/constants.js.map +1 -1
  46. package/local/daemon/components.d.ts.map +1 -1
  47. package/local/daemon/components.js +4 -2
  48. package/local/daemon/components.js.map +1 -1
  49. package/local/daemon/env.d.ts +16 -3
  50. package/local/daemon/env.d.ts.map +1 -1
  51. package/local/daemon/env.js +16 -3
  52. package/local/daemon/env.js.map +1 -1
  53. package/local/daemon/launch.d.ts.map +1 -1
  54. package/local/daemon/launch.js +5 -4
  55. package/local/daemon/launch.js.map +1 -1
  56. package/local/daemon/process.d.ts.map +1 -1
  57. package/local/daemon/process.js +8 -5
  58. package/local/daemon/process.js.map +1 -1
  59. package/local/runtime/index.d.ts +2 -2
  60. package/local/runtime/index.d.ts.map +1 -1
  61. package/local/runtime/index.js +4 -2
  62. package/local/runtime/index.js.map +1 -1
  63. package/local/runtime/node.d.ts +8 -0
  64. package/local/runtime/node.d.ts.map +1 -1
  65. package/local/runtime/node.js +74 -23
  66. package/local/runtime/node.js.map +1 -1
  67. package/local/runtime/runner.d.ts +2 -1
  68. package/local/runtime/runner.d.ts.map +1 -1
  69. package/local/runtime/runner.js +4 -31
  70. package/local/runtime/runner.js.map +1 -1
  71. package/local/runtime/runtimes-install.d.ts +13 -0
  72. package/local/runtime/runtimes-install.d.ts.map +1 -0
  73. package/local/runtime/runtimes-install.js +60 -0
  74. package/local/runtime/runtimes-install.js.map +1 -0
  75. package/local/runtime/server.d.ts +26 -30
  76. package/local/runtime/server.d.ts.map +1 -1
  77. package/local/runtime/server.js +106 -108
  78. package/local/runtime/server.js.map +1 -1
  79. package/local/seedpack/content.d.ts +3 -3
  80. package/local/seedpack/content.d.ts.map +1 -1
  81. package/local/seedpack/content.js +19 -13
  82. package/local/seedpack/content.js.map +1 -1
  83. package/local/status.d.ts.map +1 -1
  84. package/local/status.js +4 -2
  85. package/local/status.js.map +1 -1
  86. package/local/webconsole/index.d.ts +2 -3
  87. package/local/webconsole/index.d.ts.map +1 -1
  88. package/local/webconsole/index.js +24 -10
  89. package/local/webconsole/index.js.map +1 -1
  90. package/package.json +5 -5
  91. package/resources/connect/connect.d.ts +4 -3
  92. package/resources/connect/connect.d.ts.map +1 -1
  93. package/resources/connect/connect.js +17 -5
  94. package/resources/connect/connect.js.map +1 -1
  95. package/resources/connect/oauth.d.ts +5 -2
  96. package/resources/connect/oauth.d.ts.map +1 -1
  97. package/resources/connect/oauth.js +5 -4
  98. package/resources/connect/oauth.js.map +1 -1
  99. package/src/auth/login.ts +54 -9
  100. package/src/auth/token.test.ts +69 -13
  101. package/src/auth/token.ts +46 -22
  102. package/src/client/client.test.ts +10 -2
  103. package/src/commands/apikey/index.ts +42 -11
  104. package/src/commands/auth/index.ts +52 -14
  105. package/src/commands/config/backend.test.ts +122 -0
  106. package/src/commands/config/backend.ts +195 -14
  107. package/src/commands/connect.test.ts +22 -4
  108. package/src/commands/connect.ts +37 -13
  109. package/src/commands/share.ts +36 -16
  110. package/src/commands/up.ts +24 -6
  111. package/src/config/config.test.ts +62 -6
  112. package/src/config/config.ts +165 -24
  113. package/src/config/index.ts +6 -0
  114. package/src/config/keys.test.ts +27 -4
  115. package/src/config/keys.ts +46 -9
  116. package/src/config/resolve.test.ts +135 -34
  117. package/src/config/resolve.ts +88 -38
  118. package/src/local/constants.ts +8 -4
  119. package/src/local/daemon/components.test.ts +9 -1
  120. package/src/local/daemon/components.ts +4 -2
  121. package/src/local/daemon/daemon.integration.test.ts +1 -1
  122. package/src/local/daemon/env.test.ts +15 -4
  123. package/src/local/daemon/env.ts +33 -5
  124. package/src/local/daemon/launch.ts +5 -4
  125. package/src/local/daemon/process.ts +7 -4
  126. package/src/local/runtime/index.ts +6 -5
  127. package/src/local/runtime/node.ts +99 -23
  128. package/src/local/runtime/runner.ts +5 -36
  129. package/src/local/runtime/runtime.test.ts +58 -14
  130. package/src/local/runtime/runtimes-install.test.ts +56 -0
  131. package/src/local/runtime/runtimes-install.ts +72 -0
  132. package/src/local/runtime/server.test.ts +178 -114
  133. package/src/local/runtime/server.ts +149 -129
  134. package/src/local/seedpack/content.ts +52 -21
  135. package/src/local/status.ts +4 -2
  136. package/src/local/webconsole/index.ts +27 -10
  137. package/src/resources/connect/connect.integration.test.ts +79 -26
  138. package/src/resources/connect/connect.ts +72 -19
  139. package/src/resources/connect/oauth.test.ts +38 -12
  140. package/src/resources/connect/oauth.ts +27 -11
  141. package/src/resources/share.test.ts +2 -2
@@ -18,7 +18,11 @@ import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcp
18
18
  import { McpServerAuthSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
19
19
  import type { Stigmer } from "@stigmer/sdk";
20
20
  import { createNodeClient, normalizeEndpoint } from "@stigmer/sdk/node";
21
- import { createServer as createHttp2Server, type Http2Server, type ServerHttp2Session } from "node:http2";
21
+ import {
22
+ createServer as createHttp2Server,
23
+ type Http2Server,
24
+ type ServerHttp2Session,
25
+ } from "node:http2";
22
26
  import type { AddressInfo } from "node:net";
23
27
  import { fileURLToPath } from "node:url";
24
28
  import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
@@ -26,7 +30,9 @@ import { UsageError } from "../../errors/index.js";
26
30
  import { connectMcpServer } from "./connect.js";
27
31
  import { renderConnectResult } from "./display.js";
28
32
 
29
- const FIXTURE = fileURLToPath(new URL("./__fixtures__/stdio-server.mjs", import.meta.url));
33
+ const FIXTURE = fileURLToPath(
34
+ new URL("./__fixtures__/stdio-server.mjs", import.meta.url),
35
+ );
30
36
 
31
37
  let backend: Http2Server;
32
38
  let client: Stigmer;
@@ -48,7 +54,10 @@ beforeEach(() => {
48
54
  servedSpec = create(McpServerSchema, {
49
55
  metadata: { id: "mcp_1", name: "github", slug: "github", org: "acme" },
50
56
  spec: {
51
- serverType: { case: "stdio", value: { command: process.execPath, args: [FIXTURE] } },
57
+ serverType: {
58
+ case: "stdio",
59
+ value: { command: process.execPath, args: [FIXTURE] },
60
+ },
52
61
  env: { GITHUB_TOKEN: { isSecret: true } },
53
62
  },
54
63
  });
@@ -61,7 +70,9 @@ const updatedServer = create(McpServerSchema, {
61
70
  status: {
62
71
  discoveredCapabilities: {
63
72
  tools: [{ name: "search_issues", description: "search issues" }],
64
- resourceTemplates: [{ name: "issue", uriTemplate: "github:///issues/{id}" }],
73
+ resourceTemplates: [
74
+ { name: "issue", uriTemplate: "github:///issues/{id}" },
75
+ ],
65
76
  },
66
77
  },
67
78
  });
@@ -71,16 +82,23 @@ beforeAll(async () => {
71
82
  router.service(McpServerQueryController, {
72
83
  getByReference: () => servedSpec,
73
84
  get: () => servedSpec,
74
- getOAuthGrantStatus: () => create(GetOAuthGrantStatusOutputSchema, { connected: grantConnected }),
85
+ getOAuthGrantStatus: () =>
86
+ create(GetOAuthGrantStatusOutputSchema, { connected: grantConnected }),
75
87
  });
76
88
  router.service(McpServerCommandController, {
77
89
  // Mirror the backend's protovalidate rule (org min_len=1): reject an empty
78
90
  // org so a regression that drops org fails loudly here instead of silently
79
91
  // passing (the mock adapter does not run protovalidate on its own).
80
92
  connect: async (req) => {
81
- if (req.org === "") throw new ConnectError("org – value length must be at least 1 characters", Code.InvalidArgument);
93
+ if (req.org === "")
94
+ throw new ConnectError(
95
+ "org – value length must be at least 1 characters",
96
+ Code.InvalidArgument,
97
+ );
82
98
  if (connectDelayMs > 0) {
83
- await new Promise((resolve) => setTimeout(resolve, connectDelayMs).unref());
99
+ await new Promise((resolve) =>
100
+ setTimeout(resolve, connectDelayMs).unref(),
101
+ );
84
102
  }
85
103
  connectCalls.push(req);
86
104
  return updatedServer;
@@ -95,7 +113,9 @@ beforeAll(async () => {
95
113
  });
96
114
  await new Promise<void>((resolve) => backend.listen(0, "127.0.0.1", resolve));
97
115
  const port = (backend.address() as AddressInfo).port;
98
- client = createNodeClient({ baseUrl: normalizeEndpoint(`127.0.0.1:${port}`) });
116
+ client = createNodeClient({
117
+ baseUrl: normalizeEndpoint(`127.0.0.1:${port}`),
118
+ });
99
119
  });
100
120
 
101
121
  afterAll(async () => {
@@ -111,7 +131,8 @@ describe("connect push path", () => {
111
131
  timeoutMs: 30_000,
112
132
  dryRun: false,
113
133
  envOverrides: ["GITHUB_TOKEN=ghp-override"],
114
- backendType: "cloud",
134
+ consoleURL: "https://app.stigmer.ai",
135
+ probeLocalConsole: false,
115
136
  interactive: false,
116
137
  });
117
138
 
@@ -124,7 +145,9 @@ describe("connect push path", () => {
124
145
  expect(connectCalls[0].runtimeEnv.GITHUB_TOKEN.isSecret).toBe(true);
125
146
 
126
147
  expect(result.updated?.metadata?.id).toBe("mcp_1");
127
- expect(result.capabilities?.tools.map((t) => t.name)).toEqual(["search_issues"]);
148
+ expect(result.capabilities?.tools.map((t) => t.name)).toEqual([
149
+ "search_issues",
150
+ ]);
128
151
  });
129
152
  });
130
153
 
@@ -139,10 +162,13 @@ describe("--timeout bounds the server-side connect (issue #239)", () => {
139
162
  pushTimeoutMs: 150,
140
163
  dryRun: false,
141
164
  envOverrides: ["GITHUB_TOKEN=ghp-x"],
142
- backendType: "cloud",
165
+ consoleURL: "https://app.stigmer.ai",
166
+ probeLocalConsole: false,
143
167
  interactive: false,
144
168
  }),
145
- ).rejects.toThrow(/Stopped waiting for the connect of MCP server 'github' after 0\.15s/);
169
+ ).rejects.toThrow(
170
+ /Stopped waiting for the connect of MCP server 'github' after 0\.15s/,
171
+ );
146
172
  });
147
173
 
148
174
  it("does not bound the wait when pushTimeoutMs is unset (default --timeout)", async () => {
@@ -157,7 +183,8 @@ describe("--timeout bounds the server-side connect (issue #239)", () => {
157
183
  timeoutMs: 100,
158
184
  dryRun: false,
159
185
  envOverrides: ["GITHUB_TOKEN=ghp-x"],
160
- backendType: "cloud",
186
+ consoleURL: "https://app.stigmer.ai",
187
+ probeLocalConsole: false,
161
188
  interactive: false,
162
189
  });
163
190
  expect(result.updated?.metadata?.id).toBe("mcp_1");
@@ -166,7 +193,9 @@ describe("--timeout bounds the server-side connect (issue #239)", () => {
166
193
 
167
194
  describe("OAuth guidance gate", () => {
168
195
  beforeEach(() => {
169
- servedSpec.spec!.auth = create(McpServerAuthSchema, { targetEnvVar: "GITHUB_TOKEN" });
196
+ servedSpec.spec!.auth = create(McpServerAuthSchema, {
197
+ targetEnvVar: "GITHUB_TOKEN",
198
+ });
170
199
  });
171
200
 
172
201
  it("stops with actionable guidance when auth is required off an interactive terminal", async () => {
@@ -177,7 +206,8 @@ describe("OAuth guidance gate", () => {
177
206
  timeoutMs: 30_000,
178
207
  dryRun: false,
179
208
  envOverrides: [],
180
- backendType: "cloud",
209
+ consoleURL: "https://app.stigmer.ai",
210
+ probeLocalConsole: false,
181
211
  interactive: false,
182
212
  }),
183
213
  ).rejects.toThrow(UsageError);
@@ -192,7 +222,8 @@ describe("OAuth guidance gate", () => {
192
222
  timeoutMs: 30_000,
193
223
  dryRun: false,
194
224
  envOverrides: [],
195
- backendType: "cloud",
225
+ consoleURL: "https://app.stigmer.ai",
226
+ probeLocalConsole: false,
196
227
  interactive: false,
197
228
  });
198
229
  expect(connectCalls).toHaveLength(1);
@@ -205,7 +236,8 @@ describe("OAuth guidance gate", () => {
205
236
  timeoutMs: 30_000,
206
237
  dryRun: false,
207
238
  envOverrides: ["GITHUB_TOKEN=ghp-x"],
208
- backendType: "cloud",
239
+ consoleURL: "https://app.stigmer.ai",
240
+ probeLocalConsole: false,
209
241
  interactive: false,
210
242
  });
211
243
  expect(connectCalls).toHaveLength(1);
@@ -227,7 +259,8 @@ describe("oauth_only servers reject the manual-token routes", () => {
227
259
  timeoutMs: 30_000,
228
260
  dryRun: false,
229
261
  envOverrides: ["GITHUB_TOKEN=ghp-x"],
230
- backendType: "cloud",
262
+ consoleURL: "https://app.stigmer.ai",
263
+ probeLocalConsole: false,
231
264
  interactive: false,
232
265
  }).catch((e: unknown) => e);
233
266
 
@@ -245,7 +278,8 @@ describe("oauth_only servers reject the manual-token routes", () => {
245
278
  timeoutMs: 30_000,
246
279
  dryRun: false,
247
280
  envOverrides: [],
248
- backendType: "cloud",
281
+ consoleURL: "https://app.stigmer.ai",
282
+ probeLocalConsole: false,
249
283
  interactive: false,
250
284
  }).catch((e: unknown) => e);
251
285
 
@@ -261,7 +295,8 @@ describe("oauth_only servers reject the manual-token routes", () => {
261
295
  timeoutMs: 10_000,
262
296
  dryRun: true,
263
297
  envOverrides: [],
264
- backendType: "cloud",
298
+ consoleURL: "https://app.stigmer.ai",
299
+ probeLocalConsole: false,
265
300
  interactive: false,
266
301
  }).catch((e: unknown) => e);
267
302
 
@@ -280,13 +315,17 @@ describe("dry-run path", () => {
280
315
  timeoutMs: 10_000,
281
316
  dryRun: true,
282
317
  envOverrides: [],
283
- backendType: "cloud",
318
+ consoleURL: "https://app.stigmer.ai",
319
+ probeLocalConsole: false,
284
320
  interactive: false,
285
321
  });
286
322
 
287
323
  expect(connectCalls).toHaveLength(0);
288
324
  expect(result.updated).toBeUndefined();
289
- expect(result.capabilities?.tools.map((t) => t.name)).toEqual(["echo", "noop"]);
325
+ expect(result.capabilities?.tools.map((t) => t.name)).toEqual([
326
+ "echo",
327
+ "noop",
328
+ ]);
290
329
 
291
330
  const lines: string[] = [];
292
331
  renderConnectResult(result, (l) => lines.push(l), false);
@@ -301,13 +340,26 @@ describe("dry-run path", () => {
301
340
  // Server references a declared env var in its args, but it is not set.
302
341
  delete process.env.NEEDED_DIR;
303
342
  servedSpec = create(McpServerSchema, {
304
- metadata: { id: "mcp_1", name: "filesystem", slug: "filesystem", org: "acme" },
343
+ metadata: {
344
+ id: "mcp_1",
345
+ name: "filesystem",
346
+ slug: "filesystem",
347
+ org: "acme",
348
+ },
305
349
  spec: {
306
350
  serverType: {
307
351
  case: "stdio",
308
- value: { command: process.execPath, args: [FIXTURE, "${NEEDED_DIR}"] },
352
+ value: {
353
+ command: process.execPath,
354
+ args: [FIXTURE, "${NEEDED_DIR}"],
355
+ },
356
+ },
357
+ env: {
358
+ NEEDED_DIR: {
359
+ isSecret: false,
360
+ description: "Root directory the server may access",
361
+ },
309
362
  },
310
- env: { NEEDED_DIR: { isSecret: false, description: "Root directory the server may access" } },
311
363
  },
312
364
  });
313
365
 
@@ -317,7 +369,8 @@ describe("dry-run path", () => {
317
369
  timeoutMs: 10_000,
318
370
  dryRun: true,
319
371
  envOverrides: [],
320
- backendType: "cloud",
372
+ consoleURL: "https://app.stigmer.ai",
373
+ probeLocalConsole: false,
321
374
  interactive: false,
322
375
  }).catch((e: unknown) => e);
323
376
 
@@ -14,12 +14,18 @@
14
14
  import { create } from "@bufbuild/protobuf";
15
15
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
16
16
  import type { ConnectInput } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
17
- import { ConnectInputSchema, GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
17
+ import {
18
+ ConnectInputSchema,
19
+ GetOAuthGrantStatusInputSchema,
20
+ } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
18
21
  import type { DiscoveredCapabilities } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
19
22
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
20
23
  import type { Stigmer } from "@stigmer/sdk";
21
- import { connectAndWait, ConnectStillRunningError, CONNECT_SETTLE_BOUND_MS } from "@stigmer/sdk";
22
- import type { BackendType } from "../../config/config.js";
24
+ import {
25
+ connectAndWait,
26
+ ConnectStillRunningError,
27
+ CONNECT_SETTLE_BOUND_MS,
28
+ } from "@stigmer/sdk";
23
29
  import { CliExitError, ExitCode, UsageError } from "../../errors/index.js";
24
30
  import { defaultRegistry } from "../../registry/index.js";
25
31
  import { PlaceholderResolutionError } from "../mcp/placeholder-resolver.js";
@@ -42,8 +48,10 @@ export interface ConnectOptions {
42
48
  readonly pushTimeoutMs?: number;
43
49
  readonly dryRun: boolean;
44
50
  readonly envOverrides: readonly string[];
45
- /** Backend type, for resolving the OAuth web-console URL. */
46
- readonly backendType: BackendType;
51
+ /** The web console origin for the OAuth flow (resolveConsoleURL). */
52
+ readonly consoleURL: string;
53
+ /** Probe the console before opening the browser (local daemon only). */
54
+ readonly probeLocalConsole: boolean;
47
55
  /** Whether an interactive terminal is available to run the OAuth flow. */
48
56
  readonly interactive: boolean;
49
57
  }
@@ -63,23 +71,34 @@ export interface ConnectResult {
63
71
  }
64
72
 
65
73
  /** Connect to an MCP server and discover its capabilities (push or dry-run). */
66
- export async function connectMcpServer(client: Stigmer, opts: ConnectOptions): Promise<ConnectResult> {
74
+ export async function connectMcpServer(
75
+ client: Stigmer,
76
+ opts: ConnectOptions,
77
+ ): Promise<ConnectResult> {
67
78
  const server = await resolveMcpServer(client, opts.reference, opts.org);
68
79
 
69
80
  if (opts.dryRun) {
70
- if (server.spec === undefined) throw new UsageError("MCP server has no spec; cannot discover capabilities");
81
+ if (server.spec === undefined)
82
+ throw new UsageError(
83
+ "MCP server has no spec; cannot discover capabilities",
84
+ );
71
85
  // An oauth_only endpoint rejects static tokens, and the OAuth token lives in
72
86
  // the backend's managed environment — never on the caller's machine. So local
73
87
  // discovery cannot authenticate it; say so plainly instead of failing on a 401.
74
88
  if (isOAuthOnly(server)) throw oauthOnlyDryRunError(server, opts.reference);
75
89
  try {
76
- const capabilities = await localDiscover(server.spec, opts.envOverrides, opts.timeoutMs);
90
+ const capabilities = await localDiscover(
91
+ server.spec,
92
+ opts.envOverrides,
93
+ opts.timeoutMs,
94
+ );
77
95
  return { server, capabilities, updated: undefined };
78
96
  } catch (err) {
79
97
  // A ${VAR} placeholder that could not be resolved is a configuration
80
98
  // problem, not a discovery failure — surface it as actionable guidance
81
99
  // instead of a raw resolver error or a cryptic subprocess crash.
82
- if (err instanceof PlaceholderResolutionError) throw unresolvedEnvError(server, err);
100
+ if (err instanceof PlaceholderResolutionError)
101
+ throw unresolvedEnvError(server, err);
83
102
  throw err;
84
103
  }
85
104
  }
@@ -150,18 +169,29 @@ async function serverSideConnect(
150
169
 
151
170
  // Resolve a reference (id, org/slug, or bare slug) to an McpServer. Mirrors Go's
152
171
  // GetFromBackend.
153
- async function resolveMcpServer(client: Stigmer, reference: string, org: string): Promise<McpServer> {
172
+ async function resolveMcpServer(
173
+ client: Stigmer,
174
+ reference: string,
175
+ org: string,
176
+ ): Promise<McpServer> {
154
177
  const idPrefix = defaultRegistry().getByAlias("mcp-server")?.idPrefix ?? "";
155
178
  const parsed = parseReference(reference, org, idPrefix);
156
179
  if (parsed.kind === "id") return client.mcpServer.get(parsed.id);
157
- return client.mcpServer.getByReference({ org: parsed.org, slug: parsed.slug });
180
+ return client.mcpServer.getByReference({
181
+ org: parsed.org,
182
+ slug: parsed.slug,
183
+ });
158
184
  }
159
185
 
160
186
  // Ensure an OAuth grant exists before connecting an auth-configured server that
161
187
  // was given no --env credentials. On an interactive terminal, run the browser
162
188
  // flow and wait for the grant; otherwise stop with actionable guidance so
163
189
  // scripted callers get a clean, stable failure instead of a 5-minute block.
164
- async function ensureOAuthSatisfied(client: Stigmer, server: McpServer, opts: ConnectOptions): Promise<void> {
190
+ async function ensureOAuthSatisfied(
191
+ client: Stigmer,
192
+ server: McpServer,
193
+ opts: ConnectOptions,
194
+ ): Promise<void> {
165
195
  if (!oauthRequired(server)) return;
166
196
 
167
197
  const oauthOnly = isOAuthOnly(server);
@@ -183,18 +213,32 @@ async function ensureOAuthSatisfied(client: Stigmer, server: McpServer, opts: Co
183
213
  );
184
214
  if (status.connected) return;
185
215
 
186
- if (!opts.interactive) throw oauthGuidanceError(server, opts.reference, oauthOnly);
216
+ if (!opts.interactive)
217
+ throw oauthGuidanceError(server, opts.reference, oauthOnly);
187
218
 
188
219
  const { runOAuthFlow } = await import("./oauth.js");
189
- await runOAuthFlow({ client, server, org: opts.org, backendType: opts.backendType });
220
+ await runOAuthFlow({
221
+ client,
222
+ server,
223
+ org: opts.org,
224
+ consoleURL: opts.consoleURL,
225
+ probeLocalConsole: opts.probeLocalConsole,
226
+ });
190
227
  }
191
228
 
192
229
  // Turn a strict-resolution failure into actionable guidance. A declared-but-unset
193
230
  // variable is the user's to provide; an undeclared one is a bug in the server
194
231
  // definition. Both are far clearer than the cryptic subprocess crash (ENOENT on a
195
232
  // literal "${VAR}" path) that motivated issue #141.
196
- function unresolvedEnvError(server: McpServer, err: PlaceholderResolutionError): UsageError {
197
- const slug = server.metadata?.slug ?? server.metadata?.name ?? server.metadata?.id ?? "this server";
233
+ function unresolvedEnvError(
234
+ server: McpServer,
235
+ err: PlaceholderResolutionError,
236
+ ): UsageError {
237
+ const slug =
238
+ server.metadata?.slug ??
239
+ server.metadata?.name ??
240
+ server.metadata?.id ??
241
+ "this server";
198
242
  const decl = server.spec?.env?.[err.variableName];
199
243
  if (decl) {
200
244
  const hint = decl.description !== "" ? ` (${decl.description})` : "";
@@ -210,7 +254,11 @@ function unresolvedEnvError(server: McpServer, err: PlaceholderResolutionError):
210
254
  );
211
255
  }
212
256
 
213
- function oauthGuidanceError(server: McpServer, reference: string, oauthOnly: boolean): UsageError {
257
+ function oauthGuidanceError(
258
+ server: McpServer,
259
+ reference: string,
260
+ oauthOnly: boolean,
261
+ ): UsageError {
214
262
  const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
215
263
  const choices = [
216
264
  " - Re-run this command in an interactive terminal to complete OAuth in your browser",
@@ -218,7 +266,9 @@ function oauthGuidanceError(server: McpServer, reference: string, oauthOnly: boo
218
266
  // Only offer the manual-token route for servers that actually accept one.
219
267
  // Suggesting it for an oauth_only endpoint would send the user down a dead end.
220
268
  if (!oauthOnly) {
221
- choices.push(` - Provide credentials directly: stigmer connect mcp-server ${slug} --env TOKEN=...`);
269
+ choices.push(
270
+ ` - Provide credentials directly: stigmer connect mcp-server ${slug} --env TOKEN=...`,
271
+ );
222
272
  }
223
273
  return new UsageError(
224
274
  `MCP server '${slug}' requires OAuth authentication, which needs an interactive terminal.\n\n` +
@@ -241,7 +291,10 @@ function oauthOnlyEnvError(server: McpServer, reference: string): UsageError {
241
291
  // --dry-run discovers locally, but an oauth_only endpoint needs an OAuth token
242
292
  // that only the backend can obtain and store — there is nothing valid to send
243
293
  // from the caller's machine. Say so plainly instead of attempting a doomed 401.
244
- function oauthOnlyDryRunError(server: McpServer, reference: string): UsageError {
294
+ function oauthOnlyDryRunError(
295
+ server: McpServer,
296
+ reference: string,
297
+ ): UsageError {
245
298
  const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
246
299
  return new UsageError(
247
300
  `MCP server '${slug}' requires OAuth, so --dry-run cannot discover it locally — its endpoint only accepts an OAuth token that the connected backend obtains for you.\n` +
@@ -3,7 +3,12 @@ import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1
3
3
  import type { Stigmer } from "@stigmer/sdk";
4
4
  import { afterEach, describe, expect, it, vi } from "vitest";
5
5
  import { UsageError } from "../../errors/index.js";
6
- import { browserCommand, runOAuthFlow, waitForOAuthGrant, type OAuthFlowDeps } from "./oauth.js";
6
+ import {
7
+ browserCommand,
8
+ runOAuthFlow,
9
+ waitForOAuthGrant,
10
+ type OAuthFlowDeps,
11
+ } from "./oauth.js";
7
12
 
8
13
  const server = create(McpServerSchema, {
9
14
  metadata: { id: "mcp_1", slug: "github", name: "GitHub" },
@@ -11,7 +16,10 @@ const server = create(McpServerSchema, {
11
16
 
12
17
  // A fake client whose getOAuthGrantStatus reports `connected` once the call
13
18
  // count reaches `connectOnCall` (1 = first poll). Records the call count.
14
- function fakeClient(connectOnCall: number): { client: Stigmer; calls: () => number } {
19
+ function fakeClient(connectOnCall: number): {
20
+ client: Stigmer;
21
+ calls: () => number;
22
+ } {
15
23
  let calls = 0;
16
24
  const client = {
17
25
  mcpServer: {
@@ -28,9 +36,18 @@ const noopSleep = async (): Promise<void> => {};
28
36
 
29
37
  describe("browserCommand", () => {
30
38
  it("maps each supported platform to its opener", () => {
31
- expect(browserCommand("darwin", "https://x")).toEqual(["open", ["https://x"]]);
32
- expect(browserCommand("linux", "https://x")).toEqual(["xdg-open", ["https://x"]]);
33
- expect(browserCommand("win32", "https://x")).toEqual(["rundll32", ["url.dll,FileProtocolHandler", "https://x"]]);
39
+ expect(browserCommand("darwin", "https://x")).toEqual([
40
+ "open",
41
+ ["https://x"],
42
+ ]);
43
+ expect(browserCommand("linux", "https://x")).toEqual([
44
+ "xdg-open",
45
+ ["https://x"],
46
+ ]);
47
+ expect(browserCommand("win32", "https://x")).toEqual([
48
+ "rundll32",
49
+ ["url.dll,FileProtocolHandler", "https://x"],
50
+ ]);
34
51
  });
35
52
 
36
53
  it("returns no command for unsupported platforms", () => {
@@ -45,7 +62,8 @@ describe("waitForOAuthGrant", () => {
45
62
  client,
46
63
  server,
47
64
  org: "acme",
48
- backendType: "cloud",
65
+ consoleURL: "https://app.stigmer.ai",
66
+ probeLocalConsole: false,
49
67
  now: () => 0,
50
68
  sleep: noopSleep,
51
69
  log: () => {},
@@ -63,7 +81,8 @@ describe("waitForOAuthGrant", () => {
63
81
  client,
64
82
  server,
65
83
  org: "acme",
66
- backendType: "cloud",
84
+ consoleURL: "https://app.stigmer.ai",
85
+ probeLocalConsole: false,
67
86
  now: () => clock.shift() ?? 10 * 60 * 1000,
68
87
  sleep: noopSleep,
69
88
  log: () => {},
@@ -85,13 +104,16 @@ describe("runOAuthFlow", () => {
85
104
  client,
86
105
  server,
87
106
  org: "acme",
88
- backendType: "cloud",
107
+ consoleURL: "https://app.stigmer.ai",
108
+ probeLocalConsole: false,
89
109
  openBrowser,
90
110
  now: () => 0,
91
111
  sleep: noopSleep,
92
112
  log: () => {},
93
113
  });
94
- expect(openBrowser).toHaveBeenCalledWith("https://app.stigmer.ai/acme/mcp-servers/github");
114
+ expect(openBrowser).toHaveBeenCalledWith(
115
+ "https://app.stigmer.ai/acme/mcp-servers/github",
116
+ );
95
117
  });
96
118
 
97
119
  it("aborts before opening the browser when the local console is unreachable", async () => {
@@ -102,7 +124,8 @@ describe("runOAuthFlow", () => {
102
124
  client,
103
125
  server,
104
126
  org: "acme",
105
- backendType: "local",
127
+ consoleURL: "http://localhost:7234",
128
+ probeLocalConsole: true,
106
129
  probeConsole: async () => false,
107
130
  openBrowser,
108
131
  now: () => 0,
@@ -120,13 +143,16 @@ describe("runOAuthFlow", () => {
120
143
  client,
121
144
  server,
122
145
  org: "acme",
123
- backendType: "local",
146
+ consoleURL: "http://localhost:7234",
147
+ probeLocalConsole: true,
124
148
  probeConsole: async () => true,
125
149
  openBrowser,
126
150
  now: () => 0,
127
151
  sleep: noopSleep,
128
152
  log: () => {},
129
153
  });
130
- expect(openBrowser).toHaveBeenCalledWith("http://localhost:8234/acme/mcp-servers/github");
154
+ expect(openBrowser).toHaveBeenCalledWith(
155
+ "http://localhost:7234/acme/mcp-servers/github",
156
+ );
131
157
  });
132
158
  });
@@ -15,8 +15,6 @@ import { create } from "@bufbuild/protobuf";
15
15
  import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
16
16
  import { GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
17
17
  import type { Stigmer } from "@stigmer/sdk";
18
- import type { BackendType } from "../../config/config.js";
19
- import { resolveConsoleURL } from "../../config/index.js";
20
18
  import { UsageError } from "../../errors/index.js";
21
19
 
22
20
  const POLL_INTERVAL_MS = 3000;
@@ -28,7 +26,11 @@ export interface OAuthFlowDeps {
28
26
  readonly client: Stigmer;
29
27
  readonly server: McpServer;
30
28
  readonly org: string;
31
- readonly backendType: BackendType;
29
+ /** The web console origin (resolveConsoleURL over the caller's config). */
30
+ readonly consoleURL: string;
31
+ /** Probe the console before opening the browser (the local daemon's
32
+ * console may simply not be running; remote consoles answer or 404). */
33
+ readonly probeLocalConsole: boolean;
32
34
  /** Opens the page URL in the user's browser. Defaults to the OS opener. */
33
35
  readonly openBrowser?: (url: string) => Promise<void>;
34
36
  /** Probes whether the local web console is reachable. Defaults to a 2s GET. */
@@ -48,11 +50,12 @@ export interface OAuthFlowDeps {
48
50
  */
49
51
  export async function runOAuthFlow(deps: OAuthFlowDeps): Promise<void> {
50
52
  const log = deps.log ?? stderrLog;
51
- const consoleURL = resolveConsoleURL(deps.backendType);
53
+ const consoleURL = deps.consoleURL;
52
54
 
53
- if (deps.backendType === "local") {
55
+ if (deps.probeLocalConsole) {
54
56
  const probe = deps.probeConsole ?? probeWebConsole;
55
- if (!(await probe(consoleURL))) throw consoleUnavailableError(deps.server.metadata?.slug ?? "<slug>");
57
+ if (!(await probe(consoleURL)))
58
+ throw consoleUnavailableError(deps.server.metadata?.slug ?? "<slug>");
56
59
  }
57
60
 
58
61
  const slug = deps.server.metadata?.slug ?? "";
@@ -67,7 +70,9 @@ export async function runOAuthFlow(deps: OAuthFlowDeps): Promise<void> {
67
70
  try {
68
71
  await open(pageURL);
69
72
  } catch {
70
- log(" Could not open browser automatically. Please open the URL above in your browser.");
73
+ log(
74
+ " Could not open browser automatically. Please open the URL above in your browser.",
75
+ );
71
76
  }
72
77
 
73
78
  await waitForOAuthGrant(deps);
@@ -89,7 +94,9 @@ export async function waitForOAuthGrant(deps: OAuthFlowDeps): Promise<void> {
89
94
  for (;;) {
90
95
  await sleep(POLL_INTERVAL_MS);
91
96
  if (now() >= deadline) {
92
- throw new UsageError("timed out waiting for OAuth connection — please try again");
97
+ throw new UsageError(
98
+ "timed out waiting for OAuth connection — please try again",
99
+ );
93
100
  }
94
101
  if (await checkOAuthGrant(deps.client, serverId, deps.org)) {
95
102
  log("OAuth connection established.\n");
@@ -99,7 +106,11 @@ export async function waitForOAuthGrant(deps: OAuthFlowDeps): Promise<void> {
99
106
  }
100
107
 
101
108
  /** Query the backend for an existing/just-created OAuth grant. */
102
- export async function checkOAuthGrant(client: Stigmer, mcpServerId: string, org: string): Promise<boolean> {
109
+ export async function checkOAuthGrant(
110
+ client: Stigmer,
111
+ mcpServerId: string,
112
+ org: string,
113
+ ): Promise<boolean> {
103
114
  const status = await client.mcpServer.getOAuthGrantStatus(
104
115
  create(GetOAuthGrantStatusInputSchema, { resourceId: mcpServerId, org }),
105
116
  );
@@ -110,7 +121,9 @@ export async function checkOAuthGrant(client: Stigmer, mcpServerId: string, org:
110
121
  // an error status) means it's serving; a network/timeout failure means it isn't.
111
122
  async function probeWebConsole(url: string): Promise<boolean> {
112
123
  try {
113
- const response = await fetch(url, { signal: AbortSignal.timeout(CONSOLE_PROBE_TIMEOUT_MS) });
124
+ const response = await fetch(url, {
125
+ signal: AbortSignal.timeout(CONSOLE_PROBE_TIMEOUT_MS),
126
+ });
114
127
  // Drain the body so the socket can close promptly.
115
128
  await response.body?.cancel();
116
129
  return true;
@@ -153,7 +166,10 @@ function openInBrowser(url: string): Promise<void> {
153
166
  }
154
167
 
155
168
  /** The OS-specific command + args to open a URL. Exported for tests. */
156
- export function browserCommand(platform: NodeJS.Platform, url: string): [string | undefined, string[]] {
169
+ export function browserCommand(
170
+ platform: NodeJS.Platform,
171
+ url: string,
172
+ ): [string | undefined, string[]] {
157
173
  switch (platform) {
158
174
  case "darwin":
159
175
  return ["open", [url]];
@@ -12,7 +12,7 @@ import { classify, ExitCode, UsageError } from "../errors/index.js";
12
12
  import { shareAgent } from "./share.js";
13
13
 
14
14
  const CLOUD = { appOrigin: "https://app.stigmer.ai", isLocal: false };
15
- const LOCAL = { appOrigin: "http://localhost:8234", isLocal: true };
15
+ const LOCAL = { appOrigin: "http://localhost:7234", isLocal: true };
16
16
 
17
17
  function makeAgent(): Agent {
18
18
  return create(AgentSchema, {
@@ -350,7 +350,7 @@ describe("shareAgent output", () => {
350
350
  const result = await shareAgent(client, "acme/support-agent", "acme", { enabled: true, ...LOCAL });
351
351
 
352
352
  const link = result.sections.find((s) => s.title === "Public chat link");
353
- expect(link?.items).toEqual(["http://localhost:8234/chat/acme/support-agent"]);
353
+ expect(link?.items).toEqual(["http://localhost:7234/chat/acme/support-agent"]);
354
354
  expect(result.hints.some((h) => h.includes("Stigmer Cloud"))).toBe(true);
355
355
  });
356
356