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:
106
internal/mcp/protocol.go
Normal file
106
internal/mcp/protocol.go
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user