fix(server): emit valid MCP list_agents payloads
Explicit `undefined` keys on optional snapshot fields were converted to `null` by `ensureValidJson`, failing `AgentSnapshotPayloadSchema` validation on the wire. Stop writing those keys so they remain absent, which is what `.optional()` expects. Also adds a test that parses `list_agents` output through the schema to catch this class of bug.
This commit is contained in:
@@ -94,7 +94,7 @@ export function toAgentPayload(
|
|||||||
model: agent.config.model ?? null,
|
model: agent.config.model ?? null,
|
||||||
thinkingOptionId,
|
thinkingOptionId,
|
||||||
effectiveThinkingOptionId,
|
effectiveThinkingOptionId,
|
||||||
runtimeInfo,
|
...(runtimeInfo ? { runtimeInfo } : {}),
|
||||||
createdAt: agent.createdAt.toISOString(),
|
createdAt: agent.createdAt.toISOString(),
|
||||||
updatedAt: agent.updatedAt.toISOString(),
|
updatedAt: agent.updatedAt.toISOString(),
|
||||||
lastUserMessageAt: agent.lastUserMessageAt ? agent.lastUserMessageAt.toISOString() : null,
|
lastUserMessageAt: agent.lastUserMessageAt ? agent.lastUserMessageAt.toISOString() : null,
|
||||||
@@ -192,8 +192,6 @@ export function buildStoredAgentPayload(
|
|||||||
availableModes: [],
|
availableModes: [],
|
||||||
pendingPermissions: [],
|
pendingPermissions: [],
|
||||||
persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence),
|
persistence: toAgentPersistenceHandle(logger, providerRegistry, record.persistence),
|
||||||
lastUsage: undefined,
|
|
||||||
lastError: undefined,
|
|
||||||
title: record.title ?? record.config?.title ?? null,
|
title: record.title ?? record.config?.title ?? null,
|
||||||
requiresAttention: record.requiresAttention ?? false,
|
requiresAttention: record.requiresAttention ?? false,
|
||||||
attentionReason: record.attentionReason ?? null,
|
attentionReason: record.attentionReason ?? null,
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { describe, expect, it, vi } from "vitest";
|
|||||||
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
import { mkdtemp, mkdir, rm } from "node:fs/promises";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createTestLogger } from "../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../test-utils/test-logger.js";
|
||||||
import { createAgentMcpServer } from "./mcp-server.js";
|
import { createAgentMcpServer } from "./mcp-server.js";
|
||||||
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
|
import type { AgentManager, ManagedAgent } from "./agent-manager.js";
|
||||||
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
|
import type { AgentStorage, StoredAgentRecord } from "./agent-storage.js";
|
||||||
import type { ProviderDefinition } from "./provider-registry.js";
|
import type { ProviderDefinition } from "./provider-registry.js";
|
||||||
|
import { AgentSnapshotPayloadSchema } from "../../shared/messages.js";
|
||||||
|
|
||||||
type TestDeps = {
|
type TestDeps = {
|
||||||
agentManager: AgentManager;
|
agentManager: AgentManager;
|
||||||
@@ -702,6 +704,59 @@ describe("agent snapshot MCP serialization", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("emits list_agents payloads that satisfy the declared output schema", async () => {
|
||||||
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
|
const liveAgent = {
|
||||||
|
id: "live-agent",
|
||||||
|
provider: "claude",
|
||||||
|
cwd: "/tmp/live-project",
|
||||||
|
config: {},
|
||||||
|
runtimeInfo: undefined,
|
||||||
|
createdAt: new Date("2026-04-11T00:00:00.000Z"),
|
||||||
|
updatedAt: new Date("2026-04-11T00:00:00.000Z"),
|
||||||
|
lastUserMessageAt: null,
|
||||||
|
lifecycle: "idle",
|
||||||
|
capabilities: {
|
||||||
|
supportsStreaming: false,
|
||||||
|
supportsSessionPersistence: false,
|
||||||
|
supportsDynamicModes: false,
|
||||||
|
supportsMcpServers: true,
|
||||||
|
supportsReasoningStream: false,
|
||||||
|
supportsToolInvocations: true,
|
||||||
|
},
|
||||||
|
currentModeId: null,
|
||||||
|
availableModes: [],
|
||||||
|
features: [],
|
||||||
|
pendingPermissions: new Map(),
|
||||||
|
persistence: null,
|
||||||
|
labels: {},
|
||||||
|
attention: { requiresAttention: false },
|
||||||
|
} as unknown as ManagedAgent;
|
||||||
|
spies.agentManager.listAgents.mockReturnValue([liveAgent]);
|
||||||
|
spies.agentStorage.list.mockResolvedValue([
|
||||||
|
createStoredRecord({ id: "stored-non-archived", archivedAt: null }),
|
||||||
|
createStoredRecord({ id: "stored-archived", archivedAt: "2026-04-12T00:00:00.000Z" }),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const server = await createAgentMcpServer({
|
||||||
|
agentManager,
|
||||||
|
agentStorage,
|
||||||
|
logger,
|
||||||
|
providerRegistry: {
|
||||||
|
claude: createProviderDefinition({}),
|
||||||
|
} as any,
|
||||||
|
});
|
||||||
|
const tool = (server as any)._registeredTools["list_agents"];
|
||||||
|
const response = await tool.callback({ includeArchived: true });
|
||||||
|
|
||||||
|
const parsed = z.array(AgentSnapshotPayloadSchema).safeParse(response.structuredContent.agents);
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw new Error(
|
||||||
|
`list_agents response failed AgentSnapshotPayloadSchema: ${JSON.stringify(parsed.error.issues, null, 2)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("loads archived agents before reading get_agent_activity", async () => {
|
it("loads archived agents before reading get_agent_activity", async () => {
|
||||||
const { agentManager, agentStorage, spies } = createTestDeps();
|
const { agentManager, agentStorage, spies } = createTestDeps();
|
||||||
const record = createStoredRecord({ id: "archived-activity-agent" });
|
const record = createStoredRecord({ id: "archived-activity-agent" });
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function toAgentPersistenceHandle(
|
|||||||
return {
|
return {
|
||||||
provider,
|
provider,
|
||||||
sessionId: handle.sessionId,
|
sessionId: handle.sessionId,
|
||||||
nativeHandle: handle.nativeHandle,
|
...(handle.nativeHandle !== undefined ? { nativeHandle: handle.nativeHandle } : {}),
|
||||||
metadata: handle.metadata,
|
...(handle.metadata !== undefined ? { metadata: handle.metadata } : {}),
|
||||||
} satisfies AgentPersistenceHandle;
|
} satisfies AgentPersistenceHandle;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user