diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index 6c9453e2c..3ae6bdf77 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -706,6 +706,9 @@ export class AgentManager { throw new Error(`Agent with id ${agentId} already exists`); } + // Inform the session of its managed agent ID for MCP parent-child relationships + session.setManagedAgentId?.(agentId); + const managed = { id: agentId, provider: config.provider, diff --git a/packages/server/src/server/agent/agent-sdk-types.ts b/packages/server/src/server/agent/agent-sdk-types.ts index 205b8f607..e29392282 100644 --- a/packages/server/src/server/agent/agent-sdk-types.ts +++ b/packages/server/src/server/agent/agent-sdk-types.ts @@ -180,6 +180,12 @@ export interface AgentSession { describePersistence(): AgentPersistenceHandle | null; interrupt(): Promise; close(): Promise; + /** + * Set the managed agent ID for this session. This is called by AgentManager + * after registration to allow the session to include its ID in MCP requests + * (for parent-child agent relationships). + */ + setManagedAgentId?(agentId: string): void; } export interface AgentClient { diff --git a/packages/server/src/server/agent/mcp-server.ts b/packages/server/src/server/agent/mcp-server.ts index 7d4e7cd8e..f538c9140 100644 --- a/packages/server/src/server/agent/mcp-server.ts +++ b/packages/server/src/server/agent/mcp-server.ts @@ -28,6 +28,11 @@ import { WaitForAgentTracker } from "./wait-for-agent-tracker.js"; export interface AgentMcpServerOptions { agentManager: AgentManager; agentRegistry: AgentRegistry; + /** + * ID of the agent that is connecting to this MCP server. + * When set, create_agent will auto-inject this as parentAgentId. + */ + callerAgentId?: string; } const AgentProviderEnum = z.enum( @@ -166,7 +171,7 @@ async function serializeSnapshotWithMetadata( export async function createAgentMcpServer( options: AgentMcpServerOptions ): Promise { - const { agentManager, agentRegistry } = options; + const { agentManager, agentRegistry, callerAgentId } = options; const waitTracker = new WaitForAgentTracker(); const server = new McpServer({ @@ -267,12 +272,14 @@ export async function createAgentMcpServer( const provider: AgentProvider = agentType ?? "claude"; const normalizedTitle = title?.trim() ?? null; + // Use explicit parentAgentId if provided, otherwise default to caller agent ID + const resolvedParentAgentId = parentAgentId ?? callerAgentId; const snapshot = await agentManager.createAgent({ provider, cwd: resolvedCwd, modeId: initialMode, title: normalizedTitle ?? undefined, - parentAgentId, + parentAgentId: resolvedParentAgentId, }); if (initialPrompt) { diff --git a/packages/server/src/server/agent/providers/claude-agent.ts b/packages/server/src/server/agent/providers/claude-agent.ts index 51c7afaa5..e82dbf1f2 100644 --- a/packages/server/src/server/agent/providers/claude-agent.ts +++ b/packages/server/src/server/agent/providers/claude-agent.ts @@ -389,6 +389,7 @@ class ClaudeAgentSession implements AgentSession { private cancelCurrentTurn: (() => void) | null = null; private cachedRuntimeInfo: AgentRuntimeInfo | null = null; private lastOptionsModel: string | null = null; + private managedAgentId: string | null = null; constructor( config: ClaudeAgentConfig, @@ -650,6 +651,10 @@ class ClaudeAgentSession implements AgentSession { this.input = null; } + setManagedAgentId(agentId: string): void { + this.managedAgentId = agentId; + } + private async ensureQuery(): Promise { if (this.query) { return this.query; @@ -685,11 +690,16 @@ class ClaudeAgentSession implements AgentSession { // Always include the agent-control MCP server so agents can launch other agents const agentControlConfig = this.agentControlMcp ?? DEFAULT_AGENT_CONTROL_MCP; + // Merge base headers with the caller agent ID header for parent-child relationships + const agentControlHeaders: Record = { + ...agentControlConfig.headers, + ...(this.managedAgentId ? { "X-Caller-Agent-Id": this.managedAgentId } : {}), + }; const defaultMcpServers: Record = { "agent-control": { type: "http", url: agentControlConfig.url, - ...(agentControlConfig.headers ? { headers: agentControlConfig.headers } : {}), + ...(Object.keys(agentControlHeaders).length > 0 ? { headers: agentControlHeaders } : {}), }, playwright: { type: "stdio", diff --git a/packages/server/src/server/agent/providers/codex-agent.ts b/packages/server/src/server/agent/providers/codex-agent.ts index 28f8393fe..7ea209e8e 100644 --- a/packages/server/src/server/agent/providers/codex-agent.ts +++ b/packages/server/src/server/agent/providers/codex-agent.ts @@ -636,6 +636,12 @@ class CodexAgentSession implements AgentSession { this.pendingPermissions.clear(); } + // eslint-disable-next-line @typescript-eslint/no-unused-vars + setManagedAgentId(_agentId: string): void { + // Codex agents don't currently use MCP for agent-control + // This is a no-op but satisfies the AgentSession interface + } + private drainHistoryEvents(): AgentStreamEvent[] { if (!this.historyEvents.length) { return []; diff --git a/packages/server/src/server/index.ts b/packages/server/src/server/index.ts index fb9466d46..06abf0224 100644 --- a/packages/server/src/server/index.ts +++ b/packages/server/src/server/index.ts @@ -116,11 +116,13 @@ async function main() { const agentMcpTransports: AgentMcpTransportMap = new Map(); - const createAgentMcpTransport = async () => { + const createAgentMcpTransport = async (callerAgentId?: string) => { // Create a NEW McpServer instance per session (not shared across sessions) + // Pass the caller agent ID so create_agent can auto-set parentAgentId const agentMcpServer = await createAgentMcpServer({ agentManager, agentRegistry, + callerAgentId, }); const transport = new StreamableHTTPServerTransport({ @@ -183,7 +185,9 @@ async function main() { }); return; } - transport = await createAgentMcpTransport(); + // Extract caller agent ID from header (sent by agents when connecting) + const callerAgentId = req.header("X-Caller-Agent-Id"); + transport = await createAgentMcpTransport(callerAgentId); } await transport.handleRequest( diff --git a/plan.md b/plan.md index c38a29bb7..c66863531 100644 --- a/plan.md +++ b/plan.md @@ -345,13 +345,14 @@ Files requiring modification: - If issues found: add fix tasks + re-test task. - **Done (2025-12-21 22:30)**: PARTIAL PASS with issues found. Fixed infinite loop bug (added `useShallow` to `childAgents` selector). Agent screen loads correctly. Sub-Agents menu section shows "No sub-agents". Parent agent successfully created child via MCP `create_agent`. However, child appears on homepage because `parentAgentId` not set - MCP server doesn't auto-inject calling agent's ID. Fix task added. -- [ ] **Fix**: Auto-inject parentAgentId in MCP create_agent tool. +- [x] **Fix**: Auto-inject parentAgentId in MCP create_agent tool. - The MCP server needs to know which agent is calling it - Explore passing agent ID context when MCP transport is created - Or: Have each agent's MCP session be scoped to that agent - Update `create_agent` handler to automatically set `parentAgentId` - Run typecheck after changes. + - **Done (2025-12-21 23:45)**: Implemented end-to-end parent-child agent ID injection. Added `setManagedAgentId()` to `AgentSession` interface (optional). `ClaudeAgentSession` stores the ID and includes it as `X-Caller-Agent-Id` header when connecting to agent-control MCP. `AgentManager.registerSession()` calls `setManagedAgentId()` after registration. MCP server extracts the header and auto-injects it as `parentAgentId` in `create_agent` handler. Typecheck passes. - [ ] **Test**: Re-verify parent/child hierarchy after MCP fix.