feat(mcp): add project management tools#8677

closed
t3code/codex-turn-mapping
agents/mcp-projects/management
openedAug 29, 2026byJJulius Marminge
View on GitHubTwo-way sync enabledPR last imported 2d ago

Problem

Agents can work inside projects but cannot discover, register, clone, configure, or safely remove project records through T3's MCP boundary.

Change

  • Add typed t3_project_list, t3_project_read, t3_project_create, t3_project_update, and t3_project_delete tools backed by ProjectService.
  • Support existing directories, explicit missing-directory creation, and mutually exclusive URL or provider/repository clone sources.
  • Preserve omitted versus explicit-null project defaults, icons, and thread environment modes end to end.
  • Serialize receipt-aware thread creation with project deletion through the shared V2 executor.
  • Return truthful committed cascade counts while preserving ordinary HTTP and WebSocket project deletion behavior.

Behavior

Project listing is paginated before per-project configuration reads; full scripts remain on t3_project_read. Clone recovery adopts only a completed checkout whose canonical root, exact source, HEAD, origin ref, and destination match. Deletion includes archived and app-owned child threads but never removes workspace files. Accepted create retries replay before mutable project checks, while a deleted stable project ID cannot be resurrected.

Validation

  • 81 focused project MCP, clone, HTTP, ProjectService deletion, shared admission, ThreadLaunch, V2 integration, and contract tests.
  • Scoped contracts, client-runtime, shared, and server typechecks.
  • Formatting and type-aware lint for the complete changed-file set.

Stack

Bottom of native stack 8774 on t3code/codex-turn-mapping at 415ed0f73b97f1655b6282492f81d0b2bba3a9cc. #8678 is the dependent launch-targeting layer.

Implemented by GPT-5.6-Sol via Codex in T3 Code.

Note

Add project management MCP tools for list, read, create, update, and delete

  • Adds ProjectToolkit with five MCP tools and ProjectMcpService backing them, exposing project CRUD operations to MCP clients via tools.ts and ProjectMcpService.ts
  • Adds ClientCommandDispatch service that serializes thread creation through project admission and returns ProjectMutationError for new commands whose project is absent (ClientCommandDispatch.ts)
  • Adds project creation admission and project mutation locking to ThreadManagementService, serializing thread creation and fork/delegated-task dispatch per project (ThreadManagementService.ts)
  • Reworks SourceControlRepositoryService.cloneRepository to serialize clones by canonical destination, recover verified matching clones, and use explicit origin naming (SourceControlRepositoryService.ts)
  • Adds defaultThreadEnvMode and faviconPath fields to project contracts, creation commands, and the client runtime; ProjectFaviconPath is validated with a length limit and image-extension allowlist (project.ts)
  • Risk: ThreadLaunchService.launch now performs project lookup under the project lock, which changes the ordering of project validation relative to thread claiming; SourceControlRepositoryService.prepareDestination no longer rejects populated destinations at preparation time, deferring rejection to matching-clone validation in isMatchingClone

Macroscope summarized e3f2cda.

Checks

All checks passed
Synced from GitHub

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d1ae5e45-1e7c-4d8f-b87b-b4b3aea5f316

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Warning

Your free Security trial is over. An organization admin can activate Security or dismiss this notice.


Comment @coderabbitai help to get the list of available commands.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

Effect service conventions: one violation found in apps/server/src/mcp/ProjectMcpService.ts (standalone service shape interface). The rest of the new MCP service module, toolkit, handlers, and contracts follow the repository's Effect conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/ProjectMcpService.tslines 27–51Outdated · d13bde6Open thread
……@@ -0,0 +1,381 @@
48+
49+export class ProjectMcpService extends Context.Service<ProjectMcpService, ProjectMcpServiceShape>()(
50+ "t3/mcp/ProjectMcpService",
51+) {}
MmacroscopeappBoton lines 27–51Aug 29, 2026

mcp/ProjectMcpService.ts:51

The service interface should be declared inline in the Context.Service call rather than kept as a standalone ProjectMcpServiceShape type (see the sibling WorktreeMcpService.ts). Consider inlining it and referring to the inferred interface as ProjectMcpService["Service"][...] at the five method annotations (lines 147, 160, 166, 239, 278).

diff
1-export interface ProjectMcpServiceShape {
2- readonly list: (
3- scope: McpInvocationScope,
4- ) => Effect.Effect<ProjectMcpListResult, ProjectMcpFailure>;
5- readonly read: (
6- scope: McpInvocationScope,
7- projectId: ProjectId,
8- ) => Effect.Effect<ProjectMcpProject, ProjectMcpFailure>;
9- readonly create: (
10- scope: McpInvocationScope,
11- input: ProjectMcpCreateInput,
12- ) => Effect.Effect<ProjectMcpProject, ProjectMcpFailure>;
13- readonly update: (
14- scope: McpInvocationScope,
15- input: ProjectMcpUpdateInput,
16- ) => Effect.Effect<ProjectMcpProject, ProjectMcpFailure>;
17- readonly delete: (
18- scope: McpInvocationScope,
19- input: ProjectMcpDeleteInput,
20- ) => Effect.Effect<ProjectMcpDeleteResult, ProjectMcpFailure>;
21-}
22-
23-export class ProjectMcpService extends Context.Service<ProjectMcpService, ProjectMcpServiceShape>()(
24- "t3/mcp/ProjectMcpService",
25-) {}
26+export class ProjectMcpService extends Context.Service<
27+ ProjectMcpService,
28+ {
29+ readonly list: (
30+ scope: McpInvocationScope,
31+ ) => Effect.Effect<ProjectMcpListResult, ProjectMcpFailure>;
32+ readonly read: (
33+ scope: McpInvocationScope,
34+ projectId: ProjectId,
35+ ) => Effect.Effect<ProjectMcpProject, ProjectMcpFailure>;
36+ readonly create: (
37+ scope: McpInvocationScope,
38+ input: ProjectMcpCreateInput,
39+ ) => Effect.Effect<ProjectMcpProject, ProjectMcpFailure>;
40+ readonly update: (
41+ scope: McpInvocationScope,
42+ input: ProjectMcpUpdateInput,
43+ ) => Effect.Effect<ProjectMcpProject, ProjectMcpFailure>;
44+ readonly delete: (
45+ scope: McpInvocationScope,
46+ input: ProjectMcpDeleteInput,
47+ ) => Effect.Effect<ProjectMcpDeleteResult, ProjectMcpFailure>;
48+ }
49+>()("t3/mcp/ProjectMcpService") {}

