mcp/cmd/mcpcmd/cmd_mcp.go
Snider 62c1949458 fix(mcp): rewrite mcpcmd for new core/cli Command API + correct bridge test
The mcpcmd package was using the removed Cobra-style cli.Command API
(Use/Short/Long/RunE/StringFlag/AddCommand). Rewrites it to the current
core.Command{Description, Action, Flags} path-routed pattern so the
core-mcp binary compiles again. Registers both "mcp" and "mcp/serve"
for parity with the existing OnStartup service-mode flow.

Fixes the bridge DescribableGroup test that expected len == svc.Tools()
but ToolBridge.Describe prepends the GET tool-listing entry, so the
correct expectation is len + 1.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-04-14 18:02:36 +01:00

141 lines
3.6 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
// Package mcpcmd registers the `mcp` and `mcp serve` CLI commands.
//
// Wiring example:
//
// cli.Main(cli.WithCommands("mcp", mcpcmd.AddMCPCommands))
//
// Commands:
// - mcp Start the MCP server on stdio (default transport).
// - mcp serve Start the MCP server with auto-selected transport.
package mcpcmd
import (
"context"
"os"
"os/signal"
"syscall"
core "dappco.re/go/core"
"dappco.re/go/mcp/pkg/mcp"
"dappco.re/go/mcp/pkg/mcp/agentic"
"dappco.re/go/mcp/pkg/mcp/brain"
)
// newMCPService is the service constructor, indirected for tests.
var newMCPService = mcp.New
// runMCPService starts the MCP server, indirected for tests.
var runMCPService = func(svc *mcp.Service, ctx context.Context) error {
return svc.Run(ctx)
}
// shutdownMCPService performs graceful shutdown, indirected for tests.
var shutdownMCPService = func(svc *mcp.Service, ctx context.Context) error {
return svc.Shutdown(ctx)
}
// workspaceFlag mirrors the --workspace CLI flag value.
var workspaceFlag string
// unrestrictedFlag mirrors the --unrestricted CLI flag value.
var unrestrictedFlag bool
// AddMCPCommands registers the `mcp` command tree on the Core instance.
//
// cli.Main(cli.WithCommands("mcp", mcpcmd.AddMCPCommands))
func AddMCPCommands(c *core.Core) {
c.Command("mcp", core.Command{
Description: "Model Context Protocol server (stdio, TCP, Unix socket, HTTP).",
Action: runServeAction,
Flags: core.NewOptions(
core.Option{Key: "workspace", Value: ""},
core.Option{Key: "w", Value: ""},
core.Option{Key: "unrestricted", Value: false},
),
})
c.Command("mcp/serve", core.Command{
Description: "Start the MCP server with auto-selected transport (stdio, TCP, Unix, or HTTP).",
Action: runServeAction,
Flags: core.NewOptions(
core.Option{Key: "workspace", Value: ""},
core.Option{Key: "w", Value: ""},
core.Option{Key: "unrestricted", Value: false},
),
})
}
// runServeAction is the CLI entrypoint for `mcp` and `mcp serve`.
//
// opts := core.NewOptions(core.Option{Key: "workspace", Value: "."})
// result := runServeAction(opts)
func runServeAction(opts core.Options) core.Result {
workspaceFlag = core.Trim(firstNonEmpty(opts.String("workspace"), opts.String("w")))
unrestrictedFlag = opts.Bool("unrestricted")
if err := runServe(); err != nil {
return core.Result{Value: err, OK: false}
}
return core.Result{OK: true}
}
// firstNonEmpty returns the first non-empty string argument.
//
// firstNonEmpty("", "foo") == "foo"
// firstNonEmpty("bar", "baz") == "bar"
func firstNonEmpty(values ...string) string {
for _, v := range values {
if v != "" {
return v
}
}
return ""
}
// runServe wires the MCP service together and blocks until the context is
// cancelled by SIGINT/SIGTERM or a transport error.
//
// if err := runServe(); err != nil {
// core.Error("mcp serve failed", "err", err)
// }
func runServe() error {
opts := mcp.Options{}
if unrestrictedFlag {
opts.Unrestricted = true
} else if workspaceFlag != "" {
opts.WorkspaceRoot = workspaceFlag
}
// Register OpenBrain and agentic subsystems.
opts.Subsystems = []mcp.Subsystem{
brain.NewDirect(),
agentic.NewPrep(),
}
svc, err := newMCPService(opts)
if err != nil {
return core.E("mcpcmd.runServe", "create MCP service", err)
}
defer func() {
_ = shutdownMCPService(svc, context.Background())
}()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
select {
case <-sigCh:
cancel()
case <-ctx.Done():
}
}()
return runMCPService(svc, ctx)
}