mysql-neo4j-mcp
by niti007
README.md
# mysql-neo4j-mcp
MCP server exposing scoped read/write tools over a local MySQL database
(`contoso-mysql` container, db `contoso_claims`), a local Neo4j database
(`enterprise_kb_neo4j` container), and Atlassian Jira/Confluence + Bitbucket
(Atlassian Cloud).
## Why MCP instead of connecting directly to these systems?
The alternative is handing every consumer — each agent, script, or
notebook — its own copy of the raw MySQL password, Neo4j password, and
Atlassian API token. That means:
- more places a credential leak can happen (as many as there are
consumers), instead of one;
- no single point that sees every query/call for auditing — you'd have to
aggregate logs from every caller's own environment;
- access boundaries that only hold as well as whoever provisioned each
consumer's credentials bothered to scope them (nothing stops someone
from reusing an admin/root credential because it's convenient);
- rotating a credential means finding and updating every consumer that
embedded it.
This MCP server is the only thing that holds real credentials. It exposes
a small, fixed set of named tools instead of raw SQL/Cypher/API access, so
every call — from Claude Code, Claude Desktop, or any other MCP client —
flows through one process that can enforce scoping (e.g. separate
read-only vs. read-write DB users, transaction-mode access control) and be
logged/audited in one place. Full write-up, including a comparison against
scoping access at the cloud IAM layer instead (Azure Entra ID), is in
[`docs/why-mcp.md`](docs/why-mcp.md).
## Tools
| Tool | System | Access |
|---|---|---|
| `mysql_query(sql, params)` | MySQL | Read-only. Runs as `contoso_ro`, which only has `SELECT` granted. Rejects non-`SELECT` statements in code too. |
| `mysql_execute(sql, params)` | MySQL | Read-write. Runs as `contoso_rw`, which has `SELECT/INSERT/UPDATE/DELETE` — no DDL, no admin grants. |
| `neo4j_query(cypher, params)` | Neo4j | Read-only. Runs in a Neo4j `READ` transaction — the server itself rejects any write clause with an `AccessMode` error. |
| `neo4j_write(cypher, params)` | Neo4j | Read-write. Runs in a `WRITE` transaction. |
| `jira_search(jql, max_results)` | Jira | Read-only. Search issues via JQL. |
| `jira_get_issue(issue_key)` | Jira | Read-only. Fetch a single issue. |
| `jira_create_issue(project_key, summary, issue_type, description)` | Jira | Write. Create an issue. |
| `jira_add_comment(issue_key, comment)` | Jira | Write. Comment on an issue. |
| `confluence_search(cql, limit)` | Confluence | Read-only. Search content via CQL. |
| `confluence_get_page(page_id)` | Confluence | Read-only. Fetch a page's body. |
| `confluence_create_page(space_key, title, html_body, parent_id)` | Confluence | Write. Create a page. |
| `confluence_update_page(page_id, title, html_body, version)` | Confluence | Write. Update a page. |
| `bitbucket_list_prs(workspace, repo_slug, state)` | Bitbucket | Read-only. List pull requests. |
| `bitbucket_get_pr(workspace, repo_slug, pr_id)` | Bitbucket | Read-only. Fetch a pull request. |
| `bitbucket_create_pr(workspace, repo_slug, title, source_branch, destination_branch, description)` | Bitbucket | Write. Open a pull request. |
| `bitbucket_comment_pr(workspace, repo_slug, pr_id, comment)` | Bitbucket | Write. Comment on a pull request. |
Jira/Confluence auth uses an Atlassian API token (email + token, HTTP
Basic auth) against `JIRA_BASE_URL`/`CONFLUENCE_BASE_URL`. Bitbucket auth
uses a separate app password (`BITBUCKET_USERNAME` +
`BITBUCKET_APP_PASSWORD`) against the Bitbucket Cloud API. See
`.env.example` for the full variable list.
### Why Neo4j only has one DB user
Neo4j **Community edition** (what's running here) has no role-based access
control at all — `CREATE ROLE` / `GRANT ROLE` commands are Enterprise-only,
and every authenticated user has full read/write access. Creating a second
"reader" user would be theater: it would have identical permissions to the
writer user.
Instead, read/write separation for Neo4j is enforced by the **transaction
mode** (`execute_read` vs `execute_write`), which the Neo4j server honors
independent of RBAC/edition — verified directly: a write query issued
inside a read transaction is rejected server-side with
`Neo.ClientError.Statement.AccessMode`, even on this single Community
instance with no roles configured.
## Setup
1. Copy `.env.example` to `.env` and fill in credentials (see "Provisioning
DB users" below for how they were created).
2. `python3.12 -m venv .venv && .venv/bin/pip install -r requirements.txt`
(needs Python 3.10+; the `mcp` SDK doesn't support older versions).
3. Run standalone: `.venv/bin/python3 server.py`
4. Register with an MCP client (e.g. Claude Code):
```
claude mcp add mysql-neo4j-mcp -- /Users/tarunsachdeva/dev/mysql-neo4j-mcp/.venv/bin/python3 /Users/tarunsachdeva/dev/mysql-neo4j-mcp/server.py
```
## Provisioning DB users
**MySQL** (connect as root to `contoso-mysql`):
```sql
CREATE USER 'contoso_ro'@'%' IDENTIFIED BY '<password>';
GRANT SELECT ON contoso_claims.* TO 'contoso_ro'@'%';
CREATE USER 'contoso_rw'@'%' IDENTIFIED BY '<password>';
GRANT SELECT, INSERT, UPDATE, DELETE ON contoso_claims.* TO 'contoso_rw'@'%';
FLUSH PRIVILEGES;
```
**Neo4j** (connect via `cypher-shell` as `neo4j` to `enterprise_kb_neo4j`):
```cypher
CREATE USER kb_app SET PASSWORD '<password>' CHANGE NOT REQUIRED;
```
(No role grants — see "Why Neo4j only has one DB user" above.)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues