diff --git a/packages/plugins/mcp/src/api/handlers.test.ts b/packages/plugins/mcp/src/api/handlers.test.ts
index 6d9048878..cecc33e5d 100644
--- a/packages/plugins/mcp/src/api/handlers.test.ts
+++ b/packages/plugins/mcp/src/api/handlers.test.ts
@@ -25,6 +25,7 @@ const failingExtension: McpPluginExtension = {
// oxlint-disable-next-line executor/no-error-constructor -- boundary: test injects a defect to verify opaque handler error responses
probeEndpoint: () => Effect.die(new Error("Not implemented")),
addServer: () => unused,
+ updateServer: () => unused,
removeServer: () => unused,
reconcileStdioConnections: () => unused,
getServer: () => Effect.succeed(null),
diff --git a/packages/plugins/mcp/src/sdk/index.ts b/packages/plugins/mcp/src/sdk/index.ts
index 9dbb9d3d0..456a0ad0f 100644
--- a/packages/plugins/mcp/src/sdk/index.ts
+++ b/packages/plugins/mcp/src/sdk/index.ts
@@ -4,6 +4,7 @@ export {
type McpPluginExtension,
type McpPluginOptions,
type McpServerInput,
+ type McpUpdateServerInput,
type McpRemoteServerInput,
type McpStdioServerInput,
type McpProbeResult,
diff --git a/packages/plugins/mcp/src/sdk/plugin.test.ts b/packages/plugins/mcp/src/sdk/plugin.test.ts
index fc8ce7f12..fe4efbe56 100644
--- a/packages/plugins/mcp/src/sdk/plugin.test.ts
+++ b/packages/plugins/mcp/src/sdk/plugin.test.ts
@@ -956,6 +956,76 @@ describe("mcpPlugin", () => {
}),
);
+ it.effect("agent tool updateServer updates MCP server config and auth template", () =>
+ Effect.gen(function* () {
+ const config = makeTestConfig({ plugins: [mcpPlugin()] as const });
+ const executor = yield* createExecutor(config);
+
+ yield* executor.execute(ToolAddress.make("executor.mcp.addServer"), {
+ name: "Initial MCP",
+ endpoint: "https://mcp1.example.com/mcp",
+ slug: "test_update_mcp",
+ });
+
+ const updated = yield* executor.execute(ToolAddress.make("executor.mcp.updateServer"), {
+ slug: "test_update_mcp",
+ name: "Updated MCP Display",
+ endpoint: "https://mcp2.example.com/mcp",
+ authenticationTemplate: [
+ {
+ type: "apiKey",
+ headers: { Authorization: ["Bearer ", { type: "variable", name: "token" }] },
+ },
+ ],
+ });
+ expect(updated).toMatchObject({
+ ok: true,
+ data: { slug: "test_update_mcp" },
+ });
+
+ const integration = yield* executor.integrations.get(IntegrationSlug.make("test_update_mcp"));
+ expect(integration?.name).toBe("Updated MCP Display");
+ expect(integration?.authMethods.map((m) => m.kind)).toEqual(["apikey"]);
+
+ yield* executor.close();
+ yield* Effect.promise(() => config.testDb.close());
+ }),
+ );
+
+ it.effect("agent tool configureAuth updates authentication templates on remote server", () =>
+ Effect.gen(function* () {
+ const config = makeTestConfig({ plugins: [mcpPlugin()] as const });
+ const executor = yield* createExecutor(config);
+
+ yield* executor.execute(ToolAddress.make("executor.mcp.addServer"), {
+ name: "Auth MCP",
+ endpoint: "https://auth.example.com/mcp",
+ slug: "test_auth_mcp",
+ });
+
+ const configured = yield* executor.execute(ToolAddress.make("executor.mcp.configureAuth"), {
+ slug: "test_auth_mcp",
+ authenticationTemplate: [
+ {
+ type: "apiKey",
+ headers: { "X-API-Key": [{ type: "variable", name: "apiKey" }] },
+ },
+ ],
+ mode: "replace",
+ });
+ expect(configured).toMatchObject({
+ ok: true,
+ data: { slug: "test_auth_mcp" },
+ });
+
+ const integration = yield* executor.integrations.get(IntegrationSlug.make("test_auth_mcp"));
+ expect(integration?.authMethods.map((m) => m.kind)).toEqual(["apikey"]);
+
+ yield* executor.close();
+ yield* Effect.promise(() => config.testDb.close());
+ }),
+ );
+
for (const status of [401, 403] as const) {
it.effect(`returns an auth tool failure when tools/call responds HTTP ${status}`, () =>
Effect.scoped(
diff --git a/packages/plugins/mcp/src/sdk/plugin.ts b/packages/plugins/mcp/src/sdk/plugin.ts
index ec8b92a77..dba57ab4c 100644
--- a/packages/plugins/mcp/src/sdk/plugin.ts
+++ b/packages/plugins/mcp/src/sdk/plugin.ts
@@ -253,6 +253,54 @@ const McpAddServerOutputSchema = Schema.Struct({
slug: Schema.String,
});
+const McpUpdateRemoteServerInputSchema = Schema.Struct({
+ slug: Schema.String,
+ transport: Schema.optional(Schema.Literal("remote")),
+ name: Schema.optional(Schema.String),
+ family: Schema.optional(Schema.String),
+ description: Schema.optional(Schema.String),
+ endpoint: Schema.optional(Schema.String),
+ remoteTransport: Schema.optional(McpRemoteTransport),
+ headers: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+ queryParams: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+ authenticationTemplate: Schema.optional(Schema.Array(McpAuthMethodInput)),
+ auth: Schema.optional(McpAuthShorthand),
+});
+
+const McpUpdateStdioServerInputSchema = Schema.Struct({
+ slug: Schema.String,
+ transport: Schema.Literal("stdio"),
+ name: Schema.optional(Schema.String),
+ family: Schema.optional(Schema.String),
+ description: Schema.optional(Schema.String),
+ command: Schema.optional(Schema.String),
+ args: Schema.optional(Schema.Array(Schema.String)),
+ envVars: Schema.optional(Schema.Array(Schema.String)),
+ env: Schema.optional(Schema.Record(Schema.String, Schema.String)),
+ cwd: Schema.optional(Schema.String),
+ versionNegotiation: Schema.optional(McpStdioVersionNegotiation),
+});
+
+const McpUpdateServerInputSchema = Schema.Union([
+ McpUpdateRemoteServerInputSchema,
+ McpUpdateStdioServerInputSchema,
+]);
+
+const McpUpdateServerOutputSchema = Schema.Struct({
+ slug: Schema.String,
+});
+
+const McpConfigureAuthToolInputSchema = Schema.Struct({
+ slug: Schema.String,
+ authenticationTemplate: Schema.Array(McpAuthMethodInput),
+ mode: Schema.optional(Schema.Literals(["merge", "replace"])),
+});
+
+const McpConfigureAuthToolOutputSchema = Schema.Struct({
+ slug: Schema.String,
+ authenticationTemplate: Schema.Array(Schema.Unknown),
+});
+
/** Input for the custom-method-create flow. `merge` (default) appends onto the
* integration's existing `authenticationTemplate`; `replace` swaps the whole
* declared set. Mirrors the OpenAPI/GraphQL `configureAuth` inputs. */
@@ -292,6 +340,7 @@ const McpProbeEndpointOutputSchema = Schema.Struct({
export type McpRemoteServerInput = typeof McpRemoteServerInputSchema.Type;
export type McpStdioServerInput = typeof McpStdioServerInputSchema.Type;
export type McpServerInput = typeof McpAddServerInputSchema.Type;
+export type McpUpdateServerInput = typeof McpUpdateServerInputSchema.Type;
export type McpProbeResult = typeof McpProbeEndpointOutputSchema.Type;
export type McpProbeEndpointInput = typeof McpProbeEndpointInputSchema.Type;
@@ -311,6 +360,14 @@ const schemaToStaticToolSchema = (schema: Schema.Decoder): StaticToo
const McpAddServerInputStandardSchema = schemaToStaticToolSchema(McpAddServerInputSchema);
const McpAddServerOutputStandardSchema = schemaToStaticToolSchema(McpAddServerOutputSchema);
+const McpUpdateServerInputStandardSchema = schemaToStaticToolSchema(McpUpdateServerInputSchema);
+const McpUpdateServerOutputStandardSchema = schemaToStaticToolSchema(McpUpdateServerOutputSchema);
+const McpConfigureAuthToolInputStandardSchema = schemaToStaticToolSchema(
+ McpConfigureAuthToolInputSchema,
+);
+const McpConfigureAuthToolOutputStandardSchema = schemaToStaticToolSchema(
+ McpConfigureAuthToolOutputSchema,
+);
const McpProbeEndpointInputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointInputSchema);
const McpProbeEndpointOutputStandardSchema = schemaToStaticToolSchema(McpProbeEndpointOutputSchema);
const McpGetServerInputStandardSchema = schemaToStaticToolSchema(McpGetServerInputSchema);
@@ -1248,6 +1305,80 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
}),
);
+ const updateServer = (input: McpUpdateServerInput) =>
+ Effect.gen(function* () {
+ const slug = slugFrom(input.slug);
+ const record = yield* ctx.core.integrations.get(slug);
+ if (!record) {
+ return yield* new McpConnectionError({
+ message: `MCP server not found: ${input.slug}`,
+ transport: "remote",
+ });
+ }
+ const current = parseMcpIntegrationConfig(record.config);
+ if (!current) {
+ return yield* new McpConnectionError({
+ message: `Invalid configuration for MCP server: ${input.slug}`,
+ transport: "remote",
+ });
+ }
+
+ let updatedConfig: McpIntegrationConfigType;
+ if (current.transport === "stdio") {
+ const stdioInput = input as typeof McpUpdateStdioServerInputSchema.Type;
+ let authenticationTemplate = current.authenticationTemplate;
+ if (stdioInput.envVars !== undefined) {
+ authenticationTemplate =
+ stdioInput.envVars.length > 0
+ ? [{ slug: "stdio_env", kind: "stdio_env", vars: stdioInput.envVars }]
+ : [{ slug: "none", kind: "none" }];
+ }
+ updatedConfig = {
+ transport: "stdio",
+ family: stdioInput.family ?? current.family,
+ command: stdioInput.command ?? current.command,
+ args: stdioInput.args ?? current.args,
+ cwd: stdioInput.cwd !== undefined ? stdioInput.cwd : current.cwd,
+ ...(stdioInput.versionNegotiation !== undefined
+ ? { versionNegotiation: stdioInput.versionNegotiation }
+ : current.versionNegotiation !== undefined
+ ? { versionNegotiation: current.versionNegotiation }
+ : {}),
+ ...(authenticationTemplate !== undefined ? { authenticationTemplate } : {}),
+ };
+ } else {
+ const remoteInput = input as typeof McpUpdateRemoteServerInputSchema.Type;
+ let authenticationTemplate = current.authenticationTemplate;
+ if (remoteInput.authenticationTemplate !== undefined) {
+ authenticationTemplate = normalizeMcpAuthMethods(remoteInput.authenticationTemplate);
+ } else if (remoteInput.auth !== undefined) {
+ authenticationTemplate = [mcpAuthMethodFromShorthand(remoteInput.auth)];
+ }
+
+ updatedConfig = {
+ transport: "remote",
+ family: remoteInput.family ?? current.family,
+ endpoint: remoteInput.endpoint ?? current.endpoint,
+ remoteTransport: remoteInput.remoteTransport ?? current.remoteTransport,
+ headers: remoteInput.headers ?? current.headers,
+ queryParams: remoteInput.queryParams ?? current.queryParams,
+ authenticationTemplate,
+ };
+ }
+
+ yield* ctx.core.integrations.update(slug, {
+ ...(input.name !== undefined ? { name: input.name } : {}),
+ ...(input.description !== undefined ? { description: input.description } : {}),
+ config: updatedConfig,
+ });
+
+ return { slug: String(input.slug) };
+ }).pipe(
+ Effect.withSpan("mcp.plugin.update_server", {
+ attributes: { "mcp.integration.slug": input.slug },
+ }),
+ );
+
/** Merge-append auth methods onto the integration's existing
* `authenticationTemplate` (custom-method-create flow), mirroring the
* OpenAPI/GraphQL `configureAuth`. Returns the merged array. A no-op
@@ -1289,6 +1420,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
return {
probeEndpoint,
addServer,
+ updateServer,
removeServer,
reconcileStdioConnections,
getServer,
@@ -1814,6 +1946,53 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
);
},
}),
+ tool({
+ name: "updateServer",
+ description:
+ "Update the configuration of an existing registered MCP server (transport settings, endpoint/command, headers, query params, or auth templates).",
+ annotations: {
+ requiresApproval: true,
+ approvalDescription: "Update an MCP server",
+ },
+ inputSchema: McpUpdateServerInputStandardSchema,
+ outputSchema: McpUpdateServerOutputStandardSchema,
+ execute: (rawInput) => {
+ const input = rawInput as typeof McpUpdateServerInputSchema.Type;
+ return self.updateServer(input).pipe(
+ Effect.map(ToolResult.ok),
+ Effect.catchTag("McpConnectionError", ({ message, transport }) =>
+ Effect.succeed(mcpToolFailure("mcp_connection_failed", message, { transport })),
+ ),
+ );
+ },
+ }),
+ tool({
+ name: "configureAuth",
+ description:
+ "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.",
+ annotations: {
+ requiresApproval: true,
+ approvalDescription: "Configure MCP server authentication",
+ },
+ inputSchema: McpConfigureAuthToolInputStandardSchema,
+ outputSchema: McpConfigureAuthToolOutputStandardSchema,
+ execute: (rawInput) => {
+ const input = rawInput as typeof McpConfigureAuthToolInputSchema.Type;
+ return self
+ .configureAuth(input.slug, {
+ authenticationTemplate: input.authenticationTemplate,
+ mode: input.mode ?? "merge",
+ })
+ .pipe(
+ Effect.map((authenticationTemplate) =>
+ ToolResult.ok({
+ slug: input.slug,
+ authenticationTemplate,
+ }),
+ ),
+ );
+ },
+ }),
],
},
],
@@ -1836,6 +2015,9 @@ export interface McpPluginExtension {
{ readonly slug: string },
McpExtensionFailure | IntegrationAlreadyExistsError
>;
+ readonly updateServer: (
+ input: McpUpdateServerInput,
+ ) => Effect.Effect<{ readonly slug: string }, McpExtensionFailure>;
readonly removeServer: (slug: string) => Effect.Effect;
/** Ensure every stdio integration has its default connection (migrating any
* legacy inline env into the secret store). Idempotent; safe to run at boot. */