Require explicit titles for MCP create_agent
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user