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>
107 lines
3.4 KiB
Go
107 lines
3.4 KiB
Go
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,
|
|
}
|
|
}
|