Skip to content

BACK-508 - Resolve MCP project root from client workspace roots#687

Merged
MrLesk merged 3 commits into
MrLesk:mainfrom
ycaptain:tasks/back-558-mcp-workspace-root
Jun 17, 2026
Merged

BACK-508 - Resolve MCP project root from client workspace roots#687
MrLesk merged 3 commits into
MrLesk:mainfrom
ycaptain:tasks/back-558-mcp-workspace-root

Conversation

@ycaptain

@ycaptain ycaptain commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Summary

The MCP server chose its project root once at startup and only looked at the client's MCP roots when it had no project of its own (fallback mode). A server shared across projects, or one started in the main checkout while the active workspace is a git worktree, therefore kept writing tasks to whatever directory it launched in (#558).

This makes an already-initialized server follow the client workspace as well, reusing the request-scoped roots discovery that #608 added for fallback mode. On its first request the server asks the client for its roots and switches to the matching project, then re-checks on every roots/list_changed. If the workspace has no backlog of its own, the server stays on its launch directory instead of dropping to "init required". Passing --cwd or BACKLOG_CWD pins the root and turns roots discovery off, which is what a deliberately shared server wants.

Behavior change

An initialized, unpinned server now sends one roots/list request on its first handled request, where before it sent none. If the client's root matches the launch directory, nothing is re-registered. Clients that do not advertise the roots capability keep the old process.cwd() behavior. A pinned server sends no roots requests at all.

Implementation notes

  • src/commands/mcp.ts marks the root as pinned when it comes from --cwd or BACKLOG_CWD, and unpinned when it is only process.cwd().
  • src/mcp/server.ts adds a startupHasProject flag so an initialized server returns to its own project when the client offers nothing usable, while a fallback server still reverts to init-required.
  • upgradeToProject, downgradeToFallback and resolveFromRoots are unchanged, as are the root checks from BACK-406 - Use MCP roots for project root discovery when cwd fails #570: each root is checked on its own, no walking up the tree, one resolution in flight at a time.
  • README now documents workspace following and the --cwd / BACKLOG_CWD pin.

Related Tasks

Closes #558. Task backlog/tasks/back-558 - Resolve-MCP-project-root-from-client-workspace-roots.md (BACK-558), acceptance criteria and DoD checked.

Validation

  • bun test src/test/mcp-workspace-root.test.ts src/test/mcp-roots-discovery.test.ts (15 pass): following the client workspace from an unrelated launch directory, a pin that sends zero roots requests, a monorepo package kept, the launch project kept when the workspace has no backlog, the return to the launch project after the workspace loses its backlog, and the worktree case from [Bug]: MCP server writes to main repo instead of git worktree #558.
  • bunx tsc --noEmit
  • bun run check .

@ycaptain ycaptain force-pushed the tasks/back-558-mcp-workspace-root branch from a75fa5d to e81a54a Compare June 14, 2026 10:39
@ycaptain ycaptain marked this pull request as ready for review June 14, 2026 10:45
@ycaptain ycaptain force-pushed the tasks/back-558-mcp-workspace-root branch 4 times, most recently from 26074a8 to 855c315 Compare June 15, 2026 16:58
The MCP server froze its project root at startup and only consulted client MCP
roots in fallback (uninitialized) mode, so a shared/user-scope server -- or one
launched in the main checkout while working in a git worktree -- wrote tasks to
the wrong backlog directory.

Extend the request-scoped roots discovery from MrLesk#608 (BACK-434) to the normal
(initialized) startup path: unless pinned, the server now follows the client's
workspace roots and re-resolves on roots/list_changed. A startupHasProject flag
makes a launch-directory project return to itself when the client workspace has
no backlog (rather than dropping to init-required); a fallback baseline still
reverts to init-required. An explicit --cwd/BACKLOG_CWD pin opts out of roots
entirely. The existing upgradeToProject/downgradeToFallback/resolveFromRoots and
the MrLesk#570 constraints are kept intact.

