@stigmer/cli 3.1.6 → 3.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/cli",
3
- "version": "3.1.6",
3
+ "version": "3.1.7",
4
4
  "description": "Stigmer command-line interface — manage agents, workflows, MCP servers, skills, and executions from the terminal",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -35,10 +35,10 @@
35
35
  "@connectrpc/connect": "^2.0.0",
36
36
  "@connectrpc/connect-node": "^2.1.1",
37
37
  "@modelcontextprotocol/sdk": "^1.26.0",
38
- "@stigmer/ink": "3.1.6",
39
- "@stigmer/mcp-server": "3.1.6",
40
- "@stigmer/protos": "3.1.6",
41
- "@stigmer/sdk": "3.1.6",
38
+ "@stigmer/ink": "3.1.7",
39
+ "@stigmer/mcp-server": "3.1.7",
40
+ "@stigmer/protos": "3.1.7",
41
+ "@stigmer/sdk": "3.1.7",
42
42
  "commander": "^12.1.0",
43
43
  "fflate": "^0.8.3",
44
44
  "ink": "^7.0.0",
@@ -1 +1 @@
1
- {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../../src/resources/connect/connect.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wDAAwD,CAAC;AAExF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2DAA2D,CAAC;AAExG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAO1D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAC1D,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;CACzC;AAED,gFAAgF;AAChF,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CAmBpG"}
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../../src/resources/connect/connect.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wDAAwD,CAAC;AAExF,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,2DAA2D,CAAC;AAExG,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAQ1D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,6DAA6D;IAC7D,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC;IAClC,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,YAAY,EAAE,sBAAsB,GAAG,SAAS,CAAC;IAC1D,iFAAiF;IACjF,QAAQ,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC;CACzC;AAED,gFAAgF;AAChF,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,aAAa,CAAC,CA+BpG"}
@@ -12,6 +12,7 @@ import { create } from "@bufbuild/protobuf";
12
12
  import { ConnectInputSchema, GetOAuthGrantStatusInputSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/io_pb";
13
13
  import { UsageError } from "../../errors/index.js";
14
14
  import { defaultRegistry } from "../../registry/index.js";
15
+ import { PlaceholderResolutionError } from "../mcp/placeholder-resolver.js";
15
16
  import { buildRuntimeEnv } from "../mcp/runtime-env.js";
16
17
  import { parseReference } from "../reference.js";
17
18
  import { localDiscover } from "./discover.js";
@@ -21,8 +22,23 @@ export async function connectMcpServer(client, opts) {
21
22
  if (opts.dryRun) {
22
23
  if (server.spec === undefined)
23
24
  throw new UsageError("MCP server has no spec; cannot discover capabilities");
24
- const capabilities = await localDiscover(server.spec, opts.envOverrides, opts.timeoutMs);
25
- return { server, capabilities, updated: undefined };
25
+ // An oauth_only endpoint rejects static tokens, and the OAuth token lives in
26
+ // the backend's managed environment — never on the caller's machine. So local
27
+ // discovery cannot authenticate it; say so plainly instead of failing on a 401.
28
+ if (isOAuthOnly(server))
29
+ throw oauthOnlyDryRunError(server, opts.reference);
30
+ try {
31
+ const capabilities = await localDiscover(server.spec, opts.envOverrides, opts.timeoutMs);
32
+ return { server, capabilities, updated: undefined };
33
+ }
34
+ catch (err) {
35
+ // A ${VAR} placeholder that could not be resolved is a configuration
36
+ // problem, not a discovery failure — surface it as actionable guidance
37
+ // instead of a raw resolver error or a cryptic subprocess crash.
38
+ if (err instanceof PlaceholderResolutionError)
39
+ throw unresolvedEnvError(server, err);
40
+ throw err;
41
+ }
26
42
  }
27
43
  await ensureOAuthSatisfied(client, server, opts);
28
44
  const updated = await client.mcpServer.connect(create(ConnectInputSchema, {
@@ -46,8 +62,18 @@ async function resolveMcpServer(client, reference, org) {
46
62
  // flow and wait for the grant; otherwise stop with actionable guidance so
47
63
  // scripted callers get a clean, stable failure instead of a 5-minute block.
48
64
  async function ensureOAuthSatisfied(client, server, opts) {
49
- if (!oauthRequired(server) || opts.envOverrides.length > 0)
65
+ if (!oauthRequired(server))
50
66
  return;
67
+ const oauthOnly = isOAuthOnly(server);
68
+ // A manually supplied token is a valid bypass for a normal OAuth server (many
69
+ // vendors also accept a PAT), but an oauth_only endpoint rejects static tokens
70
+ // outright — so --env cannot connect it. Fail with guidance rather than push a
71
+ // token the endpoint will reject.
72
+ if (opts.envOverrides.length > 0) {
73
+ if (!oauthOnly)
74
+ return;
75
+ throw oauthOnlyEnvError(server, opts.reference);
76
+ }
51
77
  const status = await client.mcpServer.getOAuthGrantStatus(create(GetOAuthGrantStatusInputSchema, {
52
78
  resourceId: server.metadata?.id ?? "",
53
79
  org: opts.org,
@@ -55,18 +81,60 @@ async function ensureOAuthSatisfied(client, server, opts) {
55
81
  if (status.connected)
56
82
  return;
57
83
  if (!opts.interactive)
58
- throw oauthGuidanceError(server, opts.reference);
84
+ throw oauthGuidanceError(server, opts.reference, oauthOnly);
59
85
  const { runOAuthFlow } = await import("./oauth.js");
60
86
  await runOAuthFlow({ client, server, org: opts.org, backendType: opts.backendType });
61
87
  }
62
- function oauthGuidanceError(server, reference) {
88
+ // Turn a strict-resolution failure into actionable guidance. A declared-but-unset
89
+ // variable is the user's to provide; an undeclared one is a bug in the server
90
+ // definition. Both are far clearer than the cryptic subprocess crash (ENOENT on a
91
+ // literal "${VAR}" path) that motivated issue #141.
92
+ function unresolvedEnvError(server, err) {
93
+ const slug = server.metadata?.slug ?? server.metadata?.name ?? server.metadata?.id ?? "this server";
94
+ const decl = server.spec?.env?.[err.variableName];
95
+ if (decl) {
96
+ const hint = decl.description !== "" ? ` (${decl.description})` : "";
97
+ return new UsageError(`MCP server '${slug}' needs environment variable ${err.variableName}${hint}, but it is not set.\n` +
98
+ `Provide it with --env ${err.variableName}=<value> or export it in your shell before running --dry-run.`);
99
+ }
100
+ const where = err.context !== undefined ? ` in its ${err.context}` : "";
101
+ return new UsageError(`MCP server '${slug}' references \${${err.variableName}}${where} but does not declare ${err.variableName} under spec.env.\n` +
102
+ `This is a problem with the server definition — declare ${err.variableName} in the server's env, or remove the placeholder.`);
103
+ }
104
+ function oauthGuidanceError(server, reference, oauthOnly) {
63
105
  const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
106
+ const choices = [
107
+ " - Re-run this command in an interactive terminal to complete OAuth in your browser",
108
+ ];
109
+ // Only offer the manual-token route for servers that actually accept one.
110
+ // Suggesting it for an oauth_only endpoint would send the user down a dead end.
111
+ if (!oauthOnly) {
112
+ choices.push(` - Provide credentials directly: stigmer connect mcp-server ${slug} --env TOKEN=...`);
113
+ }
64
114
  return new UsageError(`MCP server '${slug}' requires OAuth authentication, which needs an interactive terminal.\n\n` +
65
- "To connect, choose one of:\n" +
66
- " - Re-run this command in an interactive terminal to complete OAuth in your browser\n" +
67
- ` - Provide credentials directly: stigmer connect mcp-server ${slug} --env TOKEN=...`);
115
+ `To connect${oauthOnly ? "" : ", choose one of"}:\n` +
116
+ choices.join("\n"));
117
+ }
118
+ // An oauth_only server whose endpoint rejects static tokens was given a manual
119
+ // token via --env. Explain that OAuth is the only path rather than pushing a
120
+ // credential the endpoint will reject with an opaque 401.
121
+ function oauthOnlyEnvError(server, reference) {
122
+ const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
123
+ return new UsageError(`MCP server '${slug}' requires OAuth and rejects manually-entered tokens, so --env cannot connect it.\n` +
124
+ `Re-run without --env in an interactive terminal to sign in: stigmer connect mcp-server ${slug}`);
125
+ }
126
+ // --dry-run discovers locally, but an oauth_only endpoint needs an OAuth token
127
+ // that only the backend can obtain and store — there is nothing valid to send
128
+ // from the caller's machine. Say so plainly instead of attempting a doomed 401.
129
+ function oauthOnlyDryRunError(server, reference) {
130
+ const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
131
+ return new UsageError(`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` +
132
+ `Run it for real and sign in when prompted: stigmer connect mcp-server ${slug}`);
68
133
  }
69
134
  function oauthRequired(server) {
70
135
  return (server.spec?.auth?.targetEnvVar ?? "") !== "";
71
136
  }
137
+ function isOAuthOnly(server) {
138
+ return server.spec?.auth?.oauthOnly === true;
139
+ }
72
140
  //# sourceMappingURL=connect.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"connect.js","sourceRoot":"","sources":["../../../src/resources/connect/connect.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"connect.js","sourceRoot":"","sources":["../../../src/resources/connect/connect.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/resources/connect/discover.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yDAAyD,CAAC;AAC7F,OAAO,EACL,KAAK,sBAAsB,EAI5B,MAAM,2DAA2D,CAAC;AAMnE,uFAAuF;AACvF,wBAAsB,aAAa,CACjC,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,CAAC,CA0CjC"}
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../../src/resources/connect/discover.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yDAAyD,CAAC;AAC7F,OAAO,EACL,KAAK,sBAAsB,EAI5B,MAAM,2DAA2D,CAAC;AAOnE,uFAAuF;AACvF,wBAAsB,aAAa,CACjC,IAAI,EAAE,aAAa,EACnB,YAAY,EAAE,SAAS,MAAM,EAAE,EAC/B,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,sBAAsB,CAAC,CA0CjC"}
@@ -12,7 +12,8 @@
12
12
  import { create } from "@bufbuild/protobuf";
13
13
  import { DiscoveredCapabilitiesSchema, DiscoveredResourceTemplateSchema, DiscoveredToolSchema, } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/status_pb";
14
14
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
15
- import { mergeProcessEnv } from "../mcp/runtime-env.js";
15
+ import { mergeProcessEnv, resolveDeclaredEnvValues } from "../mcp/runtime-env.js";
16
+ import { resolveHeaders, resolvePlaceholders } from "../mcp/placeholder-resolver.js";
16
17
  /** Discover an MCP server's capabilities locally without persisting to the backend. */
17
18
  export async function localDiscover(spec, envOverrides, timeoutMs) {
18
19
  const { transport, readStderr } = await buildTransport(spec, envOverrides);
@@ -53,14 +54,21 @@ export async function localDiscover(spec, envOverrides, timeoutMs) {
53
54
  }
54
55
  }
55
56
  async function buildTransport(spec, envOverrides) {
57
+ // ${VAR} placeholders in args/headers resolve against the exact same env the
58
+ // backend hands the runner for real connect (declared keys from the OS env +
59
+ // --env overrides) — so dry-run is a faithful preview. Resolution is strict:
60
+ // an unresolved placeholder throws before any subprocess is spawned, matching
61
+ // the proto contract (never pass a literal "${VAR}" to the server).
62
+ const resolutionEnv = resolveDeclaredEnvValues(spec.env ?? {}, envOverrides);
56
63
  if (spec.serverType?.case === "stdio") {
57
64
  const { command, args, workingDir } = spec.serverType.value;
58
65
  if (command === "")
59
66
  throw new Error("stdio transport requires a command");
67
+ const resolvedArgs = args.map((arg, i) => resolvePlaceholders(arg, resolutionEnv, `stdio arg[${i}]`));
60
68
  const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
61
69
  const transport = new StdioClientTransport({
62
70
  command,
63
- args: [...args],
71
+ args: resolvedArgs,
64
72
  cwd: workingDir !== "" ? workingDir : undefined,
65
73
  env: mergeProcessEnv([...envOverrides, ...goRunEnvOverrides(command, args)]),
66
74
  // "pipe" exposes the child stderr as a PassThrough immediately, so we can
@@ -78,8 +86,8 @@ async function buildTransport(spec, envOverrides) {
78
86
  if (url === "")
79
87
  throw new Error("HTTP transport requires a URL");
80
88
  const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
81
- const requestInit = Object.keys(headers).length > 0 ? { headers: { ...headers } } : undefined;
82
- const transport = new StreamableHTTPClientTransport(new URL(url), requestInit ? { requestInit } : undefined);
89
+ const resolvedHeaders = Object.keys(headers).length > 0 ? resolveHeaders(headers, resolutionEnv) : undefined;
90
+ const transport = new StreamableHTTPClientTransport(new URL(url), resolvedHeaders ? { requestInit: { headers: resolvedHeaders } } : undefined);
83
91
  return { transport, readStderr: () => "" };
84
92
  }
85
93
  throw new Error("MCP server has no transport configured (expected stdio or http)");
@@ -1 +1 @@
1
- {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/resources/connect/discover.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"discover.js","sourceRoot":"","sources":["../../../src/resources/connect/discover.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export declare class PlaceholderResolutionError extends Error {
2
+ readonly variableName: string;
3
+ readonly context?: string | undefined;
4
+ constructor(variableName: string, context?: string | undefined);
5
+ }
6
+ /**
7
+ * Resolve all ${VAR_NAME} placeholders in a template string.
8
+ *
9
+ * @throws PlaceholderResolutionError if any placeholder cannot be resolved.
10
+ */
11
+ export declare function resolvePlaceholders(template: string, envVars: Record<string, string>, context?: string): string;
12
+ /**
13
+ * Resolve placeholders in every value of a headers map.
14
+ *
15
+ * @throws PlaceholderResolutionError if any header value contains an
16
+ * unresolvable placeholder.
17
+ */
18
+ export declare function resolveHeaders(headers: Record<string, string>, envVars: Record<string, string>): Record<string, string>;
19
+ //# sourceMappingURL=placeholder-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholder-resolver.d.ts","sourceRoot":"","sources":["../../../src/resources/mcp/placeholder-resolver.ts"],"names":[],"mappings":"AAeA,qBAAa,0BAA2B,SAAQ,KAAK;aAEjC,YAAY,EAAE,MAAM;aACpB,OAAO,CAAC,EAAE,MAAM;gBADhB,YAAY,EAAE,MAAM,EACpB,OAAO,CAAC,EAAE,MAAM,YAAA;CASnC;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CAOR;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMxB"}
@@ -0,0 +1,52 @@
1
+ // Strict placeholder resolver for ${VAR_NAME} syntax in MCP server configs.
2
+ //
3
+ // The MCP server spec (StdioServerConfig.args, HttpServerConfig.headers) allows
4
+ // values to reference declared env vars with ${VAR_NAME}. The proto contract is
5
+ // explicit that resolution is strict: a missing variable must produce a clear
6
+ // error rather than passing a literal "${VAR}" to the subprocess/server.
7
+ //
8
+ // This mirrors the runner's shared resolver (backend/services/runner/src/shared/
9
+ // placeholder-resolver.ts) so `connect --dry-run` (which discovers locally)
10
+ // behaves identically to real connect (which resolves on the runner). Kept
11
+ // intentionally dependency-free and semantically identical so the two can be
12
+ // collapsed into one shared package later without any behavior change.
13
+ const PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
14
+ export class PlaceholderResolutionError extends Error {
15
+ variableName;
16
+ context;
17
+ constructor(variableName, context) {
18
+ const where = context ? ` in ${context}` : "";
19
+ super(`Unresolved placeholder \${${variableName}}${where}: ` +
20
+ `variable is not present in the execution environment`);
21
+ this.variableName = variableName;
22
+ this.context = context;
23
+ this.name = "PlaceholderResolutionError";
24
+ }
25
+ }
26
+ /**
27
+ * Resolve all ${VAR_NAME} placeholders in a template string.
28
+ *
29
+ * @throws PlaceholderResolutionError if any placeholder cannot be resolved.
30
+ */
31
+ export function resolvePlaceholders(template, envVars, context) {
32
+ return template.replace(PLACEHOLDER_RE, (_match, varName) => {
33
+ if (varName in envVars) {
34
+ return envVars[varName];
35
+ }
36
+ throw new PlaceholderResolutionError(varName, context);
37
+ });
38
+ }
39
+ /**
40
+ * Resolve placeholders in every value of a headers map.
41
+ *
42
+ * @throws PlaceholderResolutionError if any header value contains an
43
+ * unresolvable placeholder.
44
+ */
45
+ export function resolveHeaders(headers, envVars) {
46
+ const resolved = {};
47
+ for (const [key, value] of Object.entries(headers)) {
48
+ resolved[key] = resolvePlaceholders(value, envVars, `header "${key}"`);
49
+ }
50
+ return resolved;
51
+ }
52
+ //# sourceMappingURL=placeholder-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholder-resolver.js","sourceRoot":"","sources":["../../../src/resources/mcp/placeholder-resolver.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,8EAA8E;AAC9E,yEAAyE;AACzE,EAAE;AACF,iFAAiF;AACjF,4EAA4E;AAC5E,2EAA2E;AAC3E,6EAA6E;AAC7E,uEAAuE;AAEvE,MAAM,cAAc,GAAG,iCAAiC,CAAC;AAEzD,MAAM,OAAO,0BAA2B,SAAQ,KAAK;IAEjC;IACA;IAFlB,YACkB,YAAoB,EACpB,OAAgB;QAEhC,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC9C,KAAK,CACH,6BAA6B,YAAY,IAAI,KAAK,IAAI;YACpD,sDAAsD,CACzD,CAAC;QAPc,iBAAY,GAAZ,YAAY,CAAQ;QACpB,YAAO,GAAP,OAAO,CAAS;QAOhC,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC3C,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CACjC,QAAgB,EAChB,OAA+B,EAC/B,OAAgB;IAEhB,OAAO,QAAQ,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,MAAM,EAAE,OAAe,EAAE,EAAE;QAClE,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;YACvB,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;QACD,MAAM,IAAI,0BAA0B,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,OAA+B,EAC/B,OAA+B;IAE/B,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACnD,QAAQ,CAAC,GAAG,CAAC,GAAG,mBAAmB,CAAC,KAAK,EAAE,OAAO,EAAE,WAAW,GAAG,GAAG,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
@@ -8,6 +8,22 @@ type ExecutionValue = ReturnType<typeof create<typeof ExecutionValueSchema>>;
8
8
  * clear usage error rather than a silently-ignored credential.
9
9
  */
10
10
  export declare function parseEnvOverrides(overrides: readonly string[]): Record<string, string>;
11
+ /**
12
+ * Resolve the values behind a server's declared env vars: declared keys pulled
13
+ * from the OS environment (non-empty only), with `--env` overrides layered on
14
+ * top (override wins; an undeclared override is still included).
15
+ *
16
+ * This is the single source of truth for "which env value is in play". It backs
17
+ * both the runtime_env sent to the backend (buildRuntimeEnv) and the ${VAR}
18
+ * placeholder resolution done locally for `--dry-run` (discover.ts), so the two
19
+ * can never drift: dry-run resolves against the exact same map the backend hands
20
+ * the runner (minus platform-injected infra vars, which a local machine cannot
21
+ * know).
22
+ *
23
+ * `declarations` is the server's `spec.env` map; only its keys (the declared
24
+ * variable names) are read, so any map keyed by those names is accepted.
25
+ */
26
+ export declare function resolveDeclaredEnvValues(declarations: Record<string, unknown>, envOverrides?: readonly string[]): Record<string, string>;
11
27
  /**
12
28
  * Build the runtime_env map for the Connect RPC from the OS environment and the
13
29
  * supplied --env overrides. Only keys with a non-empty value are emitted.
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-env.d.ts","sourceRoot":"","sources":["../../../src/resources/mcp/runtime-env.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gEAAgE,CAAC;AACtG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wDAAwD,CAAC;AAGxF,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC;AAE7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUtF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EACjB,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAiBhC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASvF"}
1
+ {"version":3,"file":"runtime-env.d.ts","sourceRoot":"","sources":["../../../src/resources/mcp/runtime-env.ts"],"names":[],"mappings":"AAcA,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gEAAgE,CAAC;AACtG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wDAAwD,CAAC;AAGxF,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,OAAO,oBAAoB,CAAC,CAAC,CAAC;AAE7E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAUtF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,wBAAwB,CACtC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,SAAS,EACjB,YAAY,GAAE,SAAS,MAAM,EAAO,GACnC,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAUhC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASvF"}
@@ -30,21 +30,42 @@ export function parseEnvOverrides(overrides) {
30
30
  }
31
31
  return result;
32
32
  }
33
+ /**
34
+ * Resolve the values behind a server's declared env vars: declared keys pulled
35
+ * from the OS environment (non-empty only), with `--env` overrides layered on
36
+ * top (override wins; an undeclared override is still included).
37
+ *
38
+ * This is the single source of truth for "which env value is in play". It backs
39
+ * both the runtime_env sent to the backend (buildRuntimeEnv) and the ${VAR}
40
+ * placeholder resolution done locally for `--dry-run` (discover.ts), so the two
41
+ * can never drift: dry-run resolves against the exact same map the backend hands
42
+ * the runner (minus platform-injected infra vars, which a local machine cannot
43
+ * know).
44
+ *
45
+ * `declarations` is the server's `spec.env` map; only its keys (the declared
46
+ * variable names) are read, so any map keyed by those names is accepted.
47
+ */
48
+ export function resolveDeclaredEnvValues(declarations, envOverrides = []) {
49
+ const values = {};
50
+ for (const key of Object.keys(declarations)) {
51
+ const value = process.env[key];
52
+ if (value !== undefined && value !== "")
53
+ values[key] = value;
54
+ }
55
+ for (const [key, value] of Object.entries(parseEnvOverrides(envOverrides))) {
56
+ values[key] = value;
57
+ }
58
+ return values;
59
+ }
33
60
  /**
34
61
  * Build the runtime_env map for the Connect RPC from the OS environment and the
35
62
  * supplied --env overrides. Only keys with a non-empty value are emitted.
36
63
  */
37
64
  export function buildRuntimeEnv(server, envOverrides = []) {
38
65
  const declarations = server.spec?.env ?? {};
39
- const overrides = parseEnvOverrides(envOverrides);
66
+ const values = resolveDeclaredEnvValues(declarations, envOverrides);
40
67
  const runtime = {};
41
- for (const [key, decl] of Object.entries(declarations)) {
42
- const value = process.env[key];
43
- if (value !== undefined && value !== "") {
44
- runtime[key] = create(ExecutionValueSchema, { value, isSecret: decl.isSecret });
45
- }
46
- }
47
- for (const [key, value] of Object.entries(overrides)) {
68
+ for (const [key, value] of Object.entries(values)) {
48
69
  runtime[key] = create(ExecutionValueSchema, { value, isSecret: declarations[key]?.isSecret ?? false });
49
70
  }
50
71
  return runtime;
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-env.js","sourceRoot":"","sources":["../../../src/resources/mcp/runtime-env.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,+EAA+E;AAC/E,2EAA2E;AAC3E,EAAE;AACF,gDAAgD;AAChD,0EAA0E;AAC1E,uEAAuE;AACvE,EAAE;AACF,6EAA6E;AAC7E,2EAA2E;AAE3E,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,oBAAoB,EAAE,MAAM,gEAAgE,CAAC;AAEtG,OAAO,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAInD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,SAA4B;IAC5D,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,KAAK,IAAI,SAAS,EAAE,CAAC;QAC9B,MAAM,EAAE,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QAC9B,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YACZ,MAAM,IAAI,UAAU,CAAC,wBAAwB,KAAK,uBAAuB,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAiB,EACjB,eAAkC,EAAE;IAEpC,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,EAAE,GAAG,IAAI,EAAE,CAAC;IAC5C,MAAM,SAAS,GAAG,iBAAiB,CAAC,YAAY,CAAC,CAAC;IAClD,MAAM,OAAO,GAAmC,EAAE,CAAC;IAEnD,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACvD,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;YACxC,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC;QAClF,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACrD,OAAO,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,oBAAoB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,CAAC,GAAG,CAAC,EAAE,QAAQ,IAAI,KAAK,EAAE,CAAC,CAAC;IACzG,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,YAA+B;IAC7D,MAAM,MAAM,GAA2B,EAAE,CAAC;IAC1C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,IAAI,KAAK,KAAK,SAAS;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAC/C,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,iBAAiB,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;QAC3E,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACtB,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"runtime-env.js","sourceRoot":"","sources":["../../../src/resources/mcp/runtime-env.ts"],"names":[],"mappings":"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"}
@@ -3,8 +3,9 @@
3
3
  // Spawned as a subprocess by discover.ts via StdioClientTransport. Advertises a
4
4
  // single tool and a single resource template using the low-level Server API
5
5
  // (the most version-stable surface of @modelcontextprotocol/sdk). It also echoes
6
- // an --env-provided value through the tool description so tests can assert env
7
- // propagation when needed.
6
+ // an --env-provided value AND the CLI args it received (process.argv beyond the
7
+ // script path) through the tool description, so tests can assert env propagation
8
+ // and ${VAR} argument expansion.
8
9
 
9
10
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
10
11
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -18,11 +19,15 @@ const server = new Server(
18
19
  { capabilities: { tools: {}, resources: {} } },
19
20
  );
20
21
 
22
+ const receivedArgs = process.argv.slice(2);
23
+
21
24
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
22
25
  tools: [
23
26
  {
24
27
  name: "echo",
25
- description: `echo a message (token=${process.env.FIXTURE_TOKEN ?? "unset"})`,
28
+ description:
29
+ `echo a message (token=${process.env.FIXTURE_TOKEN ?? "unset"}) ` +
30
+ `(args=${JSON.stringify(receivedArgs)})`,
26
31
  inputSchema: { type: "object", properties: { text: { type: "string" } } },
27
32
  },
28
33
  { name: "noop", description: "", inputSchema: { type: "object" } },
@@ -168,6 +168,66 @@ describe("OAuth guidance gate", () => {
168
168
  });
169
169
  });
170
170
 
171
+ describe("oauth_only servers reject the manual-token routes", () => {
172
+ beforeEach(() => {
173
+ servedSpec.spec!.auth = create(McpServerAuthSchema, {
174
+ targetEnvVar: "GITHUB_TOKEN",
175
+ oauthOnly: true,
176
+ });
177
+ });
178
+
179
+ it("rejects --env (which cannot satisfy an OAuth-only endpoint) instead of pushing a doomed token", async () => {
180
+ const err = await connectMcpServer(client, {
181
+ reference: "github",
182
+ org: "acme",
183
+ timeoutMs: 30_000,
184
+ dryRun: false,
185
+ envOverrides: ["GITHUB_TOKEN=ghp-x"],
186
+ backendType: "cloud",
187
+ interactive: false,
188
+ }).catch((e: unknown) => e);
189
+
190
+ expect(err).toBeInstanceOf(UsageError);
191
+ expect((err as UsageError).message).toMatch(/requires OAuth/i);
192
+ // The guidance must NOT recommend the manual-token route for an oauth_only server.
193
+ expect((err as UsageError).message).not.toContain("--env TOKEN=");
194
+ expect(connectCalls).toHaveLength(0);
195
+ });
196
+
197
+ it("omits the --env suggestion from the non-interactive OAuth guidance", async () => {
198
+ const err = await connectMcpServer(client, {
199
+ reference: "github",
200
+ org: "acme",
201
+ timeoutMs: 30_000,
202
+ dryRun: false,
203
+ envOverrides: [],
204
+ backendType: "cloud",
205
+ interactive: false,
206
+ }).catch((e: unknown) => e);
207
+
208
+ expect(err).toBeInstanceOf(UsageError);
209
+ expect((err as UsageError).message).not.toContain("--env TOKEN=");
210
+ expect(connectCalls).toHaveLength(0);
211
+ });
212
+
213
+ it("refuses --dry-run local discovery (no locally-obtainable OAuth token) with a clear message", async () => {
214
+ const err = await connectMcpServer(client, {
215
+ reference: "github",
216
+ org: "acme",
217
+ timeoutMs: 10_000,
218
+ dryRun: true,
219
+ envOverrides: [],
220
+ backendType: "cloud",
221
+ interactive: false,
222
+ }).catch((e: unknown) => e);
223
+
224
+ expect(err).toBeInstanceOf(UsageError);
225
+ expect((err as UsageError).message).toMatch(/requires OAuth/i);
226
+ expect((err as UsageError).message).toContain("--dry-run");
227
+ expect(connectCalls).toHaveLength(0);
228
+ });
229
+ });
230
+
171
231
  describe("dry-run path", () => {
172
232
  it("discovers locally and never calls the Connect RPC", async () => {
173
233
  const result = await connectMcpServer(client, {
@@ -192,4 +252,35 @@ describe("dry-run path", () => {
192
252
  expect(text).toContain("Tools (2):");
193
253
  expect(text).toContain("Dry run — results not saved");
194
254
  }, 15_000);
255
+
256
+ it("maps an unresolved ${VAR} arg to actionable guidance and never calls Connect", async () => {
257
+ // Server references a declared env var in its args, but it is not set.
258
+ delete process.env.NEEDED_DIR;
259
+ servedSpec = create(McpServerSchema, {
260
+ metadata: { id: "mcp_1", name: "filesystem", slug: "filesystem", org: "acme" },
261
+ spec: {
262
+ serverType: {
263
+ case: "stdio",
264
+ value: { command: process.execPath, args: [FIXTURE, "${NEEDED_DIR}"] },
265
+ },
266
+ env: { NEEDED_DIR: { isSecret: false, description: "Root directory the server may access" } },
267
+ },
268
+ });
269
+
270
+ const err = await connectMcpServer(client, {
271
+ reference: "filesystem",
272
+ org: "acme",
273
+ timeoutMs: 10_000,
274
+ dryRun: true,
275
+ envOverrides: [],
276
+ backendType: "cloud",
277
+ interactive: false,
278
+ }).catch((e: unknown) => e);
279
+
280
+ expect(err).toBeInstanceOf(UsageError);
281
+ const message = (err as UsageError).message;
282
+ expect(message).toContain("NEEDED_DIR");
283
+ expect(message).toContain("--env");
284
+ expect(connectCalls).toHaveLength(0);
285
+ }, 15_000);
195
286
  });
@@ -18,6 +18,7 @@ import type { Stigmer } from "@stigmer/sdk";
18
18
  import type { BackendType } from "../../config/config.js";
19
19
  import { UsageError } from "../../errors/index.js";
20
20
  import { defaultRegistry } from "../../registry/index.js";
21
+ import { PlaceholderResolutionError } from "../mcp/placeholder-resolver.js";
21
22
  import { buildRuntimeEnv } from "../mcp/runtime-env.js";
22
23
  import { parseReference } from "../reference.js";
23
24
  import { localDiscover } from "./discover.js";
@@ -47,8 +48,20 @@ export async function connectMcpServer(client: Stigmer, opts: ConnectOptions): P
47
48
 
48
49
  if (opts.dryRun) {
49
50
  if (server.spec === undefined) throw new UsageError("MCP server has no spec; cannot discover capabilities");
50
- const capabilities = await localDiscover(server.spec, opts.envOverrides, opts.timeoutMs);
51
- return { server, capabilities, updated: undefined };
51
+ // An oauth_only endpoint rejects static tokens, and the OAuth token lives in
52
+ // the backend's managed environment — never on the caller's machine. So local
53
+ // discovery cannot authenticate it; say so plainly instead of failing on a 401.
54
+ if (isOAuthOnly(server)) throw oauthOnlyDryRunError(server, opts.reference);
55
+ try {
56
+ const capabilities = await localDiscover(server.spec, opts.envOverrides, opts.timeoutMs);
57
+ return { server, capabilities, updated: undefined };
58
+ } catch (err) {
59
+ // A ${VAR} placeholder that could not be resolved is a configuration
60
+ // problem, not a discovery failure — surface it as actionable guidance
61
+ // instead of a raw resolver error or a cryptic subprocess crash.
62
+ if (err instanceof PlaceholderResolutionError) throw unresolvedEnvError(server, err);
63
+ throw err;
64
+ }
52
65
  }
53
66
 
54
67
  await ensureOAuthSatisfied(client, server, opts);
@@ -77,7 +90,18 @@ async function resolveMcpServer(client: Stigmer, reference: string, org: string)
77
90
  // flow and wait for the grant; otherwise stop with actionable guidance so
78
91
  // scripted callers get a clean, stable failure instead of a 5-minute block.
79
92
  async function ensureOAuthSatisfied(client: Stigmer, server: McpServer, opts: ConnectOptions): Promise<void> {
80
- if (!oauthRequired(server) || opts.envOverrides.length > 0) return;
93
+ if (!oauthRequired(server)) return;
94
+
95
+ const oauthOnly = isOAuthOnly(server);
96
+
97
+ // A manually supplied token is a valid bypass for a normal OAuth server (many
98
+ // vendors also accept a PAT), but an oauth_only endpoint rejects static tokens
99
+ // outright — so --env cannot connect it. Fail with guidance rather than push a
100
+ // token the endpoint will reject.
101
+ if (opts.envOverrides.length > 0) {
102
+ if (!oauthOnly) return;
103
+ throw oauthOnlyEnvError(server, opts.reference);
104
+ }
81
105
 
82
106
  const status = await client.mcpServer.getOAuthGrantStatus(
83
107
  create(GetOAuthGrantStatusInputSchema, {
@@ -87,22 +111,76 @@ async function ensureOAuthSatisfied(client: Stigmer, server: McpServer, opts: Co
87
111
  );
88
112
  if (status.connected) return;
89
113
 
90
- if (!opts.interactive) throw oauthGuidanceError(server, opts.reference);
114
+ if (!opts.interactive) throw oauthGuidanceError(server, opts.reference, oauthOnly);
91
115
 
92
116
  const { runOAuthFlow } = await import("./oauth.js");
93
117
  await runOAuthFlow({ client, server, org: opts.org, backendType: opts.backendType });
94
118
  }
95
119
 
96
- function oauthGuidanceError(server: McpServer, reference: string): UsageError {
120
+ // Turn a strict-resolution failure into actionable guidance. A declared-but-unset
121
+ // variable is the user's to provide; an undeclared one is a bug in the server
122
+ // definition. Both are far clearer than the cryptic subprocess crash (ENOENT on a
123
+ // literal "${VAR}" path) that motivated issue #141.
124
+ function unresolvedEnvError(server: McpServer, err: PlaceholderResolutionError): UsageError {
125
+ const slug = server.metadata?.slug ?? server.metadata?.name ?? server.metadata?.id ?? "this server";
126
+ const decl = server.spec?.env?.[err.variableName];
127
+ if (decl) {
128
+ const hint = decl.description !== "" ? ` (${decl.description})` : "";
129
+ return new UsageError(
130
+ `MCP server '${slug}' needs environment variable ${err.variableName}${hint}, but it is not set.\n` +
131
+ `Provide it with --env ${err.variableName}=<value> or export it in your shell before running --dry-run.`,
132
+ );
133
+ }
134
+ const where = err.context !== undefined ? ` in its ${err.context}` : "";
135
+ return new UsageError(
136
+ `MCP server '${slug}' references \${${err.variableName}}${where} but does not declare ${err.variableName} under spec.env.\n` +
137
+ `This is a problem with the server definition — declare ${err.variableName} in the server's env, or remove the placeholder.`,
138
+ );
139
+ }
140
+
141
+ function oauthGuidanceError(server: McpServer, reference: string, oauthOnly: boolean): UsageError {
97
142
  const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
143
+ const choices = [
144
+ " - Re-run this command in an interactive terminal to complete OAuth in your browser",
145
+ ];
146
+ // Only offer the manual-token route for servers that actually accept one.
147
+ // Suggesting it for an oauth_only endpoint would send the user down a dead end.
148
+ if (!oauthOnly) {
149
+ choices.push(` - Provide credentials directly: stigmer connect mcp-server ${slug} --env TOKEN=...`);
150
+ }
98
151
  return new UsageError(
99
152
  `MCP server '${slug}' requires OAuth authentication, which needs an interactive terminal.\n\n` +
100
- "To connect, choose one of:\n" +
101
- " - Re-run this command in an interactive terminal to complete OAuth in your browser\n" +
102
- ` - Provide credentials directly: stigmer connect mcp-server ${slug} --env TOKEN=...`,
153
+ `To connect${oauthOnly ? "" : ", choose one of"}:\n` +
154
+ choices.join("\n"),
155
+ );
156
+ }
157
+
158
+ // An oauth_only server whose endpoint rejects static tokens was given a manual
159
+ // token via --env. Explain that OAuth is the only path rather than pushing a
160
+ // credential the endpoint will reject with an opaque 401.
161
+ function oauthOnlyEnvError(server: McpServer, reference: string): UsageError {
162
+ const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
163
+ return new UsageError(
164
+ `MCP server '${slug}' requires OAuth and rejects manually-entered tokens, so --env cannot connect it.\n` +
165
+ `Re-run without --env in an interactive terminal to sign in: stigmer connect mcp-server ${slug}`,
166
+ );
167
+ }
168
+
169
+ // --dry-run discovers locally, but an oauth_only endpoint needs an OAuth token
170
+ // that only the backend can obtain and store — there is nothing valid to send
171
+ // from the caller's machine. Say so plainly instead of attempting a doomed 401.
172
+ function oauthOnlyDryRunError(server: McpServer, reference: string): UsageError {
173
+ const slug = server.metadata?.slug ?? server.metadata?.name ?? reference;
174
+ return new UsageError(
175
+ `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` +
176
+ `Run it for real and sign in when prompted: stigmer connect mcp-server ${slug}`,
103
177
  );
104
178
  }
105
179
 
106
180
  function oauthRequired(server: McpServer): boolean {
107
181
  return (server.spec?.auth?.targetEnvVar ?? "") !== "";
108
182
  }
183
+
184
+ function isOAuthOnly(server: McpServer): boolean {
185
+ return server.spec?.auth?.oauthOnly === true;
186
+ }
@@ -8,8 +8,9 @@
8
8
  import { create } from "@bufbuild/protobuf";
9
9
  import { McpServerSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
10
10
  import { fileURLToPath } from "node:url";
11
- import { describe, expect, it } from "vitest";
11
+ import { afterEach, describe, expect, it } from "vitest";
12
12
  import { localDiscover } from "./discover.js";
13
+ import { PlaceholderResolutionError } from "../mcp/placeholder-resolver.js";
13
14
 
14
15
  const FIXTURE = fileURLToPath(new URL("./__fixtures__/stdio-server.mjs", import.meta.url));
15
16
  const CRASH_FIXTURE = fileURLToPath(new URL("./__fixtures__/stdio-crash.mjs", import.meta.url));
@@ -20,6 +21,15 @@ function stdioSpec(scriptPath: string) {
20
21
  });
21
22
  }
22
23
 
24
+ // A spec whose args reference ${ALLOWED_DIR}, declared under spec.env — the shape
25
+ // the seedpack filesystem server uses (issue #141).
26
+ function stdioSpecWithPlaceholderArg(scriptPath: string) {
27
+ return create(McpServerSpecSchema, {
28
+ serverType: { case: "stdio", value: { command: process.execPath, args: [scriptPath, "${ALLOWED_DIR}"] } },
29
+ env: { ALLOWED_DIR: { isSecret: false, description: "Directory the server may access" } },
30
+ });
31
+ }
32
+
23
33
  describe("localDiscover (stdio subprocess)", () => {
24
34
  it("discovers tools and resource templates and converts them to proto", async () => {
25
35
  const caps = await localDiscover(stdioSpec(FIXTURE), [], 10_000);
@@ -43,3 +53,32 @@ describe("localDiscover (stdio subprocess)", () => {
43
53
  await expect(localDiscover(stdioSpec(CRASH_FIXTURE), [], 10_000)).rejects.toThrow(/fixture boom: missing CONFIG/);
44
54
  }, 15_000);
45
55
  });
56
+
57
+ describe("localDiscover (${VAR} argument expansion — issue #141)", () => {
58
+ const original = process.env;
59
+ afterEach(() => {
60
+ process.env = original;
61
+ });
62
+
63
+ it("expands a declared ${VAR} arg from --env into the spawned subprocess", async () => {
64
+ const caps = await localDiscover(stdioSpecWithPlaceholderArg(FIXTURE), ["ALLOWED_DIR=/tmp/allowed"], 10_000);
65
+ // The fixture echoes the argv it actually received; the resolved value must
66
+ // reach the subprocess, never the literal placeholder.
67
+ expect(caps.tools[0].description).toContain('args=["/tmp/allowed"]');
68
+ expect(caps.tools[0].description).not.toContain("${ALLOWED_DIR}");
69
+ }, 15_000);
70
+
71
+ it("expands a declared ${VAR} arg from the exported shell environment", async () => {
72
+ process.env = { ...original, ALLOWED_DIR: "/tmp/from-shell" };
73
+ const caps = await localDiscover(stdioSpecWithPlaceholderArg(FIXTURE), [], 10_000);
74
+ expect(caps.tools[0].description).toContain('args=["/tmp/from-shell"]');
75
+ }, 15_000);
76
+
77
+ it("rejects with a placeholder error before spawning when the declared var is unset", async () => {
78
+ process.env = { ...original };
79
+ delete process.env.ALLOWED_DIR;
80
+ await expect(localDiscover(stdioSpecWithPlaceholderArg(FIXTURE), [], 10_000)).rejects.toBeInstanceOf(
81
+ PlaceholderResolutionError,
82
+ );
83
+ }, 15_000);
84
+ });
@@ -21,7 +21,8 @@ import {
21
21
  import type { JsonObject } from "@bufbuild/protobuf";
22
22
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
23
23
  import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
24
- import { mergeProcessEnv } from "../mcp/runtime-env.js";
24
+ import { mergeProcessEnv, resolveDeclaredEnvValues } from "../mcp/runtime-env.js";
25
+ import { resolveHeaders, resolvePlaceholders } from "../mcp/placeholder-resolver.js";
25
26
 
26
27
  /** Discover an MCP server's capabilities locally without persisting to the backend. */
27
28
  export async function localDiscover(
@@ -79,13 +80,21 @@ interface BuiltTransport {
79
80
  }
80
81
 
81
82
  async function buildTransport(spec: McpServerSpec, envOverrides: readonly string[]): Promise<BuiltTransport> {
83
+ // ${VAR} placeholders in args/headers resolve against the exact same env the
84
+ // backend hands the runner for real connect (declared keys from the OS env +
85
+ // --env overrides) — so dry-run is a faithful preview. Resolution is strict:
86
+ // an unresolved placeholder throws before any subprocess is spawned, matching
87
+ // the proto contract (never pass a literal "${VAR}" to the server).
88
+ const resolutionEnv = resolveDeclaredEnvValues(spec.env ?? {}, envOverrides);
89
+
82
90
  if (spec.serverType?.case === "stdio") {
83
91
  const { command, args, workingDir } = spec.serverType.value;
84
92
  if (command === "") throw new Error("stdio transport requires a command");
93
+ const resolvedArgs = args.map((arg, i) => resolvePlaceholders(arg, resolutionEnv, `stdio arg[${i}]`));
85
94
  const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js");
86
95
  const transport = new StdioClientTransport({
87
96
  command,
88
- args: [...args],
97
+ args: resolvedArgs,
89
98
  cwd: workingDir !== "" ? workingDir : undefined,
90
99
  env: mergeProcessEnv([...envOverrides, ...goRunEnvOverrides(command, args)]),
91
100
  // "pipe" exposes the child stderr as a PassThrough immediately, so we can
@@ -104,8 +113,11 @@ async function buildTransport(spec: McpServerSpec, envOverrides: readonly string
104
113
  const { url, headers } = spec.serverType.value;
105
114
  if (url === "") throw new Error("HTTP transport requires a URL");
106
115
  const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
107
- const requestInit = Object.keys(headers).length > 0 ? { headers: { ...headers } } : undefined;
108
- const transport = new StreamableHTTPClientTransport(new URL(url), requestInit ? { requestInit } : undefined);
116
+ const resolvedHeaders = Object.keys(headers).length > 0 ? resolveHeaders(headers, resolutionEnv) : undefined;
117
+ const transport = new StreamableHTTPClientTransport(
118
+ new URL(url),
119
+ resolvedHeaders ? { requestInit: { headers: resolvedHeaders } } : undefined,
120
+ );
109
121
  return { transport, readStderr: () => "" };
110
122
  }
111
123
 
@@ -0,0 +1,71 @@
1
+ // Unit tests for the strict ${VAR} placeholder resolver.
2
+
3
+ import { describe, expect, it } from "vitest";
4
+ import { PlaceholderResolutionError, resolveHeaders, resolvePlaceholders } from "./placeholder-resolver.js";
5
+
6
+ describe("resolvePlaceholders", () => {
7
+ it("returns a string with no placeholders unchanged", () => {
8
+ expect(resolvePlaceholders("/tmp/data.sqlite", {})).toBe("/tmp/data.sqlite");
9
+ });
10
+
11
+ it("resolves a single placeholder from the env map", () => {
12
+ expect(resolvePlaceholders("${DIR}", { DIR: "/home/me" })).toBe("/home/me");
13
+ });
14
+
15
+ it("resolves a placeholder embedded in surrounding text", () => {
16
+ expect(resolvePlaceholders("${DB_PATH}/data.sqlite", { DB_PATH: "/var/lib" })).toBe("/var/lib/data.sqlite");
17
+ });
18
+
19
+ it("resolves multiple and adjacent placeholders", () => {
20
+ expect(resolvePlaceholders("${A}${B}-${A}", { A: "x", B: "y" })).toBe("xy-x");
21
+ });
22
+
23
+ it("resolves to an empty string when the value is empty", () => {
24
+ // An empty-but-present value is a resolved value, not a missing variable.
25
+ expect(resolvePlaceholders("[${X}]", { X: "" })).toBe("[]");
26
+ });
27
+
28
+ it("ignores $VAR without braces (only ${VAR} is a placeholder)", () => {
29
+ expect(resolvePlaceholders("$HOME/bin", { HOME: "/root" })).toBe("$HOME/bin");
30
+ });
31
+
32
+ it("throws PlaceholderResolutionError for an unresolved variable, carrying name + context", () => {
33
+ let caught: unknown;
34
+ try {
35
+ resolvePlaceholders("${MISSING}", { OTHER: "1" }, "stdio arg[2]");
36
+ } catch (err) {
37
+ caught = err;
38
+ }
39
+ expect(caught).toBeInstanceOf(PlaceholderResolutionError);
40
+ const e = caught as PlaceholderResolutionError;
41
+ expect(e.variableName).toBe("MISSING");
42
+ expect(e.context).toBe("stdio arg[2]");
43
+ expect(e.message).toContain("${MISSING}");
44
+ expect(e.message).toContain("stdio arg[2]");
45
+ });
46
+
47
+ it("throws on the first unresolved variable even when others resolve", () => {
48
+ expect(() => resolvePlaceholders("${A}-${B}", { A: "ok" })).toThrow(PlaceholderResolutionError);
49
+ });
50
+ });
51
+
52
+ describe("resolveHeaders", () => {
53
+ it("resolves placeholders in every header value", () => {
54
+ const resolved = resolveHeaders(
55
+ { Authorization: "Bearer ${TOKEN}", "X-Static": "v1" },
56
+ { TOKEN: "abc123" },
57
+ );
58
+ expect(resolved).toEqual({ Authorization: "Bearer abc123", "X-Static": "v1" });
59
+ });
60
+
61
+ it("throws with the offending header name in the context", () => {
62
+ let caught: unknown;
63
+ try {
64
+ resolveHeaders({ Authorization: "Bearer ${API_KEY}" }, {});
65
+ } catch (err) {
66
+ caught = err;
67
+ }
68
+ expect(caught).toBeInstanceOf(PlaceholderResolutionError);
69
+ expect((caught as PlaceholderResolutionError).message).toContain('header "Authorization"');
70
+ });
71
+ });
@@ -0,0 +1,63 @@
1
+ // Strict placeholder resolver for ${VAR_NAME} syntax in MCP server configs.
2
+ //
3
+ // The MCP server spec (StdioServerConfig.args, HttpServerConfig.headers) allows
4
+ // values to reference declared env vars with ${VAR_NAME}. The proto contract is
5
+ // explicit that resolution is strict: a missing variable must produce a clear
6
+ // error rather than passing a literal "${VAR}" to the subprocess/server.
7
+ //
8
+ // This mirrors the runner's shared resolver (backend/services/runner/src/shared/
9
+ // placeholder-resolver.ts) so `connect --dry-run` (which discovers locally)
10
+ // behaves identically to real connect (which resolves on the runner). Kept
11
+ // intentionally dependency-free and semantically identical so the two can be
12
+ // collapsed into one shared package later without any behavior change.
13
+
14
+ const PLACEHOLDER_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
15
+
16
+ export class PlaceholderResolutionError extends Error {
17
+ constructor(
18
+ public readonly variableName: string,
19
+ public readonly context?: string,
20
+ ) {
21
+ const where = context ? ` in ${context}` : "";
22
+ super(
23
+ `Unresolved placeholder \${${variableName}}${where}: ` +
24
+ `variable is not present in the execution environment`,
25
+ );
26
+ this.name = "PlaceholderResolutionError";
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Resolve all ${VAR_NAME} placeholders in a template string.
32
+ *
33
+ * @throws PlaceholderResolutionError if any placeholder cannot be resolved.
34
+ */
35
+ export function resolvePlaceholders(
36
+ template: string,
37
+ envVars: Record<string, string>,
38
+ context?: string,
39
+ ): string {
40
+ return template.replace(PLACEHOLDER_RE, (_match, varName: string) => {
41
+ if (varName in envVars) {
42
+ return envVars[varName];
43
+ }
44
+ throw new PlaceholderResolutionError(varName, context);
45
+ });
46
+ }
47
+
48
+ /**
49
+ * Resolve placeholders in every value of a headers map.
50
+ *
51
+ * @throws PlaceholderResolutionError if any header value contains an
52
+ * unresolvable placeholder.
53
+ */
54
+ export function resolveHeaders(
55
+ headers: Record<string, string>,
56
+ envVars: Record<string, string>,
57
+ ): Record<string, string> {
58
+ const resolved: Record<string, string> = {};
59
+ for (const [key, value] of Object.entries(headers)) {
60
+ resolved[key] = resolvePlaceholders(value, envVars, `header "${key}"`);
61
+ }
62
+ return resolved;
63
+ }
@@ -4,12 +4,18 @@ import { create } from "@bufbuild/protobuf";
4
4
  import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
5
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
6
  import { UsageError } from "../../errors/index.js";
7
- import { buildRuntimeEnv, mergeProcessEnv, parseEnvOverrides } from "./runtime-env.js";
7
+ import { buildRuntimeEnv, mergeProcessEnv, parseEnvOverrides, resolveDeclaredEnvValues } from "./runtime-env.js";
8
8
 
9
9
  function serverWithEnv(env: Record<string, { isSecret: boolean }>) {
10
10
  return create(McpServerSchema, { metadata: { id: "mcp_1" }, spec: { env } });
11
11
  }
12
12
 
13
+ // resolveDeclaredEnvValues takes the raw declarations map; the tests only rely on
14
+ // keys being present, so a minimal declaration object is sufficient.
15
+ function decls(...keys: string[]) {
16
+ return Object.fromEntries(keys.map((k) => [k, { isSecret: false }]));
17
+ }
18
+
13
19
  describe("parseEnvOverrides", () => {
14
20
  it("parses KEY=VALUE pairs, preserving '=' in the value", () => {
15
21
  expect(parseEnvOverrides(["A=1", "B=x=y"])).toEqual({ A: "1", B: "x=y" });
@@ -66,3 +72,31 @@ describe("mergeProcessEnv", () => {
66
72
  expect(merged.NEW).toBe("1");
67
73
  });
68
74
  });
75
+
76
+ describe("resolveDeclaredEnvValues", () => {
77
+ const original = process.env;
78
+ beforeEach(() => {
79
+ process.env = { ...original, ALLOWED_DIR: "/from-os", EMPTY_VAR: "" };
80
+ });
81
+ afterEach(() => {
82
+ process.env = original;
83
+ });
84
+
85
+ it("pulls declared keys from the OS environment", () => {
86
+ expect(resolveDeclaredEnvValues(decls("ALLOWED_DIR"))).toEqual({ ALLOWED_DIR: "/from-os" });
87
+ });
88
+
89
+ it("skips declared keys that are unset or empty in the environment", () => {
90
+ expect(resolveDeclaredEnvValues(decls("EMPTY_VAR", "MISSING"))).toEqual({});
91
+ });
92
+
93
+ it("lets --env overrides win over the OS environment", () => {
94
+ expect(resolveDeclaredEnvValues(decls("ALLOWED_DIR"), ["ALLOWED_DIR=/override"])).toEqual({
95
+ ALLOWED_DIR: "/override",
96
+ });
97
+ });
98
+
99
+ it("includes an --env override even when the key is undeclared (matches buildRuntimeEnv)", () => {
100
+ expect(resolveDeclaredEnvValues(decls(), ["EXTRA=1"])).toEqual({ EXTRA: "1" });
101
+ });
102
+ });
@@ -36,6 +36,39 @@ export function parseEnvOverrides(overrides: readonly string[]): Record<string,
36
36
  return result;
37
37
  }
38
38
 
39
+ /**
40
+ * Resolve the values behind a server's declared env vars: declared keys pulled
41
+ * from the OS environment (non-empty only), with `--env` overrides layered on
42
+ * top (override wins; an undeclared override is still included).
43
+ *
44
+ * This is the single source of truth for "which env value is in play". It backs
45
+ * both the runtime_env sent to the backend (buildRuntimeEnv) and the ${VAR}
46
+ * placeholder resolution done locally for `--dry-run` (discover.ts), so the two
47
+ * can never drift: dry-run resolves against the exact same map the backend hands
48
+ * the runner (minus platform-injected infra vars, which a local machine cannot
49
+ * know).
50
+ *
51
+ * `declarations` is the server's `spec.env` map; only its keys (the declared
52
+ * variable names) are read, so any map keyed by those names is accepted.
53
+ */
54
+ export function resolveDeclaredEnvValues(
55
+ declarations: Record<string, unknown>,
56
+ envOverrides: readonly string[] = [],
57
+ ): Record<string, string> {
58
+ const values: Record<string, string> = {};
59
+
60
+ for (const key of Object.keys(declarations)) {
61
+ const value = process.env[key];
62
+ if (value !== undefined && value !== "") values[key] = value;
63
+ }
64
+
65
+ for (const [key, value] of Object.entries(parseEnvOverrides(envOverrides))) {
66
+ values[key] = value;
67
+ }
68
+
69
+ return values;
70
+ }
71
+
39
72
  /**
40
73
  * Build the runtime_env map for the Connect RPC from the OS environment and the
41
74
  * supplied --env overrides. Only keys with a non-empty value are emitted.
@@ -45,17 +78,10 @@ export function buildRuntimeEnv(
45
78
  envOverrides: readonly string[] = [],
46
79
  ): Record<string, ExecutionValue> {
47
80
  const declarations = server.spec?.env ?? {};
48
- const overrides = parseEnvOverrides(envOverrides);
81
+ const values = resolveDeclaredEnvValues(declarations, envOverrides);
49
82
  const runtime: Record<string, ExecutionValue> = {};
50
83
 
51
- for (const [key, decl] of Object.entries(declarations)) {
52
- const value = process.env[key];
53
- if (value !== undefined && value !== "") {
54
- runtime[key] = create(ExecutionValueSchema, { value, isSecret: decl.isSecret });
55
- }
56
- }
57
-
58
- for (const [key, value] of Object.entries(overrides)) {
84
+ for (const [key, value] of Object.entries(values)) {
59
85
  runtime[key] = create(ExecutionValueSchema, { value, isSecret: declarations[key]?.isSecret ?? false });
60
86
  }
61
87