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
51 changes: 50 additions & 1 deletion apps/server/src/mcp/toolkits/thread/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
type CommandId,
type RuntimeRequestId,
type ThreadId,
ThreadId,
type OrchestrationV2ThreadProjection,
type RunId,
OrchestratorMcpFailure,
Expand Down Expand Up @@ -61,6 +61,55 @@ const readQuestion = Effect.fn("mcp.readQuestion")(function* (
return { ...context, request, item };
});
export const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
t3_thread_fork: (input) =>
Effect.gen(function* () {
const { threads, projection } = yield* readWritableThread();
const commandId = yield* newCommandId();
const targetThreadId = ThreadId.make(`${commandId}:fork`);
const result = yield* threads
.dispatch({
type: "thread.fork",
commandId,
sourceThreadId: projection.thread.id,
targetThreadId,
sourcePoint: input.sourcePoint,
...(input.title === undefined ? {} : { title: input.title }),
createdBy: "agent",
creationSource: "mcp",
})
.pipe(Effect.mapError(unavailable));
return { sequence: result.sequence, targetThreadId };
}),
t3_thread_merge_back: (input) =>
Effect.gen(function* () {
const { threads, caller } = yield* readWritableThread(input.targetThreadId);
const result = yield* threads
.dispatch({
type: "thread.merge_back",
commandId: yield* newCommandId(),
sourceThreadId: caller.id,
targetThreadId: input.targetThreadId,
sourcePoint: input.sourcePoint,
createdBy: "agent",
creationSource: "mcp",
})
.pipe(Effect.mapError(unavailable));
return { sequence: result.sequence, targetThreadId: input.targetThreadId };
}),
t3_thread_transfers: (input) =>
Effect.gen(function* () {
const { projection } = yield* readThread(input.threadId);
return {
transfers: projection.contextTransfers.map(
({ id, sourceThreadId, targetThreadId, status }) => ({
id,
sourceThreadId,
targetThreadId,
status,
}),
),
};
}),
t3_thread_configuration: (input) =>
Effect.gen(function* () {
const {
Expand Down
45 changes: 45 additions & 0 deletions apps/server/src/mcp/toolkits/thread/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import {
OrchestrationV2ThreadForkSourcePoint,
OrchestrationV2ContextTransfer,
TrimmedNonEmptyString,
ModelSelection,
RuntimeMode,
ProviderInteractionMode,
Expand Down Expand Up @@ -174,7 +177,49 @@ export const ThreadConfigureTool = Tool.make("t3_thread_configure", {
parameters: Schema.Struct({ modelSelection: ModelSelection }),
}).annotate(Tool.Destructive, true);

const transferResult = Schema.Struct({ sequence: NonNegativeInt, targetThreadId: ThreadId });
export const ThreadForkTool = Tool.make("t3_thread_fork", {
...commandTool,
description:
"Fork this thread from a stable run or checkpoint using the existing fork command. The fork inherits the source configuration. Acceptance does not mean a provider turn has completed.",
parameters: Schema.Struct({
sourcePoint: OrchestrationV2ThreadForkSourcePoint,
title: Schema.optional(TrimmedNonEmptyString),
}),
success: transferResult,
}).annotate(Tool.Destructive, true);
export const ThreadMergeBackTool = Tool.make("t3_thread_merge_back", {
...commandTool,
description:
"Merge context from this thread back to a related thread in the same project. Existing lineage and transfer rules apply.",
parameters: Schema.Struct({
targetThreadId: ThreadId,
sourcePoint: OrchestrationV2ThreadForkSourcePoint,
}),
success: transferResult,
}).annotate(Tool.Destructive, true);
export const ThreadTransfersTool = Tool.make("t3_thread_transfers", {
...commandTool,
description: "Read context transfer status for a thread in the calling project.",
parameters: Schema.Struct({ threadId: Schema.optional(ThreadId) }),
success: Schema.Struct({
transfers: Schema.Array(
Schema.Struct({
id: OrchestrationV2ContextTransfer.fields.id,
sourceThreadId: OrchestrationV2ContextTransfer.fields.sourceThreadId,
targetThreadId: OrchestrationV2ContextTransfer.fields.targetThreadId,
status: OrchestrationV2ContextTransfer.fields.status,
}),
),
}),
})
.annotate(Tool.Readonly, true)
.annotate(Tool.Destructive, false);

export const ThreadToolkit = Toolkit.make(
ThreadForkTool,
ThreadMergeBackTool,
ThreadTransfersTool,
ThreadConfigurationTool,
ThreadConfigureTool,
PendingRequestListTool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,7 @@ export const CLAUDE_READ_ONLY_T3_MCP_ALLOWED_TOOLS: ReadonlyArray<string> = [
"mcp__t3-code__t3_pending_request_list",
"mcp__t3-code__t3_pending_request_read",
"mcp__t3-code__t3_thread_configuration",
"mcp__t3-code__t3_thread_transfers",
"mcp__t3-code__t3_queue_list",
"mcp__t3-code__t3_queue_read",
];
Expand Down
3 changes: 3 additions & 0 deletions packages/shared/src/t3McpToolPresentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const T3_MCP_TOOLS: Record<
t3_pending_request_respond: { displayName: "Answer pending questions" },
t3_thread_configuration: { displayName: "Read thread configuration" },
t3_thread_configure: { displayName: "Set thread model" },
t3_thread_fork: { displayName: "Fork this thread" },
t3_thread_merge_back: { displayName: "Merge thread context" },
t3_thread_transfers: { displayName: "Read thread transfers" },
t3_thread_organize: { displayName: "Organize a thread" },
t3_thread_update: { displayName: "Update T3 thread metadata" },
t3_thread_send: { displayName: "Send to a T3 thread", summaryAction: "thread-send" },
Expand Down
Loading