lime-ref-postgres-mcp
by Mawyxx
README.md
# lime-ref-postgres-mcp
**English** · [Русский](./README.ru.md)
> An open-source **showcase of [LIME](https://lime.pics) agent identity** on a real resource: PostgreSQL behind [MCP](https://modelcontextprotocol.io/).
Agents do not share a generic database password. They arrive with a **LIME passport** (`Authorization: Bearer`), get checked against a local whitelist and capabilities, then use MCP tools against Postgres. After an authorized agent is recognized, the core emits one **audit event** — who called what, and how it ended — **without** copying the tool response body.
This repository is a **reference implementation**, not a production service operated by LIME. Fork it, study the pattern, run it against your own Postgres, and plug your own audit consumers if you need them.
---
## Why this exists
| Without named agent identity | With LIME on this door |
|------------------------------|-------------------------|
| One shared `POSTGRES_URL` / API key for every bot | Each agent is a **person** (`agent_id` from the passport) |
| Logs show “someone queried the DB” | Logs/events can say **which agent** did what |
| Hard to attach corporate audit / SIEM | EventBus is an extension point — subscribe your own sink |
| MCP demos often skip real auth | Same LIME passport model as other LIME-protected resources |
**Primary goal of this package:** make [LIME](https://lime.pics) technology tangible — passport → allowlist → action → event — on a concrete door (Postgres over MCP), so developers can see how agent identity works end-to-end.
Related LIME pieces:
- Platform & docs: [lime.pics](https://lime.pics)
- Agent client (issue MCP JWT with domain): [`lime-agents-sdk`](https://pypi.org/project/lime-agents-sdk/)
- Resource-server verify (JWKS / RS256): [`lime-mcp-server-sdk`](https://pypi.org/project/lime-mcp-server-sdk/) (this server wraps it)
---
## What it is / is not
| Is | Is not |
|----|--------|
| Open showcase of **LIME agent passport** on MCP → Postgres | LIME’s production product or hosted SaaS |
| Deny-by-default **whitelist + capabilities** | “One API key opens the whole DB” |
| Shipped **ConsoleSink** (demo of the event system) | Shipped webhook / SIEM exporters |
| Process observability (JSONL + metrics, ADR-003) | Mixing agent audit cards into process logs |
| Extractable package under this folder | Coupled to the rest of a monorepo runtime |
**Agent Bearer ≠ `POSTGRES_URL`.**
The Bearer is the agent’s LIME passport. `POSTGRES_URL` is the **MCP service** database role — service credentials, not agent identity.
---
## How a call works
```
Agent (LIME passport) -- Bearer + tools/call --> MCP /mcp
│
▼
1. Verify JWT (JWKS from lime.pics, domain + aud pin)
│ fail → error to agent, NO agent-action event
▼
2. Whitelist (config/agents.json)
│ unknown agent → error, NO agent-action event
▼
3. Capabilities + SQL class guard
│ denied → reply + agent-action event (denied)
▼
4. Postgres (asyncpg)
▼
5. Agent-action event (ok | error) → reply to agent
│
└── EventBus subscribers (ConsoleSink demo / your sink)
```
Process logs (`lime.mcp.process_log.v1`) always can record preauth failures and call lifecycle; **agent-action events** exist only after a allowlisted agent is established. See [ADR-003](./ADR-003-process-observability.md).
---
## Features
- **LIME passport gate** — `Authorization: Bearer` on every `tools/call`; verify via `lime-mcp-server-sdk`
- **Policy** — JSON whitelist, permissions `READ_SCHEMA` / `READ_DATA` / `WRITE_DATA` / `DDL` / `ADMIN`, `max_rows`, lazy reload by mtime
- **SQL defense** — pglast AST → statement-class guard (readonly vs write vs DDL)
- **Eight MCP tools** — schema / data / write / admin surface only on `/mcp`
- **Event system** — `AgentActionEvent` without response payload; `bus.subscribe(...)` for custom sinks
- **ConsoleSink** — optional JSONL cards on stdout (event-system demo)
- **Process observability** — structured logs + in-process metrics + `request_id` correlation
- **Quality gate** — package-local `prime_check` + CI workflow
---
## Quick start
**Requirements:** Python ≥ 3.12, [uv](https://github.com/astral-sh/uv). Docker only for integration tests.
```bash
cd Marketing/lime-postgres-mcp # or clone this package as its own repo
uv sync --all-extras
cp .env.example .env # fill LIME_* and POSTGRES_URL
# create config/agents.json from config/agents.example.json
# map real LIME agent_id (passport sub) → permissions
uv run python -m lime_ref_postgres_mcp serve
# → http://127.0.0.1:8000/mcp
```
From an agent worker, use **lime-agents-sdk** against that URL (OAuth mints a JWT with `{"domain": "<your pin>"}` matching `LIME_EXPECTED_DOMAIN`):
```python
from lime_agents import LimeAgent
async with LimeAgent(agent_token="...") as agent:
tools = await agent.list_tools("http://127.0.0.1:8000/mcp")
result = await agent.call_tool(
"http://127.0.0.1:8000/mcp",
"list_schemas",
{},
)
```
Composition check (no HTTP):
```bash
uv run python -m lime_ref_postgres_mcp
```
---
## MCP tools
| Tool | Capability |
|------|------------|
| `list_schemas` | `READ_SCHEMA` |
| `list_tables` | `READ_SCHEMA` |
| `get_table_schema` | `READ_SCHEMA` |
| `select_rows` | `READ_DATA` |
| `execute_readonly_query` | `READ_DATA` |
| `explain_query_plan` | `READ_DATA` |
| `execute_write_query` | `WRITE_DATA` |
| `get_database_stats` | `ADMIN` |
Passport goes in the HTTP header only — never in tool arguments.
---
## Agent event system (for integrators)
The core **emits** immutable `AgentActionEvent` values after an allowlisted agent is in context. It does **not** ship webhooks or SIEM connectors — by design. You attach consumers yourself.
### What you get on each event
- `agent_id`, `status` (`ok` | `denied` | `error`)
- `request` — tool name + args (no result rows / no agent reply body)
- `outcome` — reason codes, missing capabilities, statement class, row counts
- `meta` — e.g. `request_id`, `domain`
- `duration_ms`, `event_id`, `ts`
**Privacy rule:** full `SELECT` payloads must not leave through audit. Events are cards, not response mirrors.
### When there is no event
Missing / invalid passport, JWKS failure, or agent not on the whitelist → agent gets an error; **no** `AgentActionEvent` (there was no authorized actor). Process logs still record `preauth.fail`.
### Shipped demo: ConsoleSink
With `ENABLE_CONSOLE_EVENT_SINK=1` (default), bootstrap registers `ConsoleSink` — one JSON line per event on stdout. Turn it off if you only want your own subscribers.
### Add your own sink (logging elsewhere)
Any async callable that accepts `AgentActionEvent` works. Subscribe at composition time (after `build_scaffold_runtime` / on `runtime.event_bus`):
```python
from lime_ref_postgres_mcp.bootstrap.container import build_scaffold_runtime
from lime_ref_postgres_mcp.domain.auditing.agent_action_event import AgentActionEvent
async def forward_to_my_logger(event: AgentActionEvent) -> None:
# Examples: write to your DB, push to a queue, call an internal API.
# Do not put agent response bodies here — they are not on the event.
await my_audit_store.write(
agent_id=str(event.agent_id),
tool=event.request.get("tool"),
status=event.status,
reason=(event.outcome.reason_code if event.outcome else None),
request_id=(event.meta or {}).get("request_id"),
)
runtime = build_scaffold_runtime()
runtime.event_bus.subscribe(forward_to_my_logger)
# then serve ASGI from this runtime (same pattern as `serve`)
```
**Rules of the road**
1. Sink is **one-way**: read the event; do not call back into invoke/authorize.
2. Sink errors are swallowed by the bus — they must **not** change the tool `Result` returned to the agent.
3. Prefer idempotent, fast handlers; offload heavy work to a queue inside your sink.
4. Process observability (`bootstrap.observability`) is a **different** channel from agent events — don’t overload one with the other.
`ConsoleSink` source: [`subscribers/console_sink.py`](./src/lime_ref_postgres_mcp/subscribers/console_sink.py).
Port: [`application/ports/events.py`](./src/lime_ref_postgres_mcp/application/ports/events.py).
---
## Configuration
Copy [`.env.example`](./.env.example). Important variables:
| Variable | Required | Role |
|----------|----------|------|
| `LIME_EXPECTED_DOMAIN` | yes | Hostname pin on the MCP JWT (`domain` claim) |
| `LIME_JWKS_URL` | yes | JWKS (default lime.pics well-known) |
| `LIME_EXPECTED_AUD` | no | default `mcp` |
| `POSTGRES_URL` | yes | Service DB URL (lazy pool) |
| `AGENTS_POLICY_PATH` | no | default `./config/agents.json` |
| `POLICY_RELOAD_TTL_SECONDS` | no | default `60` |
| `ENABLE_CONSOLE_EVENT_SINK` | no | default on — demo agent-action JSONL |
| `LIME_MCP_LOG_*` / `LIME_MCP_METRICS` | no | process observability (ADR-003) |
| `LIME_MCP_BIND_HOST` / `LIME_MCP_PORT` | no | `serve` bind (default `127.0.0.1:8000`) |
Policy shape: [`config/agents.example.json`](./config/agents.example.json).
There is **no** `WEBHOOK_URL` and no WebhookSink in this package.
---
## Verify / quality
```bash
uv run ruff check src tests
uv run mypy src
uv run pytest
uv run pytest tests/integration -m integration --no-cov -o addopts=
uv run python -m scripts.prime_check
uv run python -m scripts.prime_check --list
```
Nested CI: [`.github/workflows/prime_check.yml`](./.github/workflows/prime_check.yml) (extract-to-own-repo ready).
---
## Documentation map
| Doc | Content |
|-----|---------|
| [PRD.md](./PRD.md) | Product intent, access model, event rules |
| [TDD.md](./TDD.md) | Architecture, layers, module map |
| [ADR-001](./ADR-001-mcp-authentication-boundary.md) | Auth boundary on MCP |
| [ADR-003](./ADR-003-process-observability.md) | Process logs + metrics + correlation |
| [docs/verification/](./docs/verification/) | Phase reports (Day0 → P6) |
---
## Layout
```
src/lime_ref_postgres_mcp/
domain/ # policy, SQL guard, AgentActionEvent (pure)
application/ # invoke / authorize / emit ports
infrastructure/ # JWKS verify, JSON policy, asyncpg, EventBus
presentation/mcp/ # Streamable HTTP /mcp
subscribers/ # ConsoleSink (demo)
bootstrap/ # settings, container, process observability
config/ # agents.example.json
scripts/prime_check # package quality gate
tests/
```
---
## Status
Showcase sealed through **P6** (CI green + SBOM). Intended as public reference code for LIME agent identity on MCP → Postgres.
**Maintainers do not** operate this as LIME production, and do not ship outbound webhook sinks.
---
## License
See package metadata in [`pyproject.toml`](./pyproject.toml).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues