Skip to content

Commit 924bdaf

Browse files
committed
feat(mcp): add agent-facing updateServer and configureAuth tools
1 parent 8b3a468 commit 924bdaf

4 files changed

Lines changed: 256 additions & 0 deletions

File tree

packages/plugins/mcp/src/api/handlers.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const failingExtension: McpPluginExtension = {
2525
// oxlint-disable-next-line executor/no-error-constructor -- boundary: test injects a defect to verify opaque handler error responses
2626
probeEndpoint: () => Effect.die(new Error("Not implemented")),
2727
addServer: () => unused,
28+
updateServer: () => unused,
2829
removeServer: () => unused,
2930
reconcileStdioConnections: () => unused,
3031
getServer: () => Effect.succeed(null),

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export {
44
type McpPluginExtension,
55
type McpPluginOptions,
66
type McpServerInput,
7+
type McpUpdateServerInput,
78
type McpRemoteServerInput,
89
type McpStdioServerInput,
910
type McpProbeResult,

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

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -956,6 +956,76 @@ describe("mcpPlugin", () => {
956956
}),
957957
);
958958

959+
it.effect("agent tool updateServer updates MCP server config and auth template", () =>
960+
Effect.gen(function* () {
961+
const config = makeTestConfig({ plugins: [mcpPlugin()] as const });
962+
const executor = yield* createExecutor(config);
963+
964+
yield* executor.execute(ToolAddress.make("executor.mcp.addServer"), {
965+
name: "Initial MCP",
966+
endpoint: "https://mcp1.example.com/mcp",
967+
slug: "test_update_mcp",
968+
});
969+
970+
const updated = yield* executor.execute(ToolAddress.make("executor.mcp.updateServer"), {
971+
slug: "test_update_mcp",
972+
name: "Updated MCP Display",
973+
endpoint: "https://mcp2.example.com/mcp",
974+
authenticationTemplate: [
975+
{
976+
type: "apiKey",
977+
headers: { Authorization: ["Bearer ", { type: "variable", name: "token" }] },
978+
},
979+
],
980+
});
981+
expect(updated).toMatchObject({
982+
ok: true,
983+
data: { slug: "test_update_mcp" },
984+
});
985+
986+
const integration = yield* executor.integrations.get(IntegrationSlug.make("test_update_mcp"));
987+
expect(integration?.name).toBe("Updated MCP Display");
988+
expect(integration?.authMethods.map((m) => m.kind)).toEqual(["apikey"]);
989+
990+
yield* executor.close();
991+
yield* Effect.promise(() => config.testDb.close());
992+
}),
993+
);
994+
995+
it.effect("agent tool configureAuth updates authentication templates on remote server", () =>
996+
Effect.gen(function* () {
997+
const config = makeTestConfig({ plugins: [mcpPlugin()] as const });
998+
const executor = yield* createExecutor(config);
999+
1000+
yield* executor.execute(ToolAddress.make("executor.mcp.addServer"), {
1001+
name: "Auth MCP",
1002+
endpoint: "https://auth.example.com/mcp",
1003+
slug: "test_auth_mcp",
1004+
});
1005+
1006+
const configured = yield* executor.execute(ToolAddress.make("executor.mcp.configureAuth"), {
1007+
slug: "test_auth_mcp",
1008+
authenticationTemplate: [
1009+
{
1010+
type: "apiKey",
1011+
headers: { "X-API-Key": [{ type: "variable", name: "apiKey" }] },
1012+
},
1013+
],
1014+
mode: "replace",
1015+
});
1016+
expect(configured).toMatchObject({
1017+
ok: true,
1018+
data: { slug: "test_auth_mcp" },
1019+
});
1020+
1021+
const integration = yield* executor.integrations.get(IntegrationSlug.make("test_auth_mcp"));
1022+
expect(integration?.authMethods.map((m) => m.kind)).toEqual(["apikey"]);
1023+
1024+
yield* executor.close();
1025+
yield* Effect.promise(() => config.testDb.close());
1026+
}),
1027+
);
1028+
9591029
for (const status of [401, 403] as const) {
9601030
it.effect(`returns an auth tool failure when tools/call responds HTTP ${status}`, () =>
9611031
Effect.scoped(

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

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,54 @@ const McpAddServerOutputSchema = Schema.Struct({
253253
slug: Schema.String,
254254
});
255255

256+
const McpUpdateRemoteServerInputSchema = Schema.Struct({
257+
slug: Schema.String,
258+
transport: Schema.optional(Schema.Literal("remote")),
259+
name: Schema.optional(Schema.String),
260+
family: Schema.optional(Schema.String),
261+
description: Schema.optional(Schema.String),
262+
endpoint: Schema.optional(Schema.String),
263+
remoteTransport: Schema.optional(McpRemoteTransport),
264+
headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
265+
queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
266+
authenticationTemplate: Schema.optional(Schema.Array(McpAuthMethodInput)),
267+
auth: Schema.optional(McpAuthShorthand),
268+
});
269+
270+
const McpUpdateStdioServerInputSchema = Schema.Struct({
271+
slug: Schema.String,
272+
transport: Schema.Literal("stdio"),
273+
name: Schema.optional(Schema.String),
274+
family: Schema.optional(Schema.String),
275+
description: Schema.optional(Schema.String),
276+
command: Schema.optional(Schema.String),
277+
args: Schema.optional(Schema.Array(Schema.String)),
278+
envVars: Schema.optional(Schema.Array(Schema.String)),
279+
env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
280+
cwd: Schema.optional(Schema.String),
281+
versionNegotiation: Schema.optional(McpStdioVersionNegotiation),
282+
});
283+
284+
const McpUpdateServerInputSchema = Schema.Union([
285+
McpUpdateRemoteServerInputSchema,
286+
McpUpdateStdioServerInputSchema,
287+
]);
288+
289+
const McpUpdateServerOutputSchema = Schema.Struct({
290+
slug: Schema.String,
291+
});
292+
293+
const McpConfigureAuthToolInputSchema = Schema.Struct({
294+
slug: Schema.String,
295+
authenticationTemplate: Schema.Array(McpAuthMethodInput),
296+
mode: Schema.optional(Schema.Literals(["merge", "replace"])),
297+
});
298+
299+
const McpConfigureAuthToolOutputSchema = Schema.Struct({
300+
slug: Schema.String,
301+
authenticationTemplate: Schema.Array(Schema.Unknown),
302+
});
303+
256304
/** Input for the custom-method-create flow. `merge` (default) appends onto the
257305
* integration's existing `authenticationTemplate`; `replace` swaps the whole
258306
* declared set. Mirrors the OpenAPI/GraphQL `configureAuth` inputs. */
@@ -292,6 +340,7 @@ const McpProbeEndpointOutputSchema = Schema.Struct({
292340
export type McpRemoteServerInput = typeof McpRemoteServerInputSchema.Type;
293341
export type McpStdioServerInput = typeof McpStdioServerInputSchema.Type;
294342
export type McpServerInput = typeof McpAddServerInputSchema.Type;
343+
export type McpUpdateServerInput = typeof McpUpdateServerInputSchema.Type;
295344
export type McpProbeResult = typeof McpProbeEndpointOutputSchema.Type;
296345
export type McpProbeEndpointInput = typeof McpProbeEndpointInputSchema.Type;
297346

@@ -311,6 +360,14 @@ const schemaToStaticToolSchema = <A, I>(schema: Schema.Decoder<A, I>): StaticToo
311360

312361
const McpAddServerInputStandardSchema = schemaToStaticToolSchema(McpAddServerInputSchema);
313362
const McpAddServerOutputStandardSchema = schemaToStaticToolSchema(McpAddServerOutputSchema);
363+
const McpUpdateServerInputStandardSchema = schemaToStaticToolSchema(McpUpdateServerInputSchema);
364+
const McpUpdateServerOutputStandardSchema = schemaToStaticToolSchema(McpUpdateServerOutputSchema);
365+
const McpConfigureAuthToolInputStandardSchema = schemaToStaticToolSchema(
366+
McpConfigureAuthToolInputSchema,
367+
);
368+
const McpConfigureAuthToolOutputStandardSchema = schemaToStaticToolSchema(
369+
McpConfigureAuthToolOutputSchema,
370+
);
314371
const McpProbeEndpointInputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointInputSchema);
315372
const McpProbeEndpointOutputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointOutputSchema);
316373
const McpGetServerInputStandardSchema = schemaToStaticToolSchema(McpGetServerInputSchema);
@@ -1248,6 +1305,82 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
12481305
}),
12491306
);
12501307

1308+
const updateServer = (input: McpUpdateServerInput) =>
1309+
Effect.gen(function* () {
1310+
const slug = slugFrom(input.slug);
1311+
const record = yield* ctx.core.integrations.get(slug);
1312+
if (!record) {
1313+
return yield* new McpConnectionError({
1314+
message: `MCP server not found: ${input.slug}`,
1315+
transport: "remote",
1316+
});
1317+
}
1318+
const current = parseMcpIntegrationConfig(record.config);
1319+
if (!current) {
1320+
return yield* new McpConnectionError({
1321+
message: `Invalid configuration for MCP server: ${input.slug}`,
1322+
transport: "remote",
1323+
});
1324+
}
1325+
1326+
let updatedConfig: McpIntegrationConfigType;
1327+
if (current.transport === "stdio") {
1328+
const stdioInput = input as typeof McpUpdateStdioServerInputSchema.Type;
1329+
let authenticationTemplate = current.authenticationTemplate;
1330+
if (stdioInput.envVars !== undefined) {
1331+
authenticationTemplate =
1332+
stdioInput.envVars.length > 0
1333+
? [{ slug: "stdio_env", kind: "stdio_env", vars: stdioInput.envVars }]
1334+
: [{ slug: "none", kind: "none" }];
1335+
}
1336+
updatedConfig = {
1337+
transport: "stdio",
1338+
family: stdioInput.family ?? current.family,
1339+
command: stdioInput.command ?? current.command,
1340+
args: stdioInput.args ?? current.args,
1341+
cwd: stdioInput.cwd !== undefined ? stdioInput.cwd : current.cwd,
1342+
...(stdioInput.versionNegotiation !== undefined
1343+
? { versionNegotiation: stdioInput.versionNegotiation }
1344+
: current.versionNegotiation !== undefined
1345+
? { versionNegotiation: current.versionNegotiation }
1346+
: {}),
1347+
...(authenticationTemplate !== undefined ? { authenticationTemplate } : {}),
1348+
};
1349+
} else {
1350+
const remoteInput = input as typeof McpUpdateRemoteServerInputSchema.Type;
1351+
let authenticationTemplate = current.authenticationTemplate;
1352+
if (remoteInput.authenticationTemplate !== undefined) {
1353+
authenticationTemplate = normalizeMcpAuthMethods(
1354+
remoteInput.authenticationTemplate,
1355+
);
1356+
} else if (remoteInput.auth !== undefined) {
1357+
authenticationTemplate = [mcpAuthMethodFromShorthand(remoteInput.auth)];
1358+
}
1359+
1360+
updatedConfig = {
1361+
transport: "remote",
1362+
family: remoteInput.family ?? current.family,
1363+
endpoint: remoteInput.endpoint ?? current.endpoint,
1364+
remoteTransport: remoteInput.remoteTransport ?? current.remoteTransport,
1365+
headers: remoteInput.headers ?? current.headers,
1366+
queryParams: remoteInput.queryParams ?? current.queryParams,
1367+
authenticationTemplate,
1368+
};
1369+
}
1370+
1371+
yield* ctx.core.integrations.update(slug, {
1372+
...(input.name !== undefined ? { name: input.name } : {}),
1373+
...(input.description !== undefined ? { description: input.description } : {}),
1374+
config: updatedConfig,
1375+
});
1376+
1377+
return { slug: String(input.slug) };
1378+
}).pipe(
1379+
Effect.withSpan("mcp.plugin.update_server", {
1380+
attributes: { "mcp.integration.slug": input.slug },
1381+
}),
1382+
);
1383+
12511384
/** Merge-append auth methods onto the integration's existing
12521385
* `authenticationTemplate` (custom-method-create flow), mirroring the
12531386
* OpenAPI/GraphQL `configureAuth`. Returns the merged array. A no-op
@@ -1289,6 +1422,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
12891422
return {
12901423
probeEndpoint,
12911424
addServer,
1425+
updateServer,
12921426
removeServer,
12931427
reconcileStdioConnections,
12941428
getServer,
@@ -1814,6 +1948,53 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
18141948
);
18151949
},
18161950
}),
1951+
tool({
1952+
name: "updateServer",
1953+
description:
1954+
"Update the configuration of an existing registered MCP server (transport settings, endpoint/command, headers, query params, or auth templates).",
1955+
annotations: {
1956+
requiresApproval: true,
1957+
approvalDescription: "Update an MCP server",
1958+
},
1959+
inputSchema: McpUpdateServerInputStandardSchema,
1960+
outputSchema: McpUpdateServerOutputStandardSchema,
1961+
execute: (rawInput) => {
1962+
const input = rawInput as typeof McpUpdateServerInputSchema.Type;
1963+
return self.updateServer(input).pipe(
1964+
Effect.map(ToolResult.ok),
1965+
Effect.catchTag("McpConnectionError", ({ message, transport }) =>
1966+
Effect.succeed(mcpToolFailure("mcp_connection_failed", message, { transport })),
1967+
),
1968+
);
1969+
},
1970+
}),
1971+
tool({
1972+
name: "configureAuth",
1973+
description:
1974+
"Configure or update the authentication templates on a registered remote MCP server. In 'merge' mode (default), new auth methods are appended to existing ones; in 'replace' mode, the entire authentication template is replaced.",
1975+
annotations: {
1976+
requiresApproval: true,
1977+
approvalDescription: "Configure MCP server authentication",
1978+
},
1979+
inputSchema: McpConfigureAuthToolInputStandardSchema,
1980+
outputSchema: McpConfigureAuthToolOutputStandardSchema,
1981+
execute: (rawInput) => {
1982+
const input = rawInput as typeof McpConfigureAuthToolInputSchema.Type;
1983+
return self
1984+
.configureAuth(input.slug, {
1985+
authenticationTemplate: input.authenticationTemplate,
1986+
mode: input.mode ?? "merge",
1987+
})
1988+
.pipe(
1989+
Effect.map((authenticationTemplate) =>
1990+
ToolResult.ok({
1991+
slug: input.slug,
1992+
authenticationTemplate,
1993+
}),
1994+
),
1995+
);
1996+
},
1997+
}),
18171998
],
18181999
},
18192000
],
@@ -1836,6 +2017,9 @@ export interface McpPluginExtension {
18362017
{ readonly slug: string },
18372018
McpExtensionFailure | IntegrationAlreadyExistsError
18382019
>;
2020+
readonly updateServer: (
2021+
input: McpUpdateServerInput,
2022+
) => Effect.Effect<{ readonly slug: string }, McpExtensionFailure>;
18392023
readonly removeServer: (slug: string) => Effect.Effect<void, McpExtensionFailure>;
18402024
/** Ensure every stdio integration has its default connection (migrating any
18412025
* legacy inline env into the secret store). Idempotent; safe to run at boot. */

0 commit comments

Comments
 (0)