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

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

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

Visibility: public

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

Version: 5

Head commit: bc357ee0f9bf9f944ee62d3541ced375f4437c76

Base commit: 06de99030122b641312c452007302d6bd8f99887

Comparison base: 06de99030122b641312c452007302d6bd8f99887

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

## apps/server/src/mcp/McpHttpServer.ts

Status: modified; +7; −0

```diff
@@ -16,6 +16,8 @@
 import { PreviewControlsHandlersLive } from "./toolkits/previewControls/handlers.ts";
 import { EnvironmentToolkit } from "./toolkits/environment/tools.ts";
 import { EnvironmentHandlersLive } from "./toolkits/environment/handlers.ts";
+import { ProjectToolkit } from "./toolkits/project/tools.ts";
+import { ProjectHandlersLive } from "./toolkits/project/handlers.ts";
 import { ThreadToolkit } from "./toolkits/thread/tools.ts";
 import { ThreadToolkitHandlersLive } from "./toolkits/thread/handlers.ts";
 import * as ThreadMetadataMcpService from "./ThreadMetadataMcpService.ts";
@@ -251,6 +253,10 @@
   Layer.provide(EnvironmentHandlersLive),
 );
 
+export const ProjectRegistrationLive = McpServer.toolkit(ProjectToolkit).pipe(
+  Layer.provide(ProjectHandlersLive),
+);
+
 const McpTransportLive = McpServer.layerHttp({
   name: "T3 Code",
   version: packageJson.version,
@@ -262,6 +268,7 @@
   PreviewToolkitRegistrationLive,
   OrchestratorToolkitRegistrationLive,
   ThreadToolkitRegistrationLive,
+  ProjectRegistrationLive,
   EnvironmentRegistrationLive,
   PreviewControlsRegistrationLive,
   WorktreeToolkitRegistrationLive,
```

## apps/server/src/mcp/toolkits/core.test.ts

Status: modified; +2; −0

```diff
@@ -19,6 +19,7 @@
 import { PreviewControlsToolkit } from "./previewControls/tools.ts";
 import { EnvironmentToolkit } from "./environment/tools.ts";
 import * as EnvironmentHandlers from "./environment/handlers.ts";
+import { ProjectToolkit } from "./project/tools.ts";
 import { ThreadToolkit } from "./thread/tools.ts";
 import { WorktreeToolkit } from "./worktree/tools.ts";
 
@@ -29,6 +30,7 @@
     PreviewToolkit,
     WorktreeToolkit,
     ThreadToolkit,
+    ProjectToolkit,
     EnvironmentToolkit,
     PreviewControlsToolkit,
   ]) {
```

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

Status: added; +94; −0

