Skip to content

feat: add optional working_dir to MCP and LSP toolset configs#2460

Merged
simonferquel merged 5 commits intodocker:mainfrom
simonferquel-clanker:feat/toolset-working-dir
Apr 20, 2026
Merged

feat: add optional working_dir to MCP and LSP toolset configs#2460
simonferquel merged 5 commits intodocker:mainfrom
simonferquel-clanker:feat/toolset-working-dir

Conversation

@simonferquel-clanker
Copy link
Copy Markdown
Contributor

Summary

Adds an optional working_dir field to MCP and LSP toolset configurations, allowing these processes to be launched from a directory other than the agent's working directory.

Closes #2459

Changes

  • pkg/config/latest/types.go — New WorkingDir field on Toolset struct
  • pkg/config/latest/validate.go — Validation: working_dir only valid for types mcp and lsp
  • pkg/config/mcps.go — Propagate working_dir from top-level mcps: definitions to referencing agent toolsets (existing toolset value takes precedence)
  • pkg/teamloader/registry.goresolveToolsetWorkingDir helper; createMCPTool and createLSPTool resolve and pass the working directory to the process
  • agent-schema.jsonworking_dir added to Toolset and MCPToolset schema definitions
  • Tests — validate_test.go, mcps_test.go, registry_test.go coverage for the new field
  • examples/toolset-working-dir.yaml — New example demonstrating the feature

