Skip to content

DumbMachine/access-mcp

Repository files navigation

Access MCP

A local-only, single-binary execution layer for AI agents. Point it at your databases and infrastructure; agents get CLI-shaped access over MCP, with native Go execution, local guardrails, a full audit log, and a context store that makes every session start warmer than the last.

$ access serve
  access v0.1.0 — listening on http://127.0.0.1:4820
  UI:   http://127.0.0.1:4820/?_token=…
  MCP:  http://127.0.0.1:4820/mcp

Execute against: postgres, mysql, sqlite, redis, kubernetes — plus warehouses clickhouse, snowflake, bigquery over their official SDKs. Learn from (never execute): a docs folder, a git repo, a dbt project — context resources that feed your agents knowledge and attach it to the databases it describes.

Why

Every resource you hand an agent today is another MCP server: another process to run (stdio here, docker there, remote http for the third), another copy of credentials sitting in a client config, and another 5–8 bespoke tools your agent has to learn at runtime — none of which appear in its training data. And each server binds one instance — a second Kubernetes cluster isn't more config, it's a whole second server.

access collapses all four axes. Kinds become drivers inside one binary, instances become rows in a catalog, deployment becomes one process, and the tool surface stays at five:

Left: one MCP server per resource — six processes, six credential copies, ~60 bespoke tools for the agent to learn (none in its training set). Right: one access server — five tools it already knows, one vault, resources as rows in a catalog.

One connection, five tools. Agents connect to one server exposing five stable tools; the catalog is data returned by discover, not tool definitions, so your tenth resource costs the prompt nothing. Smarter clients now lazy-load tool schemas — that fixes where the schema lives, not what it costs: a bespoke tool is still an API the model has to learn from its description, every session. access doesn't shrink the textbook; it removes it.

The interface agents already know. AI labs have spent billions teaching models the terminal — psql, redis-cli, kubectl, Kibana's Dev Tools console: the flags, the idioms, the error messages. Wrapper tools throw that training away. The reference Kubernetes MCP server exposes 19 tools; not one shapes output (every call returns full objects into your context), and pods can't be filtered by CrashLoopBackOff (its own schema says so). The agent still gets there — it just pays in hops and tokens. And real debugging rarely stays in one system: the cluster tells you the pod was OOMKilled, but why it grew is in the app logs — a second resource. That crossing is where the architectures split:

The same crash-loop investigation, two ways. Both find the pod was OOMKilled — the typical Kubernetes MCP through a full-object dump, access with commands it already knows. Then the question crosses into the app logs: access runs one more execute against a Kibana resource in the same catalog, while the Kubernetes MCP server can't reach Elasticsearch at all and forces you to install a second MCP server.

With access the agent writes the command it was trained on and narrows output at the source (--tail, -o jsonpath, LIMIT 10), and reaching the next system is just another execute against a resource already in the catalog — no new server, no new tool vocabulary. Guardrails subtract dangerous verbs locally instead of a wrapper deciding the surface upfront. API-shaped resources will speak the same five tools through http-style verbs (calendar.event.create <params>) — soon.

No sandbox, because no code. Agents send argv (["psql","-c","select 1"]); access parses it into typed native driver calls (pgx, mysql, go-redis) or a fenced kubectl. Nothing an agent sends is evaluated or shelled on your machine. That makes it as safe as — usually safer than — a sandboxed shell: there is no shell to escape.

