Require explicit titles for MCP create_agent

This commit is contained in:
Mohamed Boudra
2025-12-01 11:37:02 +00:00
parent cb2ee687e9
commit ad804cb1fe
5 changed files with 140 additions and 17 deletions
+7 -7
View File
@@ -220,7 +220,7 @@ The `buildActivityPayload` function (lines 99-118) returns:
### Proposal Summary ### Proposal Summary
1. Make `waitForAgentEvent` return last assistant message text (not full activity) 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 3. When `background=false`, wait for completion and return last message
4. All three tools share same wait code path 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: Keep existing tools as non-blocking (current behavior), add new tools for blocking:
- `create_coding_agent` → remains non-blocking - `create_agent` → remains non-blocking
- `create_coding_agent_and_wait` → new blocking variant - `create_agent_and_wait` → new blocking variant
- `send_agent_prompt` → remains non-blocking - `send_agent_prompt` → remains non-blocking
- `send_agent_prompt_and_wait` → new blocking variant - `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 3. Update MCP server to include in response
**Phase 3: Add Blocking Variants (Optional)** **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 2. Implement shared wait helper in AgentManager
3. Add timeout parameter (default 5 minutes) 3. Add timeout parameter (default 5 minutes)
4. Handle permission requests gracefully (throw or return them) 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) ### Current Pattern (Works Well)
```typescript ```typescript
// Claude uses these MCP tools: // 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 }); const result = await wait_for_agent({ agentId });
// result.activity.content has curated summary // result.activity.content has curated summary
``` ```
@@ -695,7 +695,7 @@ const result = await wait_for_agent({ agentId });
### Proposed Pattern ### Proposed Pattern
```typescript ```typescript
// With background flag: // With background flag:
const result = await create_coding_agent({ const result = await create_agent({
cwd, cwd,
initialPrompt, initialPrompt,
background: false // wait for completion background: false // wait for completion
@@ -803,7 +803,7 @@ The proposed pattern is simpler:
3. Add integration tests 3. Add integration tests
**Long Term (New Features):** **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 2. Add `send_agent_prompt_and_wait` MCP tool
3. Add timeout parameter support 3. Add timeout parameter support
4. Monitor usage to see which pattern users prefer 4. Monitor usage to see which pattern users prefer
+4 -4
View File
@@ -90,7 +90,7 @@ Call them without asking when context requires it. Never fabricate their output.
### Delegated Operations (Announce + Execute) ### 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. - `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. - `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 ```javascript
// Claude Code with planning // Claude Code with planning
create_coding_agent({ create_agent({
cwd: "~/dev/paseo", cwd: "~/dev/paseo",
agentType: "claude", agentType: "claude",
initialPrompt: "add dark mode toggle to settings page", initialPrompt: "add dark mode toggle to settings page",
@@ -203,7 +203,7 @@ create_coding_agent({
}) })
// Codex for quick edits // Codex for quick edits
create_coding_agent({ create_agent({
cwd: "~/dev/paseo", cwd: "~/dev/paseo",
agentType: "codex", agentType: "codex",
initialPrompt: "clean up the logging", initialPrompt: "clean up the logging",
@@ -285,7 +285,7 @@ list_agents()
// User: "yes" // User: "yes"
// 3. Create with type + mode // 3. Create with type + mode
create_coding_agent({ create_agent({
cwd: "~/dev/project", cwd: "~/dev/project",
agentType: "claude", agentType: "claude",
initialPrompt: "add authentication", initialPrompt: "add authentication",
@@ -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<string, any>;
agentRegistry: Record<string, any>;
};
};
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"
);
});
});
+31 -4
View File
@@ -109,9 +109,9 @@ export async function createAgentMcpServer(
}); });
server.registerTool( server.registerTool(
"create_coding_agent", "create_agent",
{ {
title: "Create Coding Agent", title: "Create Agent",
description: 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.", "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: { inputSchema: {
@@ -120,6 +120,14 @@ export async function createAgentMcpServer(
.describe( .describe(
"Required working directory for the agent (absolute, relative, or ~)." "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( agentType: AgentProviderEnum.optional().describe(
"Optional agent implementation to spawn. Defaults to 'claude'." "Optional agent implementation to spawn. Defaults to 'claude'."
), ),
@@ -164,7 +172,15 @@ export async function createAgentMcpServer(
permission: AgentPermissionRequestPayloadSchema.nullable().optional(), 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); let resolvedCwd = expandPath(cwd);
if (worktreeName) { if (worktreeName) {
@@ -183,6 +199,15 @@ export async function createAgentMcpServer(
modeId: initialMode, 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) { if (initialPrompt) {
try { try {
agentManager.recordUserMessage(snapshot.id, initialPrompt); agentManager.recordUserMessage(snapshot.id, initialPrompt);
@@ -252,7 +277,9 @@ export async function createAgentMcpServer(
description: description:
"Block until the agent requests permission or the current run completes. Returns the pending permission (if any) and recent activity summary.", "Block until the agent requests permission or the current run completes. Returns the pending permission (if any) and recent activity summary.",
inputSchema: { inputSchema: {
agentId: z.string().describe("Agent identifier from create_coding_agent"), agentId: z
.string()
.describe("Agent identifier returned by the create_agent tool"),
}, },
outputSchema: { outputSchema: {
agentId: z.string(), agentId: z.string(),
+2 -2
View File
@@ -2532,8 +2532,8 @@ export class Session {
}, },
}); });
} else if (chunk.type === "tool-result") { } else if (chunk.type === "tool-result") {
// Check if this is a create_coding_agent result // Check if this is a create_agent result
if (chunk.toolName === "create_coding_agent" && chunk.output) { if (chunk.toolName === "create_agent" && chunk.output) {
const result = chunk.output as any; const result = chunk.output as any;
if (result.structuredContent?.agentId) { if (result.structuredContent?.agentId) {
const agentId = result.structuredContent.agentId; const agentId = result.structuredContent.agentId;