No longer relevant as of 2cb24f5ca3d8df9579cf13b2be9d4a79c0883211

Synced from GitHub

Thread transfer impact

⚠️ The latest CI run did not produce a thread transfer result for e3f2cda.

This comment will update automatically after the next completed run.

apps/server/src/mcp/ProjectMcpService.tsline 197Outdated · d13bde6Open thread
……@@ -0,0 +1,381 @@
194+ "Clone creation requires either remoteUrl or both provider and repository.",
195+ );
196+ }
197+ const cloned = yield* sourceControl
MmacroscopeappBoton line 197Aug 29, 2026

🟠 High mcp/ProjectMcpService.ts:197

A failed or overlapping projects.create leaves a successful clone unregistered, so retrying the same clientRequestId attempts cloneRepository again and fails because its destination is no longer empty. This breaks idempotent retries and prevents registering the already-cloned directory; reserve the project durably before cloning or make retries recover and register the existing destination.

No longer relevant as of 101793703084e93aaf8ee4312feaa22627dc20d9

apps/server/src/mcp/ProjectMcpService.tsline 297Outdated · c9605e0Open thread
……@@ -0,0 +1,380 @@
294+ if (project.deletedAt !== null) {
295+ return {
296+ projectId: input.projectId,
297+ deleted: true,
MmacroscopeappBoton line 297Aug 29, 2026

🟡 Medium mcp/ProjectMcpService.ts:297

Retrying a cascaded delete with the same clientRequestId reports deletedThreadCount: 0 after the original response is lost, even though the original operation deleted threads. This already-deleted branch hardcodes the count instead of recovering the prior operation result; persist or otherwise replay the original affected count for idempotent retries.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around line 297:
2
3Retrying a cascaded delete with the same `clientRequestId` reports `deletedThreadCount: 0` after the original response is lost, even though the original operation deleted threads. This already-deleted branch hardcodes the count instead of recovering the prior operation result; persist or otherwise replay the original affected count for idempotent retries.
apps/server/src/mcp/ProjectMcpService.tsline 254Outdated · c9605e0Open thread
……@@ -0,0 +1,380 @@
251+ const key = yield* requestKey(input.clientRequestId);
252+ const project = yield* projects
253+ .update({
254+ commandId: stableCommandId({ scope, requestKey: key, operation: "project-update" }),
MmacroscopeappBoton line 254Aug 29, 2026

🟠 High mcp/ProjectMcpService.ts:254

Repeated clientRequestId values across different projects make valid updates or deletes fail as operation_failed: the second command reuses the first project's command ID, and the orchestration engine rejects it for a different aggregate. Include input.projectId in the stable command ID for both update and delete operations.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around line 254:
2
3Repeated `clientRequestId` values across different projects make valid updates or deletes fail as `operation_failed`: the second command reuses the first project's command ID, and the orchestration engine rejects it for a different aggregate. Include `input.projectId` in the stable command ID for both update and delete operations.

Fixed in db8d3a6b6f. Stable update and delete command IDs now include the target project ID, with focused coverage for reusing the same clientRequestId across different projects.

apps/server/src/mcp/ProjectMcpService.tsline 235Outdated · c9605e0Open thread
……@@ -0,0 +1,380 @@
232+ failure("operation_failed", `Unable to create project: ${errorMessage(error)}`),
233+ ),
234+ );
235+ return yield* projectView(project);
MmacroscopeappBoton line 235Aug 29, 2026

🟠 High mcp/ProjectMcpService.ts:235

create returns operation_failed after the project has already been committed when projectView fails at line 235 because serverSettings.getSettings fails; update has the same issue at line 274. Clients may treat these failed mutations as no-ops and issue conflicting follow-up actions. Return the committed project or distinguish response-enrichment failures from mutation failures.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around line 235:
2
3`create` returns `operation_failed` after the project has already been committed when `projectView` fails at line 235 because `serverSettings.getSettings` fails; `update` has the same issue at line 274. Clients may treat these failed mutations as no-ops and issue conflicting follow-up actions. Return the committed project or distinguish response-enrichment failures from mutation failures.
apps/server/src/mcp/ProjectMcpService.tsline 107Outdated · db8d3a6Open thread
……@@ -0,0 +1,473 @@
104+ const raced = current.get(projectId);
105+ if (raced !== undefined) return [raced, current] as const;
106+ const next = new Map(current);
107+ next.set(projectId, created);
MmacroscopeappBoton line 107Aug 29, 2026

🟠 High mcp/ProjectMcpService.ts:107

createSemaphores retains a semaphore for every distinct projectId, so repeated create calls with unique client request IDs permanently grow this long-lived map—even when projects.create fails. Remove entries after the permit completes using race-safe cleanup so failed and completed requests do not accumulate unbounded state.

Also found in 1 other location(s)

apps/server/src/sourceControl/SourceControlRepositoryService.ts:110

cloneSemaphoreFor permanently adds a semaphore for every normalized destinationPath and never removes it after withPermit finishes. Because clone requests accept arbitrary destination paths, repeated failed or successful requests with distinct paths make this long-lived service's Map grow without bound, eventually exhausting server memory.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around line 107:
2
3`createSemaphores` retains a semaphore for every distinct `projectId`, so repeated `create` calls with unique client request IDs permanently grow this long-lived map—even when `projects.create` fails. Remove entries after the permit completes using race-safe cleanup so failed and completed requests do not accumulate unbounded state.
4
5Also found in 1 other location(s):
6- apps/server/src/sourceControl/SourceControlRepositoryService.ts:110 -- `cloneSemaphoreFor` permanently adds a semaphore for every normalized `destinationPath` and never removes it after `withPermit` finishes. Because clone requests accept arbitrary destination paths, repeated failed or successful requests with distinct paths make this long-lived service's `Map` grow without bound, eventually exhausting server memory.

Fixed in 56cf046791. Both project-create and repository-clone serialization now use the existing makeKeyedSerialExecutor, whose reference-counted acquire/release removes idle keys safely after completed, failed, waiting, or interrupted users. This replaces both retained semaphore maps.

……@@ -173,49 +190,93 @@ export const make = Effect.gen(function* () {
184- let repository: SourceControlRepositoryInfo | null = null;
185- let remoteUrl = input.remoteUrl?.trim() ?? null;
186- let provider: SourceControlProviderKind = input.provider ?? "unknown";
226+ const destinationPath = yield* normalizeDestinationPath(input.destinationPath);
MmacroscopeappBoton line 226Aug 29, 2026

🟠 High sourceControl/SourceControlRepositoryService.ts:226

Concurrent clones addressed through a symlink and its real path acquire different semaphores, so both can observe the destination as absent and race git clone into the same directory; one request then fails. path.resolve at line 226 only normalizes syntax and does not canonicalize symlinks. Key the lock by a canonical destination path (including the canonicalized existing parent) before acquiring it.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/sourceControl/SourceControlRepositoryService.ts around line 226:
2
3Concurrent clones addressed through a symlink and its real path acquire different semaphores, so both can observe the destination as absent and race `git clone` into the same directory; one request then fails. `path.resolve` at line 226 only normalizes syntax and does not canonicalize symlinks. Key the lock by a canonical destination path (including the canonicalized existing parent) before acquiring it.

Fixed in 56cf046791. Clone lock keys now canonicalize an existing destination or the nearest existing parent before appending missing segments, so symlink and physical aliases share one lock. The overlapping-clone test now races symlink and real-path requests and proves only one clone runs.

apps/server/src/mcp/ProjectMcpService.tsline 436Outdated · db8d3a6Open thread
……@@ -0,0 +1,473 @@
433+ failure("operation_failed", `Unable to delete project threads: ${errorMessage(error)}`),
434+ ),
435+ );
436+ yield* projects
MmacroscopeappBoton line 436Aug 29, 2026

🟠 High mcp/ProjectMcpService.ts:436

project.delete can delete a project while a newly created live thread still references it, causing later thread operations to fail and violating the cascadeThreads/nonempty guarantee. The thread snapshot is taken before this delete command, so a concurrent ThreadLaunchService.launch can persist thread.create after the snapshot without rechecking the project; serialize deletion with thread creation or enforce the invariant transactionally.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around line 436:
2
3`project.delete` can delete a project while a newly created live thread still references it, causing later thread operations to fail and violating the `cascadeThreads`/nonempty guarantee. The thread snapshot is taken before this delete command, so a concurrent `ThreadLaunchService.launch` can persist `thread.create` after the snapshot without rechecking the project; serialize deletion with thread creation or enforce the invariant transactionally.
apps/server/src/mcp/ProjectMcpService.tslines 67–71Outdated · db8d3a6Open thread
MmacroscopeappBoton lines 67–71Aug 29, 2026

🟠 High mcp/ProjectMcpService.ts:67

A clientRequestId containing a lone surrogate such as "\ud800" makes stablePart throw URIError, so project create/update/delete fails without returning a ProjectMcpFailure. The request schema accepts this value because it only trims and checks length; normalize the string to well-formed Unicode before calling encodeURIComponent.

diff
1- return encodeURIComponent(value);
2+ return encodeURIComponent(value.toWellFormed());
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around line 67:
2
3A `clientRequestId` containing a lone surrogate such as `"\ud800"` makes `stablePart` throw `URIError`, so project create/update/delete fails without returning a `ProjectMcpFailure`. The request schema accepts this value because it only trims and checks length; normalize the string to well-formed Unicode before calling `encodeURIComponent`.
apps/server/src/mcp/toolkits/project/tools.tslines 75–75Outdated · db8d3a6Open thread
……@@ -0,0 +1,100 @@
72+})
73+ .annotate(Tool.Title, "Update a T3 project")
74+ .annotate(Tool.Readonly, false)
75+ .annotate(Tool.Destructive, false)
MmacroscopeappBoton lines 75–75Aug 29, 2026

