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(() => []),
|
createPlanningBoardTools: vi.fn(() => []),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { __resetPlanningState, createSession } from "../planning.js";
|
import { __resetPlanningState, createSession, createSessionWithAgent, planningStreamManager } from "../planning.js";
|
||||||
import { resolveManualAiPromptMcpServers } from "../routes.js";
|
import { resolveManualAiPromptMcpServers } from "../routes.js";
|
||||||
import { createMissionInterviewAgent } from "../mission-interview.js";
|
import { createMissionInterviewAgent } from "../mission-interview.js";
|
||||||
import { createTargetInterviewAgent } from "../milestone-slice-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 () => {
|
it("resolves materialized MCP servers for manual AI-prompt workflow steps", async () => {
|
||||||
const store = {} as never;
|
const store = {} as never;
|
||||||
|
|
||||||
|
|||||||
@@ -61,9 +61,23 @@ type AgentResult = any;
|
|||||||
type SkillPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
type SkillPluginRunner = Parameters<typeof buildSessionSkillContextSync>[3];
|
||||||
|
|
||||||
const PLANNING_BUILTIN_WEB_TOOLS = ["WebSearch", "WebFetch"] as const;
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
let createFnAgent: any = engineCreateFnAgent;
|
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 ────────────────────────────────────────────
|
// ── Notification Integration ────────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// The planning module sends "planning-awaiting-input" notifications when an
|
// The planning module sends "planning-awaiting-input" notifications when an
|
||||||
@@ -1076,7 +1090,7 @@ export async function createSession(
|
|||||||
builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS],
|
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-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.
|
// 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,
|
allowMcpToolsInReadonly: true,
|
||||||
customTools: [
|
customTools: [
|
||||||
...createPlanningBoardTools(store),
|
...createPlanningBoardTools(store),
|
||||||
@@ -1670,7 +1684,7 @@ async function createPlanningAgent(
|
|||||||
builtinToolsAllowlist: [...PLANNING_BUILTIN_WEB_TOOLS],
|
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-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.
|
// 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,
|
allowMcpToolsInReadonly: true,
|
||||||
customTools: [
|
customTools: [
|
||||||
...createPlanningBoardTools(store),
|
...createPlanningBoardTools(store),
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ export function createEngineMock(overrides: AnyModule = {}): AnyModule {
|
|||||||
*/
|
*/
|
||||||
createChatTaskDocumentTools: vi.fn(() => []),
|
createChatTaskDocumentTools: vi.fn(() => []),
|
||||||
createChatArtifactTools: 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.
|
// 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: [] })),
|
resolveMcpServersForStore: vi.fn(async () => ({ servers: [], errors: [] })),
|
||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user