diff --git a/apps/server/src/mcp/toolkits/thread/handlers.ts b/apps/server/src/mcp/toolkits/thread/handlers.ts index d5c3cabc5dc4..a167ffbb3dc4 100644 --- a/apps/server/src/mcp/toolkits/thread/handlers.ts +++ b/apps/server/src/mcp/toolkits/thread/handlers.ts @@ -13,11 +13,13 @@ import { modelSelectionCommandType } from "@t3tools/shared/model"; import { newCommandId, readCaller, + readMutationCaller, readThread, readWritableThread, unavailable, } from "../../threadAccess.ts"; import * as ProjectionSnapshotQuery from "../../../orchestration/Services/ProjectionSnapshotQuery.ts"; +import * as ScheduledTasks from "../../../scheduledTasks/ScheduledTaskService.ts"; import { queuedRunsInDeliveryOrder } from "../../../orchestration-v2/QueuedRunOrder.ts"; import { ThreadToolkit } from "./tools.ts"; @@ -68,6 +70,36 @@ const readQuestion = Effect.fn("mcp.readQuestion")(function* ( return { ...context, request, item }; }); export const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({ + run_scheduled_task_now: (input) => + Effect.gen(function* () { + const { caller } = yield* readMutationCaller(); + if ( + caller.archivedAt !== null || + caller.runtimeMode !== "full-access" || + caller.interactionMode !== "default" + ) + return yield* new OrchestratorMcpFailure({ + code: "capability_denied", + message: "Running a scheduled task requires a live full-access/default thread.", + }); + const scheduler = yield* ScheduledTasks.ScheduledTaskService; + const { tasks } = yield* scheduler.list().pipe(Effect.mapError(unavailable)); + if (!tasks.some((task) => task.id === input.taskId && task.projectId === caller.projectId)) + return yield* new OrchestratorMcpFailure({ + code: "invalid_request", + message: "The task was not found in the calling project.", + }); + const { task } = yield* scheduler + .runNow({ id: input.taskId }) + .pipe(Effect.mapError(unavailable)); + return { + taskId: task.id, + threadId: task.threadId, + lastRunStatus: task.lastRunStatus, + runCount: task.runCount, + nextRunAt: task.nextRunAt, + }; + }), t3_thread_search: (input) => Effect.gen(function* () { const { caller } = yield* readCaller(); diff --git a/apps/server/src/mcp/toolkits/thread/tools.ts b/apps/server/src/mcp/toolkits/thread/tools.ts index 03f5bfd97f26..2eaf36545c57 100644 --- a/apps/server/src/mcp/toolkits/thread/tools.ts +++ b/apps/server/src/mcp/toolkits/thread/tools.ts @@ -1,4 +1,6 @@ import { + ScheduledTaskId, + ScheduledTask, OrchestrationSearchThreadsInput, OrchestrationSearchThreadsResult, OrchestrationV2ThreadForkSourcePoint, @@ -21,6 +23,7 @@ import * as Schema from "effect/Schema"; import { Tool, Toolkit } from "effect/unstable/ai"; import { ProjectionSnapshotQuery } from "../../../orchestration/Services/ProjectionSnapshotQuery.ts"; +import { ScheduledTaskService } from "../../../scheduledTasks/ScheduledTaskService.ts"; import { ThreadManagementService } from "../../../orchestration-v2/ThreadManagementService.ts"; import { McpInvocationContext } from "../../McpInvocationContext.ts"; @@ -230,7 +233,25 @@ export const ThreadSearchTool = Tool.make("t3_thread_search", { .annotate(Tool.Readonly, true) .annotate(Tool.Destructive, false); +export const ScheduledTaskRunTool = Tool.make("run_scheduled_task_now", { + ...commandTool, + description: + "Run a scheduled task in the calling project now through the existing scheduler. Requires a full-access/default caller. Each call is a new manual run; completion means dispatch/bookkeeping completed, not that the provider turn finished.", + parameters: Schema.Struct({ taskId: ScheduledTaskId }), + success: Schema.Struct({ + taskId: ScheduledTaskId, + threadId: ScheduledTask.fields.threadId, + lastRunStatus: ScheduledTask.fields.lastRunStatus, + runCount: NonNegativeInt, + nextRunAt: ScheduledTask.fields.nextRunAt, + }), + dependencies: [...commandTool.dependencies, ScheduledTaskService], +}) + .annotate(Tool.Destructive, true) + .annotate(Tool.OpenWorld, true); + export const ThreadToolkit = Toolkit.make( + ScheduledTaskRunTool, ThreadSearchTool, ThreadForkTool, ThreadMergeBackTool, diff --git a/packages/shared/src/t3McpToolPresentation.ts b/packages/shared/src/t3McpToolPresentation.ts index 3b4a62f168b4..9ae11d45b75c 100644 --- a/packages/shared/src/t3McpToolPresentation.ts +++ b/packages/shared/src/t3McpToolPresentation.ts @@ -34,6 +34,7 @@ const T3_MCP_TOOLS: Record< delegate_task: { displayName: "Delegate a child task", summaryAction: "delegate" }, task_status: { displayName: "Get delegated task status", summaryAction: "task-status" }, task_cancel: { displayName: "Cancel delegated task", summaryAction: "task-cancel" }, + run_scheduled_task_now: { displayName: "Run scheduled task now" }, schedule_task: { displayName: "Schedule a recurring task", summaryAction: "schedule-create" }, list_scheduled_tasks: { displayName: "List scheduled tasks", summaryAction: "schedule-list" }, update_scheduled_task: {