🟡 Medium project/tools.ts:75

t3_project_update is advertised as non-destructive even though it can clear model, workspace-mode, and icon overrides or replace the scripts configuration, allowing MCP clients to present configuration removal as a safe update. Mark Tool.Destructive as true so the safety annotation reflects these destructive operations.

suggestion
1 .annotate(Tool.Destructive, true)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/toolkits/project/tools.ts around line 75:
2
3`t3_project_update` is advertised as non-destructive even though it can clear model, workspace-mode, and icon overrides or replace the scripts configuration, allowing MCP clients to present configuration removal as a safe update. Mark `Tool.Destructive` as `true` so the safety annotation reflects these destructive operations.
apps/server/src/mcp/ProjectMcpService.tsline 242Outdated · 56cf046Open thread
……@@ -0,0 +1,464 @@
239+ );
240+ if (Option.isSome(existing)) {
241+ return yield* projectView(existing.value, yield* loadSettings);
242+ }
CcursorBoton line 242Aug 29, 2026

Create retry fails after deletion

Medium Severity

Create idempotency uses getById without includeDeleted. After a project is removed, a retry with the same clientRequestId treats the id as unused and dispatches project.create again. The V1 aggregate still occupies that id, so the retry surfaces as operation_failed instead of a clear replay or deleted-id result.

