diff --git a/cmd/core-agent/commands.go b/cmd/core-agent/commands.go index 51b8a6f..736dfb5 100644 --- a/cmd/core-agent/commands.go +++ b/cmd/core-agent/commands.go @@ -8,6 +8,7 @@ import ( "dappco.re/go/agent/pkg/agentic" "dappco.re/go/core" + coremcp "forge.lthn.ai/core/mcp/pkg/mcp" ) type applicationCommandSet struct { @@ -83,6 +84,16 @@ func registerApplicationCommands(c *core.Core) { Description: "Show all core.Env() keys and values", Action: commands.env, }) + + c.Command("mcp", core.Command{ + Description: "Start the MCP server on stdio", + Action: commands.mcp, + }) + + c.Command("serve", core.Command{ + Description: "Start the MCP server over HTTP", + Action: commands.serve, + }) } func (commands applicationCommandSet) version(_ core.Options) core.Result { @@ -133,3 +144,57 @@ func (commands applicationCommandSet) env(_ core.Options) core.Result { } return core.Result{OK: true} } + +func (commands applicationCommandSet) mcp(_ core.Options) core.Result { + service, err := commands.mcpService() + if err != nil { + return core.Result{Value: err, OK: false} + } + if err := service.ServeStdio(commands.coreApp.Context()); err != nil { + return core.Result{Value: core.E("main.mcp", "serve mcp stdio", err), OK: false} + } + return core.Result{OK: true} +} + +func (commands applicationCommandSet) serve(options core.Options) core.Result { + service, err := commands.mcpService() + if err != nil { + return core.Result{Value: err, OK: false} + } + if err := service.ServeHTTP(commands.coreApp.Context(), commands.serveAddress(options)); err != nil { + return core.Result{Value: core.E("main.serve", "serve mcp http", err), OK: false} + } + return core.Result{OK: true} +} + +func (commands applicationCommandSet) mcpService() (*coremcp.Service, error) { + if commands.coreApp == nil { + return nil, core.E("main.mcpService", "core is required", nil) + } + + result := commands.coreApp.Service("mcp") + if !result.OK { + return nil, core.E("main.mcpService", "mcp service not registered", nil) + } + + service, ok := result.Value.(*coremcp.Service) + if !ok || service == nil { + return nil, core.E("main.mcpService", "mcp service has invalid type", nil) + } + + return service, nil +} + +func (commands applicationCommandSet) serveAddress(options core.Options) string { + address := options.String("addr") + if address == "" { + address = options.String("_arg") + } + if address == "" { + address = core.Env("CORE_AGENT_HTTP_ADDR") + } + if address == "" { + address = coremcp.DefaultHTTPAddr + } + return address +} diff --git a/cmd/core-agent/commands_example_test.go b/cmd/core-agent/commands_example_test.go index 5808576..957a35d 100644 --- a/cmd/core-agent/commands_example_test.go +++ b/cmd/core-agent/commands_example_test.go @@ -11,7 +11,7 @@ func Example_registerApplicationCommands() { registerApplicationCommands(c) core.Println(len(c.Commands())) - // Output: 3 + // Output: 5 } func Example_applyLogLevel() { diff --git a/cmd/core-agent/commands_test.go b/cmd/core-agent/commands_test.go index 0b4cfbf..419cd0d 100644 --- a/cmd/core-agent/commands_test.go +++ b/cmd/core-agent/commands_test.go @@ -113,6 +113,8 @@ func TestCommands_RegisterApplicationCommands_Good(t *testing.T) { assert.Contains(t, cmds, "version") assert.Contains(t, cmds, "check") assert.Contains(t, cmds, "env") + assert.Contains(t, cmds, "mcp") + assert.Contains(t, cmds, "serve") } func TestCommands_Version_Good(t *testing.T) { @@ -169,6 +171,60 @@ func TestCommands_Env_Good(t *testing.T) { assert.True(t, r.OK) } +func TestCommands_MCPService_Good(t *testing.T) { + c := core.New( + core.WithOption("name", "core-agent"), + core.WithService(registerMCPService), + ) + registerApplicationCommands(c) + + service, err := (applicationCommandSet{coreApp: c}).mcpService() + assert.NoError(t, err) + assert.NotNil(t, service) +} + +func TestCommands_MCPService_Bad(t *testing.T) { + _, err := (applicationCommandSet{coreApp: newTestCore(t)}).mcpService() + assert.EqualError(t, err, "main.mcpService: mcp service not registered") +} + +func TestCommands_MCPService_Ugly(t *testing.T) { + c := core.New(core.WithOption("name", "core-agent")) + assert.True(t, c.RegisterService("mcp", "invalid").OK) + + _, err := (applicationCommandSet{coreApp: c}).mcpService() + assert.EqualError(t, err, "main.mcpService: mcp service has invalid type") +} + +func TestCommands_ServeAddress_Good(t *testing.T) { + c := newTestCore(t) + + addr := (applicationCommandSet{coreApp: c}).serveAddress(core.NewOptions( + core.Option{Key: "addr", Value: "0.0.0.0:9201"}, + )) + + assert.Equal(t, "0.0.0.0:9201", addr) +} + +func TestCommands_ServeAddress_Bad(t *testing.T) { + c := newTestCore(t) + t.Setenv("CORE_AGENT_HTTP_ADDR", "") + + addr := (applicationCommandSet{coreApp: c}).serveAddress(core.NewOptions()) + + assert.Equal(t, "127.0.0.1:9101", addr) +} + +func TestCommands_ServeAddress_Ugly(t *testing.T) { + c := newTestCore(t) + + addr := (applicationCommandSet{coreApp: c}).serveAddress(core.NewOptions( + core.Option{Key: "_arg", Value: "127.0.0.1:9911"}, + )) + + assert.Equal(t, "127.0.0.1:9911", addr) +} + func TestCommands_CliUnknown_Bad(t *testing.T) { c := newTestCore(t) r := c.Cli().Run("nonexistent") diff --git a/cmd/core-agent/main_test.go b/cmd/core-agent/main_test.go index 2b3ea1c..031d197 100644 --- a/cmd/core-agent/main_test.go +++ b/cmd/core-agent/main_test.go @@ -38,6 +38,8 @@ func TestMain_NewCoreAgent_Good(t *testing.T) { assert.Contains(t, c.Commands(), "version") assert.Contains(t, c.Commands(), "check") assert.Contains(t, c.Commands(), "env") + assert.Contains(t, c.Commands(), "mcp") + assert.Contains(t, c.Commands(), "serve") assert.Contains(t, c.Actions(), "process.run") service := c.Service("agentic")