Files
mcp-gateway-nexus/internal/store/store.go
drjones 72020b1901 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>
2026-07-07 09:43:08 +00:00

73 lines
2.0 KiB
Go

// Package store defines MCP Nexus's persistence interfaces (ARCHITECTURE §8)
// and an in-memory implementation used for Phase 1 and tests. A SQLite-backed
// implementation lands in a later brick behind the same interfaces.
package store
import (
"context"
"sort"
"sync"
"gitea.thetempleofdoom.com/drjones/mcp-gateway-nexus/internal/domain"
)
// InstanceStore persists MCP instances (the observed/desired state the
// reconciler manages).
type InstanceStore interface {
PutInstance(ctx context.Context, inst domain.MCPInstance) error
GetInstance(ctx context.Context, id string) (domain.MCPInstance, bool, error)
ListInstances(ctx context.Context) ([]domain.MCPInstance, error)
DeleteInstance(ctx context.Context, id string) error
}
// Store aggregates all persistence surfaces. It will grow as later modules
// (registry, identity, secrets, audit) come online.
type Store interface {
InstanceStore
}
// Memory is a thread-safe in-memory Store.
type Memory struct {
mu sync.RWMutex
instances map[string]domain.MCPInstance
}
// NewMemory creates an empty in-memory store.
func NewMemory() *Memory {
return &Memory{instances: make(map[string]domain.MCPInstance)}
}
func (m *Memory) PutInstance(_ context.Context, inst domain.MCPInstance) error {
m.mu.Lock()
defer m.mu.Unlock()
m.instances[inst.ID] = inst
return nil
}
func (m *Memory) GetInstance(_ context.Context, id string) (domain.MCPInstance, bool, error) {
m.mu.RLock()
defer m.mu.RUnlock()
inst, ok := m.instances[id]
return inst, ok, nil
}
func (m *Memory) ListInstances(_ context.Context) ([]domain.MCPInstance, error) {
m.mu.RLock()
defer m.mu.RUnlock()
out := make([]domain.MCPInstance, 0, len(m.instances))
for _, inst := range m.instances {
out = append(out, inst)
}
sort.Slice(out, func(i, j int) bool { return out[i].ID < out[j].ID })
return out, nil
}
func (m *Memory) DeleteInstance(_ context.Context, id string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.instances, id)
return nil
}
var _ Store = (*Memory)(nil)