Access MySQL over MCP with vsql-mcp VillageSQL released vsql-mcp, an extension that runs a Model Context Protocol server inside the MySQL database server process rather than as a separate sidecar, serving MCP over Streamable HTTP from a background worker. The extension ships with six tools — list_schemas, list_tables, describe_table, query, explain, and write — plus two MCP resources, a schema overview and a table's CREATE TABLE statement; the write tool stays disabled and invisible to agents until an administrator enables it. Configuration is server state set via SET GLOBAL and read via SHOW VARIABLES, including vsql_mcp.db_url, vsql_mcp.schema, require_auth with a bearer token, and vsql_mcp.vsql_mcp_enabled; the extension is not bundled with the server and requires a Rust toolchain of 1.87 or newer to build with cargo vsql package. VillageSQL https://villagesql.com/blog/tag/villagesql/ Access MySQL over MCP with vsql-mcp The Model Context Protocol MCP has become a standard way for AI agents to discover the tools an application, database, or service offers. Claude Code, Codex, Cursor, Antigravity, et al. can connect to an MCP server, ask it what tools are available, and call these tools instead of figuring out what they can or can't do on the fly or piecing together API calls. Setting up an MCP server for a database usually means running a separate process that holds a database credential and turns tool calls into queries. Then you have another process to deploy, another place a credential lives, and a set of guardrails that sit outside the database. The vsql-mcp https://github.com/villagesql/vsql-mcp extension from VillageSQL takes a different approach. It puts the MCP server inside the database. Install it, set a handful of global variables, and your MySQL server itself serves MCP over Streamable HTTP from a background worker in the database server process. There's no sidecar to run, and the rules about what an agent can see and do are server state: you set them with SET GLOBAL , read them with SHOW VARIABLES , and watch them work with SHOW STATUS . VillageSQL is the innovation platform for MySQL that adds an extension framework similar to PostgreSQL's extension framework to enable permissionless innovation. Instead of waiting for a feature to be implemented in a few years in a future version of MySQL, new functionality can be dynamically added to a version of MySQL you run today. The extension comes packaged with a set of six tools. list schemas , list tables , and describe table browse what you expose. query runs one read-only SELECT , and explain returns its plan. write is the sixth, and it stays off until you turn it on; until then an agent cannot see that it exists. Two MCP resources come with the tools: a schema overview and a table's CREATE TABLE . Guidance on adding your own tools is at the bottom of this post. The rest of this post walks through using the extension end to end. If you would rather stop reading here and have your AI agent demonstrate this for you, open this dropdown and copy the prompt into your preferred AI coding tool. Set up a working demo of the vsql-mcp extension for VillageSQL, which serves the Model Context Protocol from inside the database server. Work only against a local throwaway server, and if the only VillageSQL or MySQL server you find looks like something I depend on, stop and ask me before touching it. Do all of this yourself, and show me the real output of each step: 1. Find a VillageSQL server, or install one. The install script needs a codebase and a method: curl -fsSL https://install.villagesql.com | VSQL CODEBASE=mysql-8.4 INSTALL METHOD=prebuilt bash . Start it with --vsql allow preview extensions=ON ; on a server already running, SET PERSIST vsql allow preview extensions=ON takes effect at once. Confirm with SELECT VERSION before continuing. 2. Build the extension. It is not bundled with the server yet. Install the tooling with cargo install cargo-vsql , clone https://github.com/villagesql/vsql-mcp, run cargo vsql package in it, and copy dist/vsql mcp.veb into the directory SHOW VARIABLES LIKE 'veb dir' names. You need a Rust toolchain, 1.87 or newer. The SDK comes from crates.io. 3. Create a small demo schema with two related tables and a few dozen rows, then run INSTALL EXTENSION vsql mcp; and show me the settings that appeared under SHOW GLOBAL VARIABLES LIKE 'vsql mcp%' . 4. Configure it the least-privilege way: a dedicated database account with SELECT on the demo schema only, vsql mcp.db url pointing at that account, vsql mcp.schema set to the demo schema, require auth ON with a random bearer token, and a free port. Turn vsql mcp.vsql mcp enabled ON and prove it is listening with SELECT vsql mcp.info ; . 5. Register the endpoint with yourself for Claude Code: claude mcp add --transport http vsql http://127.0.0.1:PORT/mcp --header "Authorization: Bearer TOKEN" , then answer a question about the demo data using only the MCP tools, and show me which tools you called. 6. Try to break it. Send a request with no bearer token and show the 401. Ask the query tool to run an UPDATE. Query a table in another schema. Set vsql mcp.allowed tables to one table and show that the others disappear from list tables and refuse describe table. Show me each refusal message. Then give me a table of what you ran and what came back, tell me anything that did not behave the way this asked, and drop every user, schema, and setting you created, remove the MCP registration, and leave my machine the way you found it. Build it vsql-mcp is written in Rust and uses "preview capabilities" of the VillageSQL Extension Framework VEF . It isn't bundled with the server as of this writing, so you must build it yourself. You need a Rust toolchain 1.87 or newer and the cargo-vsql tool. The VillageSQL Rust SDK comes from crates.io, so there is nothing else to clone: cargo install cargo-vsql git clone https://github.com/villagesql/vsql-mcp cd vsql-mcp cargo vsql package That produces dist/vsql mcp.veb . Copy it into your server's extension directory, which the server can help you identify: SHOW VARIABLES LIKE 'veb dir'; Connect an agent The examples below use a toy shop schema. Create it to follow along: CREATE DATABASE shop; USE shop; CREATE TABLE customers id INT PRIMARY KEY AUTO INCREMENT, name VARCHAR 80 NOT NULL, country CHAR 2 NOT NULL, joined DATE NOT NULL ; CREATE TABLE orders id INT PRIMARY KEY AUTO INCREMENT, customer id INT NOT NULL, placed on DATE NOT NULL, status ENUM 'pending','shipped','delivered','cancelled' NOT NULL, total DECIMAL 10,2 NOT NULL, FOREIGN KEY customer id REFERENCES customers id ; INSERT INTO customers name, country, joined VALUES 'Carla Reyes', 'MX', '2024-02-11' , 'Ben Osei', 'GH', '2024-03-02' , 'Grace Liu', 'SG', '2024-03-19' , 'Tomas Vrba', 'CZ', '2024-05-07' , 'Priya Nair', 'IN', '2024-06-23' , 'Mei Tanaka', 'JP', '2024-08-14' ; INSERT INTO orders customer id, placed on, status, total VALUES 1, '2024-04-03', 'delivered', 150.00 , 1, '2024-07-21', 'delivered', 270.00 , 1, '2024-09-02', 'cancelled', 80.00 , 2, '2024-04-18', 'delivered', 99.95 , 2, '2024-08-30', 'delivered', 210.50 , 3, '2024-05-26', 'delivered', 76.25 , 3, '2024-09-11', 'delivered', 200.00 , 4, '2024-06-14', 'delivered', 180.00 , 4, '2024-10-05', 'shipped', 60.00 , 5, '2024-07-08', 'delivered', 120.00 , 5, '2024-10-19', 'pending', 900.00 , 6, '2024-09-27', 'delivered', 95.50 , 6, '2024-10-23', 'cancelled', 40.00 ; Exposing it to agents is one server setting, one INSTALL EXTENSION , one least-privilege account, and six extension settings, all run as an administrator: -- vsql-mcp declares VEF preview capabilities. SET PERSIST takes effect at -- once; a server you start yourself can take --vsql allow preview extensions=ON. SET PERSIST vsql allow preview extensions = ON; INSTALL EXTENSION vsql mcp; -- A dedicated least-privilege account the server runs tool queries as. CREATE USER 'mcp'@'127.0.0.1' IDENTIFIED BY 'change-me'; GRANT SELECT ON shop. TO 'mcp'@'127.0.0.1'; SET GLOBAL vsql mcp.db url = 'mysql://mcp:change-me@127.0.0.1:3399'; SET GLOBAL vsql mcp.schema = 'shop'; SET GLOBAL vsql mcp.require auth = ON; SET GLOBAL vsql mcp.bearer token = 'a-long-random-token'; SET GLOBAL vsql mcp.port = 3400; SET GLOBAL vsql mcp.vsql mcp enabled = ON; The db url line is the important one. Queries, EXPLAINs , writes, and a table's CREATE TABLE all reach the database over a loopback connection as that account. Schema and table browsing runs inside the server instead, so it does not need db url . That account's GRANT s are the real access boundary; the read-only and allowlist checks on top are defense in depth. Here the account can read the shop schema and nothing else. The port defaults to 3100; these examples use 3400 because something else already had 3100. The server is now listening: SELECT vsql mcp.info ; {"enabled":true,"http port":3400,"https port":0,"port":3400, "protocol version":"2025-06-18","schema":"shop","sessions active":0,"ssl port":3143} http port and https port are what's actually listening. ssl port is only the configured default 3143 , and https port stays 0 because HTTPS starts only when vsql mcp.ssl cert and vsql mcp.ssl key are set. Registering it with Claude Code is one command, and any client that speaks MCP over Streamable HTTP connects the same way, with a URL and a bearer header. There's no stdio transport, since there's no child process to spawn, so a stdio-only client needs a bridge in front the README https://github.com/villagesql/vsql-mcp covers that, plus the equivalent setup for Codex and Antigravity : claude mcp add --transport http vsql http://127.0.0.1:3400/mcp \ --header "Authorization: Bearer a-long-random-token" That's the whole integration. Ask a question: claude -p "Which customer has the highest total value of delivered orders? \ Use the vsql MCP tools." --allowedTools mcp vsql Carla Reyes has the highest total value of delivered orders: $420.00 . I joined shop.customers to shop.orders , kept only the rows with status = 'delivered' , and summed total for each customer. The top results are: | Customer | Delivered total | |---|---| | Carla Reyes | $420.00 | | Ben Osei | $310.45 | | Grace Liu | $276.25 | A trace of the run shows the agent calling list schemas , list tables , and describe table on both tables before writing the join and running it through the query tool. The database credential never left the server; the client got a URL and a bearer token, and nobody pasted a schema into a prompt. The query tool hands back JSON an agent can reason over. Here's the same question asked through it directly: SELECT c.name, c.country, SUM o.total AS delivered total FROM shop.customers c JOIN shop.orders o ON o.customer id = c.id WHERE o.status = 'delivered' GROUP BY c.id, c.name, c.country ORDER BY delivered total DESC LIMIT 3; { "columns": "name", "country", "delivered total" , "row count": 3, "rows": {"name": "Carla Reyes", "country": "MX", "delivered total": "420.00"}, {"name": "Ben Osei", "country": "GH", "delivered total": "310.45"}, {"name": "Grace Liu", "country": "SG", "delivered total": "276.25"} , "truncated": false } Push on the guardrails Sooner or later an agent asks for something you didn't intend to allow. vsql-mcp answers with a refusal that names the rule it hit, so an agent can read the message and adjust course instead of retrying blind. The query tool accepts a single read-only statement. Asked to run an UPDATE , it answers: only a single read-only statement is allowed by the query tool Writes have their own tool, and it's off by default. Until you set vsql mcp.allow write = ON , the write tool is absent from tools/list entirely, so an agent never plans around a tool it cannot use. With this configuration, tools/list returns five tools. With vsql mcp.schema = 'shop' , a query that reaches for another schema is refused: schema 'mysql' is outside the exposed schema; only 'shop' is available, and table names must be qualified with it You can narrow it further. Setting vsql mcp.allowed tables = 'orders' confines the agent to named tables. The check runs on the statement's actual plan, so a join or subquery that touches an excluded table is caught too: table 'customers' is not in vsql mcp.allowed tables The one place the check doesn't reach is a stored function's body, which EXPLAIN doesn't descend into. The README covers it. The allowlist also governs what an agent can learn, not only what it can read: describe table refuses an excluded table, and list tables omits it entirely. Beyond those, max rows caps every query result and marks it truncated so the agent knows , query timeout bounds each call, and requests without the bearer token get HTTP 401 before any of this runs. The transport also validates the Origin header, as the MCP spec requires; a request claiming to come from a non-local origin gets HTTP 403. The listener binds to 127.0.0.1 only, so exposing it beyond the machine is a decision you make deliberately, with a reverse proxy in front. The README https://github.com/villagesql/vsql-mcp known-limitations lists the remaining boundaries plainly, and the boundary to design around is the grants. Guardrails narrow what an agent can do, and the db url account's grants are what it can never exceed. Watch the agent from SQL Because the MCP server is server state, you can monitor it the way you monitor everything else in MySQL. After the agent calls above: SHOW STATUS LIKE 'vsql mcp%'; +------------------------------+-------+ | Variable name | Value | +------------------------------+-------+ | vsql mcp.http port | 3400 | | vsql mcp.https port | 0 | | vsql mcp.rows returned total | 11 | | vsql mcp.sessions active | 5 | | vsql mcp.tool calls total | 15 | | vsql mcp.tool errors total | 6 | +------------------------------+-------+ The refusals shown above are counted in tool errors total , along with any tool call that fails for ordinary reasons, like malformed SQL. If tool errors spike, something is off on the tool surface, whether that's an agent pushing at the rules or queries that simply fail, and you find out from the database rather than from a sidecar's log file. Make it your own The six tools are a list in the extension's own source, and the extension is GPL-2.0 Rust you can fork and modify. src/tools.rs holds a JSON array that advertises each tool, and a match that routes a call to its handler — so adding one of your own is a few edits. Say you want an agent to see a table's indexes before it writes a join. Advertise it in tool definitions : { "name": "list indexes", "description": "Indexes on a table, one row per indexed column.", "inputSchema": { "type": "object", "properties": { "schema": { "type": "string" }, "table": { "type": "string" } }, "required": "table" } }, Route it in call : js "list indexes" = list indexes args, cfg, exec , Then write the handler. It receives the call arguments, the live configuration, and the executor, and every seam the built-in tools use is available to it: php fn list indexes args: &Json, cfg: &RequestConfig, exec: &dyn QueryExecutor - Result