Skip to content

Commit 51483ab

Browse files
feat(mcp): expose pending user questions
1 parent 0a74c93 commit 51483ab

4 files changed

Lines changed: 110 additions & 0 deletions

File tree

‎apps/server/src/mcp/toolkits/thread/handlers.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type CommandId,
3+
type RuntimeRequestId,
34
type ThreadId,
45
type OrchestrationV2ThreadProjection,
56
type RunId,
@@ -34,7 +35,59 @@ const dispatch = Effect.fn("mcp.dispatchThreadCommand")(function* (
3435
return { sequence: result.sequence };
3536
});
3637

38+
const readQuestion = Effect.fn("mcp.readQuestion")(function* (
39+
input: {
40+
threadId?: ThreadId | undefined;
41+
requestId: RuntimeRequestId;
42+
},
43+
writable = false,
44+
) {
45+
const context = yield* writable ? readWritableThread(input.threadId) : readThread(input.threadId);
46+
const request = context.projection.runtimeRequests.find(
47+
(request) =>
48+
request.id === input.requestId &&
49+
request.kind === "user_input" &&
50+
request.status === "pending",
51+
);
52+
const item = context.projection.turnItems.find(
53+
(item) => item.type === "user_input_request" && item.requestId === input.requestId,
54+
);
55+
if (request === undefined || item?.type !== "user_input_request")
56+
return yield* new OrchestratorMcpFailure({
57+
code: "invalid_request",
58+
message: "The pending user-input request was not found.",
59+
});
60+
return { ...context, request, item };
61+
});
3762
export const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
63+
t3_pending_request_list: (input) =>
64+
Effect.gen(function* () {
65+
const { projection } = yield* readThread(input.threadId);
66+
return {
67+
requestIds: projection.runtimeRequests
68+
.filter((request) => request.kind === "user_input" && request.status === "pending")
69+
.map((request) => request.id),
70+
};
71+
}),
72+
t3_pending_request_read: (input) =>
73+
Effect.gen(function* () {
74+
const { item } = yield* readQuestion(input);
75+
return { requestId: input.requestId, questions: item.questions };
76+
}),
77+
t3_pending_request_respond: (input) =>
78+
Effect.gen(function* () {
79+
const { threads, projection } = yield* readQuestion(input, true);
80+
const result = yield* threads
81+
.dispatch({
82+
type: "runtime-request.respond",
83+
threadId: projection.thread.id,
84+
commandId: yield* newCommandId(),
85+
requestId: input.requestId,
86+
answers: input.answers,
87+
})
88+
.pipe(Effect.mapError(unavailable));
89+
return { sequence: result.sequence };
90+
}),
3891
t3_queue_list: (input) =>
3992
Effect.gen(function* () {
4093
const { projection } = yield* readThread(input.threadId);

‎apps/server/src/mcp/toolkits/thread/tools.ts‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {
2+
RuntimeRequestId,
3+
ProviderUserInputAnswers,
24
IsoDateTime,
35
OrchestratorMcpFailure,
46
OrchestrationV2DispatchCommandResult,
@@ -101,7 +103,57 @@ export const QueuePromoteTool = Tool.make("t3_queue_promote_to_steer", {
101103
parameters: Schema.Struct({ ...queueTarget, targetRunId: RunId }),
102104
}).annotate(Tool.Destructive, true);
103105

106+
const requestTarget = { threadId: Schema.optional(ThreadId), requestId: RuntimeRequestId };
107+
const question = Schema.Struct({
108+
id: Schema.String,
109+
header: Schema.String,
110+
question: Schema.String,
111+
options: Schema.Array(
112+
Schema.Struct({
113+
label: Schema.String,
114+
description: Schema.String,
115+
value: Schema.optional(Schema.String),
116+
}),
117+
),
118+
multiSelect: Schema.optional(Schema.Boolean),
119+
allowCustomAnswer: Schema.optional(Schema.Boolean),
120+
required: Schema.optional(Schema.Boolean),
121+
});
122+
const pendingRequest = Schema.Struct({
123+
requestId: RuntimeRequestId,
124+
questions: Schema.Array(question),
125+
});
126+
export const PendingRequestListTool = Tool.make("t3_pending_request_list", {
127+
...commandTool,
128+
description:
129+
"List pending user questions in a thread in the calling project. Approval requests are not included.",
130+
parameters: Schema.Struct({ threadId: Schema.optional(ThreadId) }),
131+
success: Schema.Struct({ requestIds: Schema.Array(RuntimeRequestId) }),
132+
})
133+
.annotate(Tool.Readonly, true)
134+
.annotate(Tool.Destructive, false);
135+
export const PendingRequestReadTool = Tool.make("t3_pending_request_read", {
136+
...commandTool,
137+
description:
138+
"Read a pending user question. Answer with t3_pending_request_respond; existing live or message response handling is used.",
139+
parameters: Schema.Struct(requestTarget),
140+
success: pendingRequest,
141+
})
142+
.annotate(Tool.Readonly, true)
143+
.annotate(Tool.Destructive, false);
144+
export const PendingRequestRespondTool = Tool.make("t3_pending_request_respond", {
145+
...commandTool,
146+
description:
147+
"Answer a pending user-input request using the existing runtime response command. This cannot approve a permission request.",
148+
parameters: Schema.Struct({ ...requestTarget, answers: ProviderUserInputAnswers }),
149+
})
150+
.annotate(Tool.Destructive, true)
151+
.annotate(Tool.OpenWorld, true);
152+
104153
export const ThreadToolkit = Toolkit.make(
154+
PendingRequestListTool,
155+
PendingRequestReadTool,
156+
PendingRequestRespondTool,
105157
ThreadOrganizeTool,
106158
QueueListTool,
107159
QueueReadTool,

‎apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,8 @@ export const CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS: ReadonlyArray<string> = [
808808
"mcp__t3-code__list_scheduled_tasks",
809809
"mcp__t3-code__t3_thread_list",
810810
"mcp__t3-code__t3_thread_wait",
811+
"mcp__t3-code__t3_pending_request_list",
812+
"mcp__t3-code__t3_pending_request_read",
811813
"mcp__t3-code__t3_queue_list",
812814
"mcp__t3-code__t3_queue_read",
813815
];

‎packages/shared/src/t3McpToolPresentation.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ const T3_MCP_TOOLS: Record<
5454
t3_queue_cancel: { displayName: "Cancel a queued run" },
5555
t3_queue_reorder: { displayName: "Reorder a queued run" },
5656
t3_queue_promote_to_steer: { displayName: "Steer with a queued message" },
57+
t3_pending_request_list: { displayName: "List pending questions" },
58+
t3_pending_request_read: { displayName: "Read pending questions" },
59+
t3_pending_request_respond: { displayName: "Answer pending questions" },
5760
t3_thread_organize: { displayName: "Organize a thread" },
5861
t3_thread_update: { displayName: "Update T3 thread metadata" },
5962
t3_thread_send: { displayName: "Send to a T3 thread", summaryAction: "thread-send" },

0 commit comments

Comments
 (0)