# Files changed · PR \#10565 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/10565/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(mcp\): expose project thread launch service](https://git.cafe/pingdotgg/t3code/pull/10565.md)

Version: 10

Head commit: 3492b1573c3107d2611541e2d54c28cf2c7f462e

Base commit: 71fa7c96ab7422920fedd57083b38e9992d29b42

Comparison base: 71fa7c96ab7422920fedd57083b38e9992d29b42

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/server/src/mcp/toolkits/project/handlers.ts

Status: modified; +57; −1

```diff
@@ -1,6 +1,8 @@
-import { OrchestratorMcpFailure, ProjectId } from "@t3tools/contracts";
+import { MessageId, ThreadId, OrchestratorMcpFailure, ProjectId } from "@t3tools/contracts";
 import * as Effect from "effect/Effect";
 import * as Option from "effect/Option";
+import * as ThreadMessageIntake from "../../../orchestration-v2/ThreadMessageIntake.ts";
+import * as Claims from "../../../orchestration-v2/AttachmentClaims.ts";
 import * as Project from "../../../project/ProjectService.ts";
 import * as Repositories from "../../../sourceControl/SourceControlRepositoryService.ts";
 import { newCommandId, readCaller, readMutationCaller, unavailable } from "../../threadAccess.ts";
@@ -35,6 +37,60 @@
   return yield* Project.ProjectService;
 });
 export const ProjectHandlersLive = ProjectToolkit.toLayer({
+  t3_thread_launch: (input) =>
+    Effect.gen(function* () {
+      const { caller } = yield* readMutationCaller();
+      if (caller.runtimeMode !== "full-access" || caller.interactionMode !== "default")
+        return yield* new OrchestratorMcpFailure({
+          code: "capability_denied",
+          message: "Project launches require a full-access/default calling thread.",
+        });
+      const commandId = yield* newCommandId();
+      const threadId = ThreadId.make(commandId);
+      const messageId = MessageId.make(commandId);
+      const attachments = input.attachments ?? [];
+      if (attachments.some((attachment) => !Claims.attachmentIsPendingUpload(attachment)))
+        return yield* new OrchestratorMcpFailure({
+          code: "invalid_request",
+          message: "A new thread accepts only pending attachment uploads.",
+        });
+      const result = yield* ThreadMessageIntake.launchThread({
+        commandId,
+        threadId,
+        projectId: input.projectId ?? caller.projectId,
+        title: input.title,
+        modelSelection: input.modelSelection ?? caller.modelSelection,
+        runtimeMode: input.runtimeMode ?? caller.runtimeMode,
+        interactionMode: input.interactionMode ?? caller.interactionMode,
+        workspaceStrategy: input.workspaceStrategy ?? { type: "root" },
+        ...(input.message === undefined && attachments.length === 0
+          ? {}
+          : {
+              initialMessage: {
+                messageId,
+                text: input.message ?? "",
+                attachments,
+              },
+            }),
+        createdBy: "agent",
+        creationSource: "mcp",
+      }).pipe(
+        Effect.mapError((error) =>
+          error._tag === "AttachmentClaimError"
+            ? new OrchestratorMcpFailure({ code: "orchestration_error", message: error.message })
+            : unavailable(),
+        ),
+      );
+      const thread = result.projection.thread;
+      const run = result.projection.runs.find((run) => run.userMessageId === messageId);
+      return {
+        threadId: thread.id,
+        projectId: thread.projectId,
+        modelSelection: thread.modelSelection,
+        runId: run?.id ?? null,
+        status: run?.status ?? null,
+      };
+    }),
   t3_project_list: (input) =>
     Effect.gen(function* () {
       const projects = yield* access;
```

## apps/server/src/mcp/toolkits/project/tools.ts

Status: modified; +44; −0

```diff
@@ -1,5 +1,15 @@
 import {
   NonNegativeInt,
+  ModelSelection,
+  TrimmedNonEmptyString,
+  ThreadId,
+  RunId,
+  OrchestrationV2RunStatus,
+  OrchestrationV2ThreadLaunchWorkspaceStrategy,
+  RuntimeMode,
+  ProviderInteractionMode,
+  ChatImageAttachment,
+  ChatFileAttachment,
   Project,
   ProjectCreatePayload,
   ProjectUpdatePayload,
@@ -8,6 +18,9 @@
   SourceControlCloneRepositoryInput,
   SourceControlCloneRepositoryResult,
 } from "@t3tools/contracts";
+import * as FileSystem from "effect/FileSystem";
+import { ServerConfig } from "../../../config.ts";
+import { ThreadLaunchService } from "../../../orchestration-v2/ThreadLaunchService.ts";
 import * as Crypto from "effect/Crypto";
 import * as Schema from "effect/Schema";
 import { Tool, Toolkit } from "effect/unstable/ai";
@@ -73,7 +86,38 @@
 })
   .annotate(Tool.Destructive, true)
   .annotate(Tool.OpenWorld, true);
+export const ThreadLaunchTool = Tool.make("t3_thread_launch", {
+  ...shared,
+  description:
+    "Launch a thread in a registered project using the same launch service as the app. Omit projectId/modelSelection/modes to inherit from the caller. Supports root, existing_worktree, or a new worktree. Each call is a new launch, with no retry key. Preparation can continue after acceptance; failures may leave a created thread. Attachments must be pending uploads. Requires a full-access/default caller.",
+  parameters: Schema.Struct({
+    projectId: Schema.optional(ProjectId),
+    title: TrimmedNonEmptyString,
+    modelSelection: Schema.optional(ModelSelection),
+    runtimeMode: Schema.optional(RuntimeMode),
+    interactionMode: Schema.optional(ProviderInteractionMode),
+    workspaceStrategy: Schema.optional(OrchestrationV2ThreadLaunchWorkspaceStrategy),
+    message: Schema.optional(Schema.String.check(Schema.isMaxLength(120000))),
+    attachments: Schema.optional(
+      Schema.Array(Schema.Union([ChatImageAttachment, ChatFileAttachment])).check(
+        Schema.isMaxLength(8),
+      ),
+    ),
+  }),
+  success: Schema.Struct({
+    threadId: ThreadId,
+    projectId: ProjectId,
+    modelSelection: ModelSelection,
+    runId: Schema.NullOr(RunId),
+    status: Schema.NullOr(OrchestrationV2RunStatus),
+  }),
+  dependencies: [...shared.dependencies, ThreadLaunchService, FileSystem.FileSystem, ServerConfig],
+})
+  .annotate(Tool.Destructive, true)
+  .annotate(Tool.OpenWorld, true);
+
 export const ProjectToolkit = Toolkit.make(
+  ThreadLaunchTool,
   ProjectListTool,
   ProjectReadTool,
   ProjectCreateTool,
```

## packages/shared/src/t3McpToolPresentation.ts

Status: modified; +1; −0

```diff
@@ -75,6 +75,7 @@
   t3_preview_close: { displayName: "Close a preview tab" },
   t3_environment_read: { displayName: "Read environment preferences" },
   t3_environment_preferences_update: { displayName: "Update environment preferences" },
+  t3_thread_launch: { displayName: "Launch a project thread" },
   t3_project_list: { displayName: "List projects" },
   t3_project_read: { displayName: "Read a project" },
   t3_project_create: { displayName: "Create a project" },
```
