Skip to content

Commit c2d0ef7

Browse files
author
Griffin Evans
committed
Update stdio preflight to detect required stdio_env vars and preflight with those stored values
1 parent dafec5d commit c2d0ef7

4 files changed

Lines changed: 151 additions & 12 deletions

File tree

packages/core/sdk/src/executor.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3308,6 +3308,7 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
33083308
remove: (ref) => connectionsRemove(ref),
33093309
refresh: (ref) => connectionsRefresh(ref),
33103310
resolveValue: (ref) => resolveConnectionValueByRef(ref),
3311+
resolveValues: (ref) => resolveConnectionValuesByRef(ref),
33113312
},
33123313
providers: {
33133314
list: () => providersList(),

packages/core/sdk/src/plugin.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,13 @@ export interface PluginCtx<TStore = unknown> {
219219
readonly Tool[],
220220
ConnectionNotFoundError | IntegrationNotFoundError | StorageFailure
221221
>;
222-
/** Resolve a connection's value through its provider (and OAuth refresh).
222+
/** Resolve a connection's primary value through its provider (and OAuth refresh).
223223
* null if the provider can't produce one. */
224224
readonly resolveValue: (ref: ConnectionRef) => Effect.Effect<string | null, StorageFailure>;
225+
/** Resolve every named input for a connection through its provider. */
226+
readonly resolveValues: (
227+
ref: ConnectionRef,
228+
) => Effect.Effect<Record<string, string | null>, StorageFailure>;
225229
};
226230

227231
/** Registered credential backends — for discovery (browse a backend's items). */

packages/plugins/mcp/src/sdk/plugin.test.ts

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ const withStdioFixtureScript = Effect.acquireRelease(
147147
yield* Effect.promise(() =>
148148
Fs.writeFile(
149149
script,
150-
`import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";\nimport { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";\nconst toolName = process.argv[2] ?? "one";\nconst server = new McpServer({ name: "stdio-fixture", version: "1.0.0" }, { capabilities: {} });\nserver.registerTool(toolName, { description: "Stdio fixture tool", inputSchema: {} }, async () => ({ content: [{ type: "text", text: process.env.STDIO_VALUE ?? "ok" }] }));\nawait server.connect(new StdioServerTransport());\n`,
150+
`import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";\nimport { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";\nconst toolName = process.argv[2] ?? "one";\nconst requiredEnv = process.argv[3];\nif (requiredEnv && process.env[requiredEnv] == null) process.exit(1);\nconst server = new McpServer({ name: "stdio-fixture", version: "1.0.0" }, { capabilities: {} });\nserver.registerTool(toolName, { description: "Stdio fixture tool", inputSchema: {} }, async () => ({ content: [{ type: "text", text: process.env.STDIO_VALUE ?? process.env.STDIO_SECRET ?? "ok" }] }));\nawait server.connect(new StdioServerTransport());\n`,
151151
),
152152
);
153153
return { dir, script } as const;
@@ -602,6 +602,95 @@ describe("mcpPlugin", () => {
602602
),
603603
);
604604

605+
it.effect(
606+
"configureServer preflights stdio secret env sources with existing connection values",
607+
() =>
608+
Effect.scoped(
609+
Effect.gen(function* () {
610+
const fixture = yield* withStdioFixtureScript;
611+
const executor = yield* createExecutor(
612+
makeTestConfig({
613+
plugins: [
614+
memoryCredentialsPlugin(),
615+
mcpPlugin({ dangerouslyAllowStdioMCP: true }),
616+
] as const,
617+
}),
618+
);
619+
620+
yield* executor.mcp.addServer({
621+
transport: "stdio",
622+
name: "Stdio secret edit",
623+
slug: "stdio_secret_edit",
624+
command: process.execPath,
625+
args: [fixture.script, "one", "STDIO_SECRET"],
626+
envVars: ["STDIO_SECRET"],
627+
});
628+
yield* executor.connections.create({
629+
owner: "org",
630+
name: ConnectionName.make("default"),
631+
integration: IntegrationSlug.make("stdio_secret_edit"),
632+
template: AuthTemplateSlug.make("env"),
633+
values: { STDIO_SECRET: "secret-value" },
634+
});
635+
636+
yield* executor.mcp.configureServer("stdio_secret_edit", {
637+
transport: "stdio",
638+
command: process.execPath,
639+
args: [fixture.script, "two", "STDIO_SECRET"],
640+
authenticationTemplate: [{ slug: "env", kind: "stdio_env", vars: ["STDIO_SECRET"] }],
641+
});
642+
643+
const tools = yield* executor.tools.list({
644+
integration: IntegrationSlug.make("stdio_secret_edit"),
645+
});
646+
expect(tools.map((tool) => String(tool.name))).toContain("two");
647+
expect(tools.map((tool) => String(tool.name))).not.toContain("one");
648+
}),
649+
),
650+
);
651+
652+
it.effect("configureServer reports missing stdio secret values before preflight", () =>
653+
Effect.scoped(
654+
Effect.gen(function* () {
655+
const fixture = yield* withStdioFixtureScript;
656+
const executor = yield* createExecutor(
657+
makeTestConfig({
658+
plugins: [
659+
memoryCredentialsPlugin(),
660+
mcpPlugin({ dangerouslyAllowStdioMCP: true }),
661+
] as const,
662+
}),
663+
);
664+
665+
yield* executor.mcp.addServer({
666+
transport: "stdio",
667+
name: "Stdio secret missing",
668+
slug: "stdio_secret_missing",
669+
command: process.execPath,
670+
args: [fixture.script, "one", "STDIO_SECRET"],
671+
envVars: ["STDIO_SECRET"],
672+
});
673+
674+
const result = yield* Effect.result(
675+
executor.mcp.configureServer("stdio_secret_missing", {
676+
transport: "stdio",
677+
command: process.execPath,
678+
args: [fixture.script, "two", "STDIO_SECRET"],
679+
authenticationTemplate: [{ slug: "env", kind: "stdio_env", vars: ["STDIO_SECRET"] }],
680+
}),
681+
);
682+
683+
expect(Result.isFailure(result)).toBe(true);
684+
const failure = Result.isFailure(result) ? result.failure : null;
685+
expect(failure).toMatchObject({
686+
_tag: "McpConnectionError",
687+
message:
688+
"Cannot validate this command because no visible connection has values for required environment variables: STDIO_SECRET.",
689+
});
690+
}),
691+
),
692+
);
693+
605694
it.effect("configureServer blocks invalid stdio configs without updating", () =>
606695
Effect.scoped(
607696
Effect.gen(function* () {

packages/plugins/mcp/src/sdk/plugin.ts

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1036,16 +1036,61 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
10361036
}),
10371037
);
10381038

1039-
const preflightStdioConfig = (config: McpIntegrationConfigType) =>
1039+
const requiredStdioEnvVars = (config: McpStdioIntegrationConfig): readonly string[] => [
1040+
...new Set(
1041+
(config.authenticationTemplate ?? []).flatMap((method: McpAuthMethod) =>
1042+
method.kind === "stdio_env" ? method.vars : [],
1043+
),
1044+
),
1045+
];
1046+
1047+
const missingStdioEnvVars = (
1048+
requiredVars: readonly string[],
1049+
values: Record<string, string | null>,
1050+
): readonly string[] => requiredVars.filter((variable) => values[variable] == null);
1051+
1052+
const preflightStdioConfig = (
1053+
integration: IntegrationSlug,
1054+
config: McpStdioIntegrationConfig,
1055+
) =>
10401056
Effect.gen(function* () {
1041-
const connectorInput = yield* buildConnectorInput(
1042-
config,
1043-
{},
1044-
null,
1045-
allowStdio,
1046-
httpClientLayer,
1047-
);
1048-
yield* discoverTools(createMcpConnector(connectorInput));
1057+
const requiredVars = requiredStdioEnvVars(config);
1058+
if (requiredVars.length === 0) {
1059+
const connectorInput = yield* buildConnectorInput(
1060+
config,
1061+
{},
1062+
null,
1063+
allowStdio,
1064+
httpClientLayer,
1065+
);
1066+
yield* discoverTools(createMcpConnector(connectorInput));
1067+
return;
1068+
}
1069+
1070+
const connections = yield* ctx.connections.list({ integration });
1071+
for (const connection of connections) {
1072+
const template = String(connection.template);
1073+
const method = selectAuthMethod(config, template);
1074+
if (method?.kind !== "stdio_env") continue;
1075+
1076+
const values = yield* ctx.connections.resolveValues(connection);
1077+
if (missingStdioEnvVars(method.vars, values).length > 0) continue;
1078+
1079+
const connectorInput = yield* buildConnectorInput(
1080+
config,
1081+
values,
1082+
template,
1083+
allowStdio,
1084+
httpClientLayer,
1085+
);
1086+
yield* discoverTools(createMcpConnector(connectorInput));
1087+
return;
1088+
}
1089+
1090+
return yield* new McpConnectionError({
1091+
transport: "stdio",
1092+
message: `Cannot validate this command because no visible connection has values for required environment variables: ${requiredVars.join(", ")}.`,
1093+
});
10491094
});
10501095

10511096
const removePoliciesScopedToIntegration = (integration: IntegrationSlug) =>
@@ -1141,7 +1186,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
11411186
return;
11421187
}
11431188

1144-
yield* preflightStdioConfig(config);
1189+
yield* preflightStdioConfig(integration, config);
11451190
yield* ctx.transaction(
11461191
Effect.gen(function* () {
11471192
yield* removePoliciesScopedToIntegration(integration);

0 commit comments

Comments
 (0)