Repository navigation
Releases: cubrid-lab/cubrid-mcp-server
Release list
v0.6.0
Upgrade notes
make verifynow runs the live integration lane (#404) — it runsmake check, the offline
make testand then the run-ownedmake integrationlane, so it needs Docker and fails when
CUBRID never becomes ready or every live test skips. The live lane starts only after the offline
tests pass, also undermake -j.make teststays offline; runmake check testfor the
former offline-onlymake verify. Development tooling only; the MCP tools are unchanged.
Added
make verifywrites an aggregate summary (#449) —scripts/run_verify.pynow runs the
three phases and always writestest-results/verify/summary.json(overallPASS/FAIL/
INCOMPLETEand each phase's status,NOT_RUNwhen it never started, and exit code), copies of
the lanes' JUnit files underjunit/<lane>.xmland the liveenvironment.json, with the
configured connection passwords masked, also after a failure or a signal. A missing, unreadable
or non-string lane status, an all-skipped live lane or a file that cannot be copied is never
PASS. The first SIGINT, SIGTERM or SIGHUP is forwarded to the running phase and the run exits
128 + the signal number;make integrationnowexecs its runner, so a SIGTERM to make reaches
it, and the runner also cleans up on SIGHUP. Newscripts/report_results.py aggregate
subcommand. Development tooling only; the MCP tools are unchanged.
- make: run the live integration lane in make verify (#447) (e694f56)
- make: write an aggregate verification summary under test-results/verify (#450) (3260e38), refs #449
CI
- Every new issue starts in
status: needs triage(#418) — the issue forms add the
label and the newissue-triage.ymlworkflow adds it to every opened issue. Priority and
size are still set by a maintainer, who removes the label after triage.
Tests
- Named-connection invalid values keep the prefixed variable name (#434) —
tests/test_config.pynow checks that an invalidPORT,MCP_MAX_ROWS,MCP_MAX_CHARS,MCP_MAX_SQL_LENGTHorMCP_QUERY_TIMEOUTon a named connection raisesConfigErrornaming the fullCUBRID_<NAME>_...variable and not the bare default one, and that a blank required named variable is reported like a missing one. Test-only;config.pyis unchanged. Refs #434.
Full Changelog: v0.5.6...v0.6.0
v0.5.6
Changed
- Invalid boolean settings name their variable (#435) —
CUBRID_MCP_READONLY,CUBRID_MCP_WRITEandCUBRID_MCP_AUDIT_LOG(and theCUBRID_<NAME>_forms for named connections) reported onlyexpected a boolean-like value, got 'x', so with several connections the operator had to guess which variable was wrong. The error now reads<VARIABLE> must be a boolean (1/true/yes/on or 0/false/no/off), got 'x', matching the integer settings. Accepted aliases are unchanged, and_parse_bool(value)without a key keeps its old message. Refs #435.
Fixed
Documentation
- agents: align independent MCP safety review and local title rules (7592c53)
- agents: clarify MCP safety review and local PR policy (#442) (7592c53)
Full Changelog: v0.5.5...v0.5.6
v0.5.5
Fixed
-
Audit logger invariants raise
RuntimeErrorinstead of asserting (#429) —AppContext.audit_for()andserver._audit()guarded the audit logger thatAppContext.__post_init__always sets withassert, whichpython -Ostrips. Both now raiseRuntimeErrornaming the missing field; behavior is unchanged while the invariant holds, so no tool output changes. Refs #429. -
Suppressed connection close failures are logged (#431) —
AppContext.close()re-raises only the first close failure and silently dropped any later one, hiding secondary shutdown problems. Each suppressed failure is now logged at WARNING through the module logger with the connection name and the exception type only (driver error text can echo connection details, so it is deliberately left out). The first error is still raised unchanged. Refs #431.
- context: log suppressed connection close failures (#439) (9924e2c)
- context: raise RuntimeError for unset audit loggers instead of asserting (#438) (b6d8bb0)
- reporting: redact credentials embedded in environment command metadata (#433) (702aac6)
Security
environment.jsonmasks credentials embedded in the recorded command (#426) —scripts/report_results.pystored the--commandstring, whichmake integrationbuilds from its overridable--pytestvalue, verbatim, so a password assignment, an authenticated database URL or a token option in it would have reached thetest-results/integration/environment.jsonCI artifact. It now masks the password of URL userinfo (scheme://user:***@host, percent-encoded passwords included) and the value of apassword,passwd,pwd,token,secret,api_keyorapikeyassignment or option, case-insensitively and as a key suffix (PASSWORD=***,CUBRID_PASSWORD=***,--password=***,--password ***), and keeps every other byte of the command. The schema, statuses, exit codes and the arguments pytest receives are unchanged. The masking is a best-effort safety net with documented limits (for example-p secretis not recognized);CONTRIBUTING.mdand the module docstring now say to supply secrets through environment variables or the CI secret store, not the command line. Refs #426.
Documentation
- The table-resolution and schema helpers in
server.pyare documented (#432) —_schema_definitionsand_list_indexeswere the last of the private helpers behind the schema tools without a docstring; each now states what it returns and how the table is selected by owner and name. Docstrings only: no behavior, signature or tool output changes. Refs #432.
CI
- The temporary
last-release-shaoverride is removed after 0.5.4 (#388) —v0.5.4is published, and the release run verified it against the cookbook, so the next candidate starts from that tag. The override for the abandoned 0.5.3 (8633db6) is no longer needed, and leaving it in place would make a later recovery start from a stale base (RELEASING.md, "A merged X.Y.Z is abandoned"). Refs #388.
Full Changelog: v0.5.4...v0.5.5
v0.5.4
Upgrade notes
-
0.5.3 was never published (#388). Its cookbook candidate verification failed: the pinned cookbook commit predated the cookbook's pandas 3.1 golden fix (cubrid-lab/cubrid-cookbook-python#444), so no 0.5.3 tag, GitHub Release or PyPI version exists. 0.5.4 contains every 0.5.3 change below; upgrade from 0.5.2 directly to 0.5.4.
-
make integrationneeds Docker Compose and readsCUBRID_TEST_PORT(#402) — it now runsdocker compose ... up --detach --wait, so it needs the Docker Compose plugin (v2 or later) as well as Docker. Its port isCUBRID_TEST_PORT(default 33000), the variablemake docker-upalready uses, so aCUBRID_TEST_PORTexported for a running lab also movesmake integrationonto that port (pass another one).INTEGRATION_PORTstill works and wins when set. Refs #402. -
The
devextra is now thedevdependency group (#398) —pip install -e ".[dev]"no longer installs the contributor tools, and the published wheel and sdist no longer carry adevextra (their runtime dependencies are unchanged). Install them withmake installorpython -m pip install -e . --group dev, which needs pip 25.1 or later (python -m pip install --upgrade pip). Refs #398. -
make integrationnow starts its own CUBRID (#379) — it used to runpytest -m integrationagainst a CUBRID you had already started. It now starts a uniquely namedcubrid/cubrid:$(CUBRID_VERSION)container (default 11.4) withdemodb, publishes the broker on host port$(INTEGRATION_PORT)(default 33000), waits fordemodb, runs the integration tests against127.0.0.1:$(INTEGRATION_PORT)/demodbasdba(exportedCUBRID_*connection variables are ignored), and always removes that container, so it needs Docker. The old behavior ismake integration-local.make testnow also measures coverage and fails belowfail_under = 95and writescoverage.xml,make lintchecks the whole repository (ruff check .) instead ofcubrid_mcp_server/andtests/, andmake checkaddsformat-check, so all three can fail where they passed before.
Added
-
A persistent local CUBRID with Docker Compose (#378) — a root
docker-compose.ymldefines one service,cubrid(cubrid/cubrid:${CUBRID_VERSION:-11.4},demodb,CUBRID_DB_HOST=localhost, the broker on127.0.0.1:${CUBRID_TEST_PORT:-33000}only, acsql ... SELECT 1health check, a named data volume, no fixed container name), and new Make targets drive it for manual work:make docker-upstarts it and waits until it is healthy,make docker-downremoves the container and network but keeps the data volume, andmake docker-cleanalso deletes the volume.make integration,make verifyand the CI live jobs are unchanged: the live lane still starts and always removes its own run-owned container.CONTRIBUTING.md,AGENTS.mdand the README development section (English and Korean) document the targets and how to pointmake integration-localat the service. Refs #378. -
make testandmake integrationwritetest-results/summaries (#405) — both targets now write a gitignoredtest-results/tree:make testwritestest-results/test/junit.xml,coverage.xmlandsummary.json(the rootcoverage.xmlthat CI uploads to Codecov is still written, as a copy), andmake integrationwritestest-results/integration/junit.xml,summary.json,environment.json(Python, package and CUBRID versions, platform and command; never credentials) and, on a failure,logs/cubrid.log. The newscripts/report_results.pybuildssummary.json(schema_version,statusofPASS,FAILorINCOMPLETE, counts, coverage) from the JUnit and coverage files, and each command ends with a one-line console summary of the same status and counts. A lane that collected nothing, or in which no test passed, isINCOMPLETE, neverPASS. Exit statuses, test selection and the coverage floor are unchanged, andmake cleanremovestest-results/. Refs #405.
Changed
-
make integrationruns thedocker-compose.ymlservice as a run-owned Compose project (#402) —scripts/run_integration.pyno longer starts its owndocker runcontainer: it runsdocker compose --file docker-compose.yml --project-name cubrid-mcp-it-<random id> up --detach --wait cubridwithCUBRID_VERSIONandCUBRID_TEST_PORTset, somake integrationandmake docker-upshare one service definition (image tag,demodb, health check, shared-memory size, loopback-only broker). Readiness is the service'scsql ... SELECT 1health check instead of a separatedocker execprobe loop; on a failure it printsdocker compose logs cubrid; and it always runsdown --volumeson that project, which removes its container, network and named volumes, on success, failure, a readiness failure, SIGINT or SIGTERM (the cleanup runs in its own session, so a second Ctrl-C cannot stop it). The exit-status rules are unchanged. The project name is unique per run, so concurrent runs on different ports, and amake docker-uplab, are never touched.CUBRID_TEST_PORTis now the one port variable of both lanes, withINTEGRATION_PORTkept as a compatibility override. The offline tests use a fakedocker compose;CONTRIBUTING.md,AGENTS.md, the README development section (English and Korean) and thedocker-compose.ymlcomments describe it. Refs #402. -
Contributor tools are a PEP 735
devdependency group; the Makefile runs them through one interpreter (#398) — the pins that were in[project.optional-dependencies].dev(includingruff==0.17.1andmypy==2.4.0) now live unchanged in[dependency-groups].dev; there is no lock file and no other group.make installruns$(PYTHON) -m pip install -e . --group dev, andpytest,ruffandmypyrun as$(PYTHON) -m ...instead of bare executables, so an activated virtualenv is honored even whenPATHlists another one.PYTHON ?= python3,PYTEST,RUFFandMYPYare all overridable (make lint PYTHON=.venv/bin/python,make lint RUFF=ruff); the commands, flags, coverage floor and Docker lifecycle ofcheck,test,verifyandintegrationare unchanged. Refs #398. -
The Makefile is the canonical local and CI interface (#379) — new targets:
doctor(checks Python 3.11+,ruff,mypy,pytest,pytest-covandpytest-asyncio; exits non-zero without a success line if any is missing),format-check(ruff format --check .),verify(check+test, no Docker),integration(the run-owned Docker lifecycle inscripts/run_integration.py: start, readiness wait,pytest -m integration -v, the container log on a failure, removal of the container and its anonymous volumes on success, failure, SIGINT or SIGTERM; the first failing step's exit status, or 128 + the signal number) andintegration-local.lint,format-checkandtypecheckuse the same paths and flags as CI,formatis the only target that edits files,testtakes extra pytest arguments inPYTEST_ARGS,cleanalso removes.ruff_cache,.coverage,coverage.xmlandhtmlcov(all gitignored, generated files), andmake helpgroups the targets by purpose. Refs #379.
Fixed
- A failed service-log save no longer skips
make integrationcleanup — with--results-dir, an I/O error while writinglogs/cubrid.logafter a failing lane used to escape beforedocker compose down --volumes, leaving the run-owned project behind and losing the lane's exit status. Saving the log is now best-effort: the error is printed as a warning, the project is still taken down and the summary keeps the lane's status.
Documentation
-
Install instructions use the
devgroup (#398) —CONTRIBUTING.md, the README (English and Korean),scripts/doctor.pyandTHIRD_PARTY_LICENSES.mdreplace.[dev]with--group dev, document the pip 25.1 requirement and the Makefile overrides, and explain that the dev table'scubrid-mcp-serverattribution is hand-kept because installed metadata does not record group membership. Refs #398. -
Make targets in the contributor docs (#379) —
CONTRIBUTING.md,AGENTS.md, the PR template and the README development section (English and Korean) describe the new targets, themake integrationvariables and that CI calls the same targets. Refs #379.
- agents: require repository-local issue authoring checks (#383) (4f04af7)
- enforce repository-local issue drafting (4f04af7)
- ko: replace the mistranslated redaction term in the Korean docs (#389) (2621d8b), refs #290
- triage: clarify PR overlap and execution precedence (440166a), refs #410
CI
- The release workflow verifies against the current cookbook, and preparation restarts after the abandoned 0.5.3 (#388) — 0.5.3 was not published: its cookbook candidate verification failed at the pinned cook...
v0.5.2
Fixed
- The
db_serialcolumn probe keeps transport failures and its connection (#351) —list_serialsresolves whetherdb_serialnames its columnatt_name(CUBRID 10.2–11.2) orattr_name(11.4) with a zero-row probe per connection. The probe treated every error as "this spelling is missing": a timeout or a lost connection on the first probe made it try the other spelling, and two such failures were reported asdb_serial exposes neither att_name nor attr_name. A successful probe whose read-transaction rollback then failed also cached its answer after that connection had been discarded, so the replacement connection, possibly on another server version, reused it without probing. Now only a server rejection of the column (aProgrammingError,IntegrityErrororDataErrorthat is not a timeout, as CUBRID 10.2, 11.2 and 11.4 raise) moves on to the other spelling; a timeout raisesQueryTimeoutErrorand other failures their sanitizedquery failed: ...error. If the probe's connection is discarded, the call fails withconnection was reset while probing db_serial; retry the requestand nothing is cached; the next call probes the new connection. Refs #351.
- agent-review: do not count empty reply-only Copilot records as review rounds (#349) (6d75295)
- agent-review: fail closed when a review thread lacks author or resolver identity (#347) (09d746e), refs #346
- database: preserve serial-probe failures and fence the cache to its connection (#357) (0832251)
Documentation
-
A release review checklist (#355) —
RELEASING.mdnow has a checklist to complete before merging a release PR: the candidate's base and final head, each required check's state at that head (missing, approval required, running, failed or cancelled, successful; only successful counts), the review evidence, the impact classification (including 0.x upgrade notes, the.mcpb/server.jsonversion agreement and theserverInfotests) and whether there is a user-facing reason to publish (a candidate may stay pending; a documentation-only release stays possible). A release counts as complete when the publisher summaries show publication and post-publication verification for the same tag, SHA and version (published and verified, or a published run followed by a successfulverify-onlyrecovery); the package version stays apart from FastMCP, client and registry tool versions, andserver.jsonis not proof of a registry entry. The "start CI after the final bot update" step now describes what happened for candidate #339 (runs waiting asaction_required, no check runs) instead of saying that bot pushes start no workflow.CONTRIBUTING.mdandAGENTS.mdpoint to the checklist. The release workflows and gates are unchanged. Refs #355. -
The documentation site says it is built from
main(#355) — every page of the site, English and Korean, now opens with a notice that it is development documentation for themainbranch, built at a linked commit (fromGITHUB_SHAin GitHub Actions; a local build says that no commit was recorded), that it can describe unpublished changes, and that a published version's documentation is thedocs/folder at its tag on the release list. The label never reads__version__, the release manifest or.mcpb/server.json, and the site shows no hardcoded latest version; offline tests render the template for each case. The Quick Start separates the document source, the package release (the version in the source and an open release pull request are not proof of publication) and the one verified combination recorded in #356, and names what was not run. The docs workflow stays build-only on pull requests and is unchanged. Refs #355. -
The documentation site is grouped by reader (#354) — the navigation now has Start here (the Quick Start), How-to guides (serving several databases), Reference (tools, resources and prompts; configuration settings; Security Model), Help (Troubleshooting, the issue forms and private vulnerability reporting through
SECURITY.md) and a 한국어 tab with the same groups, instead of Korean pages nested under Project → Translations. Every English page links its Korean page below the title and every Korean page links back; the documentation home lists the pages by the same groups and warns not to paste secrets or query results into issues. Pages now show previous and next links.CONTRIBUTING.mdrecords the page map (reader, question, group and which content #352 owns) and a convention for writing walkthrough steps. No page is renamed, so existing URLs and anchors keep working; no content, tool, setting or security behavior changes. Refs #354. -
The Quick Start is one safe, tested walkthrough (#352) — the English and Korean Quick Start now explain how CUBRID, the server and an MCP client fit together and what stdio means, then go step by step: a disposable CUBRID 11.4 Docker lab bound to
127.0.0.1, an install in a virtual environment, a one-time administrator script that creates the sample tablemcp_quickstart_booksand the accountmcp_readerwithSELECTon that table only (in one transaction, refusing to touch existing objects), a FastMCP Python client whosemcpServersconfiguration starts the server with an explicit environment, tool discovery andhealth_check, a firstdescribe_table/execute_queryread and a refusedDELETE, real first-connection error messages, and a cleanup script that removes only the walkthrough's own objects, withdocker rm -f -vfor the lab's volume. The model's account is neverdba, and the password stays in an environment variable. Every command and script was run as written on Linux againstcubrid/cubrid:11.4; the Claude Desktop, Claude Code and Cursor configurations are kept as optional, unverified examples using the same names. The README (English and Korean) and the documentation home point to the walkthrough first, and the README Quick Start states that its values are placeholders. Refs #352. -
Agent PR review route: Copilot or a maintainer — maintainer decision of 2026-10-15: a standalone-agent PR needs a substantive submitted Copilot review or a review by a maintainer (a code owner other than the PR's author), recorded on the PR. Reviews by other people or by other AI agents do not satisfy the route. No CI check enforces it; whether to add one is pending (#317). The two-round AI review budget stays. New: under every route, changes to
safety.py,database.py,audit.py,server.pyand workflows that hold write permissions or perform releases need a review that explicitly assesses their invariants, and a maintainer confirms that disposition before merge.AGENTS.md,CONTRIBUTING.mdand the PR template are updated; the Korean docs carry no review rules, so nothing there changes. Refs #317.
- agents: accept an independent reviewer agent in the standalone-agent review route (#342) (bbfe3a4); superseded before this release by #344: an independent reviewer agent does not satisfy the review route
- agents: require a Copilot or maintainer review in the standalone-agent route (#344) (3b18cdf), refs #317
- contributing: clarify good-first-issue access for human contributors (#322) (b319f9f)
- fix Korean documentation typos and links (#320) (6f49f05)
- ko: fix a typo and broken cross-page links (#319) (6f49f05)
- onboarding: provide a safe single-client walkthrough with a prepared read-only lab (#356) (e3e0185), refs #352
- release: add a release review checklist to the runbook (#360) (f196135), refs #355
- site: label the site as main-branch documentation with its build commit (#359) (08550e7), refs #355
- structure: group the site by reader and make Korean pages peers (#358) ([c0f900b](https:...
v0.5.1
Upgrade notes
- Owner-qualified table names on CUBRID 11.2 and later —
all_table_names,filter_table_names,describe_table,table_row_counts,list_class_hierarchyand thecubrid://schemaresources now return table names asowner.table(for exampledba.product) on CUBRID 11.2 and later. Clients that compared these names with bare table names must accept the owner prefix. Bare names are still accepted as input; one that several owners share now fails as ambiguous unless the configured user owns one of them. CUBRID 10.2 and 11.0 are unchanged. CALL,EVALUATEandKILLinexecute_querywithCUBRID_MCP_READONLY=0— these statements now fail before execution. Call stored functions from aSELECT(SELECT my_function(…)) and evaluate expressions withSELECT;KILLis not offered by any tool.INCR,DECR, serial advances and the click counter inexecute_querywithCUBRID_MCP_READONLY=0— these forms now fail before execution, as they already did in read-only mode. Useexecute_writefor writes; read a serial withCURRENT_VALUE.
Removed
- Test-only helpers left the package —
AppContext.single()(cubrid_mcp_server/context.py) andConnectionRegistry.names(cubrid_mcp_server/config.py) were used only by the tests. The tests now build single-connection contexts withtests/helpers.py:single_context()and readsorted(registry.configs). The server never called either helper, so its behavior is unchanged.
Fixed
- MCP
serverInfo.versionis the package version — the server passes__version__to FastMCP, which otherwise reported its own version (for example4.1.0) in theinitializeresponse. - Audit
categoryignores leading comments — the audit log reads the statement category after stripping comments and string literals, as it already did for table identifiers, so-- note\nSELECT …and/* note */ SELECT …areSELECTand/* SELECT */ DELETE …isDELETE. Literals and comments are found with the same CUBRID-aware scanner as the SQL checks (//line comments,\rline ends, unterminated block comments or literals), under bothno_backslash_escapessettings, and double-quoted text that is not a plain name is treated as a literal, so audit records never echo text that is a literal or comment under either reading. - SQL checks read comments, quotes and statements with CUBRID's rules —
ensure_read_only,ensure_query_statementandensure_write_allowedno longer rely onsqlparse's generic lexer for comment, quote and;boundaries, which differ from CUBRID's:sqlparsedoes not know CUBRID's//line comments, reads#and$$ … $$as a comment and a string where CUBRID does not, and reads a backslash as a quote escape. A comment that names an allowed keyword can no longer hide the real leading statement, and input that CUBRID would split into more than one statement is rejected. Input with an unterminated quote, or whose quote boundaries depend on the server parameterno_backslash_escapes, is now rejected; escape a quote by doubling the quote character used (''or""). - The release workflow is named
publish-pypi.ymlagain —.github/workflows/release.ymlis renamed back to.github/workflows/publish-pypi.yml, the workflow filename registered as the PyPI Trusted Publisher (environmentpypi). Under therelease.ymlname the 0.5.0 run failed at the PyPI upload with "Publisher with matching claims was not found". The shared release scripts (release_detect.py,release_summary.py,verify_dist.py,pypi_duplicate_guard.py,reconcile_release_labels.py) are again byte-identical with pycubrid and sqlalchemy-cubrid, andRELEASING.md,CONTRIBUTING.md,AGENTS.md, theMakefileand the workflow tests namepublish-pypi.yml.RELEASING.mdnotes that the filename is the Trusted Publisher identity and must not be renamed. No runtime change. #-prefixed names are one name in the side-effect check — CUBRID accepts#as the first character of an identifier, but the check readSELECT #incr(cnt) FROM tas#followed byINCR(and rejected it as theINCRside effect.#incr,#decr,#next_valueand the like are now read as single names and pass the check (on CUBRID 10.2 and 11.4 the server then reportsFunction #incr is undefinedunless such a function exists).INCR(,x.next_valueand the other side-effect forms are still rejected, including next to a#name (SELECT #a, INCR(cnt) …) and after a lone#.CUBRID_MCP_MAX_CHARSlimits the first row of a result —execute_queryalways returned the first row whole and capped each value on its own, so a 60-columnSELECTofREPEAT('x', 3990)returned 239,688 characters withtruncated: falseagainst the default cap of 4000. The cap now runs across the values of every row: rows are returned while they fit whole, and a first row that alone is longer is cut to the cap (later values become…, so it can exceed the cap by one character per later column).truncatedis nowtruewhenever a value was cut, including a single long value in a one-row result. A later row is left out rather than cut.docs/TOOLS.mdanddocs/ko/TOOLS.mddescribe how the cap is counted.- Table metadata is selected by owner on CUBRID 11.2 and later — the schema tools ignored the owner, so with
dba.tandother.tboth present,all_table_names()returned['t', 't'],describe_table('t')merged the columns of both tables, andtable_row_counts()failed for every table, because an unqualified"t"resolves to the configured user's ownt. The tools now readdb_class.owner_name, filterdb_attribute,db_index,db_index_keyanddb_direct_super_classon owner and table name, count"OWNER"."table", and returnowner.table. An unqualified name resolves to the only table with that name, or to the configured user's own table, and otherwise fails as ambiguous. Whether the catalog has owner columns is probed once per connection, like thedb_serialcolumn; CUBRID 10.2 and 11.0 keep unqualified names.docs/TOOLS.mdanddocs/ko/TOOLS.mddescribe the naming, and their examples show the 11.4 output. explain_queryreads, recovers and caps likeexecute_query— it found the leading keyword by splitting on whitespace, soSELECT*FROM tand(SELECT 1)were rejected with "only accepts SELECT or WITH" althoughexecute_queryruns them; it now reads the keyword with the same rules as the SQL checks. A statement the server rejects (ProgrammingError,IntegrityError,DataError) no longer discards the connection held forSET TRACE ON … SHOW TRACE: the transaction is rolled back and the session kept, asexecute_queryalready did, so a mistyped query no longer forces a reconnect. The plan is now capped atCUBRID_MCP_MAX_CHARSand the result has a newtruncatedfield.docs/TOOLS.mdanddocs/ko/TOOLS.mddescribe the behavior.
- audit: determine SQL category after stripping leading comments (#283) (2c64d0f)
- safety: reject CALL and KILL in execute_query when read-only mode is off (#304) (49bb56f)
- safety: reject INCR and serial advances in execute_query with the whitelist off (#305) (56c97eb), refs #301 #303
- safety: reject INCR, DECR and serial advances in read-only queries (#302) (d17bf58)
- server: report the package version in MCP serverInfo (#288) (55fea91)
- tools: cap the first result row at the configured character limit (#306) (094b3c6)
- tools: parse, keep connections and cap output in explain_query like execute_query (#308) (75dd0cc)
- tools: qualify table metadata by owner on CUBRID 11.2 and later (#307) (6f8a60f)
Security
- Read-only mode rejects
INCR,DECR, serial advances and the click counter —ensure_read_onlychecked onlysqlparsekeyword tokens, soSELECT INCR(cnt) …,DECR(…),serial.NEXT_VALUE,serial.NEXTVAL,SERIAL_NEXT_VALUE(serial, n)andSELECT … WITH INCREMENT FOR col/WITH DECREMENT FOR colpassedexecute_queryandexplain_queryand changed data. The rollback after the read does not undo these changes, and aSELECT-only grant does not prevent them: on CUBRID 11.4 a user granted onlySELECTon a table increased a column in it and advanced a serial it had no grant on. Read-only mode now rejects these names when they are called or used as an attribute, in any case, bare or quoted ("INCR",[incr],`incr`), with whitespace or comments in between, anywhere in the statement, including subqueries and CTEs; ...
v0.5.0
Upgrade notes
table_row_countsresult shape (#239) — clients that read the result as a list must readresult["tables"]instead, and checktruncatedafter a call withouttable_names.EXPLAINinexecute_query(#250) — it never ran on CUBRID; use theexplain_querytool for execution plans.execute_querywithCUBRID_MCP_READONLY=0(#235) — clients that sentINSERT/UPDATE/DELETEthroughexecute_querywith the whitelist disabled must useexecute_write(enable it withCUBRID_MCP_WRITE=1, orCUBRID_<NAME>_MCP_WRITE=1per connection). Those statements,COMMIT/ROLLBACK,PREPARE/EXECUTE/DEALLOCATE,DOand multi-statement input now fail before execution.- Python 3.10 users must either upgrade to Python 3.11 or later, or stay on the last cubrid-mcp-server release that supports 3.10 together with a driver release compatible with 3.10. Newer pycubrid lines (1.10+) do not support Python 3.10, and this project does not imply otherwise.
Added
- add MCP Registry server.json (f04c32e)
- add MCP Registry server.json for official listing (c45bfd5)
- CUBRID Skills — domain knowledge resources + expert prompts + tool hints + server instructions (#162) (#163) (79927e5)
- VHS terminal demo script — MCP server programmatic interaction (#165) (685a437)
Changed
table_row_countscaps the default call instead of failing beyond 50 tables (#239) — behaviour change. With notable_nameson a database with more than 50 user tables the tool raisedtoo many tables requested, although its docstring promised a capped result and theexplore_unknown_dbprompt tells the model to call it. The default call now counts the first 50 user tables in name order. The result is now an object,{"tables": [...], "truncated": bool, "total_tables": int}, matching howexecute_querysignals truncation:tablesholds the per-table entries the tool returned before,truncatedistruewhen the default call left tables out, andtotal_tablesis the number of user tables. An explicit list of more than 50 names still raises;[]still counts nothing.TOOLS.md(EN/KO) and theexplore_unknown_dbprompt describe the cap and how to fetch the rest.EXPLAINremoved fromexecute_query's read-only whitelist (#250) — CUBRID has noEXPLAINstatement: on CUBRID 11.4.6,EXPLAIN SELECT 1(andEXPLAIN PLAN FOR …) fail at the server with a syntax error, so the whitelist admitted a statement that could never run.READ_ONLY_KEYWORDSis nowSELECT,SHOW,DESC,DESCRIBE,WITH, and withCUBRID_MCP_READONLY=1(the default)EXPLAIN …is rejected before it reaches the database withUnsafeSQLError; withCUBRID_MCP_READONLY=0it is not a rejected keyword and still fails at the server. README (EN/KO),SECURITY.md, the Security Model (EN/KO), Troubleshooting (EN/KO), the docs home page, ROADMAP and thecubrid://agent-guideresource no longer list it and point to theexplain_querytool, which already uses CUBRID'sSET TRACE/SHOW TRACE. A live integration test pins both the server rejection and the whitelist rejection. No other lexer change.execute_querystays read-only whenCUBRID_MCP_READONLY=0(#235) — behaviour change.CUBRID_MCP_READONLY=0now only relaxes the read-only whitelist; it no longer letsexecute_queryrun writes. With the whitelist off,execute_queryrejects any statement that leads with a write, DDL or transaction-control keyword (INSERT,UPDATE,DELETE,REPLACE,MERGE,CREATE,ALTER,DROP,TRUNCATE,RENAME,GRANT,REVOKE,COMMIT,ROLLBACK,SAVEPOINT,SET,PREPARE,EXECUTE,DEALLOCATE,DO) before it reaches the database, with anUnsafeSQLErrorpointing toexecute_write; the first word of the leading keyword is matched, soCREATE OR REPLACE …is rejected likeCREATE. Multi-statement input (multi-statement SQL is not allowed in execute_query) and empty input — including comment-only or;-only input — are rejected too, as in read-only mode. The keyword check is a guardrail; the read-only database account remains the security boundary. Previously such a statement ran, was reported asquery failed: InterfaceErrorand cost the shared connection. As a backstop, a statement that returns no result set (cursor.description is None) is rolled back and fails withValueError: statement produced no result set; execute_query is read-only, use execute_write …, and the connection is kept instead of discarded. WithCUBRID_MCP_READONLY=1(the default) the whitelist and its error messages are unchanged. Result-set reads (SELECT,WITH … SELECT,SHOW,DESC, and with the whitelist off alsoCALLs that return a value) are unaffected.- Python 3.11 is now the minimum supported version (#221) —
requires-pythonis>=3.11(Python 3.10 reached upstream end of life on 2026-10-01); the 3.10 classifier is removed, Ruff targetspy311and mypy3.11. Python 3.14 is added to the classifiers and the unit/full-integration matrices after the offline suite, Ruff and mypy passed on 3.14. Thelowest-directjob and Codecov upload moved to 3.11; lowest-direct resolves FastMCP 3.0.0, pycubrid 1.4.0 and sqlparse 0.5.0 and passes. No MCP, SQL-safety, timeout or audit behavior changes; direct dependency floors are unchanged.
Fixed
explain_querydiscards the connection when trace cleanup fails (#264) — after the statement ran,explain_queryturns tracing off and rolls back on the shared connection. A failure to open the cleanup cursor, runSET TRACE OFFor roll back was only logged, so the next tool call could reuse a session that still had tracing on or a transaction open. Any such failure now discards the connection under the connection lock, so the next call reconnects; cleanup stops at the first failed step, and after a query timeout no cleanup statement is sent. A successful query still returns its plan, a query error is still the error the client sees, and a healthy cleanup keeps the connection.TOOLS.mdand the Security Model (EN/KO) describe the behavior.list_class_hierarchyresolves table names like the other schema tools (#240) — the tool bound the rawtable_name, so matching was case-sensitive and an unknown name, system class or view silently returned[]. It now uses_resolve_table: names match user tables case-insensitively (and are bound as the stored name), and an unknown table raisesunknown table: '…'; a whitespace-only name raisestable name must not be empty. Omittingtable_name(or passing"") still lists every class. TheTOOLS.mdsignature (EN/KO) is corrected fromlist_class_hierarchy(connection=None, ...)tolist_class_hierarchy(table_name=None, connection=None).- Reads end their transaction on the shared connection (#234) — the server never enables autocommit and pycubrid defaults to
autocommit=False, so everyfetch_all/fetch_many(all read tools, includingexecute_queryand its truncated early return) left a transaction open until a later write committed,explain_queryrolled back or the connection was dropped. That kept table locks (blocking DDL from other sessions) and, underREPEATABLE READ, a stale snapshot that hid rows committed by other sessions. Each read now rolls back once after its rows are collected, inside the connection lock; a failed rollback discards the connection so the next call reconnects. A failed read caused by a statement the server rejected (pycubridProgrammingError,IntegrityErrororDataError) now rolls back and keeps the session instead of reconnecting; timeouts,OperationalError,InterfaceError,InternalError, the genericDatabaseErrorand non-driver errors still discard it.execute_writecommit/rollback andexplain_querytrace cleanup are unchanged. WithCUBRID_MCP_READONLY=0, a write that also returns a result set is now rolled back instead of left pending; useexecute_writefor writes. - Accept leading comments in
explain_query(#178) — SELECT/WITH statements now use the same comment normalization as the read-only safety checker before the leading-token gate. - Blank required connection values are rejected (#177) — an empty or whitespace-only
CUBRID_HOST,CUBRID_USER, orCUBRID_DATABASE(and theCUBRID_<NAME>_*equivalents) now raisesConfigErrorat configuration time, like a missing variable, instead of producing a broken connection config. An emptyCUBRID_PASSWORDremains allowed. - Port range validation (#176) —
CUBRID_PORT(andCUBRID_<NAME>_PORT) must now be an integer in the TCP port range1..65535; out-of-range values such as0or65536raiseConfigErrorat configuration time instead of failing later at connect time. db_serialcolumn probe is reset on reconnect (#181) — the cachedatt_name/attr_nameprobe result used bylist_serialsis now cleared whenever the connection is discarded or closed, so a reconnect that lands on a different CUBRID version (e.g. 11.2 → 11.4 after a broker failover) re-probes instead of reusing the stale column name.- Cursor-creation failures take the normal recovery path (#180) —
connection.cursor()is now created inside the recovery block of bothDatabase.cursor()andexecute_write(), so a failure opening a cursor (e.g. on a connection the broker already closed) is sur...
v0.4.0
Added
- Published to PyPI:
uvx cubrid-mcp-serverandpipx run cubrid-mcp-servernow install from PyPI instead of a git checkout. - Documentation site (mkdocs-material) deployed to https://cubrid-lab.github.io/cubrid-mcp-server/ — quickstart, full tool reference, security model, multi-connection guide, and troubleshooting.
- Korean README translation (
docs/README.ko.md), matching the sibling repositories' translation set. upstream-canary.ymlnow also tracksfastmcp@latestin addition topycubrid@main, giving the eventual fastmcp 4.x migration decision CI evidence. The dependency pin intentionally remains>=3.0,<4until that canary is green.THIRD_PARTY_LICENSES.md(full runtime license inventory, pip-licenses generated) and aNOTICEfile (original implementation, no third-party code embedded) added.- Official MCP Registry readiness: PyPI ownership-verification marker (
mcp-name: io.github.cubrid-lab/cubrid-mcp-server) embedded in README, andglama.jsonadded at the repo root for Glama registry indexing.
Changed
- Development Status classifier raised from
2 - Pre-Alphato4 - Beta: the server has shipped multi-database connections, opt-in write mode, audit logging, Resources, and Prompts since 0.3.0. [project.urls]now includesDocumentationandChangeloglinks, matching the sibling repositories.Framework :: AsyncIOtrove classifier added for registry/search discoverability.
Fixed
- README: removed a duplicated
CUBRID_MCP_WRITErow in the configuration table.
v0.3.1
Fixed
execute_writenow honours the per-callconnectionargument instead of always writing to thedefaultconnection, matching the read tools and the v0.3.0 multi-connection contract. (#136)- Per-connection
CUBRID_<NAME>_MCP_WRITEandCUBRID_<NAME>_MCP_AUDIT_LOGsettings are now actually applied: write-enablement is enforced against the target connection's config (a connection with writes off refuses even when another enables them), theexecute_writetool is registered whenever any connection opts into write mode, and audit logging is resolved per connection so a named connection'sMCP_AUDIT_LOGtakes effect independently of the default.execute_writecalls are now audited as well. (#137)
v0.3.0
v0.3.0 — Advanced Features (epic #23)
Added
- Schema as MCP Resources —
cubrid://schemaindex +cubrid://schema/{table}template, reusing the read-only catalog queries. (#124) - Opt-in audit logging (
CUBRID_MCP_AUDIT_LOG) — one redaction-safe JSON record per statement to stderr; raw SQL/params/literals never logged. (#127) - MCP Prompt templates —
summarize_table,explain_query,inspect_schema,find_index_candidates(guidance-only, no DB access). (#125) - Multi-database connections —
CUBRID_CONNECTIONS+CUBRID_<NAME>_*, per-callconnectionargument; single-DB setups unchanged. (#126) - Opt-in write mode (
CUBRID_MCP_WRITE=1) —execute_writetool for a single atomic INSERT/UPDATE/DELETE; disabled and unregistered by default. (#123) ROADMAP.md(#96) and repo-localCONTRIBUTING.md(#94).
Security
- Write mode is strictly gated by a DML-only whitelist (
ensure_write_allowed); the read-only default path is unchanged. (#123)
Changed
- Explicit Ruff rule set (
select = ["E4","E7","E9","F"]) to stabilize CI across ruff 0.16. (#88)
Full changelog: see CHANGELOG.md.