Reviewed by Cursor Bugbot for commit 56cf04679110c37bfd0f262e6a067dd352fb7abf. Configure here.

Fixed in 01f3cf4051. Create preflight now reads the stable project ID with includeDeleted before settings, clone, or directory work. A deleted match returns typed project_deleted guidance to use a new clientRequestId and cannot resurrect the record. Focused create -> cascade delete -> same-key clone retry coverage proves clone runs once and the deleted projection remains deleted. I also pinned the underlying V1 behavior: replaying the accepted create command returns the current soft-deleted projection via its receipt and emits no new create event, so the filesystem guard belongs before ProjectService.create.

Synced from GitHub

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces five production MCP project-management tools, including repository cloning, filesystem registration, project updates, and cascading thread deletion, while also changing project-default propagation and shared orchestration/source-control behavior. The breadth of new runtime behavior and product-default changes warrants human review.

Not approved because:

  • Per-PR cost limit exceeded (workspace setting). Approvability relies on correctness review in order to determine eligibility

Review your spending limits in Billing settings, or comment @macroscope-app review this PR to bypass the limit and review now. You can add or adjust custom eligibility rules. Learn more.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

Effect service conventions: one minor import-hygiene issue in the new test module. The service definition itself (inline Context.Service interface, ProjectMcpService["Service"][...] annotations, namespaced module imports, make + layer order) follows the sibling WorktreeMcpService.ts conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/ProjectMcpService.test.tslines 38–39Outdated · 01f3cf4Open thread
……@@ -0,0 +1,890 @@
36+import * as SourceControlRepositoryService from "../sourceControl/SourceControlRepositoryService.ts";
37+import type { McpInvocationScope } from "./McpInvocationContext.ts";
38+import { ProjectMcpService } from "./ProjectMcpService.ts";
39+import * as ProjectMcp from "./ProjectMcpService.ts";
MmacroscopeappBoton lines 38–39Aug 29, 2026

mcp/ProjectMcpService.test.ts:39

./ProjectMcpService.ts is imported twice: once named for the tag and once as a namespace. Consider dropping the named import and using the module's public shape consistently (ProjectMcp.ProjectMcpService at the tag use sites, ProjectMcp.layer for the layer).

No longer relevant as of 101793703084e93aaf8ee4312feaa22627dc20d9

Fixed in 1017937030. The test now keeps only the ProjectMcp namespace import and resolves every service tag through ProjectMcp.ProjectMcpService. The focused Project MCP suite passes (8/8), formatting and diff checks are clean.

MmacroscopeappBotreviewed · 1 inline threadAug 29, 2026
Synced from GitHub

One convention issue found: an aliased layer import in apps/server/src/mcp/ProjectMcpService.test.ts erases the service module namespace (and is shadowed later in the file). The service definition itself (inline Context.Service interface, make, typed layer, Foo["Service"] annotations, dependencies acquired from the environment) follows the conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/ProjectMcpService.test.tslines 28–31Outdated · 1017937Open thread
……@@ -0,0 +1,889 @@
28+import {
29+ layer as threadManagementLayer,
30+ ThreadManagementService,
31+} from "../orchestration-v2/ThreadManagementService.ts";
MmacroscopeappBoton lines 28–31Aug 29, 2026

At a service boundary the module should be imported as a namespace rather than aliasing its layer export, which erases the module namespace. It also collides here: line 424 declares a local const threadManagementLayer = Layer.mock(ThreadManagementService)({...}), so the same identifier means the real layer in one test and a mock in another.

Consider import * as ThreadManagement from "../orchestration-v2/ThreadManagementService.ts"; and using ThreadManagement.layer / ThreadManagement.ThreadManagementService at the use sites.

Posted via Macroscope — Effect Service Conventions

Fixed in 6da97d32c2 (included in latest head 945b99f818). The test now imports ThreadManagementService only as the ThreadManagement namespace, uses ThreadManagement.layer and ThreadManagement.ThreadManagementService at every service boundary, and renames the local mock to blockedThreadManagementLayer. The focused Project MCP suite passes 8/8.

MmacroscopeappBotreviewed · 1 inline threadAug 30, 2026
Synced from GitHub

