From 22a453e23b3f5b9671d4f26655bd4cf9c2ba9cfa Mon Sep 17 00:00:00 2001 From: spoukyii <61633921+spoukyii@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:43:00 -0800 Subject: [PATCH 1/2] test(server): pull request tools given a bare thread uuid Calls link_pull_request with the bare uuid of an mcp: thread. Every thread tool answers that miss with "Did you mean `mcp:`?" since #22452; the pull request tools say only that the thread was not found, and fail here. --- .../mcp/toolkits/pullRequests/handlers.test.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts index e085feb2a155..ceaef41d459f 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.test.ts @@ -191,7 +191,7 @@ const makeHarness = Effect.fn("makePullRequestsToolkitHarness")(function* ( }), Layer.mock(Orchestrator.OrchestratorV2)({ getThreadShell: (id) => - Effect.succeed(id === THREAD_ID && thread ? v2PullRequestThread(thread) : null), + Effect.succeed(id === thread?.id ? v2PullRequestThread(thread) : null), dispatch, }), Layer.succeed(SecureRandom.SecureRandom, testCrypto), @@ -237,6 +237,21 @@ describe("pull request toolkit handlers", () => { }), ); + it.effect("points a bare thread uuid at the mcp: thread it belongs to", () => + Effect.gen(function* () { + const bareId = ThreadId.make("5d0b4a3e-8c1f-4a52-9d37-2f6c7e1b9a04"); + const harness = yield* makeHarness({ + thread: { ...makeThread([]), id: ThreadId.make(`mcp:${bareId}`) }, + }); + const error = yield* harness + .call("link_pull_request", { threadId: bareId, number: 123 }) + .pipe(Effect.flip); + expect(error.message).toBe(`Thread ${bareId} was not found. Did you mean \`mcp:${bareId}\`?`); + expect(error).toMatchObject({ _tag: "OrchestratorMcpFailure", code: "thread_not_found" }); + expect(yield* Ref.get(harness.commands)).toEqual([]); + }), + ); + it.effect("links by URL with source agent on the token's thread", () => Effect.gen(function* () { const harness = yield* makeHarness(); From d5a3bca9d219e43897f998586f3929d0a61ea2a9 Mon Sep 17 00:00:00 2001 From: spoukyii <61633921+spoukyii@users.noreply.github.com> Date: Tue, 3 Nov 2026 12:45:59 -0800 Subject: [PATCH 2/2] fix(server): pull request tools point a bare thread uuid at its mcp: thread Threads an agent starts over MCP have ids of the form `mcp:`, and agents often pass the bare uuid. Since #22452 every thread tool answers that miss with "Did you mean `mcp:`?" when such a thread exists. The pull request tools look the thread up on their own, so link_pull_request, unlink_pull_request, watch_pull_request and their siblings still said only "Thread was not found.", and the agent had no way to learn the id it meant. Their lookup now fails through the same threadNotFound helper, so the hint is decided in one place and the error carries the thread_not_found code the other tools use. The id is never rewritten, so the call still fails and every access check keyed on it holds. The pull request toolkit's own not-found error is gone with its last caller. Bug: pull request tools given a bare thread uuid do not point at its mcp: thread --- apps/server/src/mcp/toolkits/pullRequests/handlers.ts | 4 ++-- apps/server/src/mcp/toolkits/pullRequests/tools.ts | 10 ---------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts index 33ce2a0b84bd..a703a8f5f39f 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/handlers.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/handlers.ts @@ -27,6 +27,7 @@ import { } from "../../../sourceControl/threadPullRequests.ts"; import * as McpInvocationContext from "../../McpInvocationContext.ts"; import * as McpToolAccess from "../../McpToolAccess.ts"; +import { threadNotFound } from "../../threadAccess.ts"; import { type ListThreadPullRequestsResult, PullRequestLinkFailedError, @@ -40,7 +41,6 @@ import { type PullRequestTargetInput, PullRequestWatchFailedError, PullRequestWatchFromSubagentError, - PullRequestThreadNotFoundError, PullRequestThreadRequiredError, PullRequestGuideFailedError, PullRequestProjectNotFoundError, @@ -226,7 +226,7 @@ const make = Effect.gen(function* () { Effect.mapError((cause) => new Failure({ cause })), ); if (Option.isNone(thread) || thread.value.deletedAt !== null) { - return yield* new PullRequestThreadNotFoundError({ threadId }); + return yield* threadNotFound(engine, threadId, `Thread ${threadId} was not found.`); } return thread.value; }); diff --git a/apps/server/src/mcp/toolkits/pullRequests/tools.ts b/apps/server/src/mcp/toolkits/pullRequests/tools.ts index 789e9fe12523..e86bf3013bc9 100644 --- a/apps/server/src/mcp/toolkits/pullRequests/tools.ts +++ b/apps/server/src/mcp/toolkits/pullRequests/tools.ts @@ -104,15 +104,6 @@ export class PullRequestThreadRequiredError extends Schema.TaggedError()( - "PullRequestThreadNotFoundError", - { threadId: Schema.String }, -) { - override get message(): string { - return `Thread ${this.threadId} was not found.`; - } -} - export class PullRequestLinkFailedError extends Schema.TaggedError()( "PullRequestLinkFailedError", { cause: Schema.Defect() }, @@ -193,7 +184,6 @@ export const PullRequestToolError = Schema.Union([ PullRequestHostRequiredError, PullRequestUrlUnavailableError, PullRequestThreadRequiredError, - PullRequestThreadNotFoundError, PullRequestLinkFailedError, PullRequestUnlinkFailedError, PullRequestListFailedError,