It learns, so agents stop re-learning. Without memory, every session burns tokens rediscovering the same schemas and conventions. Adding a resource builds ~/.access/context/system/<name>/*.md from live introspection; agents search it before probing, and you add your own files and Agent Skills (<name>/SKILL.md) alongside.

Knowledge lives in more than databases. Point access at a docs folder, a git repo, or a dbt project and it becomes a context resource: agents read it, never execute it. A dbt project's facts (enum values, join edges) attach to the warehouse it describes and are cross-checked against the live schema; a runbook folder teaches the why introspection can't see. Every piece of knowledge carries a trust tier — introspected (from the live system) > declared (metadata a human wrote) > external-prose (free text)

agent-learned — and all of it is DATA to the agent, never instructions.

Secrets never reach the agent. Write-only vault (AES-256-GCM), decrypted only inside a connection handshake. No API returns a secret, ever.

Read-only by default, audited always. Statement classification (SQL), command allowlists (Redis), verb allowlists (kubectl) — enforced locally, before your credential is used. Every attempt, including denials, lands in the run log.

What it looks like

You ask one question in plain language; the agent does the rest. Each example is one exchange: your line (You:), then the agent's calls to accessdiscover / search / execute, with each showing what access returns — and finally the agent's plain-language reply (Agent:), written from that returned data. The pattern never changes: look things up first, run a command last. The lookups (discover, search) only read what access already knows; they never touch your database or cluster.

Query by business context. You don't tell the agent where the data is — it already knows, because access learned your infra when the resource was added.

You: how did each product category do on revenue?

discover()
   ↳ dev-postgres · postgres · read-only · 6 knowledge files

search({ resource: "dev-postgres", query: "revenue" })
   ↳ schema.demo.md  — "v_revenue_by_category: revenue rolled up per category"
   ↳ note.finance.md — "revenue excludes internal test accounts (email like '%@internal.test')"

execute({ resource: "dev-postgres",
          argv: ["psql", "-c", "select * from demo.v_revenue_by_category"] })
   ↳  category  |  revenue
      ----------+-----------
      Standard  | 1,284,300
      Premium   |   902,150
      Pool      |   318,470
      (5 rows)

Agent: Standard rides brought in the most revenue ($1.28M), then Premium ($902K) and Pool ($318K) — full breakdown above. These already exclude internal test accounts, per your finance note.

The agent never guessed at the schema or ran a throwaway query. discover and search only read what access learned when the database was added — the introspected schema docs plus the finance note you dropped in — so the single query it ran was the right one, with the test-account rule already applied.

Triage a cluster without handing over the cluster.

You: checkout is slow in staging — take a look

discover({ kind: "kubernetes" })
   ↳ staging-k8s · read-only

search({ resource: "staging-k8s", query: "checkout" })
   ↳ workloads.checkout.md — checkout-api (deployment), checkout-web (service)

execute({ resource: "staging-k8s", argv: ["kubectl", "get", "pods", "-n", "checkout"] })
   ↳ NAME                       READY   STATUS             RESTARTS
     checkout-api-7d4b9-x2vq8   0/1     CrashLoopBackOff   6
     checkout-web-5c8f6-p2m9k   1/1     Running            0

execute({ resource: "staging-k8s", argv: ["kubectl", "logs", "deploy/checkout-api", "-n", "checkout", "--tail", "200"] })
   ↳ … Killed: memory cgroup limit exceeded (OOMKilled, limit 128Mi)

Agent: checkout-api is crash-looping in staging — it's being OOMKilled against a 128Mi memory limit (6 restarts so far); checkout-web is healthy. Raising the limit should stop the restarts. I stayed read-only, so nothing was changed.

The agent can get, describe, logs, top — but exec, delete, port-forward, and credential-swapping flags are fenced off locally, and read-only mode enforces the verb allowlist before your kubeconfig is ever used.

It teaches itself, then you teach it more. Adding a resource makes access introspect it and write the knowledge docs the agent reads with search — so it starts warm, before anyone curates anything. On top of that, drop a note or a skill ("when asked about churn, use mrr_facts, not the raw events table") and every agent, in every future session, finds it with search before it starts guessing. Deterministic sync keeps the self-learned docs fresh; your curation compounds on top.

Quick start

Install the latest release — one binary, no dependencies:

curl -fsSL https://raw.githubusercontent.com/dumbmachine/access-mcp/main/scripts/install.sh | sh

This drops access into /usr/local/bin (or ~/.local/bin if that isn't writable). Pin a version by passing the env var to sh:

curl -fsSL https://raw.githubusercontent.com/dumbmachine/access-mcp/main/scripts/install.sh | ACCESS_VERSION=v0.2.0-alpha.8 sh

Or export ACCESS_VERSION=… before piping. Pick a location with ACCESS_INSTALL_DIR=~/bin. Prefer to grab a tarball yourself? They're on the releases page for macOS and Linux (arm64 + amd64), each with a checksums.txt.

Run:

access serve
Build from source instead (Go 1.25+, pnpm)
git clone https://github.com/dumbmachine/access-mcp
cd access-mcp
make            # SPA + binary → bin/access
./bin/access serve

Connect your agent — one command configures every MCP client on your machine (add-mcp detects Claude Code, Cursor, Codex, VS Code, and friends):

npx add-mcp http://127.0.0.1:4820/mcp \
  --name access \
  --header "Authorization: Bearer <your-token>"

The token is printed by access serve and shown on the UI's Connect page, alongside the raw endpoint for wiring a client by hand. Prefer stdio? Clients can spawn the binary directly — no URL or token:

npx add-mcp "access mcp" --name access      # or: claude mcp add access -- access mcp

access ships with a demo database already connected. Try:

Using the access MCP tools, explore the demo-sqlite resource and answer: which three artists sold the most tracks? Show the SQL you used.

Then add your own resources in the UI (Add resource) — credentials go straight into the vault and a context sync builds the resource's knowledge base from live introspection.

MCP surface — five tools, no more

tool what
discover lean catalog of your resources (+ how much context each has)
describe argv grammar & recipes per kind; per-resource manifest
execute run argv against a resource (dry_run supported; not_executable on a context resource)
search search / read the context store (schema docs, notes, dbt facts, folder docs)
help the invariants doc

Two transports, same data dir:

  • HTTPhttp://127.0.0.1:4820/mcp, bearer token, loopback only.
  • stdioaccess mcp, spawned by the client; no token needed.

Context resources — knowledge that isn't a database

Some resources are things agents should learn from, not run commands against: a folder of runbooks, a git docs repo, a dbt project. Add one and discover shows it as class: context with mode learn-only; execute against it returns a not_executable error that points the agent at search instead. Three kinds today:

kind what it ingests trust tier
folder a local dir of markdown/text — runbooks, ADRs, an Obsidian vault, anything that syncs to disk (Drive/Dropbox) external-prose
git the same, fetched read-only (depth-1) from a repo external-prose
dbt a dbt project's model/column descriptions + tests (accepted_values→enums, relationships→joins) declared

Knowledge attaches to what it describes. Give a context resource maps_to: ["prod-pg"] and its docs surface under search(resource="prod-pg") alongside the introspected schema, and count toward prod-pg's knowledge_files. A dbt project mapped to a warehouse is cross-checked against that warehouse's live schema — any table or column the project names that doesn't exist gets a visible ⚠ unverified marker, so a stale doc never routes an agent to a column that's gone.

Safety holds: folder ingestion refuses symlinks that escape the root; git uses pure-Go go-git (no git subprocess) into a scratch dir wiped after sync and never executes repo contents; all ingested text is sanitized (control/zero-width/bidi runes stripped) and marked with its trust tier; sync is deterministic (no LLM) and never on the MCP wire (a prompt-injected agent can't trigger or poison ingestion). Deleting a context resource evicts every doc it contributed.

Try it end to end (needs a dev-postgres first, see below):

make bootstrap postgres     # the warehouse the dbt project describes
make bootstrap docs         # Glide engineering handbook  → glide-handbook (folder)
make bootstrap dbt          # Glide dbt project, mapped    → glide-dbt (dbt)

Then ask your agent:

Using the access tools, what are the valid values of a trip's status, and which columns join trips to drivers? Then show the top drivers by earnings.

It finds the dbt-declared enum + join facts under search(resource="dev-postgres"), learns from the handbook that earnings must include tips (use v_driver_earnings, not raw fare_cents), and answers correctly — knowledge introspection alone could never give it.

Development

make            # full build: SPA + binary with SPA embedded
make build      # Go binary only → bin/access
make run        # build + serve
make test       # go test ./...
make vet
make web-dev    # Vite dev server proxying /api + /mcp to a running serve

Test resources, up and down

With a server running (make run), stand up seeded docker databases and register them through the public API — exactly like the UI would:

The seeds model one fictional ride-hailing org (Glide) across every resource — same city codes, same driver/trip id ranges — so agents can learn a coherent company, not five disconnected demos:

make bootstrap postgres     # Glide rides OLTP: riders, drivers, trips, payments → dev-postgres
make bootstrap mysql        # Glide Eats OLTP: restaurants, couriers, orders     → dev-mysql
make bootstrap redis        # live state: driver geo, surge, dispatch queue      → dev-redis
make bootstrap clickhouse   # telemetry: trip_events stream + daily rollups      → dev-clickhouse
make bootstrap kubernetes   # the services on a kind cluster                     → dev-k8s
make bootstrap docs         # Glide engineering handbook (folder)               → glide-handbook
make bootstrap dbt          # Glide dbt project, mapped to dev-postgres         → glide-dbt

The database/cluster kinds are created read-write and context-synced immediately, so search works on the first query; the docs/dbt kinds are learn-only context resources (run make bootstrap postgres before dbt so its facts have a warehouse to attach to and be cross-checked against). Every bootstrap also seeds the curated layer — notes with Glide's tribal knowledge (fares exclude tips; soft-deleted drivers; telemetry vs truth) and a cross-resource incident runbook — which is the part schema introspection can never learn. The postgres container preloads pg_stat_statements so sync also captures how the team actually queries the DB; the kind cluster (needs the kind CLI) includes a deliberately crash-looping surge-pricing deployment whose failure mode is exactly what the seeded runbook walks through; and the docs handbook ships two deliberate traps (a doc naming a table that no longer exists, and a prompt-injection line) so you can see the stale-doc and DATA-not-instructions handling for yourself. Tear everything down when you're done:

make bootstrap down            # remove containers/clusters + resources + secrets
make bootstrap down postgres   # just one kind

Ports/dirs are overridable via ACCESS_PORT, ACCESS_DATA_DIR, PG_PORT, MYSQL_PORT, REDIS_PORT, CH_PORT (see scripts/bootstrap.sh).

See PLAN.md for the full design.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors