From ad804cb1fe29424a63bb8ec4b85d36cf951a8445 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 1 Dec 2025 11:37:02 +0000 Subject: [PATCH] Require explicit titles for MCP create_agent --- packages/server/agent-mcp-review.md | 14 +-- packages/server/agent-prompt.md | 8 +- .../src/server/agent/mcp-server.test.ts | 96 +++++++++++++++++++ .../server/src/server/agent/mcp-server.ts | 35 ++++++- packages/server/src/server/session.ts | 4 +- 5 files changed, 140 insertions(+), 17 deletions(-) create mode 100644 packages/server/src/server/agent/mcp-server.test.ts diff --git a/packages/server/agent-mcp-review.md b/packages/server/agent-mcp-review.md index 7121e1b4a..dc51aa1d8 100644 --- a/packages/server/agent-mcp-review.md +++ b/packages/server/agent-mcp-review.md @@ -220,7 +220,7 @@ The `buildActivityPayload` function (lines 99-118) returns: ### Proposal Summary 1. Make `waitForAgentEvent` return last assistant message text (not full activity) -2. Add `background` flag (default `false`) to `create_coding_agent` and `send_agent_prompt` +2. Add `background` flag (default `false`) to `create_agent` and `send_agent_prompt` 3. When `background=false`, wait for completion and return last message 4. All three tools share same wait code path @@ -276,8 +276,8 @@ async waitForAgentEvent( Keep existing tools as non-blocking (current behavior), add new tools for blocking: -- `create_coding_agent` → remains non-blocking -- `create_coding_agent_and_wait` → new blocking variant +- `create_agent` → remains non-blocking +- `create_agent_and_wait` → new blocking variant - `send_agent_prompt` → remains non-blocking - `send_agent_prompt_and_wait` → new blocking variant @@ -510,7 +510,7 @@ Actually, the `waitTracker` lives in `mcp-server.ts`, not `agent-manager.ts`, so 3. Update MCP server to include in response **Phase 3: Add Blocking Variants (Optional)** -1. Create new MCP tools: `create_coding_agent_and_wait`, `send_agent_prompt_and_wait` +1. Create new MCP tools: `create_agent_and_wait`, `send_agent_prompt_and_wait` 2. Implement shared wait helper in AgentManager 3. Add timeout parameter (default 5 minutes) 4. Handle permission requests gracefully (throw or return them) @@ -687,7 +687,7 @@ The proposed changes add complexity. Consider if they're actually needed: ### Current Pattern (Works Well) ```typescript // Claude uses these MCP tools: -const { agentId } = await create_coding_agent({ cwd, initialPrompt }); +const { agentId } = await create_agent({ cwd, initialPrompt }); const result = await wait_for_agent({ agentId }); // result.activity.content has curated summary ``` @@ -695,7 +695,7 @@ const result = await wait_for_agent({ agentId }); ### Proposed Pattern ```typescript // With background flag: -const result = await create_coding_agent({ +const result = await create_agent({ cwd, initialPrompt, background: false // wait for completion @@ -803,7 +803,7 @@ The proposed pattern is simpler: 3. Add integration tests **Long Term (New Features):** -1. Add `create_coding_agent_and_wait` MCP tool +1. Add `create_agent_and_wait` MCP tool 2. Add `send_agent_prompt_and_wait` MCP tool 3. Add timeout parameter support 4. Monitor usage to see which pattern users prefer diff --git a/packages/server/agent-prompt.md b/packages/server/agent-prompt.md index 540278ce5..74300a13a 100644 --- a/packages/server/agent-prompt.md +++ b/packages/server/agent-prompt.md @@ -90,7 +90,7 @@ Call them without asking when context requires it. Never fabricate their output. ### Delegated Operations (Announce + Execute) -- `create_coding_agent()` – Ask for confirmation unless the user already issued a clear imperative (“spin up a new planner”), then acknowledge and create immediately. +- `create_agent()` – Ask for confirmation unless the user already issued a clear imperative (“spin up a new planner”), then acknowledge and create immediately. - `send_agent_prompt()` – Route the user’s request to the focused agent. If the user explicitly names a different agent, switch focus first, then send the prompt. - `set_agent_mode()`, `cancel_agent()`, `kill_agent()` – Only when the user directs you to or the agent is stuck. Confirm destructive actions. @@ -195,7 +195,7 @@ We only have two coding agents. Do not call tools to discover them—treat this ```javascript // Claude Code with planning -create_coding_agent({ +create_agent({ cwd: "~/dev/paseo", agentType: "claude", initialPrompt: "add dark mode toggle to settings page", @@ -203,7 +203,7 @@ create_coding_agent({ }) // Codex for quick edits -create_coding_agent({ +create_agent({ cwd: "~/dev/paseo", agentType: "codex", initialPrompt: "clean up the logging", @@ -285,7 +285,7 @@ list_agents() // User: "yes" // 3. Create with type + mode -create_coding_agent({ +create_agent({ cwd: "~/dev/project", agentType: "claude", initialPrompt: "add authentication", diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts new file mode 100644 index 000000000..d59648a0c --- /dev/null +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi } from "vitest"; + +import { createAgentMcpServer } from "./mcp-server.js"; +import type { AgentManager, ManagedAgent } from "./agent-manager.js"; +import type { AgentRegistry } from "./agent-registry.js"; + +type TestDeps = { + agentManager: AgentManager; + agentRegistry: AgentRegistry; + spies: { + agentManager: Record; + agentRegistry: Record; + }; +}; + +function createTestDeps(): TestDeps { + const agentManagerSpies = { + createAgent: vi.fn(), + waitForAgentEvent: vi.fn(), + recordUserMessage: vi.fn(), + setAgentMode: vi.fn(), + getAgent: vi.fn(), + streamAgent: vi.fn(() => (async function* noop() {})()), + respondToPermission: vi.fn(), + cancelAgentRun: vi.fn(), + getPendingPermissions: vi.fn(), + }; + + const agentRegistrySpies = { + get: vi.fn().mockResolvedValue(null), + setTitle: vi.fn().mockResolvedValue(undefined), + applySnapshot: vi.fn(), + list: vi.fn(), + remove: vi.fn(), + }; + + return { + agentManager: agentManagerSpies as unknown as AgentManager, + agentRegistry: agentRegistrySpies as unknown as AgentRegistry, + spies: { + agentManager: agentManagerSpies, + agentRegistry: agentRegistrySpies, + }, + }; +} + +describe("create_agent MCP tool", () => { + it("requires a concise title no longer than 40 characters", async () => { + const { agentManager, agentRegistry } = createTestDeps(); + const server = await createAgentMcpServer({ agentManager, agentRegistry }); + const tool = (server as any)._registeredTools["create_agent"]; + expect(tool).toBeDefined(); + + const missingTitle = await tool.inputSchema.safeParseAsync({ + cwd: "/tmp/repo", + }); + expect(missingTitle.success).toBe(false); + expect(missingTitle.error.issues[0].path).toEqual(["title"]); + + const tooLong = await tool.inputSchema.safeParseAsync({ + cwd: "/tmp/repo", + title: "x".repeat(41), + }); + expect(tooLong.success).toBe(false); + expect(tooLong.error.issues[0].path).toEqual(["title"]); + + const ok = await tool.inputSchema.safeParseAsync({ + cwd: "/tmp/repo", + title: "Short title", + }); + expect(ok.success).toBe(true); + }); + + it("persists provided titles immediately after agent creation", async () => { + const { agentManager, agentRegistry, spies } = createTestDeps(); + spies.agentManager.createAgent.mockResolvedValue({ + id: "agent-123", + cwd: "/tmp/repo", + lifecycle: "idle", + currentModeId: null, + availableModes: [], + } as ManagedAgent); + + const server = await createAgentMcpServer({ agentManager, agentRegistry }); + const tool = (server as any)._registeredTools["create_agent"]; + await tool.callback({ + cwd: "/tmp/repo", + title: " Fix auth bug ", + }); + + expect(spies.agentRegistry.setTitle).toHaveBeenCalledWith( + "agent-123", + "Fix auth bug" + ); + }); +}); diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index da0884edd..7dfb5f51b 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -109,9 +109,9 @@ export async function createAgentMcpServer( }); server.registerTool( - "create_coding_agent", + "create_agent", { - title: "Create Coding Agent", + title: "Create Agent", description: "Create a new Claude or Codex agent tied to a working directory. Optionally run an initial prompt immediately or create a git worktree for the agent.", inputSchema: { @@ -120,6 +120,14 @@ export async function createAgentMcpServer( .describe( "Required working directory for the agent (absolute, relative, or ~)." ), + title: z + .string() + .trim() + .min(1, "Title is required") + .max(40, "Title must be 40 characters or fewer") + .describe( + "Short descriptive title (<= 40 chars) summarizing the agent's focus. Use a single concise sentence that fits on mobile." + ), agentType: AgentProviderEnum.optional().describe( "Optional agent implementation to spawn. Defaults to 'claude'." ), @@ -164,7 +172,15 @@ export async function createAgentMcpServer( permission: AgentPermissionRequestPayloadSchema.nullable().optional(), }, }, - async ({ cwd, agentType, initialPrompt, initialMode, worktreeName, background = false }) => { + async ({ + cwd, + agentType, + initialPrompt, + initialMode, + worktreeName, + background = false, + title, + }) => { let resolvedCwd = expandPath(cwd); if (worktreeName) { @@ -183,6 +199,15 @@ export async function createAgentMcpServer( modeId: initialMode, }); + try { + await agentRegistry.setTitle(snapshot.id, title); + } catch (error) { + console.error( + `[Agent MCP] Failed to persist title for ${snapshot.id}:`, + error + ); + } + if (initialPrompt) { try { agentManager.recordUserMessage(snapshot.id, initialPrompt); @@ -252,7 +277,9 @@ export async function createAgentMcpServer( description: "Block until the agent requests permission or the current run completes. Returns the pending permission (if any) and recent activity summary.", inputSchema: { - agentId: z.string().describe("Agent identifier from create_coding_agent"), + agentId: z + .string() + .describe("Agent identifier returned by the create_agent tool"), }, outputSchema: { agentId: z.string(), diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index ece59b771..97c763bfb 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -2532,8 +2532,8 @@ export class Session { }, }); } else if (chunk.type === "tool-result") { - // Check if this is a create_coding_agent result - if (chunk.toolName === "create_coding_agent" && chunk.output) { + // Check if this is a create_agent result + if (chunk.toolName === "create_agent" && chunk.output) { const result = chunk.output as any; if (result.structuredContent?.agentId) { const agentId = result.structuredContent.agentId;