Vizru MCP server
Provides read-only SQL query access to PostgreSQL mirrors of Vizru spreadsheets on Neon, exposing REST endpoints and MCP tools for relational filtering and schema discovery.
README
Vizru MCP server
Reads the PostgreSQL mirrors of Vizru spreadsheets on Neon, for the two workflow blocks that need relational access.
| Block | Uses |
|---|---|
| Relational Filter | POST /api/query — one read-only SELECT, rows back |
| Agent Node | POST /api/query as its query_database tool, and GET /api/schema to describe each configured spreadsheet |
Sys\Workflow\Block\McpClient in v1-web-app is the client. Its expectations are
the contract: HTTP basic auth, a bare JSON array of row objects from
/api/query, and a 200 with a JSON body from /api/schema. Anything else is
read by the caller as the service being unavailable.
POST /mcp offers the same two capabilities as MCP tools over JSON-RPC, for
clients that speak the protocol rather than REST.
Before you deploy: there is already an mcp-server
The vizru-docker compose project defines a service called mcp-server
(D:\Platform\Vizru-Docker\mcp-server, image mcp-server:5.1). It serves the
same two endpoints on 9101, plus write endpoints and an MCP SSE server on 9100,
and it points at a local mcp-postgres container rather than Neon.
Two containers cannot share a name, so decide which you are doing:
- Replacing it — the clean blocks only ever read, so
/api/executeand the insert/update/delete/upsert endpoints have no caller left in the new code. Check nothing else uses them, and note that this service exposes MCP as JSON-RPC on9101/mcprather than SSE on9100; anything pointed at 9100 needs moving. - Running both — set
MCP_CONTAINER_NAMEhere andMcpServerUrlinconfig/env-*.phpto the same new name.
Pointing the blocks at this server
Nothing to configure in the blocks themselves. McpClient falls back to
http://mcp-server:9101 with vizru / vizru_secret_key when the platform
defines no McpServerUrl / McpApiUser / McpApiSecret, and .env.example
ships those same values — so a container named mcp-server on the platform's
network is picked up by both blocks with no further wiring. Override all three
in config/env-*.php for production, keeping them in step with .env here.
Two prerequisites are outside this service and easy to miss:
- The mirrors have to be in the database this service reads.
ss-sync-pipelinewrites them, and it points at whateverPG_HOSTit is given. If it still writes to the localmcp-postgreswhile this service reads Neon, every query finds noss_*tables and every schema lookup 404s. - Nothing else may hold the
mcp-servercontainer name — see below.
Configuration
Copy .env.example to .env and fill it in. Two values have to agree with the
platform or every block call fails:
| This service | Platform (config/env-*.php) |
|---|---|
MCP_API_USER |
McpApiUser |
MCP_API_SECRET |
McpApiSecret |
container name + 9101 |
McpServerUrl |
McpServerUrl, McpApiUser and McpApiSecret are optional on the platform
side; leaving them undefined falls back to http://mcp-server:9101 with
vizru / vizru_secret_key. Set them in production.
Use Neon's pooled endpoint (the host containing -pooler). This service
holds a small pool of its own, and the direct endpoint has a lower connection
ceiling.
Run it
Option A — standalone
cp .env.example .env && docker compose up -d --build
It joins the platform's existing network as an external one. Check it:
docker compose logs -f mcp-server
Option B — inside the platform's compose project
Drop the directory in next to the other services and add this to
Vizru-Docker/docker-compose.yaml, which matches the conventions there
(project-local network name, image tag variables, profiles, file secrets):
mcp-server:
restart: always
build:
context: ./mcp-server
dockerfile: Dockerfile
container_name: mcp-server
environment:
- MCP_API_USER=vizru
- STATEMENT_TIMEOUT_MS=10000
- MAX_ROWS=500
secrets:
- neon-url
expose:
- 9101
networks:
- vizru-network
image: ${DOCKER_IMAGE}mcp-server:${DOCKER_TAG}
profiles:
- dev
- prod
Reading the URL and secret from files rather than the environment needs two
lines in db.py / main.py (open("/run/secrets/neon-url").read().strip());
the rest is unchanged. Doing it that way also lets mcp-postgres be removed
from the project.
Endpoints
GET /health liveness plus a real database round trip
POST /api/query {"query": "SELECT ..."} -> [ {...}, ... ]
GET /api/schema?shortcode=EMP001 -> {"table":"ss_emp001","columns":[...]}
POST /mcp JSON-RPC: initialize, tools/list, tools/call
All except /health require basic auth.
A spreadsheet's short code names its mirror table: EMP001 is ss_emp001,
lower-cased because Postgres folds unquoted identifiers and the blocks reference
these tables unquoted.
What stops a bad query
Four layers, because the SQL is written by people and by language models:
- The block.
SqlReadGuard.phprefuses anything but a single read-only SELECT before the request is even made, and the Agent Node additionally restricts the query to the spreadsheets that block was configured with. - This service.
guard.pyapplies the identical rules again — the workflow engine is not the only thing that can reach this port. - The session. Every pooled connection runs with
default_transaction_read_only = on, so Postgres itself refuses a write. This catches what a regex cannot:SELECT … FOR SHAREtakes row locks and trips no keyword, and the database rejects it. - The clock and the row count.
statement_timeout(10s by default) kills a runaway query, andMAX_ROWS(500) caps what comes back.
For a fifth, give the service a Postgres role with only SELECT on the ss_*
tables. Nothing here depends on being able to write.
Local development
python -m venv .venv && . .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
DATABASE_URL='postgresql://...?sslmode=require' \
MCP_API_SECRET=dev \
uvicorn main:app --port 9101 --reload
Files
main.py FastAPI app: auth, the REST endpoints, the MCP entry point
tools.py the two capabilities, shared by both surfaces
guard.py read-only SQL check, mirroring SqlReadGuard.php
db.py Neon connection pool, read-only session, row serialization
mcp.py JSON-RPC framing for tools/list and tools/call
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.
E2B
Using MCP to run code via e2b.