Implements the walking skeleton from the roadmap: agents connect to one HTTP
endpoint and Nexus aggregates upstream MCP servers behind it with namespaced
tools. Written in Go (single static binary), no external services required.
What works end-to-end (verified live + hermetic e2e tests):
- MCP protocol layer: JSON-RPC 2.0 + initialize/tools.list/tools.call
(internal/mcp), with stdio (subprocess) and Streamable HTTP client
transports, and a reusable stream transport for in-process wiring.
- Router + Dynamic Tool Registry: connects/initializes upstreams in parallel,
loads tools, namespaces them as {namespace}.{tool}, dispatches tools/call to
the owning upstream; refreshes on list_changed. A failed upstream stays
not-ready without taking down the gateway (data plane stays up).
- Gateway: single MCP endpoint (POST /mcp) that is an MCP server to agents,
plus /healthz and a minimal /metrics exposition.
- CLI (cmd/nexus): `serve`, `connect` (stdio<->HTTP bridge for local agents),
`demo-mcp` (built-in zero-dep demo server: echo/add/now), `version`.
- Config: declarative YAML with env expansion + validation.
- Store: persistence interfaces + in-memory impl (SQLite lands later).
Foundations for later phases: domain types (ARCHITECTURE §7), two-plane
split, event-driven refresh seam.
Tooling: Makefile (build/test/vet/fmt with version ldflags), config.example
.yaml. GOPATH moved to /go so it doesn't collide with the module root at /root;
.gitignore whitelist extended to track Go sources while ignoring build output.
Tests: unit (registry/namespacing) + full e2e (client -> demo server over
pipes -> router -> HTTP gateway: initialize, aggregated tools/list, tool-call
routing, unknown-tool error). go vet + gofmt clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
2.9 KiB
Go
110 lines
2.9 KiB
Go
// Command nexus is the MCP Nexus control plane binary.
|
|
//
|
|
// Subcommands:
|
|
//
|
|
// nexus serve Run the aggregating MCP gateway (the control plane).
|
|
// nexus connect Bridge a local stdio agent to a Nexus gateway endpoint.
|
|
// nexus demo-mcp Run the built-in demo MCP server over stdio.
|
|
// nexus version Print version information.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/app"
|
|
"gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/bridge"
|
|
"gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/config"
|
|
"gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/demomcp"
|
|
"gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/version"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
cmd := os.Args[1]
|
|
args := os.Args[2:]
|
|
|
|
var err error
|
|
switch cmd {
|
|
case "serve":
|
|
err = runServe(args)
|
|
case "connect":
|
|
err = runConnect(args)
|
|
case "demo-mcp":
|
|
err = demomcp.Run(os.Stdin, os.Stdout)
|
|
case "version", "--version", "-v":
|
|
fmt.Println("mcp-nexus", version.String())
|
|
case "help", "-h", "--help":
|
|
usage()
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
|
usage()
|
|
os.Exit(2)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "error:", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func runServe(args []string) error {
|
|
fs := flag.NewFlagSet("serve", flag.ExitOnError)
|
|
cfgPath := fs.String("config", "config.yaml", "path to config file")
|
|
logFormat := fs.String("log", "text", "log format: text|json")
|
|
_ = fs.Parse(args)
|
|
|
|
log := newLogger(*logFormat)
|
|
|
|
cfg, err := config.Load(*cfgPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cfg.Gateway.Version == "" {
|
|
cfg.Gateway.Version = version.Version
|
|
}
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
return app.Serve(ctx, cfg, log)
|
|
}
|
|
|
|
func runConnect(args []string) error {
|
|
fs := flag.NewFlagSet("connect", flag.ExitOnError)
|
|
url := fs.String("url", "http://localhost:8080/mcp", "Nexus gateway MCP endpoint")
|
|
_ = fs.Parse(args)
|
|
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
return bridge.Run(ctx, *url, os.Stdin, os.Stdout)
|
|
}
|
|
|
|
func newLogger(format string) *slog.Logger {
|
|
// Logs go to stderr so stdio-transport subcommands keep stdout clean.
|
|
var h slog.Handler
|
|
if format == "json" {
|
|
h = slog.NewJSONHandler(os.Stderr, nil)
|
|
} else {
|
|
h = slog.NewTextHandler(os.Stderr, nil)
|
|
}
|
|
return slog.New(h)
|
|
}
|
|
|
|
func usage() {
|
|
fmt.Fprint(os.Stderr, `mcp-nexus — the control plane for MCP
|
|
|
|
Usage:
|
|
nexus serve [-config config.yaml] [-log text|json] Run the aggregating MCP gateway
|
|
nexus connect [-url http://localhost:8080/mcp] Bridge a local stdio agent to a gateway
|
|
nexus demo-mcp Run the built-in demo MCP server (stdio)
|
|
nexus version Print version
|
|
`)
|
|
}
|