How to Secure an MCP Server for AI Agents (Postgres Reference)

14 min

You have an MCP server in front of Postgres. How do you let AI agents call tools by name — without handing every bot the same database password? This how-to answers that question with a working open reference: LIME passport JWT on the wire, your ACL after verify.

Why shared API keys and DB URLs fail for MCP agents

Most MCP demos skip real authentication. In production that usually means one of these shortcuts:

  • One shared POSTGRES_URL or static API key baked into every agent config.
  • Logs that only say “someone queried the DB” — not which agent.
  • No clean way to revoke one compromised bot without rotating the fleet secret.
  • Tool ACL mixed with the database login — identity and authorization collapsed into one credential.

Shared secrets scale poorly for autonomous agents. You need a short-lived, named identity on the MCP door, and a separate service role for Postgres.

Correct boundary: Bearer passport ≠ POSTGRES_URL

Treat the MCP Authorization header as the agent’s identity document — a LIME passport JWT — not as the database password.

POSTGRES_URL stays on the MCP resource server process as the service database role. Agents never receive it. Agents present Authorization: Bearer <passport>. The server verifies the passport, then decides what that agent_id may do.

That separation is the auth boundary used in the open reference (ADR-001): raw JWT never enters audit events or RequestContext after verify; missing or invalid passport returns an error and does not emit an agent-action event.

What is a LIME passport JWT (aud=mcp, domain-bound)

LIME is a passport system for AI agents: one internet identity under an owner, Core-signed short-lived JWTs, local JWKS verify. MCP is how agents typically present that passport today — transport, not a second product.

The MCP access JWT uses audience aud=mcp, includes claim domain bound to your resource-server hostname, and typically lives about five minutes (~300s). The subject sub is the stable agent_id.

Your MCP server’s job after verify is authorization: whitelist, tool capabilities, SQL class guards — LIME attests who the agent is; you decide what it may do.

How verification works (JWKS / TokenVerifier)

On the hot path the MCP resource server verifies the Bearer locally against Core JWKS — no round trip to LIME for every tool call once keys are cached. Pin expected_domain to your public hostname so a passport minted for another host is rejected.

Canonical JWKS: https://lime.pics/api/v1/core/.well-known/jwks.json (do not use /.well-known/jwks.json — that path does not exist).

from lime_mcp_server import TokenVerifier

verifier = TokenVerifier(expected_domain="tools.example.com")
result = verifier.verify(bearer)
agent_id = result.agent_id  # sub — then YOUR ACL

Agent Token vs passport — custody rules

Agent Token (X-Agent-Token / LIME_AGENT_TOKEN) is the long-lived opaque secret the agent uses only to authenticate to LIME. It does not issue or carry a passport. The agent calls LIME with the token and a domain; LIME returns the short-lived passport JWT.

Never send LIME_AGENT_TOKEN to an MCP resource server. Never present an MCP JWT (aud=mcp) as X-Agent-Token on LIME APIs. Site Token is not required for MCP servers — it is only for site login and binding.

import os
from lime_agents import LimeAgent

# Agent runtime — Agent Token never leaves this process
async with LimeAgent(agent_token=os.environ["LIME_AGENT_TOKEN"]) as agent:
    mcp = await agent.get_mcp_access_token("tools.example.com")
    bearer = mcp.access_token  # send ONLY this to the MCP RS

Walkthrough: open MCP → Postgres reference

The open showcase lime-ref-postgres-mcp implements passport → allowlist → action → event on a real door (PostgreSQL behind MCP). Flow:

  1. Agent sends Bearer + tools/call to MCP /mcp.
  2. Verify JWT (JWKS, domain + aud=mcp). Fail → error to agent, no agent-action event.
  3. Whitelist agent_id (config/agents.json). Unknown → error, no agent-action event.
  4. Capabilities + SQL class guard. Denied → reply + agent-action event (denied), no response body in the event.
  5. Execute against Postgres via the service role (asyncpg).
  6. Emit AgentActionEvent (ok | error) without copying the tool response body — ConsoleSink is the demo subscriber.

Fork the reference, point it at your Postgres, map agent_id values to permissions and max_rows, and plug your own EventBus sink when you need SIEM. It is a reference implementation — not LIME hosted SaaS.

Checklist to copy onto your MCP server

  • Mint domain-bound MCP JWTs from LIME (agent authenticates with Agent Token; LIME issues the passport).
  • Verify Bearer with TokenVerifier(expected_domain=your hostname) / Core JWKS.
  • Deny by default: allowlist agent_id before tools run.
  • Enforce per-agent capabilities and SQL statement-class guards after verify.
  • Keep POSTGRES_URL only on the RS; never give agents the DB URL.
  • Audit who called what without storing tool response bodies or raw JWTs.
  • Never send LIME_AGENT_TOKEN to the MCP RS; never use Site Token on the MCP path.

What to read next

Protect your MCP door with named agent identity

Create a LIME owner account, register an agent, mint domain-bound passports, and verify them on your resource server — or fork the Postgres reference and wire your ACL.

How to Secure an MCP Server for AI Agents (Postgres Reference) · LIME