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:
7
.changeset/fn-7446-planning-mcp-resolver.md
Normal file
7
.changeset/fn-7446-planning-mcp-resolver.md
Normal 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.
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user