From e38d0e0fa973de5e75f1142846e55c1b02c0df48 Mon Sep 17 00:00:00 2001 From: Bolun Zhang Date: Tue, 19 May 2026 12:56:03 +0800 Subject: [PATCH] feat(mcp): consolidate provider settings tools (#1011) Closes #984 --- .../src/server/agent/mcp-parity.e2e.test.ts | 28 +-- .../src/server/agent/mcp-server.test.ts | 164 ++++++++++--- .../server/src/server/agent/mcp-server.ts | 218 ++++++++++++------ public-docs/mcp.md | 37 ++- skills/paseo/SKILL.md | 16 +- 5 files changed, 319 insertions(+), 144 deletions(-) diff --git a/packages/server/src/server/agent/mcp-parity.e2e.test.ts b/packages/server/src/server/agent/mcp-parity.e2e.test.ts index 7370e6082..9ee49887e 100644 --- a/packages/server/src/server/agent/mcp-parity.e2e.test.ts +++ b/packages/server/src/server/agent/mcp-parity.e2e.test.ts @@ -154,7 +154,7 @@ async function createTopLevelAgent(args?: Partial): Promise { title: "MCP parity parent", provider: "claude/claude-test-model", initialPrompt: "say done and stop", - mode: "bypassPermissions", + settings: { modeId: "bypassPermissions" }, background: true, }); parentAgentId = str(parentPayload.agentId); @@ -339,7 +339,7 @@ describe("Suite A: Core Fixes", () => { test("create_agent accepts provider features over MCP", async () => { let agentId: string | null = null; try { - agentId = await createTopLevelAgent({ features: { test_feature: true } }); + agentId = await createTopLevelAgent({ settings: { features: { test_feature: true } } }); const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId); expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true }); @@ -356,7 +356,7 @@ describe("Suite A: Core Fixes", () => { try { agentId = await createChildAgent({ provider: "claude/claude-test-model", - features: { test_feature: true }, + settings: { features: { test_feature: true } }, }); const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId); expect(internalSnapshot?.config.featureValues).toEqual({ test_feature: true }); @@ -369,14 +369,13 @@ describe("Suite A: Core Fixes", () => { } }); - test("set_agent_feature updates provider features over MCP", async () => { + test("update_agent updates provider features over MCP", async () => { let agentId: string | null = null; try { - agentId = await createTopLevelAgent({ features: { test_feature: false } }); - const updated = await callToolStructured(topLevelClient, "set_agent_feature", { + agentId = await createTopLevelAgent({ settings: { features: { test_feature: false } } }); + const updated = await callToolStructured(topLevelClient, "update_agent", { agentId, - featureId: "test_feature", - value: true, + settings: { features: { test_feature: true } }, }); expect(updated.success).toBe(true); const internalSnapshot = daemonHandle.daemon.agentManager.getAgent(agentId); @@ -390,15 +389,18 @@ describe("Suite A: Core Fixes", () => { } }); - test("list_provider_features returns draft provider features over MCP", async () => { - const payload = await callToolStructured(topLevelClient, "list_provider_features", { + test("inspect_provider returns draft provider features over MCP", async () => { + const payload = await callToolStructured(topLevelClient, "inspect_provider", { provider: "claude", cwd: parentAgentCwd, - model: "claude-test-model", - featureValues: { test_feature: true }, + settings: { + model: "claude-test-model", + features: { test_feature: true }, + }, }); expect(payload.provider).toBe("claude"); + expect(payload.selectedModel).toBe("claude-test-model"); expect(recordArr(payload.features)).toEqual( expect.arrayContaining([ expect.objectContaining({ diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index ab27332e5..c1f59cc8f 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -117,7 +117,9 @@ function buildAgentManagerSpies() { createAgent: vi.fn(), waitForAgentEvent: vi.fn(), recordUserMessage: vi.fn(), - setAgentMode: vi.fn(), + setAgentMode: vi.fn().mockResolvedValue(undefined), + setAgentModel: vi.fn().mockResolvedValue(undefined), + setAgentThinkingOption: vi.fn().mockResolvedValue(undefined), setAgentFeature: vi.fn().mockResolvedValue(undefined), setLabels: vi.fn().mockResolvedValue(undefined), setTitle: vi.fn().mockResolvedValue(undefined), @@ -446,7 +448,7 @@ describe("create_agent MCP tool", () => { const missingTitle = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, provider: "codex/gpt-5.4", initialPrompt: "test", }); @@ -455,7 +457,7 @@ describe("create_agent MCP tool", () => { const tooLong = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, provider: "codex/gpt-5.4", title: "x".repeat(61), initialPrompt: "test", @@ -465,7 +467,7 @@ describe("create_agent MCP tool", () => { const ok = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, provider: "codex/gpt-5.4", title: "Short title", initialPrompt: "test", @@ -479,7 +481,7 @@ describe("create_agent MCP tool", () => { const tool = registeredTool(server, "create_agent"); const parsed = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, provider: "codex/gpt-5.4", title: "Short title", }); @@ -510,7 +512,7 @@ describe("create_agent MCP tool", () => { provider: "codex/gpt-5.4", initialPrompt: "Do work", background: true, - features: { fast_mode: true }, + settings: { features: { fast_mode: true } }, }; const parsed = await tool.inputSchema.safeParseAsync(input); @@ -536,7 +538,7 @@ describe("create_agent MCP tool", () => { const missingProvider = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, title: "Short title", initialPrompt: "test", }); @@ -549,7 +551,7 @@ describe("create_agent MCP tool", () => { const providerWithoutModel = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, title: "Short title", provider: "codex", initialPrompt: "test", @@ -558,7 +560,7 @@ describe("create_agent MCP tool", () => { const providerWithEmptyModel = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, title: "Short title", provider: "codex/", initialPrompt: "test", @@ -567,7 +569,7 @@ describe("create_agent MCP tool", () => { const providerWithEmptyProvider = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, title: "Short title", provider: "/gpt-5.4", initialPrompt: "test", @@ -577,7 +579,7 @@ describe("create_agent MCP tool", () => { await expect( tool.handler({ cwd: existingCwd, - mode: "default", + settings: { modeId: "default" }, title: "Short title", provider: "codex/gpt-5.4", model: "gpt-5.4", @@ -722,10 +724,9 @@ describe("create_agent MCP tool", () => { await tool.handler({ cwd: existingCwd, title: "Config test", - mode: "auto", initialPrompt: "Do work", provider: "codex/gpt-5.4", - thinking: "think-hard", + settings: { modeId: "auto", thinkingOptionId: "think-hard" }, labels: { source: "mcp" }, }); @@ -1269,7 +1270,7 @@ describe("create_agent MCP tool", () => { const parsed = await tool.inputSchema.safeParseAsync({ cwd: existingCwd, title: "Custom provider agent", - mode: "default", + settings: { modeId: "default" }, provider: "zai/custom-model", initialPrompt: "Do work", }); @@ -1360,7 +1361,7 @@ describe("create_agent MCP tool", () => { provider: "codex/gpt-5.4", initialPrompt: "Do work", background: true, - features: { fast_mode: true }, + settings: { features: { fast_mode: true } }, }; const parsed = await tool.inputSchema.safeParseAsync(input); @@ -1403,7 +1404,7 @@ describe("create_agent MCP tool", () => { await tool.handler({ cwd: existingCwd, title: "Injected config test", - mode: "auto", + settings: { modeId: "auto" }, provider: "codex/gpt-5.4", initialPrompt: "Do work", }); @@ -1428,7 +1429,7 @@ describe("create_agent MCP tool", () => { cwd: existingCwd, title: "Bad mode", provider: "opencode/gpt-5.4", - mode: "bypassPermissions", + settings: { modeId: "bypassPermissions" }, initialPrompt: "Do work", }), ).rejects.toThrow( @@ -1567,7 +1568,7 @@ describe("create_agent MCP tool", () => { await tool.handler({ title: "Child", provider: "opencode/gpt-5.4", - mode: "build", + settings: { modeId: "build" }, initialPrompt: "Do work", }); @@ -1579,17 +1580,31 @@ describe("create_agent MCP tool", () => { }); }); -describe("set_agent_feature MCP tool", () => { +describe("update_agent MCP tool", () => { const logger = createTestLogger(); - it("sets a provider feature on an existing agent", async () => { - const { agentManager, agentStorage, spies } = createTestDeps(); + it("does not register the replaced feature-specific MCP tool", async () => { + const { agentManager, agentStorage } = createTestDeps(); const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); - const tool = registeredTool(server, "set_agent_feature"); + + expect(lookupTool(server, "set_agent_feature")).toBeUndefined(); + }); + + it("updates runtime settings before metadata", async () => { + const { agentManager, agentStorage, spies } = createTestDeps(); + spies.agentStorage.get.mockResolvedValue(createStoredRecord({ id: "agent-1" })); + const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); + const tool = registeredTool(server, "update_agent"); const input = { agentId: "agent-1", - featureId: "fast_mode", - value: true, + name: "Updated agent", + labels: { role: "worker" }, + settings: { + modeId: "full-access", + model: "gpt-5.4", + thinkingOptionId: "high", + features: { fast_mode: true }, + }, }; const parsed = await tool.inputSchema.safeParseAsync(input); @@ -1597,9 +1612,39 @@ describe("set_agent_feature MCP tool", () => { const response = await tool.handler(input); + expect(spies.agentManager.setAgentMode).toHaveBeenCalledWith("agent-1", "full-access"); + expect(spies.agentManager.setAgentModel).toHaveBeenCalledWith("agent-1", "gpt-5.4"); + expect(spies.agentManager.setAgentThinkingOption).toHaveBeenCalledWith("agent-1", "high"); expect(spies.agentManager.setAgentFeature).toHaveBeenCalledWith("agent-1", "fast_mode", true); + expect(spies.agentStorage.upsert).toHaveBeenCalledWith( + expect.objectContaining({ + id: "agent-1", + title: "Updated agent", + }), + ); + expect(spies.agentManager.setLabels).toHaveBeenCalledWith("agent-1", { role: "worker" }); expect(response.structuredContent).toEqual({ success: true }); }); + + it("does not update metadata when runtime settings fail", async () => { + const { agentManager, agentStorage, spies } = createTestDeps(); + spies.agentManager.setAgentFeature.mockRejectedValue(new Error("unsupported feature")); + const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); + const tool = registeredTool(server, "update_agent"); + + await expect( + tool.handler({ + agentId: "agent-1", + name: "Should not persist", + labels: { role: "worker" }, + settings: { features: { fast_mode: true } }, + }), + ).rejects.toThrow("unsupported feature"); + + expect(spies.agentStorage.get).not.toHaveBeenCalled(); + expect(spies.agentStorage.upsert).not.toHaveBeenCalled(); + expect(spies.agentManager.setLabels).not.toHaveBeenCalled(); + }); }); describe("create_schedule MCP tool", () => { @@ -2033,10 +2078,17 @@ describe("provider listing MCP tool", () => { }); }); -describe("model listing MCP tool", () => { +describe("provider MCP tools", () => { const logger = createTestLogger(); - it("lists provider features for a draft agent configuration", async () => { + it("does not register the replaced feature-specific provider discovery MCP tool", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const server = await createAgentMcpServer({ agentManager, agentStorage, logger }); + + expect(lookupTool(server, "list_provider_features")).toBeUndefined(); + }); + + it("inspects provider features for a draft agent configuration", async () => { const { agentManager, agentStorage, spies } = createTestDeps(); spies.agentManager.listDraftFeatures.mockResolvedValue([ { @@ -2046,19 +2098,29 @@ describe("model listing MCP tool", () => { value: false, }, ]); + const providerRegistry = { + codex: createProviderDefinition({ + id: "codex", + label: "Codex", + description: "OpenAI coding agent", + modes: [{ id: "full-access", label: "Full Access", description: "Can edit files" }], + }), + }; const server = await createAgentMcpServer({ agentManager, agentStorage, + providerRegistry, logger, }); - const tool = registeredTool(server, "list_provider_features"); + const tool = registeredTool(server, "inspect_provider"); const input = { - provider: "codex", + provider: "codex/gpt-5.4", cwd: "~/repo", - modeId: "full-access", - model: "gpt-5.4", - thinkingOptionId: "high", - featureValues: { fast_mode: true }, + settings: { + modeId: "full-access", + thinkingOptionId: "high", + features: { fast_mode: true }, + }, }; const parsed = await tool.inputSchema.safeParseAsync(input); @@ -2076,6 +2138,12 @@ describe("model listing MCP tool", () => { }); expect(response.structuredContent).toEqual({ provider: "codex", + label: "Codex", + description: "OpenAI coding agent", + enabled: true, + status: "available", + modes: [{ id: "full-access", label: "Full Access", description: "Can edit files" }], + selectedModel: "gpt-5.4", features: [ { type: "toggle", @@ -2118,6 +2186,38 @@ describe("model listing MCP tool", () => { ); expect(fetchModels).not.toHaveBeenCalled(); }); + + it("inspect_provider rejects disabled providers without fetching models", async () => { + const { agentManager, agentStorage } = createTestDeps(); + const fetchModels = vi.fn().mockResolvedValue([ + { + provider: "codex", + id: "gpt-5.4", + label: "GPT-5.4", + }, + ]); + const providerRegistry = { + codex: createProviderDefinition({ + id: "codex", + label: "Codex", + enabled: false, + fetchModels, + }), + }; + + const server = await createAgentMcpServer({ + agentManager, + agentStorage, + providerRegistry, + logger, + }); + const tool = registeredTool(server, "inspect_provider"); + + await expect(tool.handler({ provider: "codex", cwd: "~/repo" })).rejects.toThrow( + "Provider 'codex' is disabled", + ); + expect(fetchModels).not.toHaveBeenCalled(); + }); }); describe("speak MCP tool", () => { diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 89387363d..577e9f495 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -62,6 +62,7 @@ import { AgentModelSchema, AgentProviderEnum, AgentStatusEnum, + ProviderModeSchema, ProviderSummarySchema, parseDurationString, resolveRequiredProviderModel, @@ -587,6 +588,55 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom }, { message: "provider must be provider/model, for example codex/gpt-5.4" }, ); + const ProviderOrProviderModelInputSchema = AgentProviderEnum.trim() + .min(1, "provider is required") + .refine( + (value) => { + if (!value.includes("/")) { + return true; + } + try { + resolveRequiredProviderModel(value); + return true; + } catch { + return false; + } + }, + { message: "provider must be provider or provider/model, for example codex/gpt-5.4" }, + ); + const CreateAgentSettingsInputSchema = z + .object({ + modeId: z.string().optional().describe("Session mode to configure before the first run."), + thinkingOptionId: z.string().optional().describe("Thinking option ID."), + features: z + .record(z.unknown()) + .optional() + .describe("Provider-specific feature values, for example { fast_mode: true } for Codex."), + }) + .strict(); + const UpdateAgentSettingsInputSchema = z + .object({ + modeId: z.string().optional().describe("Session mode ID."), + model: z.string().nullable().optional().describe("Model ID. Pass null to clear."), + thinkingOptionId: z + .string() + .nullable() + .optional() + .describe("Thinking option ID. Pass null to clear."), + features: z + .record(z.unknown()) + .optional() + .describe("Provider-specific feature values, for example { fast_mode: true } for Codex."), + }) + .strict(); + const InspectProviderSettingsInputSchema = z + .object({ + modeId: z.string().optional().describe("Draft session mode ID."), + model: z.string().optional().describe("Draft model ID."), + thinkingOptionId: z.string().optional().describe("Draft thinking option ID."), + features: z.record(z.unknown()).optional().describe("Draft provider feature values."), + }) + .strict(); const agentToAgentInputSchema = { cwd: z .string() @@ -601,23 +651,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom provider: ProviderModelInputSchema.describe( "Required provider/model pair, for example codex/gpt-5.4.", ), - thinking: z.string().optional().describe("Thinking option ID"), - features: z - .record(z.unknown()) - .optional() - .describe("Provider-specific feature values, for example { fast_mode: true } for Codex."), labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"), + settings: CreateAgentSettingsInputSchema.optional().describe( + "Initial runtime settings for the new agent.", + ), initialPrompt: z .string() .trim() .min(1, "initialPrompt is required") .describe("Required first task to run immediately after creation."), - mode: z - .string() - .optional() - .describe( - "Optional session mode for the new agent. Required when the new agent uses a different provider than the caller agent.", - ), background: z .boolean() .optional() @@ -647,21 +689,15 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom provider: ProviderModelInputSchema.describe( "Required provider/model pair, for example codex/gpt-5.4.", ), - thinking: z.string().optional().describe("Thinking option ID"), - features: z - .record(z.unknown()) - .optional() - .describe("Provider-specific feature values, for example { fast_mode: true } for Codex."), labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"), + settings: CreateAgentSettingsInputSchema.optional().describe( + "Initial runtime settings for the new agent.", + ), initialPrompt: z .string() .trim() .min(1, "initialPrompt is required") .describe("Required first task to run immediately after creation."), - mode: z - .string() - .optional() - .describe("Optional session mode to configure before the first run."), worktreeName: z .string() .optional() @@ -700,13 +736,17 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom const createAgentInputSchema = callerAgentId ? agentToAgentInputSchema : topLevelInputSchema; const agentToAgentCreateAgentArgsSchema = z.object(agentToAgentInputSchema).strict(); const topLevelCreateAgentArgsSchema = z.object(topLevelInputSchema).strict(); - const listProviderFeaturesInputSchema = { - provider: AgentProviderEnum, - cwd: z.string().describe("Working directory used to resolve provider feature availability."), - modeId: z.string().optional(), - model: z.string().optional(), - thinkingOptionId: z.string().optional(), - featureValues: z.record(z.unknown()).optional(), + const inspectProviderInputSchema = { + provider: ProviderOrProviderModelInputSchema.describe( + "Provider ID, optionally with a model ID (for example codex or codex/gpt-5.4).", + ), + cwd: z + .string() + .optional() + .describe("Working directory used to resolve provider feature availability."), + settings: InspectProviderSettingsInputSchema.optional().describe( + "Draft provider settings used to compute available features.", + ), }; if (options.voiceOnly || options.enableVoiceTools || callerContext?.enableVoiceTools) { @@ -758,7 +798,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom background: boolean; normalizedTitle: string | null; model: string | undefined; - thinking: string | undefined; + thinkingOptionId: string | undefined; features: Record | undefined; labels: Record | undefined; notifyOnFinish: boolean; @@ -805,6 +845,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom throw new Error(`Parent agent ${parentAgentId} not found`); } const provider = resolvedProviderModel.provider; + const settings = callerArgs.settings; const resolvedCwd = resolveChildAgentCwd({ parentCwd: parentAgent.cwd, requestedCwd: callerArgs.cwd, @@ -812,7 +853,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom allowCustomCwd: callerContext?.allowCustomCwd ?? true, }); const resolvedMode = resolveAndValidateCreateAgentMode({ - requestedMode: callerArgs.mode, + requestedMode: settings?.modeId, targetProvider: provider, parent: { provider: parentAgent.provider, @@ -828,8 +869,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom background: callerArgs.background ?? false, normalizedTitle: callerArgs.title.trim(), model: resolvedProviderModel.model, - thinking: callerArgs.thinking, - features: callerArgs.features, + thinkingOptionId: settings?.thinkingOptionId, + features: settings?.features, labels: callerArgs.labels, notifyOnFinish: callerArgs.notifyOnFinish ?? false, resolvedCwd, @@ -843,9 +884,10 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom ): Promise => { const topLevelArgs = topLevelCreateAgentArgsSchema.parse(args); const resolvedProviderModel = resolveRequiredProviderModel(topLevelArgs.provider); - const { cwd, mode, worktreeName, baseBranch, refName, action, githubPrNumber } = topLevelArgs; + const { cwd, settings, worktreeName, baseBranch, refName, action, githubPrNumber } = + topLevelArgs; const resolvedMode = resolveAndValidateCreateAgentMode({ - requestedMode: mode, + requestedMode: settings?.modeId, targetProvider: resolvedProviderModel.provider, parent: null, availableModes: getAvailableModeIds(resolvedProviderModel.provider), @@ -902,8 +944,8 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom background: topLevelArgs.background ?? false, normalizedTitle: topLevelArgs.title.trim(), model: resolvedProviderModel.model, - thinking: topLevelArgs.thinking, - features: topLevelArgs.features, + thinkingOptionId: settings?.thinkingOptionId, + features: settings?.features, labels: topLevelArgs.labels, notifyOnFinish: topLevelArgs.notifyOnFinish ?? false, resolvedCwd, @@ -946,7 +988,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom background, normalizedTitle, model, - thinking, + thinkingOptionId, features, labels, notifyOnFinish, @@ -968,7 +1010,7 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom modeId: resolvedMode, title: normalizedTitle ?? undefined, model, - thinkingOptionId: thinking, + thinkingOptionId, featureValues: features, }, undefined, @@ -1057,29 +1099,6 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom }, ); - server.registerTool( - "set_agent_feature", - { - title: "Set agent feature", - description: "Set a provider-specific feature on an existing agent, such as Codex fast_mode.", - inputSchema: { - agentId: z.string(), - featureId: z.string().trim().min(1), - value: z.unknown(), - }, - outputSchema: { - success: z.boolean(), - }, - }, - async ({ agentId, featureId, value }) => { - await agentManager.setAgentFeature(agentId, featureId, value); - return { - content: [], - structuredContent: ensureValidJson({ success: true }), - }; - }, - ); - server.registerTool( "wait_for_agent", { @@ -1440,17 +1459,35 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom "update_agent", { title: "Update agent", - description: "Update an agent name and/or labels.", + description: "Update an agent name, labels, and/or runtime settings.", inputSchema: { agentId: z.string(), name: z.string().optional(), labels: z.record(z.string(), z.string()).optional().describe("Labels to set on the agent"), + settings: UpdateAgentSettingsInputSchema.optional().describe( + "Runtime settings to apply to the agent.", + ), }, outputSchema: { success: z.boolean(), }, }, - async ({ agentId, name, labels }) => { + async ({ agentId, name, labels, settings }) => { + if (settings?.modeId !== undefined) { + await agentManager.setAgentMode(agentId, settings.modeId); + } + if (settings?.model !== undefined) { + await agentManager.setAgentModel(agentId, settings.model); + } + if (settings?.thinkingOptionId !== undefined) { + await agentManager.setAgentThinkingOption(agentId, settings.thinkingOptionId); + } + if (settings?.features) { + for (const [featureId, value] of Object.entries(settings.features)) { + await agentManager.setAgentFeature(agentId, featureId, value); + } + } + const trimmedName = name?.trim(); if (trimmedName) { const record = await agentStorage.get(agentId); @@ -2022,30 +2059,63 @@ export async function createAgentMcpServer(options: AgentMcpServerOptions): Prom ); server.registerTool( - "list_provider_features", + "inspect_provider", { - title: "List provider features", + title: "Inspect provider", description: - "List provider-specific features available for a draft agent configuration, such as Codex fast_mode.", - inputSchema: listProviderFeaturesInputSchema, + "Inspect compact provider capabilities for orchestration, including modes and draft feature settings. Use list_models for the full model list.", + inputSchema: inspectProviderInputSchema, outputSchema: { provider: AgentProviderEnum, + label: z.string().nullable().optional(), + description: z.string().nullable().optional(), + enabled: z.boolean(), + status: z.string(), + modes: z.array(ProviderModeSchema).nullish(), + selectedModel: z.string().nullable(), features: z.array(AgentFeatureSchema), }, }, - async ({ provider, cwd, modeId, model, thinkingOptionId, featureValues }) => { - const features = await agentManager.listDraftFeatures({ + async ({ provider, cwd, settings }) => { + const resolvedProviderModel = resolveScheduleProviderAndModel({ provider, - cwd: expandUserPath(cwd), - ...(modeId ? { modeId } : {}), - ...(model ? { model } : {}), - ...(thinkingOptionId ? { thinkingOptionId } : {}), - ...(featureValues ? { featureValues } : {}), + defaultProvider: provider, + }); + const providerId = resolvedProviderModel.provider; + if (!providerRegistry) { + throw new Error("Provider registry is not configured"); + } + const definition = providerRegistry[providerId]; + if (!definition) { + throw new Error(`Provider ${providerId} is not configured`); + } + const summary = await resolveProviderSummary(definition, childLogger); + if (!definition.enabled) { + throw new Error(`Provider '${providerId}' is disabled`); + } + if (summary.status !== "available") { + throw new Error(summary.error ?? `Provider '${providerId}' is unavailable`); + } + const resolvedCwd = resolveScopedCwd(cwd, { required: true }); + const selectedModel = settings?.model ?? resolvedProviderModel.model; + const features = await agentManager.listDraftFeatures({ + provider: providerId, + cwd: resolvedCwd, + ...(settings?.modeId ? { modeId: settings.modeId } : {}), + ...(selectedModel ? { model: selectedModel } : {}), + ...(settings?.thinkingOptionId ? { thinkingOptionId: settings.thinkingOptionId } : {}), + ...(settings?.features ? { featureValues: settings.features } : {}), }); return { content: [], structuredContent: ensureValidJson({ - provider, + provider: providerId, + label: summary.label, + description: summary.description, + enabled: summary.enabled, + status: summary.status, + modes: summary.modes, + selectedModel: selectedModel ?? null, features, }), }; diff --git a/public-docs/mcp.md b/public-docs/mcp.md index 29d747329..54f000800 100644 --- a/public-docs/mcp.md +++ b/public-docs/mcp.md @@ -15,20 +15,19 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may ### Agents -| Tool | Function | -| -------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| `create_agent` | Create an agent tied to a working directory, optionally with an initial prompt, provider features, or a new git worktree. | -| `wait_for_agent` | Block until an agent requests permission or finishes its current run. | -| `send_agent_prompt` | Send a task to a running agent. | -| `get_agent_status` | Return the latest snapshot for an agent. | -| `list_agents` | List recent agents as compact metadata. | -| `cancel_agent` | Abort an agent's current run but keep the agent alive. | -| `archive_agent` | Soft-delete an agent and remove it from the active list. | -| `kill_agent` | Terminate an agent session permanently. | -| `update_agent` | Update an agent name or labels. | -| `get_agent_activity` | Return recent agent timeline entries as a curated summary. | -| `set_agent_mode` | Switch an agent's session mode. | -| `set_agent_feature` | Set a provider-specific feature on an existing agent, for example Codex `fast_mode`. | +| Tool | Function | +| -------------------- | ---------------------------------------------------------------------------------------------------- | +| `create_agent` | Create an agent tied to a working directory, optionally with initial settings or a new git worktree. | +| `wait_for_agent` | Block until an agent requests permission or finishes its current run. | +| `send_agent_prompt` | Send a task to a running agent. | +| `get_agent_status` | Return the latest snapshot for an agent. | +| `list_agents` | List recent agents as compact metadata. | +| `cancel_agent` | Abort an agent's current run but keep the agent alive. | +| `archive_agent` | Soft-delete an agent and remove it from the active list. | +| `kill_agent` | Terminate an agent session permanently. | +| `update_agent` | Update an agent name, labels, or runtime settings such as mode/model/thinking/features. | +| `get_agent_activity` | Return recent agent timeline entries as a curated summary. | +| `set_agent_mode` | Switch an agent's session mode. | ### Terminals @@ -53,11 +52,11 @@ The MCP server itself is controlled by `daemon.mcp.enabled`. Existing agents may ### Providers -| Tool | Function | -| ------------------------ | ------------------------------------------------------------------------------------------- | -| `list_providers` | List configured agent providers, availability, and modes. | -| `list_models` | List models for an agent provider. | -| `list_provider_features` | List provider-specific features for a draft agent configuration, such as Codex `fast_mode`. | +| Tool | Function | +| ------------------ | ----------------------------------------------------------------- | +| `list_providers` | List configured agent providers, availability, and modes. | +| `list_models` | List models for an agent provider. | +| `inspect_provider` | Inspect compact provider capabilities and draft feature settings. | ### Worktrees diff --git a/skills/paseo/SKILL.md b/skills/paseo/SKILL.md index 1115202a9..c40be9f72 100644 --- a/skills/paseo/SKILL.md +++ b/skills/paseo/SKILL.md @@ -20,25 +20,29 @@ Returns `{ branchName, worktreePath }`. Pass `cwd` to target a specific repo. ## Agents -**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`, `features`. Returns `{ agentId, … }`. +**`create_agent`** — required: `title`, `provider` (`claude/opus`, `codex/gpt-5.4`, …), `initialPrompt`. Common: `cwd` (often a `worktreePath`), `background` (default `false` — blocks until completion or permission), `notifyOnFinish`, `settings`. Returns `{ agentId, … }`. -Provider features are provider-specific. For Codex fast mode, pass `features: { "fast_mode": true }` when creating the agent. +Initial runtime settings live under `settings`: `modeId`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }` when creating the agent. Compose: call `create_worktree` first, then `create_agent` with `cwd` set to the returned `worktreePath`. **`send_agent_prompt`** — `{ agentId, prompt }`. Blocks by default; pass `background: true` to fire-and-forget. -**`set_agent_feature`** — `{ agentId, featureId, value }`. Use for provider-specific toggles on an existing agent, for example `{ agentId, featureId: "fast_mode", value: true }` for Codex. +**`update_agent`** — `{ agentId, name?, labels?, settings? }`. Use `settings` for runtime changes on an existing agent: `modeId`, `model`, `thinkingOptionId`, and provider-specific `features`. For Codex fast mode, pass `settings: { features: { "fast_mode": true } }`. **`list_agents`** — filter by `cwd`, `statuses`, `sinceHours`, `includeArchived`. **`archive_agent`** — `{ agentId }`. Interrupts if running, removes from active list. -## Provider features +## Provider discovery -**`list_provider_features`** — query provider-specific features before setting them. Required: `provider`, `cwd`. Optional: `model`, `modeId`, `thinkingOptionId`, `featureValues`. +**`list_providers`** — compact provider availability and modes. -Only set feature IDs returned by `list_provider_features`. For Codex fast mode, look for `fast_mode` and pass `features: { "fast_mode": true }` to `create_agent`. +**`list_models`** — full model list for one provider. Use only when you need model IDs or thinking options; the list can be large. + +**`inspect_provider`** — compact provider capability and feature inspection. Required: `provider`; pass `cwd` when you are not in an agent-scoped session. Optional: `settings` with draft `model`, `modeId`, `thinkingOptionId`, and `features`. + +Only set feature IDs returned by `inspect_provider`. For Codex fast mode, look for `fast_mode` and pass `settings: { features: { "fast_mode": true } }` to `create_agent` or `update_agent`. ## Heartbeats