mcp-server-pgvector

An MCP server that gives LLM agents first-class access to
pgvector-backed embedding tables in PostgreSQL: similarity
search, hybrid (vector + full-text) search, upserts, and HNSW/IVFFlat index management.
Generic Postgres MCP servers expose raw SQL or schema introspection; this one speaks pgvector
specifically โ nearest-neighbor search, distance metrics, and ANN index tuning are first-class
tools, not something the model has to hand-write SQL for.
| Tool | Description |
|---|
list_vector_tables | Discover every vector column in the database, with its dimensionality |
describe_vector_table | Columns, indexes, and approximate row count for a table |
similarity_search | k-NN search over a vector column (cosine / L2 / inner product), with structured metadata filters |
hybrid_search | Weighted blend of vector similarity and Postgres full-text search (ts_rank_cd) |
upsert_embedding | Insert or update a row's embedding + metadata |
create_vector_index | Create an HNSW or IVFFlat index with tunable parameters |
explain_similarity_query | EXPLAIN ANALYZE a similarity query to confirm the ANN index is used |
Safety
- Every table/column name is validated against
information_schema / pg_catalog before being
interpolated into SQL โ an LLM can only ever reference identifiers that already exist. Values are
always bound parameters.
- Metadata filters are a closed
{column, op, value} allowlist, not a raw SQL fragment.
- Set
MCP_PGVECTOR_READ_ONLY=true to disable upsert_embedding and create_vector_index,
leaving only read/search tools available โ useful when pointing the server at a production
database.
- Every query runs with a per-command timeout (
MCP_PGVECTOR_COMMAND_TIMEOUT_SECONDS, default
30s) so one expensive query can't occupy a pool connection โ and stall every other caller โ
indefinitely. Set it to 0 to disable.
Installation
Or with pip:
pip install mcp-server-pgvector
python -m mcp_server_pgvector
Configuration
The server reads its connection string from DATABASE_URL (or PGVECTOR_DATABASE_URL):
{
"mcpServers": {
"pgvector": {
"command": "uvx",
"args": ["mcp-server-pgvector"],
"env": {
"DATABASE_URL": "postgresql://user:password@localhost:5432/mydb",
"MCP_PGVECTOR_READ_ONLY": "false",
"MCP_PGVECTOR_COMMAND_TIMEOUT_SECONDS": "30"
}
}
}
}
Production readiness
Covered:
- Identifier-safe SQL (every table/column checked against
pg_catalog before use) and a closed
filter-operator allowlist โ no path from tool arguments to raw SQL.
- Per-query timeout, so one runaway query can't monopolize the (small, 5-connection) pool.
- 60+ tests, including dimension-mismatch and injection-attempt regressions, run in CI on every
push/PR against a real pgvector container across Python 3.10โ3.13. A separate CI job builds the
package and runs
twine check on the result.
- Connection failures surface as plain
ConnectionRefusedError/asyncpg exceptions โ verified
these don't leak the DSN's credentials into error text.
Known limitations, honestly:
- No per-tool authorization โ access control is whatever the Postgres role in
DATABASE_URL can
do. If you need different agents to have different permissions, give them different
connection strings backed by different Postgres roles, not different server instances of this
same DSN.
hybrid_search's full-text side is hardcoded to Postgres's 'english' text search
configuration; there's no parameter to change it yet.
- No structured logging โ failures are exceptions surfaced through the MCP error channel, not
written to a log you can tail. Fine for a single-user desktop MCP client, a real gap if you're
running this as a shared service.
- The connection pool is fixed at 1โ5 connections and isn't configurable via environment variable
yet.
Development
uv sync --dev
docker compose -f docker-compose.dev.yml up -d
export DATABASE_URL=postgresql://postgres:postgres@localhost:5434/postgres
uv run pytest
uv run ruff check .
uv run pyright
Contributing
See CONTRIBUTING.md. See CHANGELOG.md for release history.
License
MIT โ see LICENSE.