1import { McpAttachmentInput } from "../attachment/input.ts";
2import {
3 NonNegativeInt,
4 ModelSelection,
5 TrimmedNonEmptyString,
6 ThreadId,
7 RunId,
8 OrchestrationV2DelegatedFrom,
9 OrchestrationV2RunStatus,
10 OrchestrationV2ThreadLaunchWorkspaceStrategy,
11 RuntimeMode,
12 ProviderInteractionMode,
13 Project,
14 ProjectCreatePayload,
15 ProjectUpdatePayload,
16 ProjectId,
17 OrchestratorMcpClientRequestId,
18 OrchestratorMcpEnvironmentTarget,
19 OrchestratorMcpFailure,
20 SourceControlCloneRepositoryInput,
21 SourceControlCloneRepositoryResult,
22} from "@t3tools/contracts";
23import * as FileSystem from "effect/FileSystem";
24import * as DocumentText from "../../../provider/DocumentText.ts";
25import * as ServerConfig from "../../../config.ts";
26import * as ProviderRegistry from "../../../provider/ProviderRegistry.ts";
27import * as ServerSettings from "../../../serverSettings.ts";
28import * as ThreadLaunchService from "../../../orchestration-v2/ThreadLaunchService.ts";
29import * as Digest from "effect/crypto/Digest";
30import * as SecureRandom from "effect/crypto/SecureRandom";
31import * as Schema from "effect/Schema";
32import { Tool, Toolkit } from "effect/ai";
33import * as ProjectService from "../../../project/ProjectService.ts";
34import * as ProjectCloneTracker from "../../../project/ProjectCloneTracker.ts";
35import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts";
36import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
37import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts";
38import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts";
39import * as McpInvocationContext from "../../McpInvocationContext.ts";
40
41const shared = {
42 success: Project,
43 failure: OrchestratorMcpFailure,
44 failureMode: "return" as const,
45 dependencies: [
46 McpInvocationContext.McpInvocationContext,
47 ThreadManagementService.ThreadManagementService,
48 ProjectService.ProjectService,
49 SecureRandom.SecureRandom,
50 ],
51};
52const ProjectListTool = Tool.make("t3_project_list", {
53 ...shared,
54 description: "List projects. Pages can shift.",
55 parameters: Schema.Struct({
56 environmentId: OrchestratorMcpEnvironmentTarget,
57 cursor: Schema.optional(NonNegativeInt),
58 limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))),
59 }),
60 success: Schema.Struct({
61 projects: Schema.Array(Project),
62 nextCursor: Schema.NullOr(NonNegativeInt),
63 }),
64})
65 .annotate(Tool.Readonly, true)
66 .annotate(Tool.Destructive, false);
67const ProjectReadTool = Tool.make("t3_project_read", {
68 ...shared,
69 description: "Read one project: workspace and saved scripts.",
70 parameters: Schema.Struct({
71 environmentId: OrchestratorMcpEnvironmentTarget,
72 projectId: ProjectId,
73 }),
74})
75 .annotate(Tool.Readonly, true)
76 .annotate(Tool.Destructive, false);
77const ProjectCreateTool = Tool.make("t3_project_create", {
78 ...shared,
79 description:
80 "Register a project folder. `createWorkspaceRootIfMissing` creates it; a registered folder is rejected. No `workspaceRoot`: makes a git repo from `title` with README, icon, first commit (`commitError` on failure; project still exists). Clone first with `t3_project_clone` if needed.",
81 parameters: Schema.Struct({
82 environmentId: OrchestratorMcpEnvironmentTarget,
83 ...ProjectCreatePayload.fields,
84 workspaceRoot: Schema.optional(ProjectCreatePayload.fields.workspaceRoot),
85 }),
86 success: Schema.Struct({ ...Project.fields, commitError: Schema.optional(Schema.String) }),
87 dependencies: [...shared.dependencies, ManagedProjectFolders.ManagedProjectFolders],
88}).annotate(Tool.Destructive, true);
89const ProjectUpdateTool = Tool.make("t3_project_update", {
90 ...shared,
91 description: "Update one project's settings. Omitted fields are unchanged.",
92 parameters: Schema.Struct({
93 environmentId: OrchestratorMcpEnvironmentTarget,
94 projectId: ProjectId,
95 ...ProjectUpdatePayload.fields,
96 }),
97}).annotate(Tool.Destructive, true);
98const ProjectDeleteTool = Tool.make("t3_project_delete", {
99 ...shared,
100 description: "Delete one project. Nonempty needs `force: true`. Folder on disk stays.",
101 parameters: Schema.Struct({
102 environmentId: OrchestratorMcpEnvironmentTarget,
103 projectId: ProjectId,
104 force: Schema.optionalKey(Schema.Boolean),
105 }),
106 dependencies: [...shared.dependencies, ProjectCloneTracker.ProjectCloneTracker],
107}).annotate(Tool.Destructive, true);
108const ProjectCloneTool = Tool.make("t3_project_clone", {
109 ...shared,
110 description:
111 "Clone a repository (`destinationPath` new or empty). Register after with `t3_project_create`, with the same `environmentId`.",
112 parameters: Schema.Struct({
113 environmentId: OrchestratorMcpEnvironmentTarget,
114 ...SourceControlCloneRepositoryInput.fields,
115 }),
116 success: SourceControlCloneRepositoryResult,
117 dependencies: [
118 ...shared.dependencies,
119 SourceControlRepositoryService.SourceControlRepositoryService,
120 ],
121})
122 .annotate(Tool.Destructive, true)
123 .annotate(Tool.OpenWorld, true);
124const ThreadLaunchTool = Tool.make("t3_thread_launch", {
125 ...shared,
126 description:
127 "Create one new top-level thread, bound to its workspace before the agent starts. Task in `message`. Omit `projectId`, model and modes to inherit yours (outside a thread, pass `projectId` or `scratch`); modes cannot exceed yours. After an error or lost reply, retry with the same `clientRequestId`. Follow it with `t3_thread_read` or `t3_thread_wait`. Mention it with its `link`.",
128 parameters: Schema.Struct({
129 environmentId: OrchestratorMcpEnvironmentTarget,
130 projectId: Schema.optional(ProjectId),
131 scratch: Schema.optional(
132 Schema.Boolean.annotate({
133 description:
134 "No project: a new Scratch folder. Not with `projectId` or `workspaceStrategy`.",
135 }),
136 ),
137 title: TrimmedNonEmptyString,
138 modelSelection: Schema.optional(ModelSelection),
139 runtimeMode: Schema.optional(RuntimeMode),
140 interactionMode: Schema.optional(ProviderInteractionMode),
141 workspaceStrategy: Schema.optional(
142 OrchestrationV2ThreadLaunchWorkspaceStrategy.annotate({
143 description:
144 "'worktree': new checkout from `baseRef`. Honor a requested base. Otherwise, independent work uses the default branch as `remote/branch` (e.g. `origin/main`) with `startFromOrigin: true`; with no remote, use the local default branch. Required unpublished commits and dependent or stacked work use their local base with `startFromOrigin: false`. 'existing_worktree': bind `worktreePath`, a project worktree. 'root': project checkout, the default, not your worktree. Uncommitted edits are not copied.",
145 }),
146 ),
147 message: Schema.optional(
148 Schema.String.check(Schema.isMaxLength(120000)).annotate({
149 description: "First prompt. Omit it and `attachments` for idle.",
150 }),
151 ),
152 attachments: Schema.optional(Schema.Array(McpAttachmentInput).check(Schema.isMaxLength(8))),
153 clientRequestId: Schema.optional(OrchestratorMcpClientRequestId),
154 /** Set by a linked environment launching its agent's delegated task here; ignored otherwise. */
155 delegatedFrom: Schema.optional(OrchestrationV2DelegatedFrom),
156 }),
157 success: Schema.Struct({
158 threadId: ThreadId,
159 link: Schema.optional(Schema.String),
160 projectId: ProjectId,
161 modelSelection: ModelSelection,
162 runId: Schema.NullOr(RunId),
163 status: Schema.NullOr(OrchestrationV2RunStatus),
164 }),
165 dependencies: [
166 ...shared.dependencies,
167 ThreadLaunchService.ThreadLaunchService,
168 ManagedProjectFolders.ManagedProjectFolders,
169 GitVcsDriver.GitVcsDriver,
170 FileSystem.FileSystem,
171 ServerConfig.ServerConfig,
172 ServerSettings.ServerSettingsService,
173 ProviderRegistry.ProviderRegistry,
174 DocumentText.DocumentText,
175 Digest.Digest,
176 ],
177})
178 .annotate(Tool.Destructive, true)
179 .annotate(Tool.OpenWorld, true);
180
181export const ProjectToolkit = Toolkit.make(
182 ThreadLaunchTool,
183 ProjectListTool,
184 ProjectReadTool,
185 ProjectCreateTool,
186 ProjectUpdateTool,
187 ProjectDeleteTool,
188 ProjectCloneTool,
189);