From 06a8f952dbbd67816b89566291977fe76176d6f8 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 7 Jun 2026 23:31:03 +0700 Subject: [PATCH] Keep Paseo MCP config out of agent storage Runtime-injected MCP endpoints can change with daemon listen settings, so compute them for launches instead of persisting them. --- .../src/server/agent/agent-manager.test.ts | 107 +++++++++++++++++- .../server/src/server/agent/agent-manager.ts | 88 ++++++-------- .../src/server/agent/runtime-mcp-config.ts | 61 ++++++++++ .../src/server/persistence-hooks.test.ts | 48 ++++++++ .../server/src/server/persistence-hooks.ts | 10 +- 5 files changed, 255 insertions(+), 59 deletions(-) create mode 100644 packages/server/src/server/agent/runtime-mcp-config.ts diff --git a/packages/server/src/server/agent/agent-manager.test.ts b/packages/server/src/server/agent/agent-manager.test.ts index 27d5c60e4..252b30ff1 100644 --- a/packages/server/src/server/agent/agent-manager.test.ts +++ b/packages/server/src/server/agent/agent-manager.test.ts @@ -542,7 +542,7 @@ test("createAgent injects daemon append system prompt at runtime only", async () expect(client.createdConfigs[0]?.systemPrompt).toBe("Agent instructions."); expect(client.createdConfigs[0]?.daemonAppendSystemPrompt).toBe("Daemon instructions."); - expect(snapshot.config.daemonAppendSystemPrompt).toBe("Daemon instructions."); + expect(snapshot.config).not.toHaveProperty("daemonAppendSystemPrompt"); expect(record?.config?.systemPrompt).toBe("Agent instructions."); expect(record?.config).not.toHaveProperty("daemonAppendSystemPrompt"); }); @@ -948,7 +948,7 @@ test("createAgent passes persistSession to provider create options", async () => rmSync(workdir, { recursive: true, force: true }); }); -test("createAgent injects paseo MCP server when manager has an MCP base URL", async () => { +test("createAgent injects paseo MCP server only into provider launch config", async () => { const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); const storagePath = join(workdir, "agents"); const storage = new AgentStorage(storagePath, logger); @@ -985,6 +985,12 @@ test("createAgent injects paseo MCP server when manager has an MCP base URL", as }); expect(snapshot.config.mcpServers).toEqual({ + custom: { + type: "stdio", + command: "custom-mcp", + }, + }); + expect(client.lastConfig?.mcpServers).toEqual({ paseo: { type: "http", url: `http://127.0.0.1:6767/mcp/agents?callerAgentId=${snapshot.id}`, @@ -994,7 +1000,102 @@ test("createAgent injects paseo MCP server when manager has an MCP base URL", as command: "custom-mcp", }, }); - expect(client.lastConfig?.mcpServers).toEqual(snapshot.config.mcpServers); + + const stored = await storage.get(snapshot.id); + expect(stored?.config?.mcpServers).toEqual({ + custom: { + type: "stdio", + command: "custom-mcp", + }, + }); +}); + +test("resumeAgentFromPersistence replaces stored internal paseo MCP with current runtime URL", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const client = new TestAgentClient(); + const manager = new AgentManager({ + clients: { + codex: client, + }, + registry: storage, + logger, + mcpBaseUrl: "http://127.0.0.1:6768/mcp/agents", + idFactory: () => "00000000-0000-4000-8000-000000000105", + }); + const handle: AgentPersistenceHandle = { + provider: "codex", + sessionId: "session-123", + metadata: { + cwd: workdir, + }, + }; + + const snapshot = await manager.resumeAgentFromPersistence(handle, { + cwd: workdir, + mcpServers: { + paseo: { + type: "http", + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=stale-agent", + }, + custom: { + type: "stdio", + command: "custom-mcp", + }, + }, + }); + + expect(client.resumeOverrides[0]?.mcpServers).toEqual({ + paseo: { + type: "http", + url: `http://127.0.0.1:6768/mcp/agents?callerAgentId=${snapshot.id}`, + }, + custom: { + type: "stdio", + command: "custom-mcp", + }, + }); + expect(snapshot.config.mcpServers).toEqual({ + custom: { + type: "stdio", + command: "custom-mcp", + }, + }); +}); + +test("resumeAgentFromPersistence drops stored internal paseo MCP when runtime injection is disabled", async () => { + const workdir = mkdtempSync(join(tmpdir(), "agent-manager-test-")); + const storagePath = join(workdir, "agents"); + const storage = new AgentStorage(storagePath, logger); + const client = new TestAgentClient(); + const manager = new AgentManager({ + clients: { + codex: client, + }, + registry: storage, + logger, + }); + const handle: AgentPersistenceHandle = { + provider: "codex", + sessionId: "session-123", + metadata: { + cwd: workdir, + }, + }; + + const snapshot = await manager.resumeAgentFromPersistence(handle, { + cwd: workdir, + mcpServers: { + paseo: { + type: "http", + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=stale-agent", + }, + }, + }); + + expect(client.resumeOverrides[0]?.mcpServers).toBeUndefined(); + expect(snapshot.config.mcpServers).toBeUndefined(); }); test("createAgent preserves a user-provided paseo MCP config", async () => { diff --git a/packages/server/src/server/agent/agent-manager.ts b/packages/server/src/server/agent/agent-manager.ts index fe2988d89..dc6d61480 100644 --- a/packages/server/src/server/agent/agent-manager.ts +++ b/packages/server/src/server/agent/agent-manager.ts @@ -57,6 +57,7 @@ import { getAgentProviderDefinition } from "@getpaseo/protocol/provider-manifest import { IMPORTABLE_PROVIDERS } from "./provider-registry.js"; import { invokeRewindCapability, type RewindMode } from "./rewind/rewind.js"; import { isSystemInjectedEnvelope } from "./agent-prompt.js"; +import { stripInternalPaseoMcpServer, withRuntimePaseoMcpServer } from "./runtime-mcp-config.js"; const RELOAD_SESSION_CLOSE_TIMEOUT_MS = 3_000; const INTERRUPT_SESSION_TIMEOUT_MS = 2_000; @@ -74,6 +75,11 @@ const STORED_AGENT_CAPABILITIES: AgentCapabilityFlags = { type TimeoutResult = "completed" | "timed_out"; +interface PreparedSessionConfig { + storedConfig: AgentSessionConfig; + launchConfig: AgentSessionConfig; +} + interface TimeoutOptions { operation: Promise; timeoutMs: number; @@ -105,7 +111,7 @@ function buildStoredAgentConfig(record: StoredAgentRecord): AgentSessionConfig { config.systemPrompt = record.config.systemPrompt; } if (record.config.mcpServers != null) config.mcpServers = record.config.mcpServers; - return config; + return stripInternalPaseoMcpServer(config); } export { AGENT_LIFECYCLE_STATUSES, type AgentLifecycleStatus }; @@ -811,30 +817,15 @@ export class AgentManager { }, ): Promise { const resolvedAgentId = validateAgentId(agentId ?? this.idFactory(), "createAgent"); - const injectedConfig = - this.mcpBaseUrl == null - ? config - : { - ...config, - mcpServers: { - paseo: { - type: "http" as const, - url: `${this.mcpBaseUrl}?callerAgentId=${resolvedAgentId}`, - }, - ...config.mcpServers, - }, - }; - this.requireEnabledProvider(injectedConfig.provider); - const normalizedConfig = this.applyDaemonAppendSystemPrompt( - await this.normalizeConfig(injectedConfig), - ); + const { storedConfig, launchConfig } = await this.prepareSessionConfig(config, resolvedAgentId); + this.requireEnabledProvider(storedConfig.provider); const launchContext = this.buildLaunchContext(resolvedAgentId, options?.env); const client = await this.requireAvailableClient({ - provider: normalizedConfig.provider, + provider: storedConfig.provider, }); const createOptions = this.buildCreateSessionOptions(options); - const session = await client.createSession(normalizedConfig, launchContext, createOptions); - return this.registerSession(session, normalizedConfig, resolvedAgentId, { + const session = await client.createSession(launchConfig, launchContext, createOptions); + return this.registerSession(session, storedConfig, resolvedAgentId, { labels: options?.labels, workspaceId: options?.workspaceId, initialTitle: options?.initialTitle, @@ -872,26 +863,10 @@ export class AgentManager { ...overrides, provider: handle.provider, } as AgentSessionConfig; - const normalizedConfig = this.applyDaemonAppendSystemPrompt( - await this.normalizeConfig(mergedConfig), + const { storedConfig, launchConfig } = await this.prepareSessionConfig( + mergedConfig, + resolvedAgentId, ); - const resumeOverrides: Partial = { ...overrides }; - let hasResumeOverrides = overrides !== undefined; - - if (normalizedConfig.model !== mergedConfig.model) { - resumeOverrides.model = normalizedConfig.model; - hasResumeOverrides = true; - } - - if (normalizedConfig.modeId !== mergedConfig.modeId) { - resumeOverrides.modeId = normalizedConfig.modeId; - hasResumeOverrides = true; - } - - if (metadata.daemonAppendSystemPrompt !== normalizedConfig.daemonAppendSystemPrompt) { - resumeOverrides.daemonAppendSystemPrompt = normalizedConfig.daemonAppendSystemPrompt; - hasResumeOverrides = true; - } const launchContext = this.buildLaunchContext(resolvedAgentId); const client = this.requireClient(handle.provider); @@ -901,12 +876,8 @@ export class AgentManager { `Provider '${handle.provider}' is not available. Please ensure the CLI is installed.`, ); } - const session = await client.resumeSession( - handle, - hasResumeOverrides ? resumeOverrides : undefined, - launchContext, - ); - return this.registerSession(session, normalizedConfig, resolvedAgentId, options); + const session = await client.resumeSession(handle, launchConfig, launchContext); + return this.registerSession(session, storedConfig, resolvedAgentId, options); } // Hot-reload an active agent session with config overrides. By default the @@ -938,14 +909,12 @@ export class AgentManager { ...overrides, provider, } as AgentSessionConfig; - const normalizedConfig = this.applyDaemonAppendSystemPrompt( - await this.normalizeConfig(refreshConfig), - ); + const { storedConfig, launchConfig } = await this.prepareSessionConfig(refreshConfig, agentId); const launchContext = this.buildLaunchContext(agentId); const session = handle - ? await client.resumeSession(handle, normalizedConfig, launchContext) - : await client.createSession(normalizedConfig, launchContext); + ? await client.resumeSession(handle, launchConfig, launchContext) + : await client.createSession(launchConfig, launchContext); this.agentStreamCoalescer.flushAndDiscard(agentId); // Remove the existing agent entry before swapping sessions @@ -966,7 +935,7 @@ export class AgentManager { } // Preserve existing labels and timeline during reload. - return this.registerSession(session, normalizedConfig, agentId, { + return this.registerSession(session, storedConfig, agentId, { labels: existing.labels, createdAt: existing.createdAt, updatedAt: existing.updatedAt, @@ -3489,6 +3458,21 @@ export class AgentManager { return normalized; } + private async prepareSessionConfig( + config: AgentSessionConfig, + agentId: string, + ): Promise { + const storedConfig = await this.normalizeConfig(stripInternalPaseoMcpServer(config)); + const launchConfig = this.applyDaemonAppendSystemPrompt( + withRuntimePaseoMcpServer({ + config: storedConfig, + agentId, + mcpBaseUrl: this.mcpBaseUrl, + }), + ); + return { storedConfig, launchConfig }; + } + private applyDaemonAppendSystemPrompt(config: AgentSessionConfig): AgentSessionConfig { const daemonAppendSystemPrompt = this.appendSystemPrompt.trim(); const next = { ...config }; diff --git a/packages/server/src/server/agent/runtime-mcp-config.ts b/packages/server/src/server/agent/runtime-mcp-config.ts new file mode 100644 index 000000000..8df9ebabb --- /dev/null +++ b/packages/server/src/server/agent/runtime-mcp-config.ts @@ -0,0 +1,61 @@ +import type { AgentSessionConfig, McpServerConfig } from "./agent-sdk-types.js"; + +const PASEO_MCP_SERVER_NAME = "paseo"; +const PASEO_MCP_PATHNAME = "/mcp/agents"; + +export function stripInternalPaseoMcpServer(config: AgentSessionConfig): AgentSessionConfig { + const mcpServers = config.mcpServers; + if (!mcpServers) { + return config; + } + + const paseoServer = mcpServers[PASEO_MCP_SERVER_NAME]; + if (!paseoServer || !isInternalPaseoMcpServer(paseoServer)) { + return config; + } + + const nextMcpServers = { ...mcpServers }; + delete nextMcpServers[PASEO_MCP_SERVER_NAME]; + + const next = { ...config }; + if (Object.keys(nextMcpServers).length > 0) { + next.mcpServers = nextMcpServers; + } else { + delete next.mcpServers; + } + return next; +} + +export function withRuntimePaseoMcpServer(params: { + config: AgentSessionConfig; + agentId: string; + mcpBaseUrl: string | null; +}): AgentSessionConfig { + const storedConfig = stripInternalPaseoMcpServer(params.config); + if (!params.mcpBaseUrl || storedConfig.mcpServers?.[PASEO_MCP_SERVER_NAME]) { + return storedConfig; + } + + return { + ...storedConfig, + mcpServers: { + [PASEO_MCP_SERVER_NAME]: { + type: "http", + url: `${params.mcpBaseUrl}?callerAgentId=${params.agentId}`, + }, + ...storedConfig.mcpServers, + }, + }; +} + +function isInternalPaseoMcpServer(config: McpServerConfig): boolean { + if (config.type !== "http" && config.type !== "sse") { + return false; + } + + try { + return new URL(config.url).pathname === PASEO_MCP_PATHNAME; + } catch { + return false; + } +} diff --git a/packages/server/src/server/persistence-hooks.test.ts b/packages/server/src/server/persistence-hooks.test.ts index fe57d9082..dc9929e01 100644 --- a/packages/server/src/server/persistence-hooks.test.ts +++ b/packages/server/src/server/persistence-hooks.test.ts @@ -95,6 +95,54 @@ describe("persistence hooks", () => { }); }); + test("buildConfigOverrides drops persisted internal paseo MCP server", () => { + const record = createRecord({ + config: { + modeId: "default", + model: "gpt-5.4-mini", + mcpServers: { + paseo: { + type: "http", + url: "http://127.0.0.1:6767/mcp/agents?callerAgentId=stale-agent", + }, + custom: { + type: "stdio", + command: "custom-mcp", + }, + }, + }, + }); + + expect(buildConfigOverrides(record).mcpServers).toEqual({ + custom: { + type: "stdio", + command: "custom-mcp", + }, + }); + }); + + test("buildConfigOverrides preserves user-provided paseo MCP server", () => { + const record = createRecord({ + config: { + modeId: "default", + model: "gpt-5.4-mini", + mcpServers: { + paseo: { + type: "http", + url: "https://example.com/custom-paseo", + }, + }, + }, + }); + + expect(buildConfigOverrides(record).mcpServers).toEqual({ + paseo: { + type: "http", + url: "https://example.com/custom-paseo", + }, + }); + }); + test("buildSessionConfig accepts providers from the canonical manifest", () => { const record = createRecord({ provider: "claude", diff --git a/packages/server/src/server/persistence-hooks.ts b/packages/server/src/server/persistence-hooks.ts index e2d99eaf3..c3d4dbd69 100644 --- a/packages/server/src/server/persistence-hooks.ts +++ b/packages/server/src/server/persistence-hooks.ts @@ -1,4 +1,5 @@ import type { AgentManager } from "./agent/agent-manager.js"; +import { stripInternalPaseoMcpServer } from "./agent/runtime-mcp-config.js"; import type { AgentPersistenceHandle, AgentProvider, @@ -49,7 +50,8 @@ export function attachAgentStoragePersistence( } export function buildConfigOverrides(record: StoredAgentRecord): Partial { - return { + return stripInternalPaseoMcpServer({ + provider: record.provider, cwd: record.cwd, modeId: record.lastModeId ?? record.config?.modeId ?? undefined, model: record.config?.model ?? undefined, @@ -58,7 +60,7 @@ export function buildConfigOverrides(record: StoredAgentRecord): Partial