Skip to main content
Isthmus follows hexagonal architecture (ports & adapters). The core business logic is decoupled from infrastructure, making it straightforward to add new database backends or change the transport layer.

Directory structure

cmd/isthmus/               → Binary entrypoint (single binary, stdio + HTTP transports)
internal/
  core/
    port/                  → Interfaces: SchemaExplorer, QueryExecutor, QueryAuditor
    domain/                → SQL validation (pg_query AST), cardinality, column masking (MaskType)
    service/               → Application services: QueryService
  adapter/
    mcp/                   → MCP server factory + tool definitions (discover, describe_table, query)
    postgres/              → PostgreSQL implementation of ports (pgxpool, information_schema, pg_stats)
  audit/                   → File-based audit logging (NDJSON)
  config/                  → Environment variable + CLI flag loading
  policy/                  → Policy engine: YAML loading, context enrichment, column masking
  telemetry/               → OpenTelemetry: OTLP gRPC providers, metric instruments
Everything is under internal/ — Go’s convention enforces module privacy. There is no pkg/ directory.

Ports (interfaces)

The core defines three port interfaces that adapters implement:
// Schema discovery and table analysis
type SchemaExplorer interface {
    ListSchemas(ctx context.Context) ([]SchemaInfo, error)
    ListTables(ctx context.Context) ([]TableInfo, error)
    DescribeTable(ctx context.Context, schema, tableName string) (*TableDetail, error)
    Discover(ctx context.Context) (*DiscoveryResult, error)
}

// SQL execution
type QueryExecutor interface {
    Execute(ctx context.Context, sql string) ([]map[string]any, error)
}

// Audit trail
type QueryAuditor interface {
    Record(ctx context.Context, entry AuditEntry)
    Close() error
}

Data flow

  1. The MCP client sends a tool call (e.g. describe_table) over stdio or HTTP
  2. The MCP adapter routes it to the appropriate service
  3. The service calls the port interface (e.g. SchemaExplorer.DescribeTable)
  4. The Postgres adapter executes SQL against the database
  5. The policy engine enriches the response and applies column masks (if configured)
  6. The result is serialized to JSON and returned to the client
  7. If OpenTelemetry is enabled, spans and metrics are recorded at each step

Dependency injection

All dependencies are wired at startup in cmd/isthmus/main.go. There is no global state, service locator, or runtime reflection:
// Adapters
pool     := postgres.NewPool(ctx, cfg.DatabaseURL)
explorer := postgres.NewExplorer(pool, cfg.Schemas)
executor := postgres.NewExecutor(pool, cfg.ReadOnly, cfg.MaxRows, cfg.QueryTimeout)

// Optional decorators
if cfg.PolicyFile != "" {
    pol, _ := policy.LoadFromFile(cfg.PolicyFile)
    masks  := policy.MaskSpec(pol.Context)
    explorer = policy.NewPolicyExplorer(explorer, pol, masks)
}

// Services
querySvc := service.NewQueryService(validator, executor, auditor, logger)

// Server
mcpServer := mcp.NewServer(ver, explorer, querySvc, logger)

Adding a new database adapter

To add support for a new database (e.g. MySQL):
  1. Create internal/adapter/mysql/
  2. Implement port.SchemaExplorer and port.QueryExecutor
  3. Wire the new adapter in cmd/isthmus/main.go based on the connection string scheme
  4. Add integration tests using testcontainers
The MCP layer, services, domain validation, and policy engine all work unchanged — they only depend on port interfaces.

Testing

Isthmus uses no mocks. Integration tests use testcontainers-go to spin up real PostgreSQL containers:
go test -race -count=1 ./...                  # All tests (needs Docker)
go test -short -race -count=1 ./...           # Unit tests only
go test ./internal/core/domain/...            # Domain tests only
go test ./internal/adapter/postgres/...       # Postgres adapter tests
Domain tests (SQL validation, cardinality classification) run without Docker. Adapter and E2E tests require Docker for testcontainers.