feat: auto-inject parentAgentId in MCP create_agent tool

When an agent calls the MCP create_agent tool to spawn a child agent,
the parent-child relationship is now automatically established:

- Added setManagedAgentId() to AgentSession interface (optional method)
- ClaudeAgentSession stores its managed agent ID and includes it as
  X-Caller-Agent-Id header when connecting to the agent-control MCP
- AgentManager.registerSession() calls setManagedAgentId() after
  registration so the session knows its ID before first prompt
- MCP server extracts X-Caller-Agent-Id header from init request and
  stores it as callerAgentId
- create_agent handler auto-injects callerAgentId as parentAgentId
  if not explicitly provided

This enables parent agents to spawn child agents that are correctly
associated, so they appear in the parent's Sub-Agents menu instead
of on the homepage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-21 17:36:58 +00:00
co-authored by Claude Opus 4.5
parent 99338d3b34
commit b2850aea23
7 changed files with 43 additions and 6 deletions
@@ -706,6 +706,9 @@ export class AgentManager {
throw new Error(`Agent with id ${agentId} already exists`); 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 = { const managed = {
id: agentId, id: agentId,
provider: config.provider, provider: config.provider,
@@ -180,6 +180,12 @@ export interface AgentSession {
describePersistence(): AgentPersistenceHandle | null; describePersistence(): AgentPersistenceHandle | null;
interrupt(): Promise<void>; interrupt(): Promise<void>;
close(): Promise<void>; close(): Promise<void>;
/**
* 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 { export interface AgentClient {
@@ -28,6 +28,11 @@ import { WaitForAgentTracker } from "./wait-for-agent-tracker.js";
export interface AgentMcpServerOptions { export interface AgentMcpServerOptions {
agentManager: AgentManager; agentManager: AgentManager;
agentRegistry: AgentRegistry; 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( const AgentProviderEnum = z.enum(
@@ -166,7 +171,7 @@ async function serializeSnapshotWithMetadata(
export async function createAgentMcpServer( export async function createAgentMcpServer(
options: AgentMcpServerOptions options: AgentMcpServerOptions
): Promise<McpServer> { ): Promise<McpServer> {
const { agentManager, agentRegistry } = options; const { agentManager, agentRegistry, callerAgentId } = options;
const waitTracker = new WaitForAgentTracker(); const waitTracker = new WaitForAgentTracker();
const server = new McpServer({ const server = new McpServer({
@@ -267,12 +272,14 @@ export async function createAgentMcpServer(
const provider: AgentProvider = agentType ?? "claude"; const provider: AgentProvider = agentType ?? "claude";
const normalizedTitle = title?.trim() ?? null; 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({ const snapshot = await agentManager.createAgent({
provider, provider,
cwd: resolvedCwd, cwd: resolvedCwd,
modeId: initialMode, modeId: initialMode,
title: normalizedTitle ?? undefined, title: normalizedTitle ?? undefined,
parentAgentId, parentAgentId: resolvedParentAgentId,
}); });
if (initialPrompt) { if (initialPrompt) {
@@ -389,6 +389,7 @@ class ClaudeAgentSession implements AgentSession {
private cancelCurrentTurn: (() => void) | null = null; private cancelCurrentTurn: (() => void) | null = null;
private cachedRuntimeInfo: AgentRuntimeInfo | null = null; private cachedRuntimeInfo: AgentRuntimeInfo | null = null;
private lastOptionsModel: string | null = null; private lastOptionsModel: string | null = null;
private managedAgentId: string | null = null;
constructor( constructor(
config: ClaudeAgentConfig, config: ClaudeAgentConfig,
@@ -650,6 +651,10 @@ class ClaudeAgentSession implements AgentSession {
this.input = null; this.input = null;
} }
setManagedAgentId(agentId: string): void {
this.managedAgentId = agentId;
}
private async ensureQuery(): Promise<Query> { private async ensureQuery(): Promise<Query> {
if (this.query) { if (this.query) {
return 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 // Always include the agent-control MCP server so agents can launch other agents
const agentControlConfig = this.agentControlMcp ?? DEFAULT_AGENT_CONTROL_MCP; const agentControlConfig = this.agentControlMcp ?? DEFAULT_AGENT_CONTROL_MCP;
// Merge base headers with the caller agent ID header for parent-child relationships
const agentControlHeaders: Record<string, string> = {
...agentControlConfig.headers,
...(this.managedAgentId ? { "X-Caller-Agent-Id": this.managedAgentId } : {}),
};
const defaultMcpServers: Record<string, ClaudeMcpServerConfig> = { const defaultMcpServers: Record<string, ClaudeMcpServerConfig> = {
"agent-control": { "agent-control": {
type: "http", type: "http",
url: agentControlConfig.url, url: agentControlConfig.url,
...(agentControlConfig.headers ? { headers: agentControlConfig.headers } : {}), ...(Object.keys(agentControlHeaders).length > 0 ? { headers: agentControlHeaders } : {}),
}, },
playwright: { playwright: {
type: "stdio", type: "stdio",
@@ -636,6 +636,12 @@ class CodexAgentSession implements AgentSession {
this.pendingPermissions.clear(); 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[] { private drainHistoryEvents(): AgentStreamEvent[] {
if (!this.historyEvents.length) { if (!this.historyEvents.length) {
return []; return [];
+6 -2
View File
@@ -116,11 +116,13 @@ async function main() {
const agentMcpTransports: AgentMcpTransportMap = new Map(); const agentMcpTransports: AgentMcpTransportMap = new Map();
const createAgentMcpTransport = async () => { const createAgentMcpTransport = async (callerAgentId?: string) => {
// Create a NEW McpServer instance per session (not shared across sessions) // 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({ const agentMcpServer = await createAgentMcpServer({
agentManager, agentManager,
agentRegistry, agentRegistry,
callerAgentId,
}); });
const transport = new StreamableHTTPServerTransport({ const transport = new StreamableHTTPServerTransport({
@@ -183,7 +185,9 @@ async function main() {
}); });
return; 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( await transport.handleRequest(
+2 -1
View File
@@ -345,13 +345,14 @@ Files requiring modification:
- If issues found: add fix tasks + re-test task. - 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. - **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 - The MCP server needs to know which agent is calling it
- Explore passing agent ID context when MCP transport is created - Explore passing agent ID context when MCP transport is created
- Or: Have each agent's MCP session be scoped to that agent - Or: Have each agent's MCP session be scoped to that agent
- Update `create_agent` handler to automatically set `parentAgentId` - Update `create_agent` handler to automatically set `parentAgentId`
- Run typecheck after changes. - 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. - [ ] **Test**: Re-verify parent/child hierarchy after MCP fix.