```diff
@@ -1,0 +1,94 @@
+import { OrchestratorMcpFailure, ProjectId } from "@t3tools/contracts";
+import * as Effect from "effect/Effect";
+import * as Option from "effect/Option";
+import * as Project from "../../../project/ProjectService.ts";
+import * as Repositories from "../../../sourceControl/SourceControlRepositoryService.ts";
+import { newCommandId, readCaller, readMutationCaller, unavailable } from "../../threadAccess.ts";
+import { ProjectToolkit } from "./tools.ts";
+
+function projectFailure(error: Project.ProjectServiceError) {
+  if (error._tag === "ProjectOperationError") return unavailable();
+  const message =
+    error._tag === "ProjectNotFoundError"
+      ? "The project was not found."
+      : error._tag === "ProjectConflictError"
+        ? "The workspace is already registered to a project."
+        : "The project is not empty; force=true is required to delete it.";
+  return new OrchestratorMcpFailure({ code: "invalid_request", message });
+}
+
+const access = Effect.gen(function* () {
+  yield* readCaller();
+  return yield* Project.ProjectService;
+});
+const mutation = 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: "Project changes require a live full-access/default calling thread.",
+    });
+  return yield* Project.ProjectService;
+});
+export const ProjectHandlersLive = ProjectToolkit.toLayer({
+  t3_project_list: (input) =>
+    Effect.gen(function* () {
+      const projects = yield* access;
+      const snapshot = yield* projects.snapshot.pipe(Effect.mapError(unavailable));
+      const rows = snapshot.projects.filter((project) => project.deletedAt === null);
+      const start = input.cursor ?? 0,
+        end = start + (input.limit ?? 20);
+      return { projects: rows.slice(start, end), nextCursor: end < rows.length ? end : null };
+    }),
+  t3_project_read: (input) =>
+    Effect.gen(function* () {
+      const projects = yield* access;
+      const result = yield* projects.getById(input.projectId).pipe(Effect.mapError(unavailable));
+      if (Option.isNone(result))
+        return yield* new OrchestratorMcpFailure({
+          code: "invalid_request",
+          message: "The project was not found.",
+        });
+      return result.value;
+    }),
+  t3_project_create: (input) =>
+    Effect.gen(function* () {
+      const projects = yield* mutation;
+      const commandId = yield* newCommandId();
+      return yield* projects
+        .create({ ...input, commandId, projectId: ProjectId.make(commandId) })
+        .pipe(Effect.mapError(projectFailure));
+    }),
+  t3_project_update: (input) =>
+    Effect.gen(function* () {
+      const projects = yield* mutation;
+      return yield* projects
+        .update({ ...input, commandId: yield* newCommandId() })
+        .pipe(Effect.mapError(projectFailure));
+    }),
+  t3_project_delete: (input) =>
+    Effect.gen(function* () {
+      const projects = yield* mutation;
+      return yield* projects
+        .delete({ ...input, commandId: yield* newCommandId() })
+        .pipe(Effect.mapError(projectFailure));
+    }),
+  t3_project_clone: (input) =>
+    Effect.gen(function* () {
+      yield* mutation;
+      const repositories = yield* Repositories.SourceControlRepositoryService;
+      return yield* repositories.cloneRepository(input).pipe(
+        Effect.mapError(
+          (error) =>
+            new OrchestratorMcpFailure({
+              code: "orchestration_error",
+              message: error.detail,
+            }),
+        ),
+      );
+    }),
+});
```

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

Status: added; +83; −0

```diff
@@ -1,0 +1,83 @@
+import {
+  NonNegativeInt,
+  Project,
+  ProjectCreatePayload,
+  ProjectUpdatePayload,
+  ProjectId,
+  OrchestratorMcpFailure,
+  SourceControlCloneRepositoryInput,
+  SourceControlCloneRepositoryResult,
+} from "@t3tools/contracts";
+import * as Crypto from "effect/Crypto";
+import * as Schema from "effect/Schema";
+import { Tool, Toolkit } from "effect/unstable/ai";
+import { ProjectService } from "../../../project/ProjectService.ts";
+import { ThreadManagementService } from "../../../orchestration-v2/ThreadManagementService.ts";
+import { SourceControlRepositoryService } from "../../../sourceControl/SourceControlRepositoryService.ts";
+import { McpInvocationContext } from "../../McpInvocationContext.ts";
+
+const shared = {
+  success: Project,
+  failure: OrchestratorMcpFailure,
+  failureMode: "return" as const,
+  dependencies: [McpInvocationContext, ThreadManagementService, ProjectService, Crypto.Crypto],
+};
+export const ProjectListTool = Tool.make("t3_project_list", {
+  ...shared,
+  description:
+    "List registered projects in this environment. Pages use the current project snapshot and may shift between calls.",
+  parameters: Schema.Struct({
+    cursor: Schema.optional(NonNegativeInt),
+    limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))),
+  }),
+  success: Schema.Struct({
+    projects: Schema.Array(Project),
+    nextCursor: Schema.NullOr(NonNegativeInt),
+  }),
+})
+  .annotate(Tool.Readonly, true)
+  .annotate(Tool.Destructive, false);
+export const ProjectReadTool = Tool.make("t3_project_read", {
+  ...shared,
+  description:
+    "Read a registered project in this environment, including its workspace and saved scripts.",
+  parameters: Schema.Struct({ projectId: ProjectId }),
+})
+  .annotate(Tool.Readonly, true)
+  .annotate(Tool.Destructive, false);
+export const ProjectCreateTool = Tool.make("t3_project_create", {
+  ...shared,
+  description:
+    "Register a project directory through the existing project service. Set createWorkspaceRootIfMissing to create a directory. Each call creates a new request; an existing registered workspace is rejected. Clone separately with t3_project_clone when needed.",
+  parameters: ProjectCreatePayload,
+}).annotate(Tool.Destructive, true);
+export const ProjectUpdateTool = Tool.make("t3_project_update", {
+  ...shared,
+  description:
+    "Update a registered project's settings. Omitted fields are preserved. Uses the same project service as the app.",
+  parameters: Schema.Struct({ projectId: ProjectId, ...ProjectUpdatePayload.fields }),
+}).annotate(Tool.Destructive, true);
+export const ProjectDeleteTool = Tool.make("t3_project_delete", {
+  ...shared,
+  description:
+    "Delete a project using the existing project deletion lifecycle. Nonempty projects require force=true. This does not delete the repository directory or promise a deleted-thread count.",
+  parameters: Schema.Struct({ projectId: ProjectId, force: Schema.optionalKey(Schema.Boolean) }),
+}).annotate(Tool.Destructive, true);
+export const ProjectCloneTool = Tool.make("t3_project_clone", {
+  ...shared,
+  description:
+    "Clone a repository using the app's source-control service. This only clones; register the returned cwd with t3_project_create. An existing destination is not adopted or removed on failure.",
+  parameters: SourceControlCloneRepositoryInput,
+  success: SourceControlCloneRepositoryResult,
+  dependencies: [...shared.dependencies, SourceControlRepositoryService],
+})
+  .annotate(Tool.Destructive, true)
+  .annotate(Tool.OpenWorld, true);
+export const ProjectToolkit = Toolkit.make(
+  ProjectListTool,
+  ProjectReadTool,
+  ProjectCreateTool,
+  ProjectUpdateTool,
+  ProjectDeleteTool,
+  ProjectCloneTool,
+);
```

## apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts

Status: modified; +2; −0

```diff
@@ -47,6 +47,7 @@
 import * as McpProviderSession from "../../mcp/McpProviderSession.ts";
 import { PreviewControlsToolkit } from "../../mcp/toolkits/previewControls/tools.ts";
 import { EnvironmentToolkit } from "../../mcp/toolkits/environment/tools.ts";
+import { ProjectToolkit } from "../../mcp/toolkits/project/tools.ts";
 import { ThreadToolkit } from "../../mcp/toolkits/thread/tools.ts";
 import { OrchestratorToolkit } from "../../mcp/toolkits/orchestrator/tools.ts";
 import type { EventNdjsonLogger } from "../../provider/Layers/EventNdjsonLogger.ts";
@@ -586,6 +587,7 @@
     const readOnlyToolNames = [
       ...Object.values(OrchestratorToolkit.tools),
       ...Object.values(ThreadToolkit.tools),
+      ...Object.values(ProjectToolkit.tools),
       ...Object.values(EnvironmentToolkit.tools),
       ...Object.values(PreviewControlsToolkit.tools),
     ]
```

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

Status: modified; +2; −0

```diff
@@ -812,6 +812,8 @@
   "mcp__t3-code__t3_pending_request_read",
   "mcp__t3-code__t3_thread_configuration",
   "mcp__t3-code__t3_thread_transfers",
+  "mcp__t3-code__t3_project_list",
+  "mcp__t3-code__t3_project_read",
   "mcp__t3-code__t3_thread_search",
   "mcp__t3-code__t3_preview_list",
   "mcp__t3-code__t3_environment_read",
```

## packages/shared/src/t3McpToolPresentation.ts

Status: modified; +6; −0

```diff
@@ -75,6 +75,12 @@
   t3_preview_close: { displayName: "Close a preview tab" },
   t3_environment_read: { displayName: "Read environment preferences" },
   t3_environment_preferences_update: { displayName: "Update environment preferences" },
+  t3_project_list: { displayName: "List projects" },
+  t3_project_read: { displayName: "Read a project" },
+  t3_project_create: { displayName: "Create a project" },
+  t3_project_update: { displayName: "Update a project" },
+  t3_project_delete: { displayName: "Delete a project" },
+  t3_project_clone: { displayName: "Clone a repository" },
   preview_status: { displayName: "Get preview browser status" },
   preview_open: { displayName: "Open a page in the preview browser" },
   preview_navigate: { displayName: "Navigate the preview browser" },
```
