feat: Phase 0/1 — runnable MCP gateway (aggregation + namespaced routing)

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>
This commit is contained in:
drjones
2026-07-07 09:43:08 +00:00
parent 8d3ffef920
commit 72020b1901
25 changed files with 2450 additions and 0 deletions

106
internal/mcp/protocol.go Normal file
View File

@@ -0,0 +1,106 @@
package mcp
import "encoding/json"
// ProtocolVersion is the MCP protocol revision Nexus advertises. Nexus is
// lenient on the client's requested version and echoes a supported one.
const ProtocolVersion = "2025-06-18"
// Method names used by MCP.
const (
MethodInitialize = "initialize"
MethodInitialized = "notifications/initialized"
MethodPing = "ping"
MethodToolsList = "tools/list"
MethodToolsCall = "tools/call"
MethodToolListChged = "notifications/tools/list_changed"
)
// Implementation identifies a client or server implementation.
type Implementation struct {
Name string `json:"name"`
Version string `json:"version"`
}
// ToolsCapability describes tool-related server capabilities.
type ToolsCapability struct {
ListChanged bool `json:"listChanged,omitempty"`
}
// Capabilities is the (subset of) MCP capabilities Nexus cares about.
type Capabilities struct {
Tools *ToolsCapability `json:"tools,omitempty"`
Resources *struct {
ListChanged bool `json:"listChanged,omitempty"`
Subscribe bool `json:"subscribe,omitempty"`
} `json:"resources,omitempty"`
Prompts *struct {
ListChanged bool `json:"listChanged,omitempty"`
} `json:"prompts,omitempty"`
}
// InitializeParams are the params for the initialize request.
type InitializeParams struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities Capabilities `json:"capabilities"`
ClientInfo Implementation `json:"clientInfo"`
}
// InitializeResult is the result of an initialize request.
type InitializeResult struct {
ProtocolVersion string `json:"protocolVersion"`
Capabilities Capabilities `json:"capabilities"`
ServerInfo Implementation `json:"serverInfo"`
Instructions string `json:"instructions,omitempty"`
}
// ToolDefinition is a tool as described by tools/list.
type ToolDefinition struct {
Name string `json:"name"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
InputSchema map[string]any `json:"inputSchema,omitempty"`
}
// ListToolsParams are the params for tools/list.
type ListToolsParams struct {
Cursor string `json:"cursor,omitempty"`
}
// ListToolsResult is the result of tools/list.
type ListToolsResult struct {
Tools []ToolDefinition `json:"tools"`
NextCursor string `json:"nextCursor,omitempty"`
}
// CallToolParams are the params for tools/call.
type CallToolParams struct {
Name string `json:"name"`
Arguments json.RawMessage `json:"arguments,omitempty"`
}
// ContentBlock is one item of tool result content. Only the common fields are
// modeled; unknown fields survive round-trips because callers pass the raw
// result through where possible.
type ContentBlock struct {
Type string `json:"type"`
Text string `json:"text,omitempty"`
// For image/audio/resource blocks these carry through opaquely.
Data string `json:"data,omitempty"`
MimeType string `json:"mimeType,omitempty"`
Resource json.RawMessage `json:"resource,omitempty"`
}
// CallToolResult is the result of tools/call.
type CallToolResult struct {
Content []ContentBlock `json:"content"`
IsError bool `json:"isError,omitempty"`
}
// TextResult is a convenience constructor for a single text-content result.
func TextResult(text string, isError bool) CallToolResult {
return CallToolResult{
Content: []ContentBlock{{Type: "text", Text: text}},
IsError: isError,
}
}