> For the complete documentation index, see [llms.txt](https://docs.prestd.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.prestd.com/get-started/mcp-over-http.md).

# MCP over HTTP

pREST v2.1.0+ exposes a read-only [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) endpoint at `/_mcp` on the same server that already serves CRUD, catalog, and custom script routes. MCP clients can discover schema and read table data through JSON-RPC tools — without a separate process or transport.

Today MCP runs on the **Native PostgreSQL adapter** and [Postgres-compatible engines](/databases.md) you connect with that adapter. Other SQL families are on the [roadmap](/databases/roadmap.md).

Introduced in [v2.1.0](/releases/v2.1.0.md) ([#977](https://github.com/prest/prest/pull/977)).

***

## What is MCP?

**MCP (Model Context Protocol)** is an [open standard](https://modelcontextprotocol.io/) for connecting AI applications and agents to external tools and data sources. Instead of each product inventing a private plugin format, MCP defines how clients discover tools and call them (for example over JSON-RPC).

AI agents, IDEs, and other MCP-capable clients use that protocol to list available tools and run them with structured arguments. pREST implements a **read-only** MCP surface at `/_mcp` so those clients can:

* Discover databases, schemas, and tables
* Describe columns
* Select rows (with limits)

through the **same** `prestd` process, auth, ACL, and database routing as the REST API.

MCP in pREST is **not** a separate database, a second port, or a write/DDL interface. In v2.1.0 it does not insert, update, delete, or run arbitrary admin SQL — use REST or curated `/_QUERIES` scripts for writes when your deployment allows them.

***

## What is AI (in these docs)?

**Artificial intelligence (AI)** here means applications and agents that use models to reason and act on tools and data. pREST connects them to your SQL catalog through read-only MCP at `/_mcp` and the same auth/ACL as the REST API.

Typical clients are AI agents, IDEs, and other tools that speak MCP. For client setup (adapters, IDE config), see [docs.prestd.com/ai](https://docs.prestd.com/ai). pREST is the API layer those clients call — it is not itself an LLM or AI model.

***

## How it fits in pREST

The MCP endpoint reuses the existing pREST request pipeline:

```
MCP client → GET or POST /_mcp → auth / ACL → catalog & query execution → SQL database (PG family today)
```

That means MCP inherits the same deployment model, authentication, access control, and database routing as the REST API. There is no second port to configure: MCP runs in-process on the same `prestd` HTTP server.

Many AI clients (Cursor, Claude Desktop, and others) expect a **stdio** MCP process rather than HTTP. For those, use the official [pREST MCP Adapter](/ai/install-prest-mcp.md) (`prest-mcp`) — a tiny stdio ↔ HTTP bridge. The adapter does not implement tools; pREST still owns schema discovery and queries. See [AI and MCP](/ai.md) for install and client tutorials.

| Concern        | Behavior                                                                 |
| -------------- | ------------------------------------------------------------------------ |
| Server process | Same `prestd` instance as REST                                           |
| Authentication | HTTP auth stack — `/_mcp` is protected when auth is enabled              |
| Permissions    | `access.tables` and per-user rules apply to tool discovery and execution |
| Multi-database | Database aliases from the registry are used in tool names and arguments  |
| Writes         | **Not supported** in v2.1.0 — read-only by design                        |

***

## Endpoint shape

| Method | Path    | Purpose                                                    |
| ------ | ------- | ---------------------------------------------------------- |
| `GET`  | `/_mcp` | Discovery payload with server metadata and available tools |
| `POST` | `/_mcp` | JSON-RPC 2.0 requests for MCP operations                   |

Supported JSON-RPC methods:

| Method       | Description                                               |
| ------------ | --------------------------------------------------------- |
| `initialize` | Returns server info, capabilities, and usage instructions |
| `tools/list` | Returns the full tool catalog with `inputSchema` per tool |
| `tools/call` | Executes a named tool with optional arguments             |

Unsupported methods return `400 Bad Request` with `unsupported method`.

***

## Discovery (`GET /_mcp`)

A `GET` request returns a JSON document describing the server and all tools visible to the current caller (after auth and ACL filtering):

```json
{
  "name": "prest",
  "protocol": "0.1",
  "endpoint": "/_mcp",
  "description": "Read-only MCP endpoint backed by pREST catalog and query execution.",
  "tools": [
    {
      "name": "prest.list_databases",
      "description": "List accessible databases.",
      "inputSchema": { "type": "object", "properties": {} }
    }
  ],
  "capabilities": {
    "tools": { "listChanged": false }
  }
}
```

Use discovery to inspect available tools and their typed `inputSchema` before calling them.

***

## Tools reference

{% hint style="info" %}
Since **v2.4.1** ([#1016](https://github.com/prest/prest/pull/1016)), the three `prest.list_*` tools honour the `[expose]` settings. A denied listing is dropped from discovery and its `tools/call` returns `400` with `unauthorized listing`. See [Safety and limits](#safety-and-limits).
{% endhint %}

### `prest.list_databases`

List database aliases accessible to the current caller.

**Requires:** `expose.databases` (v2.4.1+).

**Arguments:** none.

**Returns:** array of database objects. In registry multi-database mode, each entry includes:

| Field           | Description                                           |
| --------------- | ----------------------------------------------------- |
| `name`          | Registered alias (used in URLs and tool names)        |
| `datname`       | Same as `name`                                        |
| `physical_name` | Physical Postgres database name on the target cluster |

In legacy single-host mode, returns databases from the catalog query.

***

### `prest.list_schemas`

List readable schemas for a database alias.

**Requires:** `expose.schemas` (v2.4.1+).

**Arguments:**

| Field      | Required | Description                                            |
| ---------- | -------- | ------------------------------------------------------ |
| `database` | No       | Database alias; defaults to `pg.database` when omitted |

**Returns:** array of schema objects (filtered by `access.tables` permissions).

***

### `prest.list_tables`

List readable tables for a database alias and optional schema.

**Requires:** `expose.tables` (v2.4.1+).

**Arguments:**

| Field      | Required | Description                                                       |
| ---------- | -------- | ----------------------------------------------------------------- |
| `database` | No       | Database alias; defaults to `pg.database` when omitted            |
| `schema`   | No       | Filter to a single schema; omit to list across accessible schemas |

**Returns:** array of table objects with `name`, `schema`, and related metadata.

***

### `prest.describe_table`

Return column metadata for a table.

**Arguments:**

| Field      | Required | Description                                            |
| ---------- | -------- | ------------------------------------------------------ |
| `database` | No       | Database alias; defaults to `pg.database` when omitted |
| `schema`   | Yes      | Schema name                                            |
| `table`    | Yes      | Table name                                             |

**Returns:**

```json
{
  "database": "prest-test",
  "schema": "public",
  "table": "test",
  "columns": [
    {
      "name": "id",
      "data_type": "integer",
      "nullable": false,
      "position": 1
    }
  ],
  "count": 1
}
```

Column objects may also include `max_length`, `generated`, `updatable`, and `default_value` when available from the catalog.

***

### `prest.select_table`

Read rows from a table using a generic tool name.

**Arguments:**

| Field      | Required | Description                                                         |
| ---------- | -------- | ------------------------------------------------------------------- |
| `database` | No       | Database alias; defaults to `pg.database` when omitted              |
| `schema`   | Yes      | Schema name                                                         |
| `table`    | Yes      | Table name                                                          |
| `columns`  | No       | Project specific columns; omit to select all permitted columns      |
| `filters`  | No       | Column-aware equality filters (see below)                           |
| `order_by` | No       | Sort fields — use `field` for ascending, `-field` for descending    |
| `limit`    | No       | Row limit (1–100); defaults to **100** when omitted or out of range |
| `offset`   | No       | Row offset; defaults to **0**                                       |

**Returns:**

```json
{
  "database": "prest-test",
  "schema": "public",
  "table": "Reply",
  "columns": ["id", "name"],
  "rows": [{ "id": 1, "name": "prest tester" }],
  "count": 1
}
```

***

### `prest.select.{database}.{schema}.{table}`

Schema-aware tools generated from the catalog at startup. Each readable table gets its own tool with a typed `inputSchema` derived from column metadata.

**Tool name pattern:**

```
prest.select.{database}.{schema}.{table}
```

Example: `prest.select.prest-test.public.Reply`

**Arguments:** same as `prest.select_table`, except `database`, `schema`, and `table` are encoded in the tool name and cannot be overridden.

The generated `inputSchema` exposes:

* allowed `columns` (enum of permitted column names)
* allowed `order_by` values (`field` and `-field` for each column)
* column-aware `filters` with types matching each column's data type
* `limit` (integer, 1–100) and `offset` (integer, minimum 0)

In multi-database mode, tools are generated for every registered alias. Tables the caller cannot read are omitted from the tool list.

{% hint style="warning" %}
Since **v2.4.1**, if `[expose] enabled = true` and **any** of `databases` / `schemas` / `tables` is `false`, these per-table tools are **not generated at all** — their names embed database, schema, and table names and their descriptions embed column names, so enumerating them would leak the catalog that `[expose]` is hiding. Clients that relied on auto-discovered per-table tools should use the generic `prest.select_table` instead.
{% endhint %}

***

## Examples

### Initialize

```http
POST /_mcp
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize"
}
```

Response includes `serverInfo`, `capabilities`, and `instructions`:

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "serverInfo": { "name": "prest", "version": "0.1" },
    "capabilities": { "tools": { "listChanged": false } },
    "instructions": "Read-only tools are exposed through pREST auth and ACL."
  }
}
```

### List tools

```http
POST /_mcp
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}
```

### Describe a table

```http
POST /_mcp
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "prest.describe_table",
    "arguments": {
      "database": "prest-test",
      "schema": "public",
      "table": "test"
    }
  }
}
```

### Schema-aware select

```http
POST /_mcp
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "prest.select.prest-test.public.Reply",
    "arguments": {
      "columns": ["id", "name"],
      "filters": { "name": "prest tester" },
      "order_by": ["id"],
      "limit": 5,
      "offset": 0
    }
  }
}
```

### List databases

```http
POST /_mcp
Content-Type: application/json

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "prest.list_databases"
  }
}
```

***

## Authentication and permissions

When [auth](/api-reference/auth.md) is enabled, `/_mcp` requires the same credentials as other protected routes. Send the appropriate `Authorization` header (or basic auth, depending on your configuration) with every `GET` and `POST` request.

[Permissions](/get-started/permissions.md) apply to MCP the same way they apply to REST:

* Tool discovery only lists tables and columns the caller can read.
* `tools/call` on a table without read permission returns an error.
* Per-user `[[access.users]]` rules are respected.

If a tool list appears empty or a select returns a permission error, check your `access.tables` configuration.

***

## Multi-database

MCP tools use **database aliases** — the same identifiers used in REST URLs (`/{database}/{schema}/{table}`). When the `database` argument is omitted, pREST uses the default database (`pg.database`).

| Mode                   | `database` in tools    | See also                                         |
| ---------------------- | ---------------------- | ------------------------------------------------ |
| Legacy multi-DB        | Postgres database name | [Multi-database](/get-started/multi-database.md) |
| Registry multi-cluster | Registered alias       | [Multi-database](/get-started/multi-database.md) |

`prest.list_databases` returns both the alias (`name`) and `physical_name` in registry mode, so MCP clients can distinguish routing aliases from cluster database names.

When `pg.single = true` and a registry is active, only the default database alias is accepted.

***

## Safety and limits

| Rule                        | Detail                                                                                                                |
| --------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| Read-only                   | No insert, update, delete, or DDL tools in v2.1.0                                                                     |
| Row cap                     | Maximum **100** rows per select (`limit` defaults to 100)                                                             |
| Identifier validation       | Database, schema, table, column, and filter names are validated                                                       |
| Unsupported tools           | Return `400 Bad Request` with `unsupported tool`                                                                      |
| Catalog discovery (v2.4.1+) | `[expose]` applies to `/_mcp`, not just the REST listing routes — see below                                           |
| Custom queries              | `/_QUERIES` scripts are not exposed through MCP                                                                       |
| Separate process            | MCP runs in-process on `prestd`; optional [stdio adapter](/ai/install-prest-mcp.md) for clients that cannot call HTTP |

Calling a write tool (for example `prest.drop_table`) returns:

```json
{
  "error": {
    "message": "unsupported tool: prest.drop_table"
  }
}
```

### Catalog discovery and `[expose]` (v2.4.1)

Before **v2.4.1**, `/_mcp` never passed through the exposure middleware — a deployment that disabled the `/databases`, `/schemas`, or `/tables` REST routes still served its entire catalog to any MCP client. Since [#1016](https://github.com/prest/prest/pull/1016), the MCP handler consults the same settings.

| Setting                                            | Effect on `/_mcp`                                                         |
| -------------------------------------------------- | ------------------------------------------------------------------------- |
| `expose.enabled = false` (default)                 | Everything is listable — no change from v2.4.0                            |
| `expose.databases = false` (with `enabled = true`) | `prest.list_databases` dropped from discovery; `tools/call` returns `400` |
| `expose.schemas = false` (with `enabled = true`)   | `prest.list_schemas` dropped from discovery; `tools/call` returns `400`   |
| `expose.tables = false` (with `enabled = true`)    | `prest.list_tables` dropped from discovery; `tools/call` returns `400`    |
| **Any** of the three denied                        | All `prest.select.{database}.{schema}.{table}` tools withheld             |

A denied listing returns a JSON-RPC error rather than a bare HTTP body:

```json
{"jsonrpc":"2.0","id":1,"error":{"code":400,"message":"unauthorized listing"}}
```

The REST routes return **401** for the same message — same rule, different surface.

`[expose]` governs **discovery**; `[access]` governs **reads**. `prest.describe_table` and `prest.select_table` are always advertised, so a client that already knows a table name can still read it subject to [permissions](/get-started/permissions.md). Configuration: [Configuring pREST — Expose Data](/get-started/configuring-prest.md#expose-data).

***

## Troubleshooting

| Symptom                                  | Likely cause                                                                                                               |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `401` / `403` on `/_mcp`                 | Auth enabled but credentials missing or invalid — see [Auth](/api-reference/auth.md)                                       |
| `400 unsupported tool`                   | Tool name not in the catalog or write operation attempted                                                                  |
| `400 unsupported method`                 | JSON-RPC method other than `initialize`, `tools/list`, or `tools/call`                                                     |
| `400 unsupported column`                 | `columns`, `filters`, or `order_by` references a column not permitted or not on the table                                  |
| Empty tool list                          | ACL hides all tables, or catalog is unreachable for registered aliases                                                     |
| `400 unauthorized listing`               | The listing is denied by `[expose]` (v2.4.1+) — see [Catalog discovery and `[expose]`](#catalog-discovery-and-expose-v241) |
| Per-table `prest.select.*` tools missing | `[expose]` is active with at least one listing denied (v2.4.1+); use `prest.select_table`                                  |
| Permission error on select               | Caller lacks read access to the table or specific columns — see [Permissions](/get-started/permissions.md)                 |
| `invalid identifier in path`             | Schema or table name failed identifier validation                                                                          |

For integration test examples, see [`integration/controllers/mcp_test.go`](https://github.com/prest/prest/blob/v2.1.0/integration/controllers/mcp_test.go) in the prest repository.

***

## Client tutorials

* [AI and MCP](/ai.md) — landing for agents and IDEs
* [Install pREST MCP Adapter](/ai/install-prest-mcp.md) — Homebrew, Go, Docker, MCP Registry
* [Use with Cursor](/ai/cursor.md) · [Claude Desktop](/ai/claude-desktop.md) · [Other AI tools](/ai/other-clients.md)
* [Read-only PostgreSQL for AI](/ai/read-only-postgres.md)
* [PostgreSQL to AI agent](/postgres-to-ai-agent.md)
* [Start with Homebrew](/get-prest/start-with-homebrew.md) — `prestd` and `prest/tap/prest-mcp`

## Related documentation

* [Acronyms](/readme/acronyms.md) ([MCP](/readme/acronyms.md#mcp), [AI](/readme/acronyms.md#ai), [REST](/readme/acronyms.md#rest))
* [MCP Overview](/ai/mcp-overview.md)
* [v2.1.0 release notes](/releases/v2.1.0.md)
* [Multi-database](/get-started/multi-database.md)
* [Permissions](/get-started/permissions.md)
* [Auth](/api-reference/auth.md)
* [API Reference](/api-reference.md)
