Skip to content

Commit 84e8fd1

Browse files
authored
fix(worker): unblock internal workflow dogfood (#288)
1 parent f6e777b commit 84e8fd1

9 files changed

Lines changed: 95 additions & 20 deletions

File tree

apps/worker/src/mcp/server.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe("createMcpServer", () => {
156156
);
157157
expect(called.structuredContent).toMatchObject({
158158
data: {
159-
protocolVersions: ["2025-11-25"],
159+
protocolVersions: ["2025-11-25", "2025-06-18"],
160160
serverVersion: "0.1.0",
161161
enabledDomains: ["system", "tickets", "runs", "workflows", "prompts"],
162162
// These deps carry no messaging adapter, which is the same answer a

apps/worker/src/mcp/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ import { registerWorkflowAuthoringTools } from "./tools/workflow-authoring.js";
1616
import { registerWorkflowTools } from "./tools/workflows.js";
1717

1818
export const MCP_PROTOCOL_VERSION = "2025-11-25" as const;
19+
export const MCP_SUPPORTED_PROTOCOL_VERSIONS = [
20+
MCP_PROTOCOL_VERSION,
21+
"2025-06-18",
22+
] as const;
1923

2024
export function createMcpServer(deps: McpToolDependencies): McpServer {
2125
const server = new McpServer({
@@ -31,7 +35,7 @@ export function createMcpServer(deps: McpToolDependencies): McpServer {
3135
toolName: "system.capabilities",
3236
targetRefs: [],
3337
operation: async () => ({
34-
protocolVersions: [MCP_PROTOCOL_VERSION],
38+
protocolVersions: [...MCP_SUPPORTED_PROTOCOL_VERSIONS],
3539
serverVersion: env.MCP_SERVER_VERSION,
3640
contractHash: MCP_CONTRACT_HASH,
3741
deploymentClass: "dedicated-worker",

apps/worker/src/mcp/surface-e2e.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -753,7 +753,7 @@ describe("A. the client cycle and the published surface", () => {
753753

754754
expect(result.isError).not.toBe(true);
755755
expect(envelope.data).toMatchObject({
756-
protocolVersions: ["2025-11-25"],
756+
protocolVersions: ["2025-11-25", "2025-06-18"],
757757
serverVersion: "0.1.0",
758758
contractHash: SNAPSHOT.contractHash,
759759
deploymentClass: "dedicated-worker",

apps/worker/src/mcp/transport.test.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,26 @@ describe("stateless MCP Streamable HTTP", () => {
184184
});
185185
});
186186

187+
it("negotiates 2025-06-18 and accepts it on a sessionless follow-up", async () => {
188+
const initialized = await post(initializeRequest(11, "2025-06-18"));
189+
190+
expect(initialized.status).toBe(200);
191+
expect(initialized.headers.get("mcp-session-id")).toBeNull();
192+
await expect(initialized.json()).resolves.toMatchObject({
193+
jsonrpc: "2.0",
194+
id: 11,
195+
result: { protocolVersion: "2025-06-18" },
196+
});
197+
198+
const listed = await post(
199+
{ jsonrpc: "2.0", id: 12, method: "tools/list", params: {} },
200+
{ "mcp-protocol-version": "2025-06-18" },
201+
);
202+
203+
expect(listed.status).toBe(200);
204+
await expect(listedToolNames(listed)).resolves.toEqual([...PUBLISHED].sort());
205+
});
206+
187207
it("creates a fresh server and transport for every POST", async () => {
188208
const first = await post(initializeRequest(7));
189209
const second = await post(initializeRequest(7));
@@ -261,26 +281,36 @@ describe("stateless MCP Streamable HTTP", () => {
261281
});
262282
});
263283

264-
it("rejects batches and protocol versions other than 2025-11-25", async () => {
284+
it("rejects batches and unknown initialize protocol versions", async () => {
265285
const batch = await post([initializeRequest(1), initializeRequest(2)]);
266-
const oldVersion = await post({
267-
...initializeRequest(5),
268-
params: { ...initializeRequest(5).params, protocolVersion: "2025-06-18" },
269-
});
286+
const unknownVersion = await post(initializeRequest(5, "2099-01-01"));
270287

271288
expect(batch.status).toBe(400);
272-
expect(oldVersion.status).toBe(400);
289+
expect(unknownVersion.status).toBe(400);
273290
await expect(batch.json()).resolves.toMatchObject({
274291
error: { data: { code: "VALIDATION_FAILED" } },
275292
});
276-
await expect(oldVersion.json()).resolves.toMatchObject({
293+
await expect(unknownVersion.json()).resolves.toMatchObject({
294+
error: { data: { code: "VALIDATION_FAILED" } },
295+
});
296+
});
297+
298+
it("rejects an unknown initialize header before authentication", async () => {
299+
const response = await post(initializeRequest(6), {
300+
"mcp-protocol-version": "2099-01-01",
301+
});
302+
303+
expect(response.status).toBe(400);
304+
expect(state.requireMcpActor).not.toHaveBeenCalled();
305+
await expect(response.json()).resolves.toMatchObject({
306+
id: 6,
277307
error: { data: { code: "VALIDATION_FAILED" } },
278308
});
279309
});
280310

281311
it.each([
282312
["missing", {}],
283-
["wrong", { "mcp-protocol-version": "2025-06-18" }],
313+
["unknown", { "mcp-protocol-version": "2099-01-01" }],
284314
])("rejects a tools/list follow-up with a %s protocol version before auth", async (_case, headers) => {
285315
const response = await post(
286316
{ jsonrpc: "2.0", id: 9, method: "tools/list", params: {} },
@@ -710,13 +740,13 @@ async function toolErrorText(response: Response): Promise<string> {
710740
return body.result?.content?.[0]?.text ?? "";
711741
}
712742

713-
function initializeRequest(id: number) {
743+
function initializeRequest(id: number, protocolVersion = "2025-11-25") {
714744
return {
715745
jsonrpc: "2.0" as const,
716746
id,
717747
method: "initialize" as const,
718748
params: {
719-
protocolVersion: "2025-11-25",
749+
protocolVersion,
720750
capabilities: {},
721751
clientInfo: { name: "task-5-test", version: "1.0.0" },
722752
},

apps/worker/src/mcp/transport.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { authorizeTool, policyFor } from "./policy.js";
3030
import { consumeMcpRateLimit } from "./rate-limit-store.js";
3131
import { requireMcpActor } from "./request-context.js";
3232
import { hashCanonicalJson } from "./sanitize-result.js";
33-
import { createMcpServer, MCP_PROTOCOL_VERSION } from "./server.js";
33+
import { createMcpServer, MCP_SUPPORTED_PROTOCOL_VERSIONS } from "./server.js";
3434
import { catalogedTool, mcpToolErrorResult } from "./tool-catalog.js";
3535

3636
type JsonRpcId = string | number | null;
@@ -591,15 +591,21 @@ function hasSupportedProtocol(event: H3Event, body: unknown): boolean {
591591
if (request.method === "initialize") {
592592
const params = request.params;
593593
return (
594-
(!headerVersion || headerVersion === MCP_PROTOCOL_VERSION) &&
594+
(!headerVersion || isSupportedProtocolVersion(headerVersion)) &&
595595
Boolean(
596596
params &&
597597
typeof params === "object" &&
598-
(params as Record<string, unknown>).protocolVersion === MCP_PROTOCOL_VERSION,
598+
isSupportedProtocolVersion(
599+
(params as Record<string, unknown>).protocolVersion,
600+
),
599601
)
600602
);
601603
}
602-
return typeof request.method !== "string" || headerVersion === MCP_PROTOCOL_VERSION;
604+
return typeof request.method !== "string" || isSupportedProtocolVersion(headerVersion);
605+
}
606+
607+
function isSupportedProtocolVersion(value: unknown): boolean {
608+
return MCP_SUPPORTED_PROTOCOL_VERSIONS.some((version) => version === value);
603609
}
604610

605611
function jsonRpcId(body: unknown): JsonRpcId {

apps/worker/src/workflow-definition/scenarios/support-investigation.scenario.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,9 @@ function scriptInvestigation(
150150
describe("support investigation workflow", () => {
151151
it("answers a Zendesk question with a bounded response and never prepares a workspace", async () => {
152152
const scenario = baseScenario("zendesk", ZENDESK_CASE);
153-
scriptInvestigation(scenario, "question", "The evidence explains the expected Safari behaviour.");
153+
scriptInvestigation(scenario, "question", "The evidence explains the expected Safari behaviour.", {
154+
evidence: [{ ref: "slack:C_SUPPORT:1", source: "slack", title: "Raw support thread", excerpt: "Internal customer details", author: "U123", origin: "C_SUPPORT", timestamp: "2026-08-12T09:00:00Z", link: "https://slack.example.test/archives/C_SUPPORT/p1" }],
155+
});
154156
scenario.script({ nodeId: "classify" }, {
155157
kind: "next",
156158
output: classifierOutput("question", "The requester asks for an explanation, not a code change."),
@@ -160,6 +162,9 @@ describe("support investigation workflow", () => {
160162
const outcome = await scenario.execute();
161163
expect(outcome.result.outcome).toBe("completed");
162164
expect(portsOf(outcome, "code-route")).toEqual(["false"]);
165+
expect(executorRunsOf(outcome, "notify-non-code")[0]?.resolvedInputs?.message).toBe(
166+
"Support investigation summary\n\nCase: zendesk #35436 — The login button does nothing on Safari\nClassification: question\nRationale / evidence summary: The requester asks for an explanation, not a code change.\n\nResponse draft / investigation theory:\nThe evidence explains the expected Safari behaviour.",
167+
);
163168
expectNeverInvoked(outcome, ["prepare", "implementation", "checks", "finalize", "open-pr"]);
164169
});
165170

apps/worker/src/workflow-definition/templates.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ function supportInvestigationDefinition(
756756
configuration: {
757757
operation: "format_text",
758758
template:
759-
"Support investigation summary\n\nCase:\n{{data:steps.entry.output.supportCase}}\n\nClassification: {{data:steps.classify.output.classification}}\nRationale: {{data:steps.classify.output.rationale}}\n\nResponse draft:\n{{data:steps.investigate.output.theory}}\n\nEvidence:\n{{data:steps.investigate.output.evidence}}",
759+
"Support investigation summary\n\nCase: {{data:steps.entry.output.supportCase.provider}} #{{data:steps.entry.output.supportCase.sourceId}} — {{data:steps.entry.output.supportCase.title}}\nClassification: {{data:steps.classify.output.classification}}\nRationale / evidence summary: {{data:steps.classify.output.rationale}}\n\nResponse draft / investigation theory:\n{{data:steps.investigate.output.theory}}",
760760
},
761761
},
762762
{

apps/worker/src/workflows/blocks/investigate.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,37 @@ describe("investigate execute", () => {
355355
expectOutputConformsToRegistry("investigate", result.output!);
356356
});
357357

358+
it("omits maxItems from the keyword schema and caps normalized keywords at runtime", async () => {
359+
const keywords = [
360+
" keyword-1 ",
361+
"",
362+
" ",
363+
...Array.from({ length: 11 }, (_, index) => `keyword-${index + 2}`),
364+
];
365+
mocks.generateStructured
366+
.mockResolvedValueOnce({ object: { keywords }, text: "", usage: null })
367+
.mockResolvedValueOnce(THEORY_RESULT);
368+
mocks.searchSlackChannels.mockResolvedValue({ matches: [], skipped: [] });
369+
370+
await execute(
371+
makeNode("investigate", { providers: ["slack"], slackChannels: ["C1"] }),
372+
{},
373+
makeCtx(),
374+
);
375+
376+
const keywordSchema = JSON.parse(mocks.generateStructured.mock.calls[0][0].schema);
377+
expect(keywordSchema.properties.keywords).toEqual({
378+
type: "array",
379+
items: { type: "string" },
380+
});
381+
expect(keywordSchema.properties.keywords).not.toHaveProperty("maxItems");
382+
expect(mocks.searchSlackChannels).toHaveBeenCalledWith(
383+
expect.objectContaining({
384+
keywords: Array.from({ length: 10 }, (_, index) => `keyword-${index + 1}`),
385+
}),
386+
);
387+
});
388+
358389
it("normalizes both providers onto the same evidence fields", async () => {
359390
mockHappyPath();
360391

apps/worker/src/workflows/blocks/investigate.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ const KEYWORDS_SCHEMA = JSON.stringify({
9595
keywords: {
9696
type: "array",
9797
items: { type: "string" },
98-
maxItems: MAX_KEYWORDS,
9998
},
10099
},
101100
required: ["keywords"],

0 commit comments

Comments
 (0)