Effect service conventions review of the new project MCP service and its contracts. One finding on error modeling; the service definition (inline Context.Service interface, Foo["Service"][...] annotations, namespace service imports, canonical layer) otherwise follows the conventions.

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/ProjectMcpService.tslines 57–63Outdated · 99c3643Open thread
……@@ -0,0 +1,470 @@
60+
61+function errorMessage(error: unknown): string {
62+ return error instanceof Error ? error.message : String(error);
63+}
MmacroscopeappBoton lines 57–63Aug 30, 2026

ProjectMcpFailure carries only code plus a free-form message, and every wrapper builds that message by stringifying the underlying failure (errorMessage(error)), so the message is derived from the cause while the cause itself is discarded. That also lets clone/dispatch failures leak remote URLs or git output into a caller-visible message.

Consider giving ProjectMcpFailure stable structural attributes (e.g. code, operation, projectId/workspaceRoot) plus a required cause, deriving message from those attributes only, and constructing the error at each failure boundary — which also removes the need for the failure(...) factory, whose only behavior is new ProjectMcpFailure({ ... }).

Posted via Macroscope — Effect Service Conventions

apps/server/src/mcp/toolkits/project/tools.tslines 61–61Outdated · 99c3643Open thread
……@@ -0,0 +1,100 @@
58+ .annotate(Tool.Title, "Create a T3 project")
59+ .annotate(Tool.Readonly, false)
60+ .annotate(Tool.Destructive, false)
61+ .annotate(Tool.Idempotent, true)
MmacroscopeappBoton lines 61–61Aug 30, 2026

🟡 Medium project/tools.ts:61

Repeating ProjectCreateTool with identical arguments creates additional project records (and can repeat a clone) when clientRequestId is omitted, so advertising .annotate(Tool.Idempotent, true) falsely claims that retries have no additional effect. Mark this tool non-idempotent unless creation deduplicates requests without that field.

suggestion
1 .annotate(Tool.Idempotent, false)
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/toolkits/project/tools.ts around line 61:
2
3Repeating `ProjectCreateTool` with identical arguments creates additional project records (and can repeat a clone) when `clientRequestId` is omitted, so advertising `.annotate(Tool.Idempotent, true)` falsely claims that retries have no additional effect. Mark this tool non-idempotent unless creation deduplicates requests without that field.

Fixed in 725e31db1944d1a106c8331cda6252e08fa43cd7. ProjectCreateTool no longer advertises idempotency when clientRequestId is omitted; keyed retries retain the existing durable behavior.

