Skip to content

Commit f344e5c

Browse files
author
Griffin Evans
committed
Backfill existing stdio MCP with zero connections to org/default
1 parent 3a94353 commit f344e5c

2 files changed

Lines changed: 117 additions & 45 deletions

File tree

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,55 @@ describe("mcpPlugin", () => {
475475
),
476476
);
477477

478+
it.effect(
479+
"backfills a default connection for existing stdio MCP servers with no connections",
480+
() =>
481+
Effect.scoped(
482+
Effect.gen(function* () {
483+
const fixture = yield* Effect.acquireRelease(
484+
Effect.sync(makeStdioMcpFixture),
485+
({ dir }) => Effect.sync(() => rmSync(dir, { recursive: true, force: true })),
486+
);
487+
const executor = yield* createExecutor(
488+
makeTestConfig({
489+
plugins: [
490+
memoryCredentialsPlugin(),
491+
mcpPlugin({ dangerouslyAllowStdioMCP: true }),
492+
] as const,
493+
}),
494+
);
495+
const integration = IntegrationSlug.make("stdio_backfill");
496+
497+
yield* executor.mcp.addServer({
498+
transport: "stdio",
499+
name: "Stdio MCP Backfill",
500+
command: fixture.command,
501+
args: fixture.args,
502+
slug: String(integration),
503+
});
504+
yield* executor.connections.remove({
505+
owner: "org",
506+
integration,
507+
name: ConnectionName.make("default"),
508+
});
509+
510+
expect(yield* executor.connections.list({ integration })).toHaveLength(0);
511+
yield* executor.mcp.getServer(String(integration));
512+
513+
const connections = yield* executor.connections.list({ integration });
514+
const tools = (yield* executor.tools.list()).filter(
515+
(tool) => String(tool.integration) === String(integration),
516+
);
517+
518+
expect(connections).toHaveLength(1);
519+
expect(connections[0]?.address).toBe("tools.stdio_backfill.org.default");
520+
expect(tools.map((tool) => String(tool.address))).toEqual([
521+
"tools.stdio_backfill.org.default.hello",
522+
]);
523+
}),
524+
),
525+
);
526+
478527
it.effect("removing an MCP server removes the OAuth client used by its connection", () =>
479528
Effect.scoped(
480529
Effect.gen(function* () {

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

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,57 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
752752
}),
753753
);
754754

755+
const defaultStdioConnectionRef = (integration: IntegrationSlug) => ({
756+
owner: "org" as const,
757+
name: DEFAULT_STDIO_CONNECTION_NAME,
758+
integration,
759+
});
760+
761+
const defaultStdioConnectionFailure = (message: string) =>
762+
new McpConnectionError({
763+
transport: "stdio",
764+
message: `Failed creating the default stdio MCP connection: ${message}`,
765+
});
766+
767+
const refreshDefaultStdioConnection = (integration: IntegrationSlug) =>
768+
ctx.connections.refresh(defaultStdioConnectionRef(integration)).pipe(
769+
Effect.asVoid,
770+
Effect.catchTags({
771+
ConnectionNotFoundError: (error) =>
772+
Effect.fail(defaultStdioConnectionFailure(error.message)),
773+
IntegrationNotFoundError: (error) =>
774+
Effect.fail(defaultStdioConnectionFailure(error.message)),
775+
}),
776+
);
777+
778+
const ensureDefaultStdioConnection = (integration: IntegrationSlug, slug: string) =>
779+
Effect.gen(function* () {
780+
const connections = yield* ctx.connections.list({ integration });
781+
if (connections.length > 0) return;
782+
783+
yield* ctx.connections
784+
.create({
785+
...defaultStdioConnectionRef(integration),
786+
template: NO_AUTH_TEMPLATE,
787+
values: {},
788+
})
789+
.pipe(
790+
Effect.asVoid,
791+
Effect.catchTags({
792+
CredentialProviderNotRegisteredError: (error) =>
793+
Effect.fail(defaultStdioConnectionFailure(error.message)),
794+
IntegrationNotFoundError: (error) =>
795+
Effect.fail(defaultStdioConnectionFailure(error.message)),
796+
InvalidConnectionInputError: (error) =>
797+
Effect.fail(defaultStdioConnectionFailure(error.message)),
798+
UniqueViolationError: () => refreshDefaultStdioConnection(integration),
799+
}),
800+
Effect.withSpan("mcp.plugin.ensure_stdio_default_connection", {
801+
attributes: { "mcp.integration.slug": slug },
802+
}),
803+
);
804+
});
805+
755806
const addServer = (input: McpServerInput) =>
756807
Effect.gen(function* () {
757808
const slug = normalizeSlug(input);
@@ -785,49 +836,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
785836
);
786837

787838
if (config.transport === "stdio") {
788-
yield* ctx.connections
789-
.create({
790-
owner: "org",
791-
name: DEFAULT_STDIO_CONNECTION_NAME,
792-
integration,
793-
template: NO_AUTH_TEMPLATE,
794-
values: {},
795-
})
796-
.pipe(
797-
Effect.catchTags({
798-
CredentialProviderNotRegisteredError: (error) =>
799-
Effect.fail(
800-
new McpConnectionError({
801-
transport: "stdio",
802-
message: `Failed creating the default stdio MCP connection: ${error.message}`,
803-
}),
804-
),
805-
IntegrationNotFoundError: (error) =>
806-
Effect.fail(
807-
new McpConnectionError({
808-
transport: "stdio",
809-
message: `Failed creating the default stdio MCP connection: ${error.message}`,
810-
}),
811-
),
812-
InvalidConnectionInputError: (error) =>
813-
Effect.fail(
814-
new McpConnectionError({
815-
transport: "stdio",
816-
message: `Failed creating the default stdio MCP connection: ${error.message}`,
817-
}),
818-
),
819-
UniqueViolationError: () =>
820-
Effect.fail(
821-
new McpConnectionError({
822-
transport: "stdio",
823-
message: `Failed creating the default stdio MCP connection: a default connection already exists for ${slug}. Refresh the connection or remove and re-add the MCP server.`,
824-
}),
825-
),
826-
}),
827-
Effect.withSpan("mcp.plugin.create_stdio_default_connection", {
828-
attributes: { "mcp.integration.slug": slug },
829-
}),
830-
);
839+
yield* ensureDefaultStdioConnection(integration, slug);
831840
}
832841

833842
return { slug };
@@ -916,14 +925,28 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
916925
);
917926

918927
const getServer = (slug: string) =>
919-
ctx.core.integrations.get(slugFrom(slug)).pipe(
928+
Effect.gen(function* () {
929+
const integration = slugFrom(slug);
930+
const record = yield* ctx.core.integrations.get(integration);
931+
const config = record ? parseMcpIntegrationConfig(record.config) : null;
932+
if (config?.transport === "stdio") {
933+
yield* ensureDefaultStdioConnection(integration, slug);
934+
}
935+
return record;
936+
}).pipe(
920937
Effect.withSpan("mcp.plugin.get_server", {
921938
attributes: { "mcp.integration.slug": slug },
922939
}),
923940
);
924941

925942
const configureServer = (slug: string, config: McpIntegrationConfigType) =>
926-
ctx.core.integrations.update(slugFrom(slug), { config }).pipe(
943+
Effect.gen(function* () {
944+
const integration = slugFrom(slug);
945+
yield* ctx.core.integrations.update(integration, { config });
946+
if (config.transport === "stdio") {
947+
yield* ensureDefaultStdioConnection(integration, slug);
948+
}
949+
}).pipe(
927950
Effect.withSpan("mcp.plugin.configure_server", {
928951
attributes: { "mcp.integration.slug": slug },
929952
}),

0 commit comments

Comments
 (0)