From ffcb54b8bf8df15b19a188ec0064c7c8cf069598 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 2 Jul 2026 13:58:13 -0700 Subject: [PATCH] FN-7446: handle missing planning MCP resolver results Keep planning session creation resilient when the MCP resolver test seam returns no shaped result. - Add a planning MCP resolution helper that defaults malformed resolver output to an empty server list. - Cover both non-streaming and streaming planning sessions when MCP resolution returns undefined. - Update the shared engine mock to return the full empty MCP resolver shape. - Add a patch changeset for the published CLI package. Files changed: .changeset/fn-7446-planning-mcp-resolver.md | 7 +++++ .../src/__tests__/mcp-lane-forwarding.test.ts | 35 +++++++++++++++++++++- packages/dashboard/src/planning.ts | 18 +++++++++-- packages/dashboard/src/test/mockCoreEngine.ts | 4 +-- 4 files changed, 59 insertions(+), 5 deletions(-) Fusion-Task-Id: FN-7446 Fusion-Task-Lineage: 998c30d2-2bed-4d25-b49f-3475048d6c07 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7446-planning-mcp-resolver.md | 7 ++++ .../src/__tests__/mcp-lane-forwarding.test.ts | 35 ++++++++++++++++++- packages/dashboard/src/planning.ts | 18 ++++++++-- packages/dashboard/src/test/mockCoreEngine.ts | 4 +-- 4 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 .changeset/fn-7446-planning-mcp-resolver.md diff --git a/.changeset/fn-7446-planning-mcp-resolver.md b/.changeset/fn-7446-planning-mcp-resolver.md new file mode 100644 index 0000000000..99e1e7598d --- /dev/null +++ b/.changeset/fn-7446-planning-mcp-resolver.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Prevent Planning Mode sessions from failing when MCP resolution returns no shaped result. +category: fix +dev: Dashboard planning lanes now default malformed MCP resolver output to an empty server set while preserving MCP forwarding. diff --git a/packages/dashboard/src/__tests__/mcp-lane-forwarding.test.ts b/packages/dashboard/src/__tests__/mcp-lane-forwarding.test.ts index eda0707742..2ea5ece800 100644 --- a/packages/dashboard/src/__tests__/mcp-lane-forwarding.test.ts +++ b/packages/dashboard/src/__tests__/mcp-lane-forwarding.test.ts @@ -56,7 +56,7 @@ vi.mock("../planning-board-tools.js", () => ({ createPlanningBoardTools: vi.fn(() => []), })); -import { __resetPlanningState, createSession } from "../planning.js"; +import { __resetPlanningState, createSession, createSessionWithAgent, planningStreamManager } from "../planning.js"; import { resolveManualAiPromptMcpServers } from "../routes.js"; import { createMissionInterviewAgent } from "../mission-interview.js"; import { createTargetInterviewAgent } from "../milestone-slice-interview.js"; @@ -82,6 +82,39 @@ describe("dashboard MCP lane forwarding", () => { })); }); + it("defaults an undefined MCP resolver result to empty servers for non-streaming planning", async () => { + resolveMcpServersForStoreMock.mockResolvedValueOnce(undefined as never); + + await createSession("127.0.0.1", "Build without MCP", {} as never, "/tmp/fusion-dashboard-test"); + + expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({ + tools: "readonly", + allowMcpToolsInReadonly: true, + mcpServers: [], + })); + }); + + it("defaults an undefined MCP resolver result to empty servers for streaming planning", async () => { + resolveMcpServersForStoreMock.mockResolvedValueOnce(undefined as never); + + const sessionId = await createSessionWithAgent( + "127.0.0.1", + "Stream without MCP", + "/tmp/fusion-dashboard-test", + {} as never, + ); + + const startInitialTurn = planningStreamManager.consumeInitialTurn(sessionId); + expect(startInitialTurn).toBeTypeOf("function"); + startInitialTurn?.(); + + await vi.waitFor(() => expect(createFnAgentMock).toHaveBeenCalledWith(expect.objectContaining({ + tools: "readonly", + allowMcpToolsInReadonly: true, + mcpServers: [], + }))); + }); + it("resolves materialized MCP servers for manual AI-prompt workflow steps", async () => { const store = {} as never; diff --git a/packages/dashboard/src/planning.ts b/packages/dashboard/src/planning.ts index f2c05b8d11..9e6044f390 100644 --- a/packages/dashboard/src/planning.ts +++ b/packages/dashboard/src/planning.ts @@ -61,9 +61,23 @@ type AgentResult = any; type SkillPluginRunner = Parameters[3]; const PLANNING_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const; +type PlanningMcpServers = Awaited>["servers"]; // eslint-disable-next-line @typescript-eslint/no-explicit-any let createFnAgent: any = engineCreateFnAgent; +async function resolvePlanningMcpServers(store: TaskStore): Promise { + const resolved = await resolveMcpServersForStore(store); + /* + FNXC:McpConfig 2026-07-02-13:45: + Planning lanes must forward shaped MCP server arrays, while dashboard route-test mocks may omit the resolver result entirely. + Default only malformed test-seam output to an empty in-memory set so configured servers and secret-bearing materialized fields are never logged or persisted here. + */ + if (!resolved || !Array.isArray(resolved.servers)) { + return []; + } + return resolved.servers; +} + // ── Notification Integration ──────────────────────────────────────────── // // The planning module sends "planning-awaiting-input" notifications when an @@ -1076,7 +1090,7 @@ export async function createSession( builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS], // FNXC:McpConfig 2026-06-25-22:31: Planning/chat session creation resolves trusted MCP servers through the dashboard-scoped store and forwards only the materialized in-memory set to the engine runtime guard. // FNXC:McpConfig 2026-06-29-00:00: Planning sessions are intentionally read-only but still need configured MCP documentation/context tools; opt in at the session boundary while preserving engine-side namespacing, filtering, wrappers, and disposal. - mcpServers: (await resolveMcpServersForStore(store)).servers, + mcpServers: await resolvePlanningMcpServers(store), allowMcpToolsInReadonly: true, customTools: [ ...createPlanningBoardTools(store), @@ -1670,7 +1684,7 @@ async function createPlanningAgent( builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS], // FNXC:McpConfig 2026-06-25-22:31: Streaming planning uses the same dashboard-scoped MCP resolution seam as non-streaming planning so no planning lane silently drops enabled servers. // FNXC:McpConfig 2026-06-29-00:00: Streaming planning uses the explicit read-only MCP opt-in; non-planning read-only lanes remain denied unless they set the same reviewed policy flag. - mcpServers: (await resolveMcpServersForStore(store)).servers, + mcpServers: await resolvePlanningMcpServers(store), allowMcpToolsInReadonly: true, customTools: [ ...createPlanningBoardTools(store), diff --git a/packages/dashboard/src/test/mockCoreEngine.ts b/packages/dashboard/src/test/mockCoreEngine.ts index 8bad8239ef..f45ecc5d4c 100644 --- a/packages/dashboard/src/test/mockCoreEngine.ts +++ b/packages/dashboard/src/test/mockCoreEngine.ts @@ -66,8 +66,8 @@ export function createEngineMock(overrides: AnyModule = {}): AnyModule { */ createChatTaskDocumentTools: vi.fn(() => []), createChatArtifactTools: vi.fn(() => []), - // FNXC:McpConfig 2026-07-02-00:00: Planning/mission route tests share this engine mock; MCP resolution must return a shaped empty server set so readonly session creation can proceed without importing real engine stores. - resolveMcpServersForStore: vi.fn(async () => ({ servers: [] })), + // FNXC:McpConfig 2026-07-02-13:45: Planning/mission route tests share this engine mock; MCP resolution must return the full shaped empty result so readonly session creation can proceed without importing real engine stores. + resolveMcpServersForStore: vi.fn(async () => ({ servers: [], errors: [] })), ...overrides, }); }