refactor(mcp): align AX DTO defaults and IDE config
Normalize the MCP constructor workspace defaulting to use the actual working directory, and replace the IDE subsystem's functional options with a Config DTO so the codebase stays aligned with AX-style configuration objects. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
5177dc391b
commit
985bc2017f
6 changed files with 61 additions and 66 deletions
2
go.sum
2
go.sum
|
|
@ -1,5 +1,3 @@
|
|||
dappco.re/go/core v0.4.7 h1:KmIA/2lo6rl1NMtLrKqCWfMlUqpDZYH3q0/d10dTtGA=
|
||||
dappco.re/go/core v0.4.7/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk=
|
||||
dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A=
|
||||
forge.lthn.ai/core/api v0.1.5 h1:NwZrcOyBjaiz5/cn0n0tnlMUodi8Or6FHMx59C7Kv2o=
|
||||
|
|
|
|||
|
|
@ -412,11 +412,10 @@ func TestBridge_Good_NoAuthHeaderWhenTokenEmpty(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBridge_Good_WithTokenOption(t *testing.T) {
|
||||
// Verify the WithToken option function works.
|
||||
func TestBridge_Good_ConfigToken(t *testing.T) {
|
||||
// Verify the Config DTO carries token settings through unchanged.
|
||||
cfg := DefaultConfig()
|
||||
opt := WithToken("my-token")
|
||||
opt(&cfg)
|
||||
cfg.Token = "my-token"
|
||||
|
||||
if cfg.Token != "my-token" {
|
||||
t.Errorf("expected token 'my-token', got %q", cfg.Token)
|
||||
|
|
@ -424,14 +423,14 @@ func TestBridge_Good_WithTokenOption(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestSubsystem_Good_Name(t *testing.T) {
|
||||
sub := New(nil)
|
||||
sub := New(nil, Config{})
|
||||
if sub.Name() != "ide" {
|
||||
t.Errorf("expected name 'ide', got %q", sub.Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubsystem_Good_NilHub(t *testing.T) {
|
||||
sub := New(nil)
|
||||
sub := New(nil, Config{})
|
||||
if sub.Bridge() != nil {
|
||||
t.Error("expected nil bridge when hub is nil")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,33 +25,22 @@ type Config struct {
|
|||
|
||||
// DefaultConfig returns sensible defaults for local development.
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
LaravelWSURL: "ws://localhost:9876/ws",
|
||||
WorkspaceRoot: ".",
|
||||
ReconnectInterval: 2 * time.Second,
|
||||
MaxReconnectInterval: 30 * time.Second,
|
||||
}
|
||||
return Config{}.WithDefaults()
|
||||
}
|
||||
|
||||
// Option configures the IDE subsystem.
|
||||
type Option func(*Config)
|
||||
|
||||
// WithLaravelURL sets the Laravel WebSocket endpoint.
|
||||
func WithLaravelURL(url string) Option {
|
||||
return func(c *Config) { c.LaravelWSURL = url }
|
||||
// WithDefaults fills unset fields with the default development values.
|
||||
func (c Config) WithDefaults() Config {
|
||||
if c.LaravelWSURL == "" {
|
||||
c.LaravelWSURL = "ws://localhost:9876/ws"
|
||||
}
|
||||
|
||||
// WithWorkspaceRoot sets the workspace root directory.
|
||||
func WithWorkspaceRoot(root string) Option {
|
||||
return func(c *Config) { c.WorkspaceRoot = root }
|
||||
if c.WorkspaceRoot == "" {
|
||||
c.WorkspaceRoot = "."
|
||||
}
|
||||
|
||||
// WithReconnectInterval sets the base reconnect interval.
|
||||
func WithReconnectInterval(d time.Duration) Option {
|
||||
return func(c *Config) { c.ReconnectInterval = d }
|
||||
if c.ReconnectInterval == 0 {
|
||||
c.ReconnectInterval = 2 * time.Second
|
||||
}
|
||||
|
||||
// WithToken sets the Bearer token for WebSocket authentication.
|
||||
func WithToken(token string) Option {
|
||||
return func(c *Config) { c.Token = token }
|
||||
if c.MaxReconnectInterval == 0 {
|
||||
c.MaxReconnectInterval = 30 * time.Second
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,13 +19,12 @@ type Subsystem struct {
|
|||
hub *ws.Hub
|
||||
}
|
||||
|
||||
// New creates an IDE subsystem. The ws.Hub is used for real-time forwarding;
|
||||
// pass nil if headless (tools still work but real-time streaming is disabled).
|
||||
func New(hub *ws.Hub, opts ...Option) *Subsystem {
|
||||
cfg := DefaultConfig()
|
||||
for _, opt := range opts {
|
||||
opt(&cfg)
|
||||
}
|
||||
// New creates an IDE subsystem from a Config DTO.
|
||||
//
|
||||
// The ws.Hub is used for real-time forwarding; pass nil if headless
|
||||
// (tools still work but real-time streaming is disabled).
|
||||
func New(hub *ws.Hub, cfg Config) *Subsystem {
|
||||
cfg = cfg.WithDefaults()
|
||||
var bridge *Bridge
|
||||
if hub != nil {
|
||||
bridge = NewBridge(hub, cfg)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
// newNilBridgeSubsystem returns a Subsystem with no hub/bridge (headless mode).
|
||||
func newNilBridgeSubsystem() *Subsystem {
|
||||
return New(nil)
|
||||
return New(nil, Config{})
|
||||
}
|
||||
|
||||
// newConnectedSubsystem returns a Subsystem with a connected bridge and a
|
||||
|
|
@ -42,10 +42,10 @@ func newConnectedSubsystem(t *testing.T) (*Subsystem, context.CancelFunc, *httpt
|
|||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go hub.Run(ctx)
|
||||
|
||||
sub := New(hub,
|
||||
WithLaravelURL(wsURL(ts)),
|
||||
WithReconnectInterval(50*time.Millisecond),
|
||||
)
|
||||
sub := New(hub, Config{
|
||||
LaravelWSURL: wsURL(ts),
|
||||
ReconnectInterval: 50 * time.Millisecond,
|
||||
})
|
||||
sub.StartBridge(ctx)
|
||||
|
||||
waitConnected(t, sub.Bridge(), 2*time.Second)
|
||||
|
|
@ -690,7 +690,7 @@ func TestSubsystem_Good_RegisterTools(t *testing.T) {
|
|||
// RegisterTools requires a real mcp.Server which is complex to construct
|
||||
// in isolation. This test verifies the Subsystem can be created and
|
||||
// the Bridge/Shutdown path works end-to-end.
|
||||
sub := New(nil)
|
||||
sub := New(nil, Config{})
|
||||
if sub.Bridge() != nil {
|
||||
t.Error("expected nil bridge with nil hub")
|
||||
}
|
||||
|
|
@ -701,20 +701,20 @@ func TestSubsystem_Good_RegisterTools(t *testing.T) {
|
|||
|
||||
// TestSubsystem_Good_StartBridgeNilHub verifies StartBridge is a no-op with nil hub.
|
||||
func TestSubsystem_Good_StartBridgeNilHub(t *testing.T) {
|
||||
sub := New(nil)
|
||||
sub := New(nil, Config{})
|
||||
// Should not panic
|
||||
sub.StartBridge(context.Background())
|
||||
}
|
||||
|
||||
// TestSubsystem_Good_WithOptions verifies all config options apply correctly.
|
||||
func TestSubsystem_Good_WithOptions(t *testing.T) {
|
||||
// TestSubsystem_Good_WithConfig verifies the Config DTO applies correctly.
|
||||
func TestSubsystem_Good_WithConfig(t *testing.T) {
|
||||
hub := ws.NewHub()
|
||||
sub := New(hub,
|
||||
WithLaravelURL("ws://custom:1234/ws"),
|
||||
WithWorkspaceRoot("/tmp/test"),
|
||||
WithReconnectInterval(5*time.Second),
|
||||
WithToken("secret-123"),
|
||||
)
|
||||
sub := New(hub, Config{
|
||||
LaravelWSURL: "ws://custom:1234/ws",
|
||||
WorkspaceRoot: "/tmp/test",
|
||||
ReconnectInterval: 5 * time.Second,
|
||||
Token: "secret-123",
|
||||
})
|
||||
|
||||
if sub.cfg.LaravelWSURL != "ws://custom:1234/ws" {
|
||||
t.Errorf("expected custom URL, got %q", sub.cfg.LaravelWSURL)
|
||||
|
|
@ -761,7 +761,10 @@ func TestChatSend_Good_BridgeMessageType(t *testing.T) {
|
|||
ctx := t.Context()
|
||||
go hub.Run(ctx)
|
||||
|
||||
sub := New(hub, WithLaravelURL(wsURL(ts)), WithReconnectInterval(50*time.Millisecond))
|
||||
sub := New(hub, Config{
|
||||
LaravelWSURL: wsURL(ts),
|
||||
ReconnectInterval: 50 * time.Millisecond,
|
||||
})
|
||||
sub.StartBridge(ctx)
|
||||
waitConnected(t, sub.Bridge(), 2*time.Second)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"iter"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
|
|
@ -93,10 +94,18 @@ func New(opts Options) (*Service, error) {
|
|||
} else {
|
||||
root := opts.WorkspaceRoot
|
||||
if root == "" {
|
||||
root = core.Env("DIR_CWD")
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, core.E("mcp.New", "failed to get working directory", err)
|
||||
}
|
||||
s.workspaceRoot = root
|
||||
m, merr := io.NewSandboxed(root)
|
||||
root = cwd
|
||||
}
|
||||
abs, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, core.E("mcp.New", "failed to resolve workspace root", err)
|
||||
}
|
||||
s.workspaceRoot = abs
|
||||
m, merr := io.NewSandboxed(abs)
|
||||
if merr != nil {
|
||||
return nil, core.E("mcp.New", "failed to create workspace medium", merr)
|
||||
}
|
||||
|
|
@ -177,7 +186,6 @@ func (s *Service) Shutdown(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
|
||||
// WSHub returns the WebSocket hub, or nil if not configured.
|
||||
//
|
||||
// if hub := svc.WSHub(); hub != nil {
|
||||
|
|
@ -668,7 +676,6 @@ func (s *Service) Run(ctx context.Context) error {
|
|||
})
|
||||
}
|
||||
|
||||
|
||||
// countOccurrences counts non-overlapping instances of substr in s.
|
||||
func countOccurrences(s, substr string) int {
|
||||
if substr == "" {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue