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