diff --git a/apps/server/src/mcp/toolkits/project/tools.ts b/apps/server/src/mcp/toolkits/project/tools.ts index 56799959f6cd8..27dde8060d5f3 100644 --- a/apps/server/src/mcp/toolkits/project/tools.ts +++ b/apps/server/src/mcp/toolkits/project/tools.ts @@ -14,7 +14,7 @@ import { ProjectCreatePayload, ProjectUpdatePayload, ProjectId, - OrchestratorMcpClientRequestId, + OrchestratorMcpOptionalClientRequestId, OrchestratorMcpEnvironmentTarget, OrchestratorMcpFailure, SourceControlCloneRepositoryInput, @@ -150,7 +150,7 @@ const ThreadLaunchTool = Tool.make("t3_thread_launch", { }), ), attachments: Schema.optional(Schema.Array(McpAttachmentInput).check(Schema.isMaxLength(8))), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, /** Set by a linked environment launching its agent's delegated task here; ignored otherwise. */ delegatedFrom: Schema.optional(OrchestrationV2DelegatedFrom), }), diff --git a/apps/server/src/provider/agentContextBudget.json b/apps/server/src/provider/agentContextBudget.json index 8ff4546b354a4..f71174aa2ac69 100644 --- a/apps/server/src/provider/agentContextBudget.json +++ b/apps/server/src/provider/agentContextBudget.json @@ -1,5 +1,5 @@ { "claudeSystemAppendBytes": 5597, "codexAdditionalContextBytes": 7327, - "mcpToolListJsonBytes": 48130 + "mcpToolListJsonBytes": 48895 } diff --git a/packages/contracts/src/orchestratorMcp.test.ts b/packages/contracts/src/orchestratorMcp.test.ts index 951727e68e1d3..ad1437da241ab 100644 --- a/packages/contracts/src/orchestratorMcp.test.ts +++ b/packages/contracts/src/orchestratorMcp.test.ts @@ -4,6 +4,7 @@ import * as Schema from "effect/Schema"; import { OrchestratorMcpDelegateTaskInput, OrchestratorMcpDelegateTaskResult, + OrchestratorMcpThreadSendInput, } from "./orchestratorMcp.ts"; const decodeDelegateTaskInput = Schema.decodeUnknownSync(OrchestratorMcpDelegateTaskInput); @@ -66,4 +67,16 @@ describe("orchestrator MCP contracts", () => { }), ).toThrow(); }); + + it.each([OrchestratorMcpDelegateTaskInput, OrchestratorMcpThreadSendInput])( + "advertises the clientRequestId description in the encoded JSON schema", + (input) => { + const { properties } = Schema.toJsonSchemaDocument(input).schema as { + properties: Record; + }; + expect(properties.clientRequestId?.description).toBe( + "Reuse it to retry the same call. A new call gets a new key.", + ); + }, + ); }); diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts index a036620022d19..25b704872f5ca 100644 --- a/packages/contracts/src/orchestratorMcp.ts +++ b/packages/contracts/src/orchestratorMcp.ts @@ -46,15 +46,19 @@ import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts"; const OrchestratorMcpPrompt = TrimmedNonEmptyString.check(Schema.isMaxLength(120_000)); const OrchestratorMcpTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)); -export const OrchestratorMcpClientRequestId = TrimmedNonEmptyString.check(Schema.isMaxLength(256)) - .check( - Schema.makeFilter((value) => - hasOnlyPairedSurrogates(value) - ? true - : "clientRequestId must contain valid paired Unicode surrogates.", - ), - ) - .annotate({ description: "Reuse it to retry the same call. A new call gets a new key." }); +export const OrchestratorMcpClientRequestId = TrimmedNonEmptyString.check( + Schema.isMaxLength(256), +).check( + Schema.makeFilter((value) => + hasOnlyPairedSurrogates(value) + ? true + : "clientRequestId must contain valid paired Unicode surrogates.", + ), +); +// Described on the optional field: a description on a trimmed string misses its encoded JSON schema. +export const OrchestratorMcpOptionalClientRequestId = Schema.optional( + OrchestratorMcpClientRequestId, +).annotate({ description: "Reuse it to retry the same call. A new call gets a new key." }); // OpenCode 1.15 serializes nested MCP union objects as JSON strings; decode that form at the boundary. const OrchestratorMcpScheduleFromJsonString = Schema.fromJsonString( @@ -186,7 +190,7 @@ export const OrchestratorMcpDelegateTaskInput = Schema.Struct({ timeoutMs: Schema.optional(Schema.Finite).annotate({ description: "Mode 'wait' only. Default 4 min. A timeout does not cancel the child.", }), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, runtimeMode: Schema.optional( OrchestratorMcpRuntimeMode.annotate({ description: @@ -256,7 +260,7 @@ export type OrchestratorMcpTaskStatusInput = typeof OrchestratorMcpTaskStatusInp export const OrchestratorMcpTaskCancelInput = Schema.Struct({ taskId: NodeId, reason: Schema.optional(Schema.String.check(Schema.isMaxLength(2_000))), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, }); export type OrchestratorMcpTaskCancelInput = typeof OrchestratorMcpTaskCancelInput.Type; @@ -280,7 +284,7 @@ export const OrchestratorMcpCreateThreadsInput = Schema.Struct({ Schema.isMinLength(1), Schema.isMaxLength(20), ), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, }); export type OrchestratorMcpCreateThreadsInput = typeof OrchestratorMcpCreateThreadsInput.Type; @@ -524,7 +528,7 @@ export const OrchestratorMcpThreadSendInput = Schema.Struct({ "Start a new provider session without the thread's earlier context, like /clear. Never steers.", }), ), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, }); export type OrchestratorMcpThreadSendInput = typeof OrchestratorMcpThreadSendInput.Type; @@ -575,7 +579,7 @@ export const OrchestratorMcpThreadInterruptInput = Schema.Struct({ * watches and stop every task it delegated. `runId` is then not used. */ stop: Schema.optional(Schema.Boolean), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, }); export type OrchestratorMcpThreadInterruptInput = typeof OrchestratorMcpThreadInterruptInput.Type; @@ -688,7 +692,7 @@ export const OrchestratorMcpScheduleTaskInput = Schema.Struct({ "Runs share one thread but each starts a fresh session without earlier runs' context. Unbound: the first run makes the thread.", }), ), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, }); export type OrchestratorMcpScheduleTaskInput = typeof OrchestratorMcpScheduleTaskInput.Type; @@ -837,7 +841,7 @@ export const OrchestratorMcpEnvironmentLinkInput = Schema.Struct({ requestedAccess: Schema.optional(AuthMcpClientAccess).annotate({ description: "Suggested access. The user picks; default approval-required.", }), - clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), + clientRequestId: OrchestratorMcpOptionalClientRequestId, }); export type OrchestratorMcpEnvironmentLinkInput = typeof OrchestratorMcpEnvironmentLinkInput.Type; diff --git a/packages/contracts/src/threadMetadataMcp.test.ts b/packages/contracts/src/threadMetadataMcp.test.ts index 884741b762d65..59e8f445a6c23 100644 --- a/packages/contracts/src/threadMetadataMcp.test.ts +++ b/packages/contracts/src/threadMetadataMcp.test.ts @@ -19,4 +19,18 @@ describe("ThreadMetadataMcpUpdateInput", () => { decodeUpdate({ action: "regenerate_title", clientRequestId: "metadata-\udc00" }), ); }); + + it("advertises its parameter descriptions in the encoded JSON schema", () => { + const { properties } = Schema.toJsonSchemaDocument(ThreadMetadataMcpUpdateInput).schema as { + properties: Record; + }; + assert.equal( + properties.title?.description, + "New concise display title. Required only when action is rename.", + ); + assert.equal( + properties.clientRequestId?.description, + "Stable idempotency key to reuse when retrying this mutation.", + ); + }); }); diff --git a/packages/contracts/src/threadMetadataMcp.ts b/packages/contracts/src/threadMetadataMcp.ts index e5332ac56ab47..ddc9ada52d2de 100644 --- a/packages/contracts/src/threadMetadataMcp.ts +++ b/packages/contracts/src/threadMetadataMcp.ts @@ -26,19 +26,15 @@ export function hasOnlyPairedSurrogates(value: string): boolean { return true; } -const ThreadMetadataTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)).annotate({ - description: "New concise display title. Required only when action is rename.", -}); +const ThreadMetadataTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)); -const ThreadMetadataClientRequestId = TrimmedNonEmptyString.check(Schema.isMaxLength(256)) - .check( - Schema.makeFilter((value) => - hasOnlyPairedSurrogates(value) - ? true - : "clientRequestId must contain valid paired Unicode surrogates.", - ), - ) - .annotate({ description: "Stable idempotency key to reuse when retrying this mutation." }); +const ThreadMetadataClientRequestId = TrimmedNonEmptyString.check(Schema.isMaxLength(256)).check( + Schema.makeFilter((value) => + hasOnlyPairedSurrogates(value) + ? true + : "clientRequestId must contain valid paired Unicode surrogates.", + ), +); export const ThreadMetadataMcpAction = Schema.Literals(["rename", "regenerate_title"]); export type ThreadMetadataMcpAction = typeof ThreadMetadataMcpAction.Type; @@ -46,8 +42,13 @@ export type ThreadMetadataMcpAction = typeof ThreadMetadataMcpAction.Type; export const ThreadMetadataMcpUpdateInput = Schema.Struct({ threadId: Schema.optional(ThreadId), action: ThreadMetadataMcpAction, - title: Schema.optional(ThreadMetadataTitle), - clientRequestId: Schema.optional(ThreadMetadataClientRequestId), + // Described on the optional fields: a description on a trimmed string misses its encoded JSON schema. + title: Schema.optional(ThreadMetadataTitle).annotate({ + description: "New concise display title. Required only when action is rename.", + }), + clientRequestId: Schema.optional(ThreadMetadataClientRequestId).annotate({ + description: "Stable idempotency key to reuse when retrying this mutation.", + }), }).check( Schema.makeFilter((input) => (input.action === "rename") === (input.title !== undefined)