Skip to content

Commit edb83f5

Browse files
JAORMXclaude
andcommitted
Warn instead of failing when MCP services are unavailable
When ToolHive is not running, the MCP provider returns "no available runtime found" which previously caused a fatal error. Now we log a warning and continue without MCP support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2c3d946 commit edb83f5

2 files changed

Lines changed: 106 additions & 8 deletions

File tree

pkg/sandbox/sandbox.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -369,15 +369,16 @@ func (s *SandboxRunner) Prepare(ctx context.Context, agentName string, opts RunO
369369
s.observer.Start(progress.PhaseConfiguringMCP, "Discovering MCP servers...")
370370
services, mcpErr := s.mcpProvider.Services(ctx)
371371
if mcpErr != nil {
372-
s.observer.Fail("Failed to configure MCP")
373-
return nil, fmt.Errorf("configuring MCP services: %w", mcpErr)
374-
}
375-
for _, svc := range services {
376-
hostServices = append(hostServices, domvm.HostService{
377-
Name: svc.Name, Port: svc.Port, Handler: svc.Handler,
378-
})
372+
s.observer.Warn("MCP unavailable, continuing without MCP support")
373+
s.logger.Warn("failed to configure MCP services", "error", mcpErr)
374+
} else {
375+
for _, svc := range services {
376+
hostServices = append(hostServices, domvm.HostService{
377+
Name: svc.Name, Port: svc.Port, Handler: svc.Handler,
378+
})
379+
}
380+
s.observer.Complete(fmt.Sprintf("MCP proxy ready on port %d", mcpCfg.Port))
379381
}
380-
s.observer.Complete(fmt.Sprintf("MCP proxy ready on port %d", mcpCfg.Port))
381382
}
382383

383384
// 5. Set up workspace path (possibly with snapshot isolation).

pkg/sandbox/sandbox_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"fmt"
1212
"io"
1313
"log/slog"
14+
"net/http"
1415
"os"
1516
"strings"
1617
"testing"
@@ -21,6 +22,7 @@ import (
2122

2223
"github.com/stacklok/brood-box/pkg/domain/agent"
2324
"github.com/stacklok/brood-box/pkg/domain/egress"
25+
"github.com/stacklok/brood-box/pkg/domain/hostservice"
2426
"github.com/stacklok/brood-box/pkg/domain/session"
2527
"github.com/stacklok/brood-box/pkg/domain/snapshot"
2628
domvm "github.com/stacklok/brood-box/pkg/domain/vm"
@@ -175,6 +177,20 @@ func (m *mockFlusher) Flush(_, _ string, accepted []snapshot.FileChange) error {
175177
return m.flushErr
176178
}
177179

180+
// mockMCPProvider implements hostservice.Provider for testing.
181+
type mockMCPProvider struct {
182+
services []hostservice.Service
183+
err error
184+
called bool
185+
}
186+
187+
func (m *mockMCPProvider) Services(_ context.Context) ([]hostservice.Service, error) {
188+
m.called = true
189+
return m.services, m.err
190+
}
191+
192+
func (m *mockMCPProvider) Close() error { return nil }
193+
178194
func testLogger() *slog.Logger {
179195
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
180196
}
@@ -1227,3 +1243,84 @@ func TestSandboxRunner_Prepare_InvalidSessionID(t *testing.T) {
12271243
})
12281244
}
12291245
}
1246+
1247+
func TestSandboxRunner_Prepare_MCPFailure_WarnsAndContinues(t *testing.T) {
1248+
t.Parallel()
1249+
1250+
testAgent := agent.Agent{
1251+
Name: "test",
1252+
Image: "img:latest",
1253+
Command: []string{"cmd"},
1254+
DefaultCPUs: 2,
1255+
DefaultMemory: 2048,
1256+
}
1257+
1258+
mvm := &mockVM{sshPort: 2222, sshKeyPath: "/tmp/key"}
1259+
mcpProvider := &mockMCPProvider{
1260+
err: fmt.Errorf("no available runtime found"),
1261+
}
1262+
1263+
runner := NewSandboxRunner(SandboxDeps{
1264+
Registry: &mockRegistry{agents: map[string]agent.Agent{"test": testAgent}},
1265+
VMRunner: &mockVMRunner{vm: mvm},
1266+
SessionRunner: &mockSessionRunner{},
1267+
Config: &SandboxConfig{},
1268+
EnvProvider: &mockEnvProvider{},
1269+
Logger: testLogger(),
1270+
MCPProvider: mcpProvider,
1271+
})
1272+
1273+
sb, err := runner.Prepare(t.Context(), "test", RunOpts{
1274+
Workspace: "/tmp/workspace",
1275+
EgressProfile: string(egress.ProfilePermissive),
1276+
SessionID: "abcd1234",
1277+
})
1278+
require.NoError(t, err, "MCP failure should not be fatal")
1279+
defer func() { _ = sb.Cleanup() }()
1280+
1281+
assert.True(t, mcpProvider.called, "MCP provider should have been called")
1282+
assert.Empty(t, sb.VMConfig.HostServices, "no host services should be configured on MCP failure")
1283+
}
1284+
1285+
func TestSandboxRunner_Prepare_MCPSuccess_AddsHostServices(t *testing.T) {
1286+
t.Parallel()
1287+
1288+
testAgent := agent.Agent{
1289+
Name: "test",
1290+
Image: "img:latest",
1291+
Command: []string{"cmd"},
1292+
DefaultCPUs: 2,
1293+
DefaultMemory: 2048,
1294+
}
1295+
1296+
handler := http.NewServeMux()
1297+
mvm := &mockVM{sshPort: 2222, sshKeyPath: "/tmp/key"}
1298+
mcpProvider := &mockMCPProvider{
1299+
services: []hostservice.Service{
1300+
{Name: "mcp", Port: 4483, Handler: handler},
1301+
},
1302+
}
1303+
1304+
runner := NewSandboxRunner(SandboxDeps{
1305+
Registry: &mockRegistry{agents: map[string]agent.Agent{"test": testAgent}},
1306+
VMRunner: &mockVMRunner{vm: mvm},
1307+
SessionRunner: &mockSessionRunner{},
1308+
Config: &SandboxConfig{},
1309+
EnvProvider: &mockEnvProvider{},
1310+
Logger: testLogger(),
1311+
MCPProvider: mcpProvider,
1312+
})
1313+
1314+
sb, err := runner.Prepare(t.Context(), "test", RunOpts{
1315+
Workspace: "/tmp/workspace",
1316+
EgressProfile: string(egress.ProfilePermissive),
1317+
SessionID: "abcd1234",
1318+
})
1319+
require.NoError(t, err)
1320+
defer func() { _ = sb.Cleanup() }()
1321+
1322+
assert.True(t, mcpProvider.called)
1323+
require.Len(t, sb.VMConfig.HostServices, 1)
1324+
assert.Equal(t, "mcp", sb.VMConfig.HostServices[0].Name)
1325+
assert.Equal(t, uint16(4483), sb.VMConfig.HostServices[0].Port)
1326+
}

0 commit comments

Comments
 (0)