Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions apps/server/src/mcp/toolkits/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ProviderDriverKind,
ProviderInstanceId,
RunId,
RuntimeRequestId,
ThreadId,
TurnItemId,
type OrchestrationV2ProjectedTurnItem,
Expand Down Expand Up @@ -125,6 +126,85 @@ it.effect("checks capability through the production registration", () =>
),
);

it.effect("an answered question reports already answered instead of not found", () => {
const shell = McpToolAccessTestkit.liveThreadShell(threadId);
const requestId = RuntimeRequestId.make("answered-question");
const projection = McpToolAccessTestkit.idleThreadProjection(shell);
return Effect.gen(function* () {
const server = yield* McpServer.McpServer;
const respond = (id: string) =>
server
.callTool({
name: "t3_pending_request_respond",
arguments: { requestId: id, answers: { size: "large" } },
})
.pipe(
Effect.provideService(McpInvocationContext.McpInvocationContext, scope),
Effect.provideService(McpSchema.McpServerClient, client),
);
expect(declaredFailure(yield* respond(requestId))).toMatchObject({
code: "invalid_request",
message: "The user-input request was already answered.",
});
expect(declaredFailure(yield* respond("missing-question"))).toMatchObject({
code: "invalid_request",
message: "The pending user-input request was not found.",
});
}).pipe(
Effect.provide(
layerThreadToolkit.pipe(
Layer.provideMerge(McpServer.McpServer.layer),
Layer.provide(NodeCrypto.layer),
Layer.provide(
Layer.mock(ThreadManagement.ThreadManagementService)({
getThreadShell: () => Effect.succeed(shell),
getProjectThreadRecords: () =>
Effect.succeed({
...projection,
runtimeRequests: [
{
id: requestId,
nodeId: NodeId.make("root"),
providerTurnId: null,
nativeRequestRef: null,
kind: "user_input",
status: "resolved",
responseCapability: { type: "message" },
createdAt: shell.createdAt,
resolvedAt: shell.updatedAt,
answers: { size: "large" },
},
],
turnItems: [
{
id: TurnItemId.make("answered-question-item"),
threadId,
runId: null,
nodeId: null,
providerThreadId: null,
providerTurnId: null,
nativeItemRef: null,
parentItemId: null,
ordinal: 0,
status: "completed",
title: null,
startedAt: shell.createdAt,
completedAt: shell.updatedAt,
updatedAt: shell.updatedAt,
type: "user_input_request",
requestId,
questions: [],
},
],
}),
dispatch: () => Effect.die("an answered question must not dispatch again"),
}),
),
),
),
);
});

it("bounds public command rejections and redacts internal dispatch causes", () => {
const command = { commandId: CommandId.make("mcp-core-command"), commandType: "thread.settle" };
expect(
Expand Down
13 changes: 9 additions & 4 deletions apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,7 @@ const readQuestion = Effect.fn("mcp.readQuestion")(function* (input: {
}) {
const context = yield* readThread(input.threadId, ["runtimeRequests", "turnItems"]);
const request = context.projection.runtimeRequests.find(
(request) =>
request.id === input.requestId &&
request.kind === "user_input" &&
request.status === "pending",
(request) => request.id === input.requestId && request.kind === "user_input",
);
const item = context.projection.turnItems.find(
(item) => item.type === "user_input_request" && item.requestId === input.requestId,
Expand All @@ -68,6 +65,14 @@ const readQuestion = Effect.fn("mcp.readQuestion")(function* (input: {
code: "invalid_request",
message: "The pending user-input request was not found.",
});
if (request.status !== "pending")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
return yield* new OrchestratorMcpFailure({
code: "invalid_request",
message:
request.status === "resolved"
? "The user-input request was already answered."
: `The user-input request is no longer pending (${request.status}).`,
});
return { ...context, request, item };
});
const writesThread = <P extends { readonly threadId?: ThreadId | undefined }, A, E, R>(
Expand Down
Loading