Behaviour

  • working_dir is optional; omitting it keeps existing behaviour (process starts from agent's working directory)
  • Relative paths are resolved relative to the agent's working directory
  • Absolute paths are used as-is
  • When using the top-level mcps: section, working_dir is inherited by referencing toolsets; a toolset-level value overrides the definition

@krissetto
Copy link
Copy Markdown
Contributor

/review

Copy link
Copy Markdown

@docker-agent docker-agent bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assessment: 🟡 NEEDS ATTENTION

Note: Automated verification returned inconclusive results. Findings are source-confirmed by manual code inspection.

Comment thread pkg/teamloader/registry.go Outdated

// Resolve the working directory once; used for all subprocess-based branches.
// The remote branch never reaches here because working_dir is rejected by
// validation for toolsets with a remote.url.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: working_dir silently ignored when a ref: MCP catalog entry resolves to a remote server

The comment on this line is incorrect: the "remote branch" can be reached for ref:-based MCPs whose catalog entry resolves to serverSpec.Type == "remote" at runtime. In that case, createMCPTool returns early via mcp.NewRemoteToolset(...) (see line 323) without passing cwd:

if serverSpec.Type == "remote" {
    return mcp.NewRemoteToolset(toolset.Name, serverSpec.Remote.URL, ..., nil, nil), nil
    // ^ cwd is never passed — working_dir is silently discarded
}

The validation in validate.go only blocks working_dir when Remote.URL is set directly in the config; it does not block ref:-based toolsets. A user who writes:

toolsets:
  - type: mcp
    ref: docker:some-remote-server
    working_dir: ./workspace

will:

  1. Pass config validation ✓
  2. Have their directory existence-checked at startup (unnecessary work)
  3. Have working_dir silently discarded at runtime

Fix options:

  • Update the comment to accurately state the invariant (the remote Ref path is NOT blocked by validation)
  • Optionally add a warning or error when working_dir is set but the resolved gateway spec is remote

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit af057f34.

The misleading comment ("The remote branch never reaches here because working_dir is rejected by validation for toolsets with a remote.url") has been removed and replaced with an accurate one explaining the limitation: validation only blocks explicit remote.url toolsets; ref-based MCPs are not covered at parse time.

The runtime fix is in the serverSpec.Type == "remote" early-return inside the Ref branch:

if serverSpec.Type == "remote" {
    if toolset.WorkingDir != "" {
        return nil, fmt.Errorf("working_dir is not supported for MCP toolset %q: ref %q resolves to a remote server (no local subprocess)",
            toolset.Name, toolset.Ref)
    }
    return mcp.NewRemoteToolset(...), nil
}

The unnecessary checkDirExists call for ref-based toolsets is also deferred to after the server spec is resolved, so it only runs for subprocess-based refs.

return errors.New("working_dir can only be used with type 'mcp' or 'lsp'")
}
// working_dir requires a local subprocess; it is meaningless for remote MCP toolsets.
if t.WorkingDir != "" && t.Type == "mcp" && t.Remote.URL != "" {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MEDIUM: Validation gap — working_dir not rejected for ref:-based MCPs that may resolve to remote at runtime

This check only blocks working_dir when Remote.URL is set explicitly:

if t.WorkingDir != "" && t.Type == "mcp" && t.Remote.URL != "" {
    return errors.New("working_dir is not valid for remote MCP toolsets (no local subprocess)")
}

ref:-based MCPs (e.g. ref: docker:context7) are not covered. Their serverSpec.Type is only known at runtime (via the MCP Catalog API), so if the catalog entry resolves to a remote server, working_dir is silently dropped with no user-visible error.

The companion misleading comment in registry.go ("The remote branch never reaches here because working_dir is rejected by validation for toolsets with a remote.url") reinforces this false assumption.

Suggested fix: At minimum, update the comment in registry.go to accurately describe that only explicit-remote.url toolsets are guarded at validation time, not Ref-based ones. Optionally emit a warning when a Ref-based MCP with working_dir resolves to remote at runtime.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit af057f34.

  • Updated the comment on this validation rule to clarify it only blocks explicit remote.url toolsets; ref-based MCPs are not covered at parse time because their transport type is only known at runtime.
  • Added a runtime check in the serverSpec.Type == "remote" branch inside createMCPTool: if toolset.WorkingDir != "", an explicit error is returned ("working_dir is not supported for MCP toolset ... ref ... resolves to a remote server") instead of silently discarding the field.
  • Also deferred the checkDirExists call for ref-based toolsets so it only runs after the server spec is resolved (avoids unnecessary work when the ref turns out to be remote).
  • Added gateway.OverrideCatalogForTesting helper + TestMain in pkg/teamloader to enable unit tests without network, plus TestCreateMCPTool_RefRemote_WorkingDir_ReturnsError and TestCreateMCPTool_RefRemote_NoWorkingDir_Succeeds.

krissetto
krissetto previously approved these changes Apr 20, 2026
Copy link
Copy Markdown
Contributor

@krissetto krissetto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall lgtm, we should also updated the docs around this though

@simonferquel
Copy link
Copy Markdown
Contributor

doc updated

- Add WorkingDir field to Toolset struct (pkg/config/latest/types.go)
- Validate that working_dir is only used with type 'mcp' or 'lsp'
- Resolve working_dir relative to agent's working directory at process start
- Propagate working_dir from top-level mcps: definitions to agent toolsets
- Add resolveToolsetWorkingDir helper in registry.go
- Add tests: validate_test.go, mcps_test.go, registry_test.go
- Add example: examples/toolset-working-dir.yaml
- Update agent-schema.json for Toolset and MCPToolset

Closes docker#2459

Assisted-By: docker-agent
B1: pass resolved CWD to NewGatewayToolset; gateway-based MCPs now
honour working_dir. Remote MCP toolsets reject working_dir at
validation time (no local subprocess).

B2: call path.ExpandPath in resolveToolsetWorkingDir so ~ and
${VAR}/$VAR are expanded before path operations. Use filepath.Abs
when joining a relative path with the agent dir (fixes file://./backend
LSP root URI).

S1: add checkDirExists helper; createMCPTool and createLSPTool now
surface a clear error if working_dir does not exist at tool-creation
time.

S2: doc comment on resolveToolsetWorkingDir explains no-containment-
check posture (working_dir is treated like command/args).

S3: filepath.Abs on relative+non-empty-agentDir path ensures LSP
rootURI is always absolute.

S4: validation rejects working_dir on remote MCP toolsets.

S5: comment in applyMCPDefaults explains empty-string inheritance
semantics.

S6: doc comment covers the empty-agent-dir+relative edge case.

Nits: example model → gpt-5-mini; test name → 'bare relative dir';
schema descriptions aligned; import groups tidied; hard-coded
non-existent path in test → t.TempDir()+'/missing'.

N5: integration tests added:
- TestCreateMCPTool_WorkingDir_ReachesSubprocess
- TestCreateMCPTool_RelativeWorkingDir_ResolvedAgainstAgentDir
- TestCreateMCPTool_NonexistentWorkingDir_ReturnsError
- TestCreateLSPTool_WorkingDir_ReachesHandler

Also add WorkingDir() accessors on *mcp.Toolset and *builtin.LSPTool
to support the above integration tests.

Assisted-By: docker-agent
…epathJoin lint rule

Assisted-By: docker-agent
…er with working_dir

When a toolset uses ref: to point to a catalog entry that resolves to a
remote server at runtime, working_dir cannot be validated at config-parse
time (the transport type is only known after the catalog API call). The
previous code silently discarded working_dir in this case.

- Fix the misleading comment in createMCPTool that claimed the remote
  branch was unreachable due to validation; ref-based MCPs are not
  blocked at validation time.
- Defer the checkDirExists call for ref-based toolsets until after the
  server spec is resolved (avoids unnecessary work for remote refs).
- Return a clear error when serverSpec.Type == "remote" and
  working_dir is set, instead of silently discarding the field.
- Update the Remote.URL branch comment to accurately describe what it
  does and does not cover.
- Add gateway.OverrideCatalogForTesting helper so teamloader tests can
  seed a fake catalog without a live network call.
- Add TestMain in pkg/teamloader that seeds remote and local catalog
  entries for use by tests.
- Add TestCreateMCPTool_RefRemote_WorkingDir_ReturnsError and
  TestCreateMCPTool_RefRemote_NoWorkingDir_Succeeds.

Closes docker#2461

Assisted-By: docker-agent
- docs/tools/lsp/index.md: add working_dir to Properties table;
  extend the Go/gopls example to show the monorepo subdirectory use-case
- docs/configuration/tools/index.md: add working_dir to both the
  Local MCP (stdio) and Docker MCP (ref) property tables
- CHANGELOG.md: add Unreleased section noting the new field

Assisted-By: docker-agent
Copy link
Copy Markdown
Contributor

@simonferquel simonferquel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-approving: force pushed was only a doc updated after a rebase

@simonferquel simonferquel merged commit 15945f4 into docker:main Apr 20, 2026
5 checks passed
simonferquel-clanker added a commit to simonferquel-clanker/cagent that referenced this pull request Apr 20, 2026
PR docker#2460 adds native working_dir support to LSP/MCP toolset configs,
making the env -C ./backend gopls wrapper pattern unnecessary. Remove
all code that was introduced to handle that pattern:

- HealthChecker interface and StartableToolSet.Start() health-check block
- LSPTool.Healthy(), watchExit goroutine, dead atomic.Bool, exited chan
- stopAndWait reverted to stopLocked with direct cmd.Wait()
- Toolset.Healthy() (MCP) — no longer needed without HealthChecker
- _ tools.HealthChecker interface assertions
- startable_test.go: healthyStub type + TestStartableToolSet_HealthChecker_* tests
- lsp_test.go: TestLSPTool_Healthy_* + TestLSPTool_StartableToolSet_* tests

What is retained:
- MCP watcherAlive flag (B1 fix: prevents double watcher goroutine on reprobe)
- reprobe() loop in runtime (fires after each tool-call batch)
- MCPInitStarted/Finished gating in reprobe path (S3)
- ShouldReportFailure / ConsumeRecovery deduplication
- NoticeEvent + recovery notices
- All existing reprobe and agent-level tests

Assisted-By: docker-agent
simonferquel-clanker added a commit to simonferquel-clanker/cagent that referenced this pull request Apr 20, 2026
Both files are fully reverted — no changes from this PR remain in
pkg/tools/builtin/. The LSP-side modifications (WorkingDir() accessor,
import/variable renames in tests) were introduced for the env-C wrapper
pattern which is moot with PR docker#2460.

Assisted-By: docker-agent
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.

feat: add optional working_dir to MCP and LSP toolset configs

3 participants