apps/server/src/sourceControl/SourceControlRepositoryService.tslines 323–323Outdated · 99c3643Open thread
……@@ -173,49 +190,144 @@ export const make = Effect.gen(function* () {
320+ yield* git.execute({
321+ operation: "SourceControlRepositoryService.cloneRepository",
322+ cwd: preparedDestination.parentPath,
323+ args: ["clone", remoteUrl, preparedDestination.directoryName],
MmacroscopeappBoton lines 323–323Aug 30, 2026

🟡 Medium sourceControl/SourceControlRepositoryService.ts:323

Retries reject a completed clone as unrelated when Git is configured with a non-default clone.defaultRemoteName, because recovery always checks remote.origin.url and refs/remotes/origin. Force the clone remote name to origin so these checks match the repository that was just created.

suggestion
1 args: ["clone", "--origin", "origin", remoteUrl, preparedDestination.directoryName],
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/sourceControl/SourceControlRepositoryService.ts around line 323:
2
3Retries reject a completed clone as unrelated when Git is configured with a non-default `clone.defaultRemoteName`, because recovery always checks `remote.origin.url` and `refs/remotes/origin`. Force the clone remote name to `origin` so these checks match the repository that was just created.

Fixed in 725e31db1944d1a106c8331cda6252e08fa43cd7. Clone now explicitly uses --origin origin, matching the recovery checks. Focused clone recovery coverage runs with a non-default clone.defaultRemoteName and proves recovery still uses the created origin remote.

Verified the implementation now explicitly uses --origin origin, which resolves the reported issue. I could not find the claimed non-default clone.defaultRemoteName recovery test, though; the existing coverage only asserts the clone arguments.

packages/contracts/src/projectMcp.tsline 94Outdated · 99c3643Open thread
……@@ -0,0 +1,147 @@
91+ source: ProjectMcpCreateSource,
92+ defaultModelSelection: Schema.optional(Schema.NullOr(ModelSelection)),
93+ defaultThreadEnvMode: Schema.optional(Schema.NullOr(ThreadEnvMode)),
94+ faviconPath: Schema.optional(Schema.NullOr(TrimmedNonEmptyString)),

🟡 Medium src/projectMcp.ts:94

ProjectMcpCreateInput accepts and persists non-image values such as .env for faviconPath, but the icon asset endpoint rejects them, leaving the created project with a broken favicon configuration. Use ProjectFaviconPath here (and in the update schema) instead of TrimmedNonEmptyString so the extension constraint is enforced.

Also found in 1 other location(s)

packages/contracts/src/project.ts:81

The newly exposed faviconPath mutation accepts every non-empty string, rather than ProjectFaviconPath. A websocket client can therefore create a project with faviconPath: &#34;.env&#34;; the mutation route forwards it to ProjectService, which persists it, while the icon asset path rejects non-image values. This leaves the project with a broken saved favicon configuration (the update branch at line 92 has the same omission).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @packages/contracts/src/projectMcp.ts around line 94:
2
3`ProjectMcpCreateInput` accepts and persists non-image values such as `.env` for `faviconPath`, but the icon asset endpoint rejects them, leaving the created project with a broken favicon configuration. Use `ProjectFaviconPath` here (and in the update schema) instead of `TrimmedNonEmptyString` so the extension constraint is enforced.
4
5Also found in 1 other location(s):
6- packages/contracts/src/project.ts:81 -- The newly exposed `faviconPath` mutation accepts every non-empty string, rather than `ProjectFaviconPath`. A websocket client can therefore create a project with `faviconPath: ".env"`; the mutation route forwards it to `ProjectService`, which persists it, while the icon asset path rejects non-image values. This leaves the project with a broken saved favicon configuration (the update branch at line 92 has the same omission).
Synced from GitHub

Macroscope skipped reviewing this pull request. Per-PR cost limit exceeded (workspace setting).

Reviews on this PR have cost $35.22 so far. This review would add an estimated $4.68, bringing the total to $39.90 — above your per-PR limit of $30.00.

Tip

To get this pull request reviewed, you can:

  1. Comment @macroscope-app on this PR to request a manual review (monthly spend limits still apply).
  2. Exclude large or generated files from review by adding a pattern to your .macroscope/ignore.md — note that creating this file replaces Macroscope's built-in default ignores rather than extending them.
  3. Raise your cost limit in your workspace billing settings.

Turn off this reminder going forward

Synced from GitHub

One finding: the new ClientCommandDispatch module exports make without a Context.Service tag or layer, which breaks the canonical service-module shape used everywhere else in apps/server/src/orchestration-v2.

Posted via Macroscope — Effect Service Conventions

apps/server/src/project/http.tsline 92Outdated · 4f8d2eaOpen thread
……@@ -47,36 +89,7 @@ export const projectHttpApiLayer = HttpApiBuilder.group(
77- commandId: mutation.commandId,
78- projectId: mutation.projectId,
79- });
92+ const operation = projectMutationOperation(projects, args.payload);

🟠 High project/http.ts:92

Requests with createWorkspaceRootIfMissing: true are rejected when the workspace root does not exist, instead of creating the directory. projectMutationOperation drops this field before calling ProjectService.create, so the service receives its default false; forward the field in the project.create mapping.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/project/http.ts around line 92:
2
3Requests with `createWorkspaceRootIfMissing: true` are rejected when the workspace root does not exist, instead of creating the directory. `projectMutationOperation` drops this field before calling `ProjectService.create`, so the service receives its default `false`; forward the field in the `project.create` mapping.

Fixed in fe9b7c3790d08deecfc11faad3aa31013faf5247. The HTTP create mapping now forwards createWorkspaceRootIfMissing only when supplied, preserving omission, and its handler test covers true, false, and omitted. The focused lower suite passes 14/14 and the scoped server typecheck passes.

apps/server/src/project/http.tsline 92Outdated · 4f8d2eaOpen thread
……@@ -47,36 +89,7 @@ export const projectHttpApiLayer = HttpApiBuilder.group(
77- commandId: mutation.commandId,
78- projectId: mutation.projectId,
79- });
92+ const operation = projectMutationOperation(projects, args.payload);

🟠 High project/http.ts:92

The HTTP mutate endpoint can delete a nonempty project while leaving its existing threads pointing at the deleted project. projectMutationOperation calls ProjectService.delete directly, bypassing withProjectMutationLock, active/archived thread checks, and force cleanup; route deletes through the same guarded mutation path as WebSocket mutations.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/project/http.ts around line 92:
2
3The HTTP `mutate` endpoint can delete a nonempty project while leaving its existing threads pointing at the deleted project. `projectMutationOperation` calls `ProjectService.delete` directly, bypassing `withProjectMutationLock`, active/archived thread checks, and `force` cleanup; route deletes through the same guarded mutation path as WebSocket mutations.

Verified against the rebased V2 base c01ff207: the HTTP V1 mutation route already called ProjectService.delete directly before this PR. This PR extracted that unchanged mapping only to test the newly forwarded project fields; its guarded cascade/admission contract is intentionally the new MCP/V2 project-delete path. Migrating the older HTTP auth/result contract is a separate inherited limitation and is explicitly documented in the handoff, so it is out of scope here.

CcursorBotreviewed · 1 inline threadAug 30, 2026
Synced from GitHub

Stale Bugbot comment from a previous run.

apps/server/src/mcp/ProjectMcpService.tsline 196Outdated · 4f8d2eaOpen thread
……@@ -0,0 +1,459 @@
193+ const limit = input.limit ?? 25;
194+ const sorted = snapshot.projects.toSorted(
195+ (left, right) =>
196+ right.updatedAt.localeCompare(left.updatedAt) || left.id.localeCompare(right.id),
CcursorBoton line 196Aug 30, 2026

Project list sorts timestamps as text

Medium Severity

t3_project_list orders projects with localeCompare on updatedAt. IsoDateTime is an unconstrained string, so mixed timezone offsets invert recency and shift cursor pages. Nearby thread listing already orders with DateTime.toEpochMillis.

Triggered by learned rule: Use parseTimestampDate for ISO string parsing, return null for invalid

Reviewed by Cursor Bugbot for commit 4f8d2ea2119a51a8bcfc696e635f89804451599a. Configure here.

MmacroscopeappBotreviewed · 1 inline threadAug 30, 2026
Synced from GitHub

Effect service conventions review: one finding — a compatibility re-export left behind after moving ProjectFaviconPath to project.ts. The new ProjectMcpService, ClientCommandDispatch, and project toolkit modules otherwise follow the canonical tag/make/layer layout, namespace imports, and environment-based dependency acquisition.

Posted via Macroscope — Effect Service Conventions

packages/contracts/src/orchestration.tslines 65–70Outdated · fe9b7c3Open thread
MmacroscopeappBoton lines 65–70Aug 30, 2026

ProjectFaviconPath now lives in project.ts, and this line is a compatibility re-export from its old home. The only remaining consumer of the old path is packages/contracts/src/assets.ts (import { ProjectFaviconPath } from "./orchestration.ts"); everything else reaches it through the barrel, which already re-exports project.ts.

Consider deleting this re-export and pointing assets.ts at the canonical module (import { ProjectFaviconPath } from "./project.ts";) so the moved declaration has a single import path.

Posted via Macroscope — Effect Service Conventions

Keeping this re-export is intentional compatibility. ProjectFaviconPath was public from orchestration.ts on the V2 base, and the project contract move must not break existing direct consumers that are not visible to an in-repo grep. The canonical declaration is in project.ts and orchestration.ts only re-exports that same schema, so there is no duplicate implementation or divergent service shape. Removing the old import path would violate the rollout's old-client/source compatibility requirement.

apps/server/src/orchestration-v2/Orchestrator.tsline 7281Outdated · fb49467Open thread
……@@ -7262,6 +7278,7 @@ export const layerUnavailable: Layer.Layer<OrchestratorV2> = Layer.succeed(
72627278 cause: "Orchestration V2 live runtime is not configured.",
72637279 }),
72647280 ),
7281+ getCommandReceipt: () => Effect.die("Orchestration V2 live runtime is not configured."),
MmacroscopeappBoton line 7281Aug 30, 2026

🟠 High orchestration-v2/Orchestrator.ts:7281

When a thread.create command reaches ClientCommandDispatch with the orchestration runtime unavailable, withProjectCreationAdmission calls getCommandReceipt and the fiber terminates with an untyped defect instead of returning the structured OrchestratorDispatchError used by this layer's other unavailable operations. Implement this stub with the appropriate structured OrchestratorDispatchError failure rather than Effect.die.

✅ Resolved in 4e9c9a50cd3789059e364ff93f948b580525e0ac

Fixed in 4e9c9a50cd3789059e364ff93f948b580525e0ac. The unavailable orchestrator now returns CommandReceiptStoreReadError, and the receipt-aware thread.create admission boundary maps that failure to OrchestratorDispatchError with the original command ID and type. The actual ClientCommandDispatch test proves the WebSocket creation path returns the structured error instead of a defect.

apps/server/src/mcp/ProjectMcpService.tslines 65–67Outdated · fb49467Open thread
……@@ -0,0 +1,462 @@
64+ effect.pipe(
65+ Effect.tapError((error) =>
66+ Effect.logWarning("Project MCP operation failed.", { operation, error }),
67+ ),
MmacroscopeappBoton lines 65–67Aug 30, 2026

🟠 High mcp/ProjectMcpService.ts:65

redactOperationFailure writes the original error to warning logs before mapping it to the public ProjectMcpFailure. On the clone path, SourceControlRepositoryError.detail can contain credential-bearing remote URLs, so OAuth tokens are exposed in server logs even though the returned MCP error is redacted. Log only non-sensitive operation metadata here, or sanitize the error before logging.

suggestion
1 Effect.tapError(() =>
2 Effect.logWarning("Project MCP operation failed.", { operation }),
3 ),
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/ProjectMcpService.ts around lines 65-67:
2
3`redactOperationFailure` writes the original error to warning logs before mapping it to the public `ProjectMcpFailure`. On the clone path, `SourceControlRepositoryError.detail` can contain credential-bearing remote URLs, so OAuth tokens are exposed in server logs even though the returned MCP error is redacted. Log only non-sensitive operation metadata here, or sanitize the error before logging.
apps/server/src/project/ProjectService.tsline 416Outdated · 001fdc8Open thread
……@@ -387,103 +399,120 @@ export const make = Effect.gen(function* () {
413+ (cause) => new ProjectOperationError({ operation: "read-project", projectId, cause }),
414+ ),
415+ );
416+ if (Option.isNone(existing) || existing.value.deletedAt !== null) {
MmacroscopeappBoton line 416Sep 5, 2026

🟡 Medium project/ProjectService.ts:416

A concurrent duplicate delete returns ProjectNotFoundError instead of the required alreadyDeleted result when the first delete completes while the second call waits for withProjectLock. This guard rejects the now-deleted projection before the stable commandId can replay the accepted project.delete command; handle an already-deleted project as an idempotent replay (while still returning ProjectNotFoundError for a genuinely missing project).

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/project/ProjectService.ts around line 416:
2
3A concurrent duplicate delete returns `ProjectNotFoundError` instead of the required `alreadyDeleted` result when the first delete completes while the second call waits for `withProjectLock`. This guard rejects the now-deleted projection before the stable `commandId` can replay the accepted `project.delete` command; handle an already-deleted project as an idempotent replay (while still returning `ProjectNotFoundError` for a genuinely missing project).

Fixed in 87026840af41cd93d7e836d1204a284ca37eeac3. If deleteDetailed loses an overlapping deletion and returns typed ProjectNotFoundError, the MCP boundary reloads includeDeleted. Only an explicit deleted projection becomes alreadyDeleted:true with a per-attempt count of zero; genuine absence remains project_not_found and reload failures remain finite operation errors. The gated MCP regression proves one fresh count and one already-deleted result.

……@@ -163,49 +180,144 @@ export const make = Effect.gen(function* () {
233+ maxOutputBytes: 64 * 1024,
234+ })
235+ .pipe(
236+ Effect.map((result) => new Set(result.stdout.split("\n").map((line) => line.trim()))),
MmacroscopeappBoton line 236Sep 5, 2026

🟡 Medium sourceControl/SourceControlRepositoryService.ts:236

maxOutputBytes: 64 * 1024 truncates for-each-ref output for repositories with roughly 1,600 or more origin branches, so remoteCommits.has(headCommit) returns false when the matching ref falls beyond the retained prefix and a completed clone is rejected on retry. Remove this fixed truncation or verify the commit without relying on a capped list of refs.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/sourceControl/SourceControlRepositoryService.ts around line 236:
2
3`maxOutputBytes: 64 * 1024` truncates `for-each-ref` output for repositories with roughly 1,600 or more `origin` branches, so `remoteCommits.has(headCommit)` returns `false` when the matching ref falls beyond the retained prefix and a completed clone is rejected on retry. Remove this fixed truncation or verify the commit without relying on a capped list of refs.

Fixed in 87026840af41cd93d7e836d1204a284ca37eeac3. Recovery now asks Git for at most one origin ref pointing at the checked-out HEAD and requires the returned OID to match exactly, using a 1 KiB cap. The real-Git regression creates 1,700 unrelated origin refs and a lexically last matching ref, proves the former 64 KiB inventory overflows, then verifies conservative recovery succeeds without fetch or reachability expansion.

apps/server/src/mcp/OrchestratorMcpService.tsline 1454Outdated · 001fdc8Open thread
……@@ -1423,32 +1424,59 @@ const make = Effect.gen(function* () {
1451+ Effect.gen(function* () {
1452+ if (Option.isNone(receipt)) {
1453+ const freshParent = yield* loadProjection(scope.threadId);
1454+ const freshParentRun = latestActiveRun(freshParent);
MmacroscopeappBoton line 1454Sep 5, 2026

🟡 Medium mcp/OrchestratorMcpService.ts:1454

A thread created after waiting for the project lock can be recorded against the completed parentRun.id and parentNodeId, so the active run's timeline does not show the new thread. The admission recheck validates a fresh active run, but thread.created.record later uses the stale values captured before the lock; use the run validated at admission (or revalidate immediately before recording) for the parent linkage.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/mcp/OrchestratorMcpService.ts around line 1454:
2
3A thread created after waiting for the project lock can be recorded against the completed `parentRun.id` and `parentNodeId`, so the active run's timeline does not show the new thread. The admission recheck validates a fresh active run, but `thread.created.record` later uses the stale values captured before the lock; use the run validated at admission (or revalidate immediately before recording) for the parent linkage.
……@@ -163,49 +180,144 @@ export const make = Effect.gen(function* () {
222+ Effect.map((result) => result.stdout.trim() || null),
223+ Effect.orElseSucceed(() => null),
224+ );
225+ if (headCommit === null) return false;
MmacroscopeappBoton line 225Sep 5, 2026

🟡 Medium sourceControl/SourceControlRepositoryService.ts:225

isMatchingClone returns false for a successfully cloned empty repository, so retrying an empty remote reports the occupied destination as unrelated. The early headCommit === null return rejects unborn HEAD before the clone's clean checkout can be validated; defer commit/ref checks and allow a clean repository with no remote refs.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
text
1In file @apps/server/src/sourceControl/SourceControlRepositoryService.ts around line 225:
2
3`isMatchingClone` returns `false` for a successfully cloned empty repository, so retrying an empty remote reports the occupied destination as unrelated. The early `headCommit === null` return rejects unborn `HEAD` before the clone's clean checkout can be validated; defer commit/ref checks and allow a clean repository with no remote refs.
CcursorBotreviewed · 1 inline threadSep 5, 2026
Synced from GitHub

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but a cloud agent failed to start.

Reviewed by Cursor Bugbot for commit 001fdc890fc460492bed5aec2eae18f5b8d72d8f. Configure here.

apps/server/src/mcp/ProjectMcpService.tsline 427Outdated · 001fdc8Open thread
……@@ -0,0 +1,453 @@
424+ : error._tag === "ProjectNotFoundError"
425+ ? failure("project_not_found", `Project '${input.projectId}' was not found.`)
426+ : failure("operation_failed", "Unable to delete the requested project."),
427+ ),
CcursorBoton line 427Sep 5, 2026

Delete retry can miss alreadyDeleted

Low Severity

The alreadyDeleted short-circuit reads the project before the mutation lock, while a concurrent deleteDetailed that then sees deletedAt is mapped to project_not_found. Overlapping deletes or a retry that races the first commit can return project_not_found instead of alreadyDeleted: true.

Additional Locations (1)

Reviewed by Cursor Bugbot for commit 001fdc890fc460492bed5aec2eae18f5b8d72d8f. Configure here.

Fixed in 87026840af41cd93d7e836d1204a284ca37eeac3 via the same typed overlap recovery. The losing delete confirms an explicit deleted projection and returns alreadyDeleted:true/deletedThreadCount:0; it does not blanket-convert ProjectNotFoundError, so a genuinely absent project still returns project_not_found.

Synced from GitHub

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Superseded as the active MCP proposal by #10563 in the rebuilt MCP stack, following the maintainer request to review thin service wrappers separately from service hardening.

This is not a full feature-parity replacement. Mutations require a full-access/default caller. Clone and registration are separate operations; no lost-result adoption, journal or custom cascade/count implementation. Existing service validation and deletion behavior remain authoritative.

Closing this mixed proposal without merging. The remote branch agents/mcp-projects/management and exact head e3f2cdaae2c69d7d84ee05baabec32b1b26c4b10 are retained so the service changes can be extracted for separate review later.