cubrid-mcp-server 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cubrid_mcp_server-0.4.0/LICENSE +21 -0
- cubrid_mcp_server-0.4.0/NOTICE +8 -0
- cubrid_mcp_server-0.4.0/PKG-INFO +326 -0
- cubrid_mcp_server-0.4.0/README.md +285 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/__init__.py +3 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/__main__.py +4 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/audit.py +200 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/config.py +175 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/context.py +130 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/database.py +343 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/py.typed +0 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/safety.py +167 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server/server.py +656 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server.egg-info/PKG-INFO +326 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server.egg-info/SOURCES.txt +29 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server.egg-info/dependency_links.txt +1 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server.egg-info/entry_points.txt +2 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server.egg-info/requires.txt +12 -0
- cubrid_mcp_server-0.4.0/cubrid_mcp_server.egg-info/top_level.txt +1 -0
- cubrid_mcp_server-0.4.0/pyproject.toml +111 -0
- cubrid_mcp_server-0.4.0/setup.cfg +4 -0
- cubrid_mcp_server-0.4.0/tests/test_audit.py +251 -0
- cubrid_mcp_server-0.4.0/tests/test_config.py +204 -0
- cubrid_mcp_server-0.4.0/tests/test_context.py +158 -0
- cubrid_mcp_server-0.4.0/tests/test_coverage_gaps.py +199 -0
- cubrid_mcp_server-0.4.0/tests/test_database.py +416 -0
- cubrid_mcp_server-0.4.0/tests/test_edge_cases.py +309 -0
- cubrid_mcp_server-0.4.0/tests/test_integration.py +146 -0
- cubrid_mcp_server-0.4.0/tests/test_safety.py +107 -0
- cubrid_mcp_server-0.4.0/tests/test_server.py +599 -0
- cubrid_mcp_server-0.4.0/tests/test_write_mode.py +475 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Yeongseon Choe, Gyeongjun Paik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
cubrid-mcp-server
|
|
2
|
+
Copyright (c) 2025-2026 Yeongseon Choe, Gyeongjun Paik
|
|
3
|
+
|
|
4
|
+
This product is licensed under the MIT License (see LICENSE).
|
|
5
|
+
|
|
6
|
+
This package is an original implementation. It contains no third-party
|
|
7
|
+
source code and embeds no third-party assets. Runtime dependencies and
|
|
8
|
+
their licenses are enumerated in THIRD_PARTY_LICENSES.md.
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cubrid-mcp-server
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Model Context Protocol server for CUBRID database
|
|
5
|
+
Author-email: Yeongseon Choe <yeongseon.choe@gmail.com>, Gyeongjun Paik <paikend@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cubrid-lab/cubrid-mcp-server
|
|
8
|
+
Project-URL: Repository, https://github.com/cubrid-lab/cubrid-mcp-server
|
|
9
|
+
Project-URL: Issues, https://github.com/cubrid-lab/cubrid-mcp-server/issues
|
|
10
|
+
Project-URL: Documentation, https://cubrid-lab.github.io/cubrid-mcp-server/
|
|
11
|
+
Project-URL: Changelog, https://github.com/cubrid-lab/cubrid-mcp-server/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: CUBRID,database,MCP,Model Context Protocol,LLM
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Framework :: AsyncIO
|
|
17
|
+
Classifier: Programming Language :: Python
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Database
|
|
24
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
License-File: NOTICE
|
|
29
|
+
Requires-Dist: fastmcp<4,>=3.0
|
|
30
|
+
Requires-Dist: pycubrid<2,>=1.4
|
|
31
|
+
Requires-Dist: sqlparse<1,>=0.5
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.2; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
|
|
35
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
36
|
+
Requires-Dist: ruff==0.16.6; extra == "dev"
|
|
37
|
+
Requires-Dist: mypy==2.3.1; extra == "dev"
|
|
38
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
39
|
+
Requires-Dist: tox; extra == "dev"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# cubrid-mcp-server
|
|
43
|
+
|
|
44
|
+
[](https://codecov.io/gh/cubrid-lab/cubrid-mcp-server)
|
|
45
|
+
|
|
46
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for [CUBRID](https://www.cubrid.org/), enabling LLMs to safely inspect schemas and execute read-only queries via [pycubrid](https://pypi.org/project/pycubrid/).
|
|
47
|
+
|
|
48
|
+
<!-- mcp-name: io.github.cubrid-lab/cubrid-mcp-server -->
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
| Tool | Description |
|
|
53
|
+
|------|-------------|
|
|
54
|
+
| `all_table_names` | List every user table in the database |
|
|
55
|
+
| `filter_table_names` | Substring search over table names |
|
|
56
|
+
| `schema_definitions` | Column types, nullability, defaults, and primary key info |
|
|
57
|
+
| `describe_table` | Full metadata: columns, primary key, and indexes in one call |
|
|
58
|
+
| `list_indexes` | Indexes for a table with key columns and flags |
|
|
59
|
+
| `explain_query` | Execution plan/trace for a `SELECT`/`WITH` (via CUBRID `SHOW TRACE`) |
|
|
60
|
+
| `table_row_counts` | `COUNT(*)` for one or many tables |
|
|
61
|
+
| `list_serials` | CUBRID `SERIAL` sequences with current value and bounds |
|
|
62
|
+
| `list_class_hierarchy` | CUBRID `CLASS` inheritance relationships |
|
|
63
|
+
| `execute_query` | Run read-only SQL with automatic output truncation |
|
|
64
|
+
| `health_check` | Verify database connectivity on demand |
|
|
65
|
+
| `execute_write` | Run a single `INSERT`/`UPDATE`/`DELETE` in an atomic transaction (**only registered when opt-in write mode is enabled**) |
|
|
66
|
+
|
|
67
|
+
### Resources
|
|
68
|
+
|
|
69
|
+
Schema metadata is also exposed as read-only [MCP Resources](https://modelcontextprotocol.io/docs/concepts/resources), so clients can discover and read schema context without a tool call. Resources reuse the same read-only catalog queries as the tools — no additional data access or write surface.
|
|
70
|
+
|
|
71
|
+
| Resource URI | Description |
|
|
72
|
+
|--------------|-------------|
|
|
73
|
+
| `cubrid://schema` | Whole-schema index: every user table with its per-table resource URI |
|
|
74
|
+
| `cubrid://schema/{table}` | Per-table metadata (columns, primary key, indexes) — mirrors `describe_table` |
|
|
75
|
+
|
|
76
|
+
Both return `application/json`. Table names in `{table}` are percent-decoded by URI-template matching; an unknown or system table produces a resource-read error, matching the `describe_table` tool.
|
|
77
|
+
|
|
78
|
+
## Prompts
|
|
79
|
+
|
|
80
|
+
The server also exposes a small set of **MCP Prompt templates** — reusable, guided
|
|
81
|
+
starting points for common inspection tasks. Prompts are **guidance-only**: each one
|
|
82
|
+
returns text that tells the client which of the existing read-only tools to call and
|
|
83
|
+
in what order. They never touch the database, execute SQL, or add any new data-access
|
|
84
|
+
surface, and any argument you pass is fenced and treated strictly as untrusted data.
|
|
85
|
+
The prompts are advisory templates only — the actual read-only enforcement remains in
|
|
86
|
+
the underlying tools (`execute_query`/`explain_query` via `safety.py`).
|
|
87
|
+
|
|
88
|
+
| Prompt | Arguments | Description |
|
|
89
|
+
|--------|-----------|-------------|
|
|
90
|
+
| `summarize_table` | `table` | Describe a table, then sample it with a bounded read-only query |
|
|
91
|
+
| `explain_query` | `sql` | Obtain and interpret a `SELECT`/`WITH` execution plan via `explain_query` |
|
|
92
|
+
| `inspect_schema` | _(none)_ | Build a high-level overview of the whole schema from the read-only tools |
|
|
93
|
+
| `find_index_candidates` | `table` | Review a table's index coverage for potential review areas |
|
|
94
|
+
|
|
95
|
+
## Quick Start
|
|
96
|
+
|
|
97
|
+
### Configure
|
|
98
|
+
|
|
99
|
+
Set the required environment variables:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
export CUBRID_HOST=localhost
|
|
103
|
+
export CUBRID_PORT=33000 # optional, default: 33000
|
|
104
|
+
export CUBRID_USER=readonly_user # a CUBRID user with SELECT-only grants (see Security)
|
|
105
|
+
export CUBRID_PASSWORD=secret
|
|
106
|
+
export CUBRID_DATABASE=mydb
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Optional settings:
|
|
110
|
+
|
|
111
|
+
| Variable | Default | Description |
|
|
112
|
+
|----------|---------|-------------|
|
|
113
|
+
| `CUBRID_MCP_READONLY` | `1` | Enforce read-only SQL whitelist |
|
|
114
|
+
| `CUBRID_MCP_MAX_CHARS` | `4000` | Max characters in query output |
|
|
115
|
+
| `CUBRID_MCP_MAX_ROWS` | `1000` | Max rows returned by `execute_query` before truncation |
|
|
116
|
+
| `CUBRID_MCP_MAX_SQL_LENGTH` | `65536` | Max length (characters) of a submitted SQL statement |
|
|
117
|
+
| `CUBRID_MCP_QUERY_TIMEOUT` | `30` | Per-statement socket read timeout in seconds. If the server sends no data within this window the query is aborted and the connection is reset. This is a socket read timeout, not a true server-side statement timeout. |
|
|
118
|
+
| `CUBRID_MCP_AUDIT_LOG` | `0` | Opt-in audit logging. When enabled, emits one redaction-safe JSON record per executed statement (`execute_query`/`explain_query`/`execute_write`) to **stderr** — see [Logging](#logging). Honoured per connection (`CUBRID_<NAME>_MCP_AUDIT_LOG`). |
|
|
119
|
+
| `CUBRID_MCP_WRITE` | `0` | Opt-in write mode. When enabled (`1`), registers the `execute_write` tool for single-statement DML. Honoured per connection (`CUBRID_<NAME>_MCP_WRITE`); the tool is registered when any connection enables it. Off by default. |
|
|
120
|
+
|
|
121
|
+
### Multiple connections
|
|
122
|
+
|
|
123
|
+
By default the bare `CUBRID_*` variables define a single connection named `default`.
|
|
124
|
+
You can serve additional CUBRID databases from the same process by listing extra
|
|
125
|
+
connection names in `CUBRID_CONNECTIONS` (comma-separated) and providing
|
|
126
|
+
`CUBRID_<NAME>_*` variables for each. Every tool accepts an optional `connection`
|
|
127
|
+
argument selecting which connection to target; omitting it (or passing `default`)
|
|
128
|
+
uses the bare-variable connection, so existing single-database setups are unchanged.
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
# Default connection (unchanged)
|
|
132
|
+
export CUBRID_HOST=localhost
|
|
133
|
+
export CUBRID_USER=readonly_user
|
|
134
|
+
export CUBRID_PASSWORD=secret
|
|
135
|
+
export CUBRID_DATABASE=mydb
|
|
136
|
+
|
|
137
|
+
# Additional named connections
|
|
138
|
+
export CUBRID_CONNECTIONS=reporting,analytics
|
|
139
|
+
|
|
140
|
+
export CUBRID_REPORTING_HOST=reporting-db
|
|
141
|
+
export CUBRID_REPORTING_USER=readonly_user
|
|
142
|
+
export CUBRID_REPORTING_PASSWORD=secret
|
|
143
|
+
export CUBRID_REPORTING_DATABASE=reports
|
|
144
|
+
export CUBRID_REPORTING_MCP_MAX_ROWS=500 # optional per-connection tuning
|
|
145
|
+
|
|
146
|
+
export CUBRID_ANALYTICS_HOST=analytics-db
|
|
147
|
+
export CUBRID_ANALYTICS_USER=readonly_user
|
|
148
|
+
export CUBRID_ANALYTICS_PASSWORD=secret
|
|
149
|
+
export CUBRID_ANALYTICS_DATABASE=analytics
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Notes:
|
|
153
|
+
|
|
154
|
+
- Connection names must match `[A-Za-z0-9_]+` and are matched case-insensitively.
|
|
155
|
+
- `default` is reserved (it always comes from the bare `CUBRID_*` variables) and
|
|
156
|
+
cannot appear in `CUBRID_CONNECTIONS`.
|
|
157
|
+
- For a named connection `<NAME>`, connection fields live at `CUBRID_<NAME>_HOST`
|
|
158
|
+
etc. and the optional MCP tuning knobs at `CUBRID_<NAME>_MCP_*` (same suffixes as
|
|
159
|
+
the global ones). Named connections do **not** inherit values from the bare vars.
|
|
160
|
+
- Selecting an unknown connection returns a clear error listing the available names.
|
|
161
|
+
- Each connection has its own read-only enforcement, so a named connection can set
|
|
162
|
+
`CUBRID_<NAME>_MCP_READONLY` independently of the default.
|
|
163
|
+
|
|
164
|
+
### Run
|
|
165
|
+
|
|
166
|
+
### Run from PyPI
|
|
167
|
+
|
|
168
|
+
Use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to run directly from PyPI:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uvx cubrid-mcp-server
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Or with `pipx`:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
pipx run cubrid-mcp-server
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Run from source
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
git clone https://github.com/cubrid-lab/cubrid-mcp-server.git
|
|
184
|
+
cd cubrid-mcp-server
|
|
185
|
+
python -m venv .venv && source .venv/bin/activate
|
|
186
|
+
pip install -e .
|
|
187
|
+
cubrid-mcp-server
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## MCP Client Integration
|
|
191
|
+
|
|
192
|
+
### Claude Desktop
|
|
193
|
+
|
|
194
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"mcpServers": {
|
|
199
|
+
"cubrid": {
|
|
200
|
+
"command": "uvx",
|
|
201
|
+
"args": ["cubrid-mcp-server"],
|
|
202
|
+
"env": {
|
|
203
|
+
"CUBRID_HOST": "localhost",
|
|
204
|
+
"CUBRID_USER": "readonly_user",
|
|
205
|
+
"CUBRID_PASSWORD": "secret",
|
|
206
|
+
"CUBRID_DATABASE": "mydb"
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Claude Code
|
|
214
|
+
|
|
215
|
+
Add to `.mcp.json` in your project root:
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"mcpServers": {
|
|
220
|
+
"cubrid": {
|
|
221
|
+
"command": "uvx",
|
|
222
|
+
"args": ["cubrid-mcp-server"],
|
|
223
|
+
"env": {
|
|
224
|
+
"CUBRID_HOST": "localhost",
|
|
225
|
+
"CUBRID_USER": "readonly_user",
|
|
226
|
+
"CUBRID_PASSWORD": "secret",
|
|
227
|
+
"CUBRID_DATABASE": "mydb"
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Cursor
|
|
235
|
+
|
|
236
|
+
Add to `.cursor/mcp.json`:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"mcpServers": {
|
|
241
|
+
"cubrid": {
|
|
242
|
+
"command": "uvx",
|
|
243
|
+
"args": ["cubrid-mcp-server"],
|
|
244
|
+
"env": {
|
|
245
|
+
"CUBRID_HOST": "localhost",
|
|
246
|
+
"CUBRID_USER": "readonly_user",
|
|
247
|
+
"CUBRID_PASSWORD": "secret",
|
|
248
|
+
"CUBRID_DATABASE": "mydb"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Security
|
|
256
|
+
|
|
257
|
+
The server is **read-only by default**. A code-level SQL whitelist allows only `SELECT`, `SHOW`, `DESC`, `DESCRIBE`, `EXPLAIN`, and `WITH` statements. Multi-statement queries are rejected.
|
|
258
|
+
|
|
259
|
+
> **The SQL whitelist is defense-in-depth, not a security boundary.** It is a non-validating parser-based guardrail against obvious mistakes. The real enforcement layer is the database itself: **always run the server as a CUBRID user that has only `SELECT` grants** on the tables the model may read. See [`SECURITY.md`](./SECURITY.md).
|
|
260
|
+
|
|
261
|
+
For production use, also configure a read-only database user. See [`SECURITY.md`](./SECURITY.md) for the recommended setup.
|
|
262
|
+
|
|
263
|
+
### Write mode (opt-in)
|
|
264
|
+
|
|
265
|
+
Write access is **disabled by default**. Setting `CUBRID_MCP_WRITE=1` registers an additional `execute_write` tool that accepts a **single** `INSERT`, `UPDATE`, or `DELETE` statement and runs it in an explicit transaction (commit on success, rollback on any error). When write mode is off the tool is not registered at all, so no write path is exposed in MCP capability discovery. With multiple connections configured, the tool is registered when **any** connection enables writes (via `CUBRID_MCP_WRITE` or `CUBRID_<NAME>_MCP_WRITE`).
|
|
266
|
+
|
|
267
|
+
Constraints and rationale:
|
|
268
|
+
|
|
269
|
+
- **Single-statement DML only.** Standalone reads, DDL (`CREATE`/`ALTER`/`DROP`/`TRUNCATE`), transaction-control, and multi-statement input are rejected. (A single DML statement may still legally contain subqueries, e.g. `INSERT ... SELECT`.)
|
|
270
|
+
- **DDL is intentionally unsupported.** CUBRID auto-commits DDL, which defeats the rollback guarantee, so it is excluded from write mode.
|
|
271
|
+
- **Write mode is per-connection.** `execute_write` accepts the same optional `connection` argument as the read tools and runs against that connection; a connection whose `CUBRID_<NAME>_MCP_WRITE` is off refuses the write even when another connection enables it.
|
|
272
|
+
- `execute_query` remains **read-only regardless** of the write-mode flag.
|
|
273
|
+
- Enforcement is defense-in-depth; still run the server as a CUBRID user granted only the privileges it needs. See [`SECURITY.md`](./SECURITY.md).
|
|
274
|
+
|
|
275
|
+
## Logging
|
|
276
|
+
|
|
277
|
+
The server speaks the MCP **stdio transport**, where `stdout` carries the JSON-RPC protocol stream. Anything written to `stdout` by the server or its dependencies will corrupt that stream and break the client connection. For this reason **all logging is routed to `stderr`**, and you should keep it that way: when adding custom logging or diagnostics, never `print()` to `stdout` — use the standard `logging` module (which is configured to emit on `stderr`) or write to `stderr` explicitly. The log level defaults to `INFO`.
|
|
278
|
+
|
|
279
|
+
Errors surfaced back to the LLM client are **sanitized**: only the exception category (e.g. `query failed: OperationalError`) is returned, while the full exception detail is logged to `stderr` for operators. This keeps schema details, hostnames, SQL fragments, and configuration values out of client-visible messages.
|
|
280
|
+
|
|
281
|
+
### Audit logging (opt-in)
|
|
282
|
+
|
|
283
|
+
Set `CUBRID_MCP_AUDIT_LOG=1` to record every executed statement (`execute_query`, `explain_query`, and `execute_write`) as a single structured JSON line on **stderr**. It is **off by default** and honoured **per connection** — a named connection sets `CUBRID_<NAME>_MCP_AUDIT_LOG` independently of the default. Each record is redaction-safe and contains only:
|
|
284
|
+
|
|
285
|
+
| Field | Description |
|
|
286
|
+
|-------|-------------|
|
|
287
|
+
| `tool` | The MCP tool that ran the statement (`execute_query` / `explain_query` / `execute_write`). |
|
|
288
|
+
| `status` | `ok` or `error`. |
|
|
289
|
+
| `category` | The leading SQL keyword only (e.g. `SELECT`, `WITH`) — never the full statement. |
|
|
290
|
+
| `identifiers` | Table names extracted after `FROM`/`JOIN` via a strict identifier regex; anything that is not a bare identifier (values, literals, expressions) is dropped. |
|
|
291
|
+
| `sql_length` | Length of the submitted SQL, in characters. |
|
|
292
|
+
| `row_count`, `truncated` | Result size and whether output was truncated (success only). |
|
|
293
|
+
| `duration_ms` | Wall-clock duration of the call, in integer milliseconds. |
|
|
294
|
+
| `error_type` | On failure, the exception **class name only** (via the same sanitization as client-facing errors). |
|
|
295
|
+
|
|
296
|
+
The **raw SQL text, bound parameters, and literal values are never logged**, so secrets embedded in a query (e.g. `WHERE token = '...'`) do not reach the audit stream. Records go to `stderr` only and never to `stdout`.
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
## Development
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
git clone https://github.com/cubrid-lab/cubrid-mcp-server.git
|
|
303
|
+
cd cubrid-mcp-server
|
|
304
|
+
python -m venv .venv && source .venv/bin/activate
|
|
305
|
+
pip install -e ".[dev]"
|
|
306
|
+
|
|
307
|
+
# Lint & type check
|
|
308
|
+
ruff check .
|
|
309
|
+
mypy cubrid_mcp_server
|
|
310
|
+
|
|
311
|
+
# Unit tests
|
|
312
|
+
pytest -m "not integration"
|
|
313
|
+
|
|
314
|
+
# Integration tests (requires running CUBRID)
|
|
315
|
+
export CUBRID_HOST=localhost CUBRID_USER=dba CUBRID_PASSWORD="" CUBRID_DATABASE=demodb
|
|
316
|
+
pytest -m integration
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Disclaimer
|
|
320
|
+
|
|
321
|
+
> This project is part of [CUBRID Lab](https://github.com/cubrid-lab), an independent open-source initiative for CUBRID developer tooling, and is not affiliated with, sponsored by, or endorsed by CUBRID Corporation or the official CUBRID project.
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
## License
|
|
325
|
+
|
|
326
|
+
MIT (see [`LICENSE`](./LICENSE)).
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# cubrid-mcp-server
|
|
2
|
+
|
|
3
|
+
[](https://codecov.io/gh/cubrid-lab/cubrid-mcp-server)
|
|
4
|
+
|
|
5
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server for [CUBRID](https://www.cubrid.org/), enabling LLMs to safely inspect schemas and execute read-only queries via [pycubrid](https://pypi.org/project/pycubrid/).
|
|
6
|
+
|
|
7
|
+
<!-- mcp-name: io.github.cubrid-lab/cubrid-mcp-server -->
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
| Tool | Description |
|
|
12
|
+
|------|-------------|
|
|
13
|
+
| `all_table_names` | List every user table in the database |
|
|
14
|
+
| `filter_table_names` | Substring search over table names |
|
|
15
|
+
| `schema_definitions` | Column types, nullability, defaults, and primary key info |
|
|
16
|
+
| `describe_table` | Full metadata: columns, primary key, and indexes in one call |
|
|
17
|
+
| `list_indexes` | Indexes for a table with key columns and flags |
|
|
18
|
+
| `explain_query` | Execution plan/trace for a `SELECT`/`WITH` (via CUBRID `SHOW TRACE`) |
|
|
19
|
+
| `table_row_counts` | `COUNT(*)` for one or many tables |
|
|
20
|
+
| `list_serials` | CUBRID `SERIAL` sequences with current value and bounds |
|
|
21
|
+
| `list_class_hierarchy` | CUBRID `CLASS` inheritance relationships |
|
|
22
|
+
| `execute_query` | Run read-only SQL with automatic output truncation |
|
|
23
|
+
| `health_check` | Verify database connectivity on demand |
|
|
24
|
+
| `execute_write` | Run a single `INSERT`/`UPDATE`/`DELETE` in an atomic transaction (**only registered when opt-in write mode is enabled**) |
|
|
25
|
+
|
|
26
|
+
### Resources
|
|
27
|
+
|
|
28
|
+
Schema metadata is also exposed as read-only [MCP Resources](https://modelcontextprotocol.io/docs/concepts/resources), so clients can discover and read schema context without a tool call. Resources reuse the same read-only catalog queries as the tools — no additional data access or write surface.
|
|
29
|
+
|
|
30
|
+
| Resource URI | Description |
|
|
31
|
+
|--------------|-------------|
|
|
32
|
+
| `cubrid://schema` | Whole-schema index: every user table with its per-table resource URI |
|
|
33
|
+
| `cubrid://schema/{table}` | Per-table metadata (columns, primary key, indexes) — mirrors `describe_table` |
|
|
34
|
+
|
|
35
|
+
Both return `application/json`. Table names in `{table}` are percent-decoded by URI-template matching; an unknown or system table produces a resource-read error, matching the `describe_table` tool.
|
|
36
|
+
|
|
37
|
+
## Prompts
|
|
38
|
+
|
|
39
|
+
The server also exposes a small set of **MCP Prompt templates** — reusable, guided
|
|
40
|
+
starting points for common inspection tasks. Prompts are **guidance-only**: each one
|
|
41
|
+
returns text that tells the client which of the existing read-only tools to call and
|
|
42
|
+
in what order. They never touch the database, execute SQL, or add any new data-access
|
|
43
|
+
surface, and any argument you pass is fenced and treated strictly as untrusted data.
|
|
44
|
+
The prompts are advisory templates only — the actual read-only enforcement remains in
|
|
45
|
+
the underlying tools (`execute_query`/`explain_query` via `safety.py`).
|
|
46
|
+
|
|
47
|
+
| Prompt | Arguments | Description |
|
|
48
|
+
|--------|-----------|-------------|
|
|
49
|
+
| `summarize_table` | `table` | Describe a table, then sample it with a bounded read-only query |
|
|
50
|
+
| `explain_query` | `sql` | Obtain and interpret a `SELECT`/`WITH` execution plan via `explain_query` |
|
|
51
|
+
| `inspect_schema` | _(none)_ | Build a high-level overview of the whole schema from the read-only tools |
|
|
52
|
+
| `find_index_candidates` | `table` | Review a table's index coverage for potential review areas |
|
|
53
|
+
|
|
54
|
+
## Quick Start
|
|
55
|
+
|
|
56
|
+
### Configure
|
|
57
|
+
|
|
58
|
+
Set the required environment variables:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export CUBRID_HOST=localhost
|
|
62
|
+
export CUBRID_PORT=33000 # optional, default: 33000
|
|
63
|
+
export CUBRID_USER=readonly_user # a CUBRID user with SELECT-only grants (see Security)
|
|
64
|
+
export CUBRID_PASSWORD=secret
|
|
65
|
+
export CUBRID_DATABASE=mydb
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Optional settings:
|
|
69
|
+
|
|
70
|
+
| Variable | Default | Description |
|
|
71
|
+
|----------|---------|-------------|
|
|
72
|
+
| `CUBRID_MCP_READONLY` | `1` | Enforce read-only SQL whitelist |
|
|
73
|
+
| `CUBRID_MCP_MAX_CHARS` | `4000` | Max characters in query output |
|
|
74
|
+
| `CUBRID_MCP_MAX_ROWS` | `1000` | Max rows returned by `execute_query` before truncation |
|
|
75
|
+
| `CUBRID_MCP_MAX_SQL_LENGTH` | `65536` | Max length (characters) of a submitted SQL statement |
|
|
76
|
+
| `CUBRID_MCP_QUERY_TIMEOUT` | `30` | Per-statement socket read timeout in seconds. If the server sends no data within this window the query is aborted and the connection is reset. This is a socket read timeout, not a true server-side statement timeout. |
|
|
77
|
+
| `CUBRID_MCP_AUDIT_LOG` | `0` | Opt-in audit logging. When enabled, emits one redaction-safe JSON record per executed statement (`execute_query`/`explain_query`/`execute_write`) to **stderr** — see [Logging](#logging). Honoured per connection (`CUBRID_<NAME>_MCP_AUDIT_LOG`). |
|
|
78
|
+
| `CUBRID_MCP_WRITE` | `0` | Opt-in write mode. When enabled (`1`), registers the `execute_write` tool for single-statement DML. Honoured per connection (`CUBRID_<NAME>_MCP_WRITE`); the tool is registered when any connection enables it. Off by default. |
|
|
79
|
+
|
|
80
|
+
### Multiple connections
|
|
81
|
+
|
|
82
|
+
By default the bare `CUBRID_*` variables define a single connection named `default`.
|
|
83
|
+
You can serve additional CUBRID databases from the same process by listing extra
|
|
84
|
+
connection names in `CUBRID_CONNECTIONS` (comma-separated) and providing
|
|
85
|
+
`CUBRID_<NAME>_*` variables for each. Every tool accepts an optional `connection`
|
|
86
|
+
argument selecting which connection to target; omitting it (or passing `default`)
|
|
87
|
+
uses the bare-variable connection, so existing single-database setups are unchanged.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
# Default connection (unchanged)
|
|
91
|
+
export CUBRID_HOST=localhost
|
|
92
|
+
export CUBRID_USER=readonly_user
|
|
93
|
+
export CUBRID_PASSWORD=secret
|
|
94
|
+
export CUBRID_DATABASE=mydb
|
|
95
|
+
|
|
96
|
+
# Additional named connections
|
|
97
|
+
export CUBRID_CONNECTIONS=reporting,analytics
|
|
98
|
+
|
|
99
|
+
export CUBRID_REPORTING_HOST=reporting-db
|
|
100
|
+
export CUBRID_REPORTING_USER=readonly_user
|
|
101
|
+
export CUBRID_REPORTING_PASSWORD=secret
|
|
102
|
+
export CUBRID_REPORTING_DATABASE=reports
|
|
103
|
+
export CUBRID_REPORTING_MCP_MAX_ROWS=500 # optional per-connection tuning
|
|
104
|
+
|
|
105
|
+
export CUBRID_ANALYTICS_HOST=analytics-db
|
|
106
|
+
export CUBRID_ANALYTICS_USER=readonly_user
|
|
107
|
+
export CUBRID_ANALYTICS_PASSWORD=secret
|
|
108
|
+
export CUBRID_ANALYTICS_DATABASE=analytics
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Notes:
|
|
112
|
+
|
|
113
|
+
- Connection names must match `[A-Za-z0-9_]+` and are matched case-insensitively.
|
|
114
|
+
- `default` is reserved (it always comes from the bare `CUBRID_*` variables) and
|
|
115
|
+
cannot appear in `CUBRID_CONNECTIONS`.
|
|
116
|
+
- For a named connection `<NAME>`, connection fields live at `CUBRID_<NAME>_HOST`
|
|
117
|
+
etc. and the optional MCP tuning knobs at `CUBRID_<NAME>_MCP_*` (same suffixes as
|
|
118
|
+
the global ones). Named connections do **not** inherit values from the bare vars.
|
|
119
|
+
- Selecting an unknown connection returns a clear error listing the available names.
|
|
120
|
+
- Each connection has its own read-only enforcement, so a named connection can set
|
|
121
|
+
`CUBRID_<NAME>_MCP_READONLY` independently of the default.
|
|
122
|
+
|
|
123
|
+
### Run
|
|
124
|
+
|
|
125
|
+
### Run from PyPI
|
|
126
|
+
|
|
127
|
+
Use [`uvx`](https://docs.astral.sh/uv/guides/tools/) to run directly from PyPI:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
uvx cubrid-mcp-server
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Or with `pipx`:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pipx run cubrid-mcp-server
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Run from source
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
git clone https://github.com/cubrid-lab/cubrid-mcp-server.git
|
|
143
|
+
cd cubrid-mcp-server
|
|
144
|
+
python -m venv .venv && source .venv/bin/activate
|
|
145
|
+
pip install -e .
|
|
146
|
+
cubrid-mcp-server
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## MCP Client Integration
|
|
150
|
+
|
|
151
|
+
### Claude Desktop
|
|
152
|
+
|
|
153
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"mcpServers": {
|
|
158
|
+
"cubrid": {
|
|
159
|
+
"command": "uvx",
|
|
160
|
+
"args": ["cubrid-mcp-server"],
|
|
161
|
+
"env": {
|
|
162
|
+
"CUBRID_HOST": "localhost",
|
|
163
|
+
"CUBRID_USER": "readonly_user",
|
|
164
|
+
"CUBRID_PASSWORD": "secret",
|
|
165
|
+
"CUBRID_DATABASE": "mydb"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Claude Code
|
|
173
|
+
|
|
174
|
+
Add to `.mcp.json` in your project root:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"mcpServers": {
|
|
179
|
+
"cubrid": {
|
|
180
|
+
"command": "uvx",
|
|
181
|
+
"args": ["cubrid-mcp-server"],
|
|
182
|
+
"env": {
|
|
183
|
+
"CUBRID_HOST": "localhost",
|
|
184
|
+
"CUBRID_USER": "readonly_user",
|
|
185
|
+
"CUBRID_PASSWORD": "secret",
|
|
186
|
+
"CUBRID_DATABASE": "mydb"
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Cursor
|
|
194
|
+
|
|
195
|
+
Add to `.cursor/mcp.json`:
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"mcpServers": {
|
|
200
|
+
"cubrid": {
|
|
201
|
+
"command": "uvx",
|
|
202
|
+
"args": ["cubrid-mcp-server"],
|
|
203
|
+
"env": {
|
|
204
|
+
"CUBRID_HOST": "localhost",
|
|
205
|
+
"CUBRID_USER": "readonly_user",
|
|
206
|
+
"CUBRID_PASSWORD": "secret",
|
|
207
|
+
"CUBRID_DATABASE": "mydb"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Security
|
|
215
|
+
|
|
216
|
+
The server is **read-only by default**. A code-level SQL whitelist allows only `SELECT`, `SHOW`, `DESC`, `DESCRIBE`, `EXPLAIN`, and `WITH` statements. Multi-statement queries are rejected.
|
|
217
|
+
|
|
218
|
+
> **The SQL whitelist is defense-in-depth, not a security boundary.** It is a non-validating parser-based guardrail against obvious mistakes. The real enforcement layer is the database itself: **always run the server as a CUBRID user that has only `SELECT` grants** on the tables the model may read. See [`SECURITY.md`](./SECURITY.md).
|
|
219
|
+
|
|
220
|
+
For production use, also configure a read-only database user. See [`SECURITY.md`](./SECURITY.md) for the recommended setup.
|
|
221
|
+
|
|
222
|
+
### Write mode (opt-in)
|
|
223
|
+
|
|
224
|
+
Write access is **disabled by default**. Setting `CUBRID_MCP_WRITE=1` registers an additional `execute_write` tool that accepts a **single** `INSERT`, `UPDATE`, or `DELETE` statement and runs it in an explicit transaction (commit on success, rollback on any error). When write mode is off the tool is not registered at all, so no write path is exposed in MCP capability discovery. With multiple connections configured, the tool is registered when **any** connection enables writes (via `CUBRID_MCP_WRITE` or `CUBRID_<NAME>_MCP_WRITE`).
|
|
225
|
+
|
|
226
|
+
Constraints and rationale:
|
|
227
|
+
|
|
228
|
+
- **Single-statement DML only.** Standalone reads, DDL (`CREATE`/`ALTER`/`DROP`/`TRUNCATE`), transaction-control, and multi-statement input are rejected. (A single DML statement may still legally contain subqueries, e.g. `INSERT ... SELECT`.)
|
|
229
|
+
- **DDL is intentionally unsupported.** CUBRID auto-commits DDL, which defeats the rollback guarantee, so it is excluded from write mode.
|
|
230
|
+
- **Write mode is per-connection.** `execute_write` accepts the same optional `connection` argument as the read tools and runs against that connection; a connection whose `CUBRID_<NAME>_MCP_WRITE` is off refuses the write even when another connection enables it.
|
|
231
|
+
- `execute_query` remains **read-only regardless** of the write-mode flag.
|
|
232
|
+
- Enforcement is defense-in-depth; still run the server as a CUBRID user granted only the privileges it needs. See [`SECURITY.md`](./SECURITY.md).
|
|
233
|
+
|
|
234
|
+
## Logging
|
|
235
|
+
|
|
236
|
+
The server speaks the MCP **stdio transport**, where `stdout` carries the JSON-RPC protocol stream. Anything written to `stdout` by the server or its dependencies will corrupt that stream and break the client connection. For this reason **all logging is routed to `stderr`**, and you should keep it that way: when adding custom logging or diagnostics, never `print()` to `stdout` — use the standard `logging` module (which is configured to emit on `stderr`) or write to `stderr` explicitly. The log level defaults to `INFO`.
|
|
237
|
+
|
|
238
|
+
Errors surfaced back to the LLM client are **sanitized**: only the exception category (e.g. `query failed: OperationalError`) is returned, while the full exception detail is logged to `stderr` for operators. This keeps schema details, hostnames, SQL fragments, and configuration values out of client-visible messages.
|
|
239
|
+
|
|
240
|
+
### Audit logging (opt-in)
|
|
241
|
+
|
|
242
|
+
Set `CUBRID_MCP_AUDIT_LOG=1` to record every executed statement (`execute_query`, `explain_query`, and `execute_write`) as a single structured JSON line on **stderr**. It is **off by default** and honoured **per connection** — a named connection sets `CUBRID_<NAME>_MCP_AUDIT_LOG` independently of the default. Each record is redaction-safe and contains only:
|
|
243
|
+
|
|
244
|
+
| Field | Description |
|
|
245
|
+
|-------|-------------|
|
|
246
|
+
| `tool` | The MCP tool that ran the statement (`execute_query` / `explain_query` / `execute_write`). |
|
|
247
|
+
| `status` | `ok` or `error`. |
|
|
248
|
+
| `category` | The leading SQL keyword only (e.g. `SELECT`, `WITH`) — never the full statement. |
|
|
249
|
+
| `identifiers` | Table names extracted after `FROM`/`JOIN` via a strict identifier regex; anything that is not a bare identifier (values, literals, expressions) is dropped. |
|
|
250
|
+
| `sql_length` | Length of the submitted SQL, in characters. |
|
|
251
|
+
| `row_count`, `truncated` | Result size and whether output was truncated (success only). |
|
|
252
|
+
| `duration_ms` | Wall-clock duration of the call, in integer milliseconds. |
|
|
253
|
+
| `error_type` | On failure, the exception **class name only** (via the same sanitization as client-facing errors). |
|
|
254
|
+
|
|
255
|
+
The **raw SQL text, bound parameters, and literal values are never logged**, so secrets embedded in a query (e.g. `WHERE token = '...'`) do not reach the audit stream. Records go to `stderr` only and never to `stdout`.
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
## Development
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
git clone https://github.com/cubrid-lab/cubrid-mcp-server.git
|
|
262
|
+
cd cubrid-mcp-server
|
|
263
|
+
python -m venv .venv && source .venv/bin/activate
|
|
264
|
+
pip install -e ".[dev]"
|
|
265
|
+
|
|
266
|
+
# Lint & type check
|
|
267
|
+
ruff check .
|
|
268
|
+
mypy cubrid_mcp_server
|
|
269
|
+
|
|
270
|
+
# Unit tests
|
|
271
|
+
pytest -m "not integration"
|
|
272
|
+
|
|
273
|
+
# Integration tests (requires running CUBRID)
|
|
274
|
+
export CUBRID_HOST=localhost CUBRID_USER=dba CUBRID_PASSWORD="" CUBRID_DATABASE=demodb
|
|
275
|
+
pytest -m integration
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## Disclaimer
|
|
279
|
+
|
|
280
|
+
> This project is part of [CUBRID Lab](https://github.com/cubrid-lab), an independent open-source initiative for CUBRID developer tooling, and is not affiliated with, sponsored by, or endorsed by CUBRID Corporation or the official CUBRID project.
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
## License
|
|
284
|
+
|
|
285
|
+
MIT (see [`LICENSE`](./LICENSE)).
|