Behavior change: the normal/initialized path now issues one roots/list request to
follow the client workspace (MrLesk#608 issued none in normal mode); a pin opts out.

Closes MrLesk#558.
@ycaptain ycaptain force-pushed the tasks/back-558-mcp-workspace-root branch from 855c315 to 3b175ae Compare June 15, 2026 17:06
@MrLesk MrLesk changed the title BACK-558 - Resolve MCP project root from client workspace roots BACK-508 - Resolve MCP project root from client workspace roots Jun 17, 2026
@MrLesk

MrLesk commented Jun 17, 2026

Copy link
Copy Markdown
Owner

Thanks @ycaptain
This is a really nice fix

@MrLesk MrLesk merged commit 113df37 into MrLesk:main Jun 17, 2026
6 checks passed
kuwork added a commit to kuwork/Backlog.md that referenced this pull request Jun 23, 2026
…sk#687)

## Summary
The MCP server chose its project root once at startup and only looked at
the client's MCP roots when it had no project of its own (fallback
mode). A server shared across projects, or one started in the main
checkout while the active workspace is a git worktree, therefore kept
writing tasks to whatever directory it launched in (MrLesk#558).

This makes an already-initialized server follow the client workspace as
well, reusing the request-scoped roots discovery that MrLesk#608 added for
fallback mode. On its first request the server asks the client for its
roots and switches to the matching project, then re-checks on every
`roots/list_changed`. If the workspace has no backlog of its own, the
server stays on its launch directory instead of dropping to "init
required". Passing `--cwd` or `BACKLOG_CWD` pins the root and turns
roots discovery off, which is what a deliberately shared server wants.

## Behavior change
An initialized, unpinned server now sends one `roots/list` request on
its first handled request, where before it sent none. If the client's
root matches the launch directory, nothing is re-registered. Clients
that do not advertise the roots capability keep the old `process.cwd()`
behavior. A pinned server sends no roots requests at all.

## Implementation notes
* `src/commands/mcp.ts` marks the root as pinned when it comes from
`--cwd` or `BACKLOG_CWD`, and unpinned when it is only `process.cwd()`.
* `src/mcp/server.ts` adds a `startupHasProject` flag so an initialized
server returns to its own project when the client offers nothing usable,
while a fallback server still reverts to init-required.
* `upgradeToProject`, `downgradeToFallback` and `resolveFromRoots` are
unchanged, as are the root checks from MrLesk#570: each root is checked on its
own, no walking up the tree, one resolution in flight at a time.
* README now documents workspace following and the `--cwd` /
`BACKLOG_CWD` pin.

## Related Tasks
Closes MrLesk#558. Task `backlog/tasks/back-558 -
Resolve-MCP-project-root-from-client-workspace-roots.md` (BACK-558),
acceptance criteria and DoD checked.

## Validation
* `bun test src/test/mcp-workspace-root.test.ts
src/test/mcp-roots-discovery.test.ts` (15 pass): following the client
workspace from an unrelated launch directory, a pin that sends zero
roots requests, a monorepo package kept, the launch project kept when
the workspace has no backlog, the return to the launch project after the
workspace loses its backlog, and the worktree case from MrLesk#558.
* `bunx tsc --noEmit`
* `bun run check .`

---------

Co-authored-by: Alex Gavrilescu <leskcorp@gmail.com>
yaleh pushed a commit to yaleh/epicd that referenced this pull request Jul 7, 2026
…sk#687)

## Summary
The MCP server chose its project root once at startup and only looked at
the client's MCP roots when it had no project of its own (fallback
mode). A server shared across projects, or one started in the main
checkout while the active workspace is a git worktree, therefore kept
writing tasks to whatever directory it launched in (MrLesk#558).

This makes an already-initialized server follow the client workspace as
well, reusing the request-scoped roots discovery that MrLesk#608 added for
fallback mode. On its first request the server asks the client for its
roots and switches to the matching project, then re-checks on every
`roots/list_changed`. If the workspace has no backlog of its own, the
server stays on its launch directory instead of dropping to "init
required". Passing `--cwd` or `BACKLOG_CWD` pins the root and turns
roots discovery off, which is what a deliberately shared server wants.

## Behavior change
An initialized, unpinned server now sends one `roots/list` request on
its first handled request, where before it sent none. If the client's
root matches the launch directory, nothing is re-registered. Clients
that do not advertise the roots capability keep the old `process.cwd()`
behavior. A pinned server sends no roots requests at all.

## Implementation notes
* `src/commands/mcp.ts` marks the root as pinned when it comes from
`--cwd` or `BACKLOG_CWD`, and unpinned when it is only `process.cwd()`.
* `src/mcp/server.ts` adds a `startupHasProject` flag so an initialized
server returns to its own project when the client offers nothing usable,
while a fallback server still reverts to init-required.
* `upgradeToProject`, `downgradeToFallback` and `resolveFromRoots` are
unchanged, as are the root checks from MrLesk#570: each root is checked on its
own, no walking up the tree, one resolution in flight at a time.
* README now documents workspace following and the `--cwd` /
`BACKLOG_CWD` pin.

## Related Tasks
Closes MrLesk#558. Task `backlog/tasks/back-558 -
Resolve-MCP-project-root-from-client-workspace-roots.md` (BACK-558),
acceptance criteria and DoD checked.

## Validation
* `bun test src/test/mcp-workspace-root.test.ts
src/test/mcp-roots-discovery.test.ts` (15 pass): following the client
workspace from an unrelated launch directory, a pin that sends zero
roots requests, a monorepo package kept, the launch project kept when
the workspace has no backlog, the return to the launch project after the
workspace loses its backlog, and the worktree case from MrLesk#558.
* `bunx tsc --noEmit`
* `bun run check .`

---------

Co-authored-by: Alex Gavrilescu <leskcorp@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: MCP server writes to main repo instead of git worktree

2 participants