SQL-MCP-101
Provides tools for interacting with MySQL databases, including schema inspection, query execution, data modification, and safety controls.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@SQL-MCP-101describe the customers table"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
SQL-MCP-101
New to MCP? Start with the interactive tutorial, a click-through walk through tools, resources and prompts, and how to decide which one a feature should be.
A small, heavily-commented MCP server that demonstrates all three Model Context Protocol primitives (tools, resources, and prompts) over a semantic layer, rather than over a raw database schema.
This repository exists to be read, not just run. If you have seen MCP mentioned and want to understand what building a governed server actually involves, this is a complete, working example small enough to read in one sitting: one primitive per file, comments that explain why rather than what, and an NYU academic database behind a semantic layer that hides it.
mcp_server/
├── semantic.py the ENTITY REGISTRY: one source of truth for what exists
├── database.py the only file that knows SQL; every governance control
├── entities.py 10 TOOLS business questions, plus one governed write
├── resources.py 4 RESOURCES content the APPLICATION attaches (+2 templates)
├── prompts.py 6 PROMPTS workflows the USER invokes
├── errors.py turning expected failures into messages a model can act on
└── server.py wires them together, about ten meaningful linesThe idea this repository is built to demonstrate
Do not hand a model your database schema. Give it business entities.
A model that can list your tables and read your columns is a model that has seen your PII column names, has to write its own joins, and breaks the day you rename something. The alternative is a semantic layer:
Model / Client (Claude Desktop, Claude Code, any MCP client)
│
│ business entities only: no SQL, no schema
▼
┌──────────────────────────────────────────────────────────┐
│ SQL-MCP-101 SERVER │
│ semantic.py entity registry, the single source │
│ entities.py business tools, no SQL accepted │
└────────────────────────┬─────────────────────────────────┘
│
│ translates internally
▼
┌──────────────────────────────────────────────────────────┐
│ MySQL: nyu_demo │
│ v_semantic_course_catalog, v_semantic_* (readable) │
│ students, enrollments, employees ... (hidden) │
└──────────────────────────────────────────────────────────┘A model connected to this server never learns that a students table exists,
that it has a passport_num column, or that answering "how many international
graduates are in Computer Science?" needs a three-way join.
Related MCP server: mysql-mcp-server
The other idea worth taking away
Most MCP tutorials only cover tools, which leaves people thinking MCP is tools. It is three primitives, and they differ by who is in control:
Primitive | Who decides | When it happens | Analogy |
Tool | the model | mid-conversation, autonomously | a button it may press |
Resource | the application | up front, chosen by a human | a file you attach |
Prompt | the user | explicitly, from a menu | a saved expert question |
The same content can appear as more than one. Here get_courses is a tool
and nyu://semantic/course-catalog is a resource: the same answer, reached
two ways, because "the model fetches it when it decides it needs it" and "a
human attaches it before starting" are genuinely different needs.
Quick start
git clone https://github.com/Khushboo-Mishra/SQL-MCP-101.git
cd SQL-MCP-101
bash scripts/setup.shsetup.sh checks prerequisites, creates the virtualenv, installs the two
dependencies, creates the demo database, and verifies the server end to end. It
stops with a specific message at the first thing that is missing.
Then see all three primitives in one pass:
bash scripts/run_explorer.shRequirements
Python 3.10+
MySQL 8.x running locally (
brew services start mysql)Node.js: optional, only for the MCP Inspector
Ollama: optional, only for the UI's Chat panel
Defaults to root on 127.0.0.1:3306 with no password, which is the Homebrew
default, so most people change nothing. Otherwise export MYSQL_USER, MYSQL_PASSWORD,
MYSQL_HOST, MYSQL_PORT.
What gets built
10 tools, 4 resources + 2 URI templates, and 6 prompts, over seven semantic views that hide nine raw tables.
Tools: the model calls these
entities.py: business questions. Every argument is a business word. Not one
of them is a table, a column, or a query.
Tool | Answers |
| What can I ask about? Returns the business catalogue. |
| The course catalogue, filtered by department, level, international eligibility, enrolment status. |
| Student numbers per department, by degree level and residency. Counts only. |
| How many students are on each course, by term. Never a roster. |
| Publication counts and citation totals, by department and year. |
| Awards granted, aggregated by department, year and award name. |
| Competition finalists, counted by competition, year and department. |
| Staff headcount by department and role. No names, no salaries. |
| What this server has been asked to do, as business intent. |
One tool can change data, registered separately so the write surface stays small and obvious:
Tool | Does |
| Opens, waitlists or closes enrolment on one course. One field, three allowed values. |
There is deliberately no run_query and no execute_statement. A
caller cannot express a statement the server did not design, so there is
nothing to inject into and no schema to know.
Resources: the application attaches these
resources.py: business vocabulary, not schema dumps.
URI | Type | Content |
| Markdown | the semantic dictionary, and what is deliberately unavailable |
| JSON | the same catalogue, structured |
| JSON | every course |
| Markdown | what the business terms mean |
| Markdown | one entity's definition (templated) |
| JSON | one department: courses, headcount, staffing, research (templated) |
Prompts: the user invokes these
prompts.py: saved expert questions. Note that none of them names a view, a
table or a column: they name tools and let the semantic layer decide the
rest.
Prompt | Starts |
| A rounded review: size, teaching, demand, staffing, research. |
| How much of a department's catalogue is actually open to international students. |
| Any business question, grounded in |
| Finds pressure points: waitlisted courses with high demand, open courses with none. |
| Compares output across departments, with the caveats stated. |
| A guided first look, tailored to a role. |
Deciding: tool, resource, or prompt?
The question people get stuck on. Work through it in this order.
1. Does it perform an action, or fetch something the model chooses? → Tool. Anything the model should be able to decide to do on its own.
2. Is it a document a human would sensibly attach before starting? → Resource. Reference material, whole-schema context, anything stable.
3. Is it a task someone repeats, where the way you ask is the expertise? → Prompt. Ship the good question instead of expecting rediscovery.
Two heuristics that resolve most remaining doubt:
Who initiates? Model → tool. Application → resource. User → prompt.
Would you want this in a menu? If yes, it is a prompt. Menus are for people, and only prompts are surfaced to people as commands.
Worked examples from this repo
Feature | Choice | Why |
Student numbers for a department | tool | the model picks the filters mid-reasoning, unpredictably |
The course catalogue | both | tool for the model; resource for a human to attach up front |
Departmental review | prompt | a repeatable task where knowing what to ask is the value |
The business vocabulary | resource | passive reference, no decision required |
Opening or closing enrolment | tool | an action, and the only one that writes |
Where people get it wrong
Everything as tools. Works, but the model burns calls fetching context a human could have attached once, and users get no discoverable entry points.
Resources for things that need arguments the model picks. If the model decides the parameter, it is a tool.
Prompts that do work. A prompt returns text. If you find yourself querying the database inside a prompt, you wanted a tool.
Schema leaking through any of the three. A tool that takes a table name, a resource that publishes column types, or a prompt that names a view. The last is the easiest to write by accident, and it breaks the day that view is renamed. Name the tool and let the semantic layer decide the rest.
The demo database
nyu_demo has two layers, and the split between them is what the server enforces.
Nine raw tables hold the real data: colleges, departments, courses,
students, employees, enrollments, publications, rewards, finalists.
students carries ssn, passport_num and visa_type; employees carries
salary. The MCP server never reads any of them directly and never names one
to a model.
Seven semantic views are the only objects the server may query. Each is pre-joined, pre-aggregated and PII-free:
View | Exposes |
| course, level, department, college, credits, international eligibility, enrolment status |
| student counts by department, degree level, residency |
| enrolment counts by course and term |
| publications and citations by department and year |
| awards granted and totals by department and year |
| finalist counts by competition, year, department |
| staff headcount by department and role |
Refactor a raw table and you fix one view. Nothing the model knows changes.
Running it
The explorer: every primitive in one pass
bash scripts/run_explorer.shPrints the initialize handshake, then lists and exercises tools, resources
(static and templated), and prompts. Run this first, it confirms the setup
works and shows the entire protocol surface in one screenful.
The web UI: all three primitives in a browser
bash scripts/run_ui.sh # http://127.0.0.1:8000
PORT=9000 bash scripts/run_ui.shFour panels, one per thing worth showing:
Panel | What it demonstrates |
Chat | ask in plain English; every tool the model chose is listed inline above the answer |
Tools | all 12, grouped by blast radius, each callable from a form |
Resources | static and templated, readable in place |
Prompts | expand one to see the text, or send it straight to the chat |
A live Activity strip along the bottom shows the real JSON-RPC underneath,
tools/call, resources/read, prompts/get, so the protocol is visible the
whole time.
The page is itself an MCP client: it has no access to MySQL of its own. Everything on screen arrived through the same protocol Claude Desktop uses.
Chat needs a local LLM via Ollama, free, no API key, and nothing leaves the machine:
brew install ollama && ollama serve
ollama pull qwen2.5:7bSet ANTHROPIC_API_KEY instead and it switches to the Claude API automatically.
The Tools, Resources and Prompts panels work with no LLM at all.
The MCP Inspector: Anthropic's own client
bash scripts/run_inspector.shOpen the printed http://localhost:6274?... URL; the token is required. It has
separate Tools, Resources and Prompts tabs. None of it is our code,
so if the Inspector drives the server, the server is spec-compliant.
Try get_courses with Computer Science, read nyu://semantic/entities, and
expand the analyze_department prompt.
Claude Desktop / Claude Code
bash scripts/add_to_claude_desktop.sh # Claude Desktop, run from Terminal.app
bash scripts/install_claude.sh # Claude Code, safe to run anywhereadd_to_claude_desktop.sh backs up your config, preserves any servers already
registered, validates the JSON, smoke-tests the exact launch command, and
relaunches the app.
Then ask "Review the Physics department", or pick analyze_department from
the prompt menu. Asking for student names and email addresses is the quickest
way to see the semantic layer refuse something.
--desktopmust be run from Terminal.app, not from inside Claude Desktop. Claude Desktop holds its config in memory and rewrites the file from that copy, so an edit made while it is running is silently discarded. The script quits the app, edits, and relaunches, which would kill the session you launched it from.
Reading the code
The repo is meant to be read. This order builds up without forward references:
# | File | What it shows |
1 |
| The whole architecture on one screen: verify, register three primitives, run. |
2 |
| The entity registry. Tools, resources and the catalogue are all generated from it, so they cannot drift apart. |
3 |
|
|
4 |
| Static URIs versus templated ones, and why the course catalogue is deliberately both a tool and a resource. |
5 |
| Prompts return text, not data, and name tools rather than views. |
6 |
| The only file that knows SQL, and all six governance controls. |
7 |
| The other side of the protocol: a minimal client, so you can see what crosses the wire. |
Every file opens with a docstring explaining why it is shaped the way it is.
Going further
This server is scoped to one database to keep the examples short. To take it further:
More entities: add a row to
ENTITIESinsemantic.pyand a matchingv_semantic_*view. The tool, the catalogue entry and the dictionary all follow from that one definition.Do not add a raw SQL tool. It is the obvious next step and it undoes the design: the server would need credentials that read your tables, the model would need the schema to write against, and results would enter its context unfiltered. If a question cannot be expressed as an entity plus filters, add the entity.
Remote transport:
mcp.run(transport="streamable-http"). Same tools, same code, different pipe. Add authentication before exposing it.Caching: every entity tool hits the database on every call. A short TTL cache is worth it once a model starts calling it in a loop.
Security notes
This server can change your data, though only in one specific way: it can open, waitlist or close enrolment on a single course. That is deliberate. "Can an agent write to my database?" is the question every team asks, and a narrow working example is more useful than avoiding the subject.
The six controls
Semantic layer only. Every query targets a
v_semantic_*view drawn from the registry insemantic.py. A view not on the allowlist is refused before a connection is opened, so the raw tables are unreachable rather than merely undocumented.No SQL from callers, ever. There is no
run_queryand noexecute_statement. Callers name an entity and pass filters; the statement is built indatabase.pyfrom the registry, with values bound as parameters.Filters are an allowlist. Filter names map to columns through the registry, and filters with a fixed value set reject anything else by name. A caller cannot reach a column nobody chose to expose.
PII guard at startup.
verify_semantic_layer()reads every exposed view at boot and raises if one has grown a forbidden field, so a careless view edit is a startup crash rather than a quiet leak months later.Row cap. A ceiling on rows returned, with truncation reported rather than hidden.
Audit log of intent. Every call is recorded as the business action and its parameters, never as SQL, so the log itself is safe to show a user.
The one write, set_course_enrollment_status, changes a single field on a
single course, validates the status against a fixed set, and verifies the
result afterwards.
Point it at a restricted user
The controls above are defense in depth, not the defense. In anything beyond
a demo, connect as a MySQL user granted SELECT on the semantic views only. If
the credentials cannot reach a raw table, neither can a prompt injection or a
model mistake.
Two more things worth stating plainly
The MySQL grant is the real boundary. Grant
SELECTon thev_semantic_*views and nothing else. Then a bug in this code cannot reach a raw table, because the credentials cannot.The views are a security control, not a convenience. A column left out of a view is not hidden by policy, it is absent from the result. No query against that view returns it, whatever the model asks for.
License
MIT, see LICENSE.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables interaction with MySQL databases through MCP, supporting query execution, table operations (insert, update, delete), and schema inspection for natural language database management.121MIT
- AlicenseNot gradedqualityDmaintenanceEnables MySQL database operations through MCP, including executing SQL queries, listing databases and tables, and describing table structures.4545MIT
- AlicenseNot gradedqualityDmaintenanceEnables natural language interaction with MySQL databases through MCP, supporting SQL execution, schema exploration, and database management via tools, resources, and prompts.5MIT
- AlicenseNot gradedqualityCmaintenanceEnables natural language interaction with MySQL databases through MCP tools for querying, executing DDL/DML, listing databases/tables, and describing table schemas, with parameterized queries and read-only mode.454MIT
Related MCP Connectors
GibsonAI MCP server: manage your databases with natural language
Connect to PlanetScale databases, branches, schema, query insights, and execute SQL
MCP server for managing Prisma Postgres.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Khushboo-Mishra/SQL-MCP-101'
If you have feedback or need assistance with the MCP directory API, please join our Discord server