Skip to content

Commit d610013

Browse files
fix: return explicit error when ref-based MCP resolves to remote server 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 #2461 Assisted-By: docker-agent
1 parent 293e359 commit d610013

4 files changed

Lines changed: 108 additions & 4 deletions

File tree

‎pkg/gateway/testing.go‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package gateway
2+
3+
// OverrideCatalogForTesting replaces the catalog loader with a fixed function
4+
// that returns the given catalog. It should only be called from TestMain or
5+
// equivalent test-setup code, before any call to ServerSpec.
6+
func OverrideCatalogForTesting(catalog Catalog) {
7+
catalogOnce = func() (Catalog, error) {
8+
return catalog, nil
9+
}
10+
}

‎pkg/teamloader/main_test.go‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package teamloader
2+
3+
import (
4+
"os"
5+
"testing"
6+
7+
"github.com/docker/docker-agent/pkg/gateway"
8+
)
9+
10+
// TestMain seeds a fake MCP catalog so that teamloader tests that invoke
11+
// createMCPTool with a ref can run without a live network call.
12+
func TestMain(m *testing.M) {
13+
gateway.OverrideCatalogForTesting(gateway.Catalog{
14+
// A local (subprocess-based) server entry.
15+
"local-server": {
16+
Type: "server",
17+
},
18+
// A remote (no subprocess) server entry — used to test that
19+
// working_dir is rejected at runtime for ref-based remote MCPs.
20+
"remote-server": {
21+
Type: "remote",
22+
Remote: gateway.Remote{
23+
URL: "https://mcp.example.com/sse",
24+
TransportType: "sse",
25+
},
26+
},
27+
})
28+
os.Exit(m.Run())
29+
}

‎pkg/teamloader/registry.go‎

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,18 @@ func createMCPTool(ctx context.Context, toolset latest.Toolset, _ string, runCon
298298
envProvider := runConfig.EnvProvider()
299299

300300
// Resolve the working directory once; used for all subprocess-based branches.
301-
// The remote branch never reaches here because working_dir is rejected by
302-
// validation for toolsets with a remote.url.
301+
// Note: validation only rejects working_dir for toolsets with an explicit
302+
// remote.url. Ref-based MCPs (e.g. ref: docker:context7) pass validation
303+
// regardless, because their transport type is only known at runtime via the
304+
// MCP Catalog API. If such a ref resolves to a remote server at runtime, we
305+
// return an explicit error below rather than silently discarding the field.
303306
cwd := resolveToolsetWorkingDir(toolset.WorkingDir, runConfig.WorkingDir)
304307

305308
// S1: validate the resolved directory exists (if one was specified) so we
306309
// surface a clear error now rather than a cryptic exec failure later.
307-
if toolset.WorkingDir != "" {
310+
// Skip this check for ref-based toolsets whose transport type is not yet
311+
// known — the check would be premature and potentially wrong.
312+
if toolset.WorkingDir != "" && toolset.Ref == "" {
308313
if err := checkDirExists(cwd, "mcp"); err != nil {
309314
return nil, err
310315
}
@@ -321,9 +326,23 @@ func createMCPTool(ctx context.Context, toolset latest.Toolset, _ string, runCon
321326

322327
// TODO(dga): until the MCP Gateway supports oauth with docker agent, we fetch the remote url and directly connect to it.
323328
if serverSpec.Type == "remote" {
329+
// working_dir cannot be validated at config-parse time for ref-based
330+
// MCPs because their transport type is only known here. Return a clear
331+
// error rather than silently discarding the field.
332+
if toolset.WorkingDir != "" {
333+
return nil, fmt.Errorf("working_dir is not supported for MCP toolset %q: ref %q resolves to a remote server (no local subprocess)",
334+
toolset.Name, toolset.Ref)
335+
}
324336
return mcp.NewRemoteToolset(toolset.Name, serverSpec.Remote.URL, serverSpec.Remote.TransportType, nil, nil), nil
325337
}
326338

339+
// The ref resolves to a local subprocess — validate the working directory now.
340+
if toolset.WorkingDir != "" {
341+
if err := checkDirExists(cwd, "mcp"); err != nil {
342+
return nil, err
343+
}
344+
}
345+
327346
env, err := environment.ExpandAll(ctx, environment.ToValues(toolset.Env), envProvider)
328347
if err != nil {
329348
return nil, fmt.Errorf("failed to expand the tool's environment variables: %w", err)
@@ -361,7 +380,9 @@ func createMCPTool(ctx context.Context, toolset latest.Toolset, _ string, runCon
361380

362381
return mcp.NewToolsetCommand(toolset.Name, resolvedCommand, toolset.Args, env, cwd), nil
363382

364-
// Remote MCP Server — working_dir is rejected at validation time for this branch.
383+
// Remote MCP Server — working_dir is rejected at validation time for this
384+
// branch (explicit remote.url in config). Ref-based MCPs that resolve to
385+
// remote at runtime are handled with an explicit error in the Ref branch above.
365386
case toolset.Remote.URL != "":
366387
expander := js.NewJsExpander(envProvider)
367388

‎pkg/teamloader/registry_test.go‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,47 @@ func TestCreateLSPTool_WorkingDir_ReachesHandler(t *testing.T) {
273273
require.True(t, ok, "expected *builtin.LSPTool")
274274
assert.Equal(t, customDir, lsp.WorkingDir())
275275
}
276+
277+
// TestCreateMCPTool_RefRemote_WorkingDir_ReturnsError verifies that when a
278+
// ref-based MCP resolves to a remote server at runtime, setting working_dir
279+
// returns a clear error rather than silently discarding the field.
280+
func TestCreateMCPTool_RefRemote_WorkingDir_ReturnsError(t *testing.T) {
281+
// The "docker:remote-server" ref is seeded as type "remote" in TestMain.
282+
toolset := latest.Toolset{
283+
Type: "mcp",
284+
Ref: "docker:remote-server",
285+
WorkingDir: "./workspace",
286+
}
287+
288+
registry := NewDefaultToolsetRegistry()
289+
runConfig := &config.RuntimeConfig{
290+
Config: config.Config{WorkingDir: t.TempDir()},
291+
EnvProviderForTests: environment.NewOsEnvProvider(),
292+
}
293+
294+
_, err := registry.CreateTool(t.Context(), toolset, ".", runConfig, "test-agent")
295+
require.Error(t, err)
296+
assert.Contains(t, err.Error(), "working_dir is not supported")
297+
assert.Contains(t, err.Error(), "remote server")
298+
}
299+
300+
// TestCreateMCPTool_RefRemote_NoWorkingDir_Succeeds verifies that a ref-based
301+
// MCP that resolves to a remote server still works fine when working_dir is
302+
// not set (the common case — regression guard).
303+
func TestCreateMCPTool_RefRemote_NoWorkingDir_Succeeds(t *testing.T) {
304+
// The "docker:remote-server" ref is seeded as type "remote" in TestMain.
305+
toolset := latest.Toolset{
306+
Type: "mcp",
307+
Ref: "docker:remote-server",
308+
}
309+
310+
registry := NewDefaultToolsetRegistry()
311+
runConfig := &config.RuntimeConfig{
312+
Config: config.Config{WorkingDir: t.TempDir()},
313+
EnvProviderForTests: environment.NewOsEnvProvider(),
314+
}
315+
316+
tool, err := registry.CreateTool(t.Context(), toolset, ".", runConfig, "test-agent")
317+
require.NoError(t, err)
318+
require.NotNil(t, tool)
319+
}

0 commit comments

Comments
 (0)