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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-02 13:58:13 -07:00
parent 8d7abb8c33
commit ffcb54b8bf
4 changed files with 59 additions and 5 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -61,9 +61,23 @@ type AgentResult = any;
type SkillPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
const PLANNING_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const;
type PlanningMcpServers = Awaited<ReturnType<typeof resolveMcpServersForStore>>["servers"];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let createFnAgent: any = engineCreateFnAgent;
async function resolvePlanningMcpServers(store: TaskStore): Promise<PlanningMcpServers> {
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),

View File

@@ -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,
});
}