20. MCP server: AI assistants on Miraj
The Miraj MCP server is an endpoint built into miraj-server that lets AI assistants supporting the Model Context Protocol browse, read, modify or administer databases. Access uses a token created in SQL, bound to an account and capped at an access level. It is served over HTTP on port 7008 and disabled until you enable it.
The Miraj MCP server lets an artificial intelligence assistant (Claude, ChatGPT, DeepSeek, or any client of the MCP protocol, Model Context Protocol) work on the databases of a Miraj server: browse their structure, read the data, modify it or administer the server, according to what you grant it. The assistant needs neither a driver nor a password: it receives a token created in SQL, bound to an account and capped at an access level.
The MCP endpoint is built into miraj-server: it is the same process and the same engine as the main port (7007), on a separate HTTP port (7008 by default), disabled until you open it.
AI assistant (Claude, ChatGPT, DeepSeek…)
│ JSON-RPC 2.0 over HTTP or HTTPS: POST http(s)://host:7008/mcp
│ Authorization: Bearer mjt_…
▼
┌──────────────────────────── miraj-server ────────────────────────────┐
│ port 7007: Miraj network protocol (applications, miraj-cli, …) │
│ port 7008: MCP endpoint (--mcp ON) │
│ token ─► account ─► Miraj session capped at the token level │
│ tools ─► SQL statements ─► privileges, locks, triggers, │
│ foreign keys, journal, transactions, replication │
└──────────────────────────────────────────────────────────────────────┘Everything goes through the engine as an ordinary SQL statement: account privileges, locks, CHECK constraints, foreign keys, triggers, write journal and replication apply exactly as for a client application. Nothing is ever written directly to the data files.
20.1 What is the MCP server for?#
MCP is an open protocol through which an AI assistant discovers tools (functions it can call, with their arguments described in JSON) and resources (documents it can read). Miraj exposes sixteen tools:
- structure tools: list databases and their objects, describe a table, read a
CREATEdefinition, search for a table or column by name; - read tools: read rows with a structured condition, read a row by its primary key, count, display an execution plan;
- write tools: insert, modify, delete rows;
- administration tools: create a database or a table, drop an object;
- a free SQL tool,
execute_sql, for everything else.
The structured tools are preferable to free SQL for everyday operations: the assistant supplies column names and JSON values, never SQL text; names are checked against the table and values become literals, which makes any injection impossible.
Typical uses: let a development assistant explore an application's schema, write and check queries, prepare test data, diagnose an execution plan, or, with an ADMIN token on a working database, create the tables of a prototype itself.
20.2 Which editions include the MCP server?#
| Edition | MCP endpoint |
|---|---|
| Enterprise | Available |
| Cluster | Available |
| Developer | Available (with the edition's own limits: one core, 20 GB, 24 hours) |
| Express | Not available: with mcp = ON, the server starts, displays a warning and does not open the MCP port; CREATE MCP TOKEN is refused (error 9001 "MCP is not available in the Miraj Express edition") |
20.3 How do I enable and configure the MCP server?#
20.3.1 Variables#
The endpoint is configured like the main port: default value, then the miraj_config.xml file, then the command line, the command line taking precedence (see 11.2). It is disabled by default.
Variable (miraj_config.xml) | Argument | Default | Values | Role |
|---|---|---|---|---|
mcp | --mcp ON|OFF | OFF | ON, OFF (also true, false, 1, 0) | Opens the endpoint or not |
mcp_port | --mcp-port <port> | 7008 | 0 to 65535 | TCP port of the endpoint, distinct from port; 0: port chosen by the system, displayed at startup |
mcp_bind | --mcp-bind <address> | 127.0.0.1 | IP address or name | Listening address, independent of bind; outside the loopback, TLS is mandatory (20.3.5) |
mcp_idle_timeout | --mcp-idle-timeout <s> | 300 | 1 to 31,536,000 seconds | An MCP session left without a request for this delay is closed; its open transaction is rolled back |
mcp_statement_timeout | --mcp-statement-timeout <s> | 30 | 0 to 31,536,000 seconds, fractions allowed (2.5); 0: no limit | Maximum duration of a tool call (errors 1969 or 1317 beyond it, see 20.13) |
mcp_max_rows | --mcp-max-rows <n> | 5000 | 1 to 1,000,000 | Cap on the rows returned by a result, whatever the requested limit |
The loopback is written exactly 127.0.0.1, localhost or ::1; any other address, including 0.0.0.0 (all interfaces), is considered public.
The mcp* variables appear in miraj_default.xml (regenerated at each startup) and in the MCP group of the graphical configuration editor, which flags an mcp_port equal to port and a public mcp_bind without a certificate.
20.3.2 Through the miraj_config.xml file#
<?xml version="1.0" encoding="UTF-8"?>
<miraj>
<!-- MCP endpoint open (AI assistants, CREATE MCP TOKEN tokens) -->
<mcp>ON</mcp>
<mcp_port>7008</mcp_port>
<mcp_bind>127.0.0.1</mcp_bind>
<mcp_idle_timeout>300</mcp_idle_timeout>
<mcp_statement_timeout>30</mcp_statement_timeout>
<mcp_max_rows>5000</mcp_max_rows>
</miraj>20.3.3 Through the command line#
miraj-server.exe --root D:\miraj\data --log --mcp ON
miraj-server.exe --root D:\miraj\data --log --mcp ON --mcp-port 7100 --mcp-max-rows 1000Always start the server with --log: session openings, tool calls and refusals are then recorded (20.13). An argument takes precedence over the file in both directions: --mcp OFF closes an endpoint enabled by the file, --mcp-port 0 replaces the file's port.
20.3.4 Startup messages and configuration errors#
| Situation | Message | Effect |
|---|---|---|
| Endpoint open | Miraj <version> <edition> - MCP endpoint on 127.0.0.1:7008. (followed by , TLS with a certificate) | Port opened after the main port |
mcp = OFF but an mcp_* variable given | Note: mcp_* variables ignored, the MCP endpoint is disabled (mcp = OFF; --mcp ON to open it). | Server started, MCP port closed |
| Express edition | WARNING: the MCP endpoint requires the Enterprise, Cluster or Developer edition; MCP port not opened. | Server started, MCP port closed |
mcp_port equal to port | MCP endpoint: mcp_port (7007) is also the main port; choose another port (--mcp-port); server stopped. | Server stopped (exit code 2) |
mcp_bind outside the loopback without a certificate | MCP endpoint: listening on 0.0.0.0 outside the loopback without TLS refused; provide a certificate (--tls-cert, --tls-key) or keep mcp_bind on 127.0.0.1; server stopped. | Server stopped (code 2) |
| Port already taken | MCP endpoint: unable to listen on 127.0.0.1:7008: … ; server stopped. | Server stopped (code 2) |
| Value out of range | Invalid value for mcp_port: 70000 (from 0 to 65535), Invalid value for mcp_max_rows: 0 (from 1 to 1000000)…; --mcp: "maybe" unknown (ON, OFF) | Server stopped (code 2) |
The port and TLS rules are checked before the databases are opened: a wrong configuration is never discovered after a long load.
20.3.5 TLS (HTTPS)#
The endpoint uses the same certificate as the main port (tls_cert and tls_key in miraj_config.xml, or --tls-cert and --tls-key, see chapter 11):
- without a certificate, the endpoint speaks plain HTTP and can only listen on the loopback;
- with a certificate, all MCP connections are encrypted, including on the loopback: the address becomes
https://host:7008/mcpand a plain client gets no response.--require-tlsand--allow-plain-with-tlsonly concern the main port; - outside the loopback (public
mcp_bind), the certificate is mandatory: without it the server refuses to start.
miraj-server.exe --root D:\miraj\data --log --mcp ON --mcp-bind 0.0.0.0 ^
--tls-cert D:\miraj\tls\cert.pem --tls-key D:\miraj\tls\key.pemThe client must trust the certificate: with a certificate issued by an internal authority or self-signed, declare it to the client (for clients written in Node.js, such as many command-line clients, the NODE_EXTRA_CA_CERTS environment variable points to a PEM file of additional authorities).
20.4 How do MCP tokens work?#
An assistant authenticates with a token: a secret mjt_… presented in the HTTP header Authorization: Bearer mjt_…. The token belongs to a Miraj account, whose privileges it takes, and carries an access level that caps them (20.5). It is revoked without touching the account.
20.4.1 CREATE MCP TOKEN#
CREATE MCP TOKEN name [FOR account]
ACCESS {STRUCTURE | READ | WRITE | ADMIN}
[DATABASES (db1, db2, ...)]
[EXPIRE {NEVER | INTERVAL n DAY}]name: identifier, backtick-quoted identifier or string, 1 to 64 characters (1470 beyond). It is unique on the server, case-insensitively, whatever the account (error 9035 if it is taken).FOR account:'user'@'host'oruser, as inCREATE USER; withoutFOR, the token belongs to the session's account. An unknown role or account is refused (1396).ACCESS: level of the token (20.5). Any other word is a syntax error (1064).DATABASES (…): scope of the token; without this clause, all the databases the account can reach. Names are not checked at creation: a database created later under that name enters the scope.EXPIRE INTERVAL n DAY: the token expiresndays after its creation (n≥ 1;0is a syntax error);EXPIRE NEVER(default): never.- The clauses that follow
ACCESSmay be written in any order, each once.
The statement returns one row with the secret, displayed this one time only:
CREATE MCP TOKEN assistant_ventes FOR 'app'@'localhost'
ACCESS READ DATABASES (ventes, catalogue) EXPIRE INTERVAL 90 DAY;+------------------+------------------------------+-------------------+--------+---------------------+
| name | token | account | access | expires |
+------------------+------------------------------+-------------------+--------+---------------------+
| assistant_ventes | mjt_Xq7mPt3hKc9WbZ2rNf8sLd4v | 'app'@'localhost' | READ | 2026-12-24 10:15:00 |
+------------------+------------------------------+-------------------+--------+---------------------+The secret is mjt_ followed by 24 characters drawn by the system generator. Miraj keeps only its SHA-256 fingerprint, in the encrypted account vault: a lost secret cannot be recovered, you must drop the token and create another. expires is in UTC, NULL for a token without expiration.
20.4.2 SHOW MCP TOKENS#
SHOW MCP TOKENS;+------------------+------+-----------+--------+-------------------+---------------------+---------------------+
| Name | User | Host | Access | Databases | Created | Expires |
+------------------+------+-----------+--------+-------------------+---------------------+---------------------+
| assistant_ventes | app | localhost | READ | ventes,catalogue | 2026-09-25 10:15:00 | 2026-12-24 10:15:00 |
+------------------+------+-----------+--------+-------------------+---------------------+---------------------+One row per token, sorted by name; Databases is NULL without a scope, Expires NULL without expiration; dates in UTC. The secret and its fingerprint are never shown. An account that has the CREATE USER privilege sees all the tokens of the server, the others only those of their own account. Miraj does not keep a "last used" date for tokens.
20.4.3 DROP MCP TOKEN#
DROP MCP TOKEN assistant_ventes;
DROP MCP TOKEN IF EXISTS assistant_ventes; -- warning 9036 if it does not existRevocation is immediate: the token is rechecked on every HTTP request, so the next request of an already open session receives 401. Without IF EXISTS, an unknown token gives error 9036.
20.4.4 Required rights#
| Operation | Right |
|---|---|
| Create or drop a token of your own account | None |
| Create or drop a token of another account | CREATE USER (1227 otherwise); plus SYSTEM_USER if that account holds SYSTEM_USER |
SHOW MCP TOKENS | None (your own tokens); CREATE USER to see them all |
Tokens are managed through the main port (miraj-cli, application, administration tool), never from an MCP session: even with an ADMIN token, CREATE MCP TOKEN and DROP MCP TOKEN are refused there (9034). A limited token can therefore never forge a broader token. SHOW MCP TOKENS is only allowed there at the ADMIN level.
20.4.5 Token and account#
- Privileges: the token grants none; the session has those of the account (and of its default roles), reduced by the level and scope of the token.
- Account host: the address of the MCP client must be accepted by the account's host, as for a password connection. A token of
'app'@'localhost'can only be used from the server machine; for an assistant on another machine, bind the token to an account whose host accepts that machine ('app'@'10.0.0.%', for example). - Locked account (
ALTER USER … ACCOUNT LOCK): its tokens are refused (3118 in the journal, 401 for the client) until it is unlocked. DROP USERdeletes the account's tokens.- Storage: tokens are kept in the account vault; they survive a restart and, under Cluster, follow the accounts.
20.4.6 Token errors#
| Code | Case |
|---|---|
| 9001 | CREATE MCP TOKEN in the Express edition |
| 9034 | Token management from an MCP session; SHOW MCP TOKENS below the ADMIN level |
| 9035 | Token name already taken on the server |
| 9036 | DROP MCP TOKEN of an unknown token (warning with IF EXISTS) |
| 1396 | Unknown account, or role, in FOR |
| 1227 | Token of another account without CREATE USER |
| 1470 | Empty token name or one longer than 64 characters |
| 1064 | Unknown level, EXPIRE INTERVAL 0 DAY |
At connection, an unknown secret, an expired token, an account that is a role or whose host does not accept the client give 1045, a locked account 3118; in all cases the MCP client receives a 401 without detail, the precise error being recorded only in the journal.
20.5 Which access levels exist?#
Four levels, from the most restricted to the broadest: STRUCTURE < READ < WRITE < ADMIN.
20.5.1 Tools by level#
tools/list shows the assistant only the tools of its level; calling a tool above it is refused (JSON-RPC error -32602, 20.11).
| Level | Tools |
|---|---|
STRUCTURE | list_databases, list_objects, describe_table, show_create, search_schema |
READ | + read_rows, get_row, count_rows, explain, execute_sql |
WRITE | + insert_rows, update_rows, delete_rows |
ADMIN | + create_database, create_table, drop_object (all sixteen tools) |
20.5.2 Allowed and refused statements#
Each statement executed by the session (through execute_sql, through a tool, or inside a routine or a trigger) requires a level according to its nature; above the token's level, it is refused with error 9034:
| Level | Allowed | Refused (9034) |
|---|---|---|
STRUCTURE | SHOW, DESCRIBE, USE, session SET, transactions, SELECT without a table or on information_schema only | Any data read: SELECT on a table, including in a subquery (SET @n = (SELECT …)), EXPLAIN, CALL; any write |
READ | + SELECT, SELECT … INTO variables, EXPLAIN, CALL, CHECK TABLE, LOCK TABLES … READ, REFRESH VIEW, LISTEN, UNLISTEN, WAIT FOR CHANGES, SHOW LISTENERS | INSERT, UPDATE, DELETE, REPLACE, LOAD DATA, NOTIFY, temporary tables, and any write in the body of a routine or trigger |
WRITE | + INSERT, UPDATE, DELETE, REPLACE, LOAD DATA, NOTIFY, LOCK TABLES … WRITE, CREATE / DROP TEMPORARY TABLE | DDL (CREATE, ALTER, DROP, RENAME of databases, tables, views, routines, triggers, events), TRUNCATE, REPAIR / OPTIMIZE TABLE, accounts and GRANT / REVOKE, BACKUP / RESTORE, SET GLOBAL, KILL, FLUSH, SELECT … INTO OUTFILE |
ADMIN | Everything the account allows | MCP token management (always) |
Two session settings are reserved to the ADMIN level: SET max_statement_time and SET max_execution_time, because the endpoint sets the mcp_statement_timeout delay there. SET ROLE remains allowed at all levels: the privileges of the activated roles remain reduced to the token's level.
The message names the statement and the level:
ERROR 9034 (HY000): INSERT is not allowed at MCP access level READ20.5.3 A cap added on top of privileges#
The level never grants a privilege: it removes those that exceed it. An MCP session can only do what its account can do and what its level allows. Even root is capped by a READ token; an ADMIN token of an account that only has SELECT on a table reads only that table.
Concretely, the effective privileges of the session are reduced to those of the level:
| Level | Privileges kept |
|---|---|
STRUCTURE | SHOW VIEW, SHOW DATABASES, REFERENCES |
READ | + SELECT, EXECUTE, LOCK TABLES |
WRITE | + INSERT, UPDATE, DELETE, CREATE TEMPORARY TABLES |
ADMIN | All those of the account |
An object on which the account has a privilege removed by the level remains visible: a STRUCTURE token describes a table the account can read, without being able to read it. Under ADMIN, the privileges removed by the level (PROCESS, FILE…) come back; below it, SHOW PROCESSLIST thus shows only the account's own sessions.
20.5.4 The DATABASES scope#
With DATABASES (…), the other databases no longer exist for the session, even for an account that has all global privileges:
SHOW DATABASES,list_databases,search_schemaandinformation_schemado not show them;- naming them is refused as if the account had no right on them (1044, 1142),
USE other_dbincluded; - privileges granted at the global level (
ON *.*) apply on each database of the scope, without globalSHOW DATABASESor globalGRANToption;CREATE DATABASEof a database outside the scope is refused (1044).
20.5.5 Routines, triggers and views#
- The body of a procedure, a function or a trigger is subject to the session's level, statement by statement: with a
READtoken,CALLof a procedure that inserts is refused (9034) at the moment of theINSERT, as is aSELECTthat calls a function that writes.PREPAREpasses,EXECUTEis checked on the prepared statement. - With a
WRITEtoken, triggers fire normally; a procedure that does DDL is refused. - Definer rights (view,
SQL SECURITY DEFINERroutine) are not reduced: a view reads what its definer allows it, as for any account. The statements of its body remain subject to the level. Take this into account before opening to a token a database that contains views or routines defined by a broader account. - A refusal occurs before the implicit commit of a DDL: an open transaction is not committed by a refused
CREATE TABLE.
20.6 How do I connect an MCP client?#
The endpoint speaks the MCP "Streamable HTTP" transport: a single address, http://host:port/mcp (or https://… with a certificate), and the token in the Authorization header.
20.6.1 Generic MCP client (JSON file)#
Most MCP clients describe their servers in a JSON file. For example, the .mcp.json file of a project:
{
"mcpServers": {
"miraj": {
"type": "http",
"url": "https://db-server.example.local:7008/mcp",
"headers": {
"Authorization": "Bearer mjt_Xq7mPt3hKc9WbZ2rNf8sLd4v"
}
}
}
}The names of the keys (type, transport, headers…) vary from one client to another: refer to its documentation. Two constants: the transport is HTTP ("streamable HTTP"), and the Authorization: Bearer <secret> header must accompany every request. The secret gives access to your data: do not put this file under version control with the secret in clear (prefer, if the client allows it, an environment variable).
20.6.2 Clients that can only launch a process (stdio)#
Some clients only speak MCP to a process they launch (stdio transport). A miraj-mcp bridge (stdio to HTTP, token in an environment variable) is planned but not yet delivered. In the meantime, use a client that accepts the HTTP transport, or a third-party stdio-to-HTTP bridge able to forward the Authorization header (not provided or validated by Miraj).
20.7 Tool reference#
20.7.1 Shape of responses and errors#
A tool call (tools/call) always returns:
{
"content": [{"type": "text", "text": "…the same JSON as structuredContent, as text…"}],
"structuredContent": { … },
"isError": false
}structuredContentfollows the tool's output schema (outputSchema);content[0].textis its text copy, for clients that do not read structured results.- An SQL error or refused arguments are not protocol errors: the call succeeds with
isError: trueand anerrorfield, so that the assistant reads the error and corrects its call:
{"results": [], "warnings": [],
"error": {"code": 9034, "sqlstate": "HY000", "message": "INSERT is not allowed at MCP access level READ"}}- Malformed arguments (unknown argument, wrong type, missing value, unknown operator…) give error 1210 (
Incorrect arguments to read_rows ('where' is required; …)), before any execution. An unknown column name gives 1054. - Read results all have the same shape (
read_rows,get_row,explain,execute_sql):
{
"results": [
{"columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(20)"}],
"rows": [[2, "cahier"]],
"row_count": 1, "truncated": true,
"affected_rows": null, "last_insert_id": null}
],
"warnings": []
}truncated is true when rows were not returned (row or size limit); warnings lists at most 64 warnings {code, message}.
- Common safeguards: at most
mcp_max_rowsrows per result, and about 340 KB of rows as JSON per call (the whole response stays under 1 MB); beyond that,truncated: true. - Visibility: tools see what
SHOWwould show the capped account. An invisible database gives 1044; a table on which the account has no right gives 1142, whether or not it exists (its existence is not revealed); then 1049 (unknown database) and 1146 (unknown table).information_schemais not served by the structured tools (1210): query it throughexecute_sql. - Database, table and column names are compared case-insensitively; results return the exact name.
20.7.2 list_databases — STRUCTURE#
Lists the databases visible to the token (account privileges and DATABASES scope), without information_schema.
| Argument | Type | Required | Role |
|---|---|---|---|
| (none) |
Result: {"databases": ["catalogue", "ventes"]}.
{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "list_databases", "arguments": {}}}20.7.3 list_objects — STRUCTURE#
Lists the objects of a database: tables (with an estimated row count), views, procedures, functions, triggers, events. An object on which the account has no right is not listed; routines and events require a right on the database, a trigger is visible with its table.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
kind | table, view, procedure, function, trigger, event | no | A single kind of object (1210 for another value) |
Result: {"database": …, "objects": [ … ]}, each object with name and kind, plus rows (tables: physical rows minus deleted rows), table, timing (BEFORE, AFTER) and event (INSERT, UPDATE, DELETE) for a trigger, status for an event, comment for a routine or an event.
{"name": "list_objects", "arguments": {"database": "ventes"}}{"database": "ventes", "objects": [
{"name": "article", "kind": "table", "rows": 3},
{"name": "v_stock", "kind": "view"},
{"name": "maj_stock", "kind": "trigger", "table": "ligne", "timing": "AFTER", "event": "INSERT"}]}20.7.4 describe_table — STRUCTURE#
Describes a table or a view without reading its data.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table or view |
Result for a table: database, table, kind (table), rows (estimated), columns (for each one name, full SQL type such as decimal(8,2), nullable, default as text or null, key PRI/UNI/MUL, extra such as auto_increment, comment), primary_key, unique and indexes ({name, columns}), vector_index ({name, column, m, distance} or null), foreign_keys ({name, columns, ref_database, ref_table, ref_columns, on_delete, on_update}), checks ({name, expression, enforced}). For a view: kind view and its columns only.
{"name": "describe_table", "arguments": {"database": "ventes", "table": "article"}}{"database": "ventes", "table": "article", "kind": "table", "rows": 3,
"columns": [
{"name": "id", "type": "int", "nullable": false, "default": null, "key": "PRI", "extra": "auto_increment", "comment": ""},
{"name": "prix", "type": "decimal(8,2)", "nullable": true, "default": null, "key": "", "extra": "", "comment": ""}],
"primary_key": ["id"], "unique": [], "indexes": [], "vector_index": null, "foreign_keys": [], "checks": []}20.7.5 show_create — STRUCTURE#
Returns the SQL definition (CREATE …) of an object, like SHOW CREATE.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
name | string | yes | Object name |
kind | table, view, procedure, function, trigger, event | no | Without kind, the name is searched as a table, then as a view |
Result: {"database", "name", "kind", "sql"}.
{"name": "show_create", "arguments": {"database": "ventes", "name": "article"}}{"database": "ventes", "name": "article", "kind": "table", "sql": "CREATE TABLE `article` (…)"}20.7.6 search_schema — STRUCTURE#
Searches for tables, views, columns, procedures and functions whose name contains a text, in all visible databases. Case-insensitive search, plain text (no % or _ wildcards). Routines require a right on the database, as in list_objects.
| Argument | Type | Required | Role |
|---|---|---|---|
pattern | string | yes | Text searched in names |
limit | integer, 1 to 1000 | no | Maximum matches returned (100 by default) |
Result: {"matches": [ … ], "truncated": …}, each match with database and kind (table, view, column, procedure or function): table for a table, a view or a column (plus column and type for a column), name for a routine.
{"name": "search_schema", "arguments": {"pattern": "prix"}}{"matches": [{"database": "ventes", "table": "article", "kind": "column", "column": "prix", "type": "decimal(8,2)"}],
"truncated": false}20.7.7 read_rows — READ#
Reads the rows of a table or a view without writing SQL.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table or view |
columns | array of names | no | Columns returned (all by default, or if the array is empty) |
where | object | no | Structured condition (20.8) |
order_by | array | no | Column names, or {"column": name, "desc": true}; by default, the primary key (stable pagination) |
limit | integer ≥ 1 | no | Maximum rows returned: 100 by default, brought down to mcp_max_rows |
offset | integer ≥ 0 | no | Rows skipped first |
Result: common read shape (20.7.1), a single result; truncated is exact: it is true if at least one more row than limit exists.
{"name": "read_rows", "arguments": {
"database": "ventes", "table": "article",
"columns": ["id", "nom"],
"where": {"column": "prix", "op": ">=", "value": 2},
"order_by": [{"column": "prix", "desc": true}],
"limit": 1}}{"results": [{"columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(20)"}],
"rows": [[2, "cahier"]], "row_count": 1, "truncated": true, "affected_rows": null, "last_insert_id": null}],
"warnings": []}To paginate, keep the default ordering and increase offset by limit as long as truncated is true. A column or sort-order name that is not an exact column name ("prix DESC", "id, nom FROM x") is refused (1054).
20.7.8 get_row — READ#
Reads the row of a table whose primary key equals key.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table (with a primary key) |
key | object {column: value} | yes | All the columns of the primary key, and only them |
Result: common read shape, zero or one row. An incomplete key, a column outside the key or a table without a primary key (a view, for example) are refused (1210).
{"name": "get_row", "arguments": {"database": "ventes", "table": "article", "key": {"id": 3}}}{"results": [{"columns": [ … ], "rows": [[3, "règle", 3.25, -1, null]], "row_count": 1, "truncated": false,
"affected_rows": null, "last_insert_id": null}], "warnings": []}20.7.9 count_rows — READ#
Counts the rows of a table or a view, optionally those that satisfy a condition.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table or view |
where | object | no | Structured condition (20.8) |
Result: {"count": n, "warnings": []}.
{"name": "count_rows", "arguments": {"database": "ventes", "table": "article",
"where": {"column": "nom", "op": "like", "value": "%e%"}}}{"count": 2, "warnings": []}20.7.10 explain — READ#
Displays the execution plan of one SELECT, UPDATE, DELETE or INSERT … SELECT statement without executing it (EXPLAIN).
| Argument | Type | Required | Role |
|---|---|---|---|
sql | string | yes | A single statement |
Result: common read shape, one row per table access (at most 1000 rows). A text with several statements (SELECT 1; DROP DATABASE ventes) or another kind of statement is refused (1210): a ; cannot smuggle in a second statement. A syntax error returns 1064.
{"name": "explain", "arguments": {"sql": "SELECT nom FROM ventes.article WHERE id = 1"}}20.7.11 insert_rows — WRITE#
Inserts up to 1000 rows, each given as {column: value}.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table |
rows | array of 1 to 1000 objects | yes | Rows; an omitted column takes its default value |
on_duplicate | error, ignore, update | no | Row whose key already exists: error 1062 (error, default), row skipped (ignore, like INSERT IGNORE), existing row updated with the given non-primary-key values (update, like ON DUPLICATE KEY UPDATE) |
Rows that give the same columns are inserted by a single statement; different sets of columns give several statements, executed in a transaction opened by the tool and rolled back at the first failure (nothing is inserted). If the session already has an open transaction, the tool works in it without committing it. CHECK constraints (4025), foreign keys (1452), uniqueness and triggers apply.
Result: {"affected_rows", "last_insert_id", "statements", "warnings"}; last_insert_id is the first AUTO_INCREMENT value generated by the last insertion, null if the table has no AUTO_INCREMENT column; statements: statements executed. On failure, error and affected_rows at 0 if the tool's transaction was rolled back.
{"name": "insert_rows", "arguments": {"database": "ventes", "table": "article",
"rows": [{"nom": "gomme", "prix": 0.5}, {"nom": "feutre", "prix": 1}]}}{"affected_rows": 2, "last_insert_id": 4, "statements": 1, "warnings": []}20.7.12 update_rows — WRITE#
Modifies the rows that satisfy a condition.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table |
set | object {column: value} | yes | New values (at least one column) |
where | object | yes, unless all_rows | Structured condition (20.8) |
all_rows | boolean | no | true to modify all rows without a condition |
A missing or empty where ({}, {"and": []}) is refused (1210) without "all_rows": true: an assistant does not modify an entire table by oversight. Result: {"affected_rows", "last_insert_id" (null), "statements", "warnings"}.
{"name": "update_rows", "arguments": {"database": "ventes", "table": "article",
"set": {"prix": 9.99}, "where": {"column": "id", "op": "=", "value": 2}}}{"affected_rows": 1, "last_insert_id": null, "statements": 1, "warnings": []}20.7.13 delete_rows — WRITE#
Deletes the rows that satisfy a condition; same rule as update_rows for where and all_rows.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table |
where | object | yes, unless all_rows | Structured condition (20.8) |
all_rows | boolean | no | true to delete all rows |
{"name": "delete_rows", "arguments": {"database": "ventes", "table": "article",
"where": {"column": "nom", "op": "=", "value": "gomme"}}}{"affected_rows": 1, "last_insert_id": null, "statements": 1, "warnings": []}20.7.14 create_database — ADMIN#
| Argument | Type | Required | Role |
|---|---|---|---|
name | string, 1 to 64 characters | yes | Database name |
charset | string | no | Character set (utf8mb4): letters, digits and _ only |
collation | string | no | Collation (utf8mb4_general_ci), same characters |
if_not_exists | boolean | no | No error if the database exists (warning 1007) |
Result: {"sql", "warnings"}, sql being the statement executed, returned even in case of error.
{"name": "create_database", "arguments": {"name": "atelier"}}{"sql": "CREATE DATABASE `atelier`", "warnings": []}20.7.15 create_table — ADMIN#
Creates a table from a structured definition. Miraj writes the canonical SQL text (identifiers in backticks, escaped strings, types taken from an allow list, CHECK expressions re-parsed then rewritten) before executing it: nothing is copied as is.
| Argument | Type | Required | Role |
|---|---|---|---|
database | string | yes | Database |
table | string | yes | Table name |
columns | array of objects (at least one) | yes | Columns, see below |
primary_key | array of names | no | Primary key |
unique | array of arrays of names | no | UNIQUE constraints, one per list |
indexes | array of {name?, columns} | no | Secondary indexes |
foreign_keys | array of objects | no | {name?, columns, ref_database?, ref_table, ref_columns, on_delete?, on_update?}; ref_database defaults to the table's database; actions RESTRICT, CASCADE, SET NULL, SET DEFAULT, NO ACTION |
checks | array of strings | no | CHECK expressions ("prix >= 0"), without subquery or parameter |
vector_index | object | no | HNSW vector index, see below |
partitioning | object | no | Partitioning, see below |
if_not_exists | boolean | no | No error if the table exists |
Each column:
| Key | Type | Required | Role |
|---|---|---|---|
name | string | yes | Name (1 to 64 characters) |
type | string | yes | SQL type: TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT (optional size), BIT, FLOAT, DOUBLE, DECIMAL(p,s), BOOLEAN, CHAR(n), VARCHAR(n), BINARY(n), VARBINARY(n), TEXT and BLOB (and their TINY, MEDIUM, LONG variants), DATE, TIME, DATETIME, TIMESTAMP (optional precision), YEAR, JSON, UUID, VECTOR(n), ENUM, SET; UNSIGNED for integers, DECIMAL, FLOAT and DOUBLE. Spatial types are refused |
values | array of strings | for ENUM and SET | Members |
nullable | boolean | no | true by default; false: NOT NULL |
default | JSON value | no | Literal value (null, boolean, number, string, {"hex": …}); "CURRENT_TIMESTAMP" or "CURRENT_TIMESTAMP(n)" for a date and time column. Any other expression ("NOW()") becomes a string |
auto_increment | boolean | no | AUTO_INCREMENT |
comment | string | no | Comment |
An unknown key, a type outside the list, a wrong number of parameters (VARCHAR without a size) are refused (1210) before any execution. Result: {"sql", "warnings"}.
{"name": "create_table", "arguments": {
"database": "atelier", "table": "commande",
"columns": [
{"name": "id", "type": "int", "nullable": false, "auto_increment": true},
{"name": "client", "type": "int", "nullable": false},
{"name": "montant", "type": "decimal(10,2)", "default": 0},
{"name": "cree", "type": "datetime", "default": "CURRENT_TIMESTAMP"}],
"primary_key": ["id"],
"indexes": [{"name": "i_montant", "columns": ["montant"]}],
"foreign_keys": [{"columns": ["client"], "ref_table": "client", "ref_columns": ["id"], "on_delete": "CASCADE"}],
"checks": ["montant >= 0"]}}{"sql": "CREATE TABLE `atelier`.`commande` (`id` INT NOT NULL AUTO_INCREMENT, `client` INT NOT NULL, `montant` DECIMAL(10,2) DEFAULT 0, `cree` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `i_montant` (`montant`), FOREIGN KEY (`client`) REFERENCES `atelier`.`client` (`id`) ON DELETE CASCADE, CHECK (…))",
"warnings": []}Vector index (vector_index):
| Key | Type | Required | Role |
|---|---|---|---|
column | string | yes | VECTOR(n) column declared NOT NULL |
name | string | no | Name (by default, that of the column) |
m | integer | no | Links per node (3 to 200, 1912 otherwise) |
distance | euclidean, cosine, dot | no | Metric served |
comment | string | no | Comment |
Partitioning (partitioning, PARTITION BY clause):
| Key | Type | Required | Role |
|---|---|---|---|
method | string | yes | RANGE, RANGE COLUMNS, LIST, LIST COLUMNS, HASH, LINEAR HASH, KEY, LINEAR KEY |
expression | string | for RANGE, LIST, HASH | Expression, re-parsed then rewritten like a CHECK |
columns | array of names | for … COLUMNS | Columns; optional for KEY (primary key) |
algorithm | integer | no | KEY algorithm |
count | integer, 1 to 8192 | no | PARTITIONS n (HASH, KEY) |
partitions | array of objects | for RANGE, LIST | {name, less_than?, values_in?, comment?, subpartitions?}: less_than is an array of literals ({"maxvalue": true} for MAXVALUE) or the string "MAXVALUE"; values_in, an array of literals (one array per value for LIST COLUMNS on several columns) |
subpartition | object | no | {method, expression?, columns?, algorithm?, count?} with HASH, LINEAR HASH, KEY or LINEAR KEY |
The engine's rules (primary key containing the partitioning columns, increasing bounds…) apply: their errors are returned as is. Table options, generated columns and ON UPDATE are not supported by this tool: use execute_sql.
20.7.16 drop_object — ADMIN#
Drops a table, a view or an entire database with its data. Irreversible.
| Argument | Type | Required | Role |
|---|---|---|---|
kind | table, view, database | yes | Kind of object |
name | string | yes | Object name |
database | string | for a table or a view | Database of the object |
confirm | string | yes | Must repeat name exactly (same case); otherwise nothing is dropped (1210) |
Result: {"sql", "warnings"}.
{"name": "drop_object", "arguments": {"database": "atelier", "name": "commande", "kind": "table", "confirm": "commande"}}{"sql": "DROP TABLE `atelier`.`commande`", "warnings": []}Routines, triggers, events and indexes are dropped through execute_sql.
20.7.17 execute_sql — READ and beyond#
Executes an SQL script (one or several statements separated by ;) and returns each result. The token's level filters what passes (20.5.2): with a READ token, the tool only reads.
| Argument | Type | Required | Role |
|---|---|---|---|
sql | string | yes | SQL script |
params | array | no | Values of the ? markers, in order (20.9) |
max_rows | integer ≥ 1 | no | Rows kept per result: 100 by default, brought down to mcp_max_rows |
Result: common read shape, one result per statement; a statement that writes gives a result without columns with affected_rows and last_insert_id. On error, the results already obtained are kept, followed by error; the following statements of the script are not executed.
{"name": "execute_sql", "arguments": {
"sql": "SELECT id, nom, prix, gros, image FROM ventes.article WHERE id <= ? ORDER BY id",
"params": [2]}}{"results": [{"columns": [{"name": "id", "type": "int"}, {"name": "nom", "type": "varchar(20)"},
{"name": "prix", "type": "decimal(8,2)"}, {"name": "gros", "type": "bigint"}, {"name": "image", "type": "varbinary(4)"}],
"rows": [[1, "stylo", 1.50, "9007199254740993", "0x00FF"], [2, "cahier", 20.00, 7, null]],
"row_count": 2, "truncated": false, "affected_rows": null, "last_insert_id": null}],
"warnings": []}Good to know:
- the MCP session is persistent: session variables,
USE, temporary tables and open transactions (START TRANSACTIONin one call,COMMITin another) are kept from one call to the next, until the session is closed; max_rowsbounds what is returned, not what is computed: for a large table, write aLIMITin the query;LOAD DATA LOCAL INFILEis refused (1148): the MCP client has no file to supply;- the tool's annotations follow the level: read-only at
READ, destructive beyond.
20.8 Structured where conditions#
read_rows, count_rows, update_rows and delete_rows take their condition as a JSON object, never as SQL text.
20.8.1 Simple condition#
{"column": "prix", "op": ">=", "value": 10}op | Equivalent SQL | value |
|---|---|---|
=, != (or <>) | =, <> | a value; with null, IS NULL / IS NOT NULL |
<, <=, >, >= | comparison | a value |
like, not_like | LIKE, NOT LIKE | a string, SQL wildcards % and _ |
in, not_in | IN (…), NOT IN (…) | an array of 1 to 10,000 values |
between | BETWEEN a AND b | an array of two bounds |
is_null, is_not_null | IS NULL, IS NOT NULL | none (value refused) |
column must be a column name of the table (1054 otherwise, case-insensitively); values become literals: "' OR 1=1 --" remains a string compared as is.
20.8.2 Groups#
{"and": [ condition, … ]} {"or": [ condition, … ]} {"not": condition}Each group is the only key of its object. Example:
{"and": [
{"column": "prix", "op": ">=", "value": 10},
{"or": [{"column": "nom", "op": "=", "value": "a"}, {"column": "nom", "op": "=", "value": "b"}]},
{"not": {"column": "id", "op": "=", "value": 3}}
]}is equivalent to prix >= 10 AND (nom = 'a' OR nom = 'b') AND NOT (id = 3).
Rules:
{}and{"and": []}set no condition (and count as a missingwhereforupdate_rowsanddelete_rows); an emptyoris refused rather than read as "all rows";- nesting depth 32 at most, 1000 simple conditions at most;
- an unknown key (
"sql": "1=1") or a text condition ("where": "id = 1") is refused (1210).
20.9 JSON values#
20.9.1 Returned values#
| SQL type | JSON returned | Example |
|---|---|---|
| Integers | number; string beyond 2^53 (9,007,199,254,740,992) in absolute value, for JSON readers that read numbers as floating point | 7, "9007199254740993" |
DECIMAL | number written exactly, without going through a floating point | 1.50 |
FLOAT, DOUBLE | number; null for an infinite value or NaN | 2.5 |
Strings, JSON | string | "stylo" |
| Dates and times | string in SQL format | "2026-09-25 10:15:00" |
Binary (BINARY, VARBINARY, BLOB) | hexadecimal string 0x… in uppercase | "0x00FF" |
NULL | null | null |
A decimal is exact in the response text (1.50); a JSON reader that converts it to floating point may display it as 1.5. The SQL type of each column appears in columns[].type.
20.9.2 Values given#
In where, rows, set, key and default:
| JSON | SQL value |
|---|---|
null, true, false | NULL, TRUE, FALSE |
| integer | integer; beyond the range of signed 64-bit integers, passed as a string of digits converted by the column |
number with a decimal point and no exponent (12.50, at most 18 decimals) | exact decimal |
number with exponent (1e300) | floating point |
| string | string; dates are written "YYYY-MM-DD" or "YYYY-MM-DD hh:mm:ss" |
{"hex": "00ff"} (0x prefix accepted) or {"base64": "AP8="} | bytes |
object or array, for a JSON column of a table | serialized JSON text |
An array or another object is refused for a non-JSON column (1210).
In the params of execute_sql, the same values, with two differences: a number with a decimal point is passed there as a floating point (give a string, "12.50", for an exact decimal), and objects or arrays are only accepted in the {"hex"} / {"base64"} form.
20.10 MCP resources#
In addition to tools, the endpoint publishes JSON resources (application/json), filtered by the same privileges as the STRUCTURE tools:
| URI | Content |
|---|---|
miraj://{base} | Objects of the database, like list_objects |
miraj://{base}/{table} | Description of the table or view, like describe_table, plus create: its CREATE definition |
resources/listreturns one resource per visible database (miraj://ventes, nameventes, titleDatabase ventes);resources/templates/listreturns the two templatesmiraj://{database}andmiraj://{database}/{table};resources/readreturns{"contents": [{"uri", "mimeType", "text"}]},textbeing the JSON of the content;- names other than letters, digits,
-,.,_and~are percent-encoded (miraj://ma%20base/a%2Fb); - a malformed URI gives JSON-RPC error -32602, a resource that is not found or refused -32002;
- subscription to resource changes is not offered.
20.11 Protocol (for integrators)#
This section describes the exchanges for those who write their own client; an ordinary MCP client handles them on its own.
20.11.1 Transport#
- Streamable HTTP without an SSE stream: each JSON-RPC request is a
POST /mcpwhose response is anapplication/jsonbody; HTTP/1.1 (persistent connections) or HTTP/1.0. - A single message per request: batches (JSON array) are refused (-32600).
- Body of 16 MB at most;
Transfer-Encoding: chunkedandExpect: 100-continueaccepted. DELETE /mcpcloses the session;GET /mcp(server stream) is not offered (405); any other path: 404.- Recognized protocol versions:
2025-11-25,2025-06-18,2025-03-26.initializeretains the client's if it is recognized, otherwise proposes the most recent.
20.11.2 Headers#
| Header | Meaning | Rule |
|---|---|---|
Authorization: Bearer mjt_… | request | Required on every request, DELETE included (401 otherwise) |
Content-Type: application/json | request | If present, must be application/json (415 otherwise) |
Mcp-Session-Id | response to initialize, then every request | 32 hexadecimal characters drawn at random; required after initialize (400 without it, 404 unknown or expired); a session only accepts the token that opened it (401) |
MCP-Protocol-Version | request | Optional; if present, must be a recognized version (400 otherwise) |
Origin | request | If present, must be a loopback origin (http(s)://localhost, 127.0.0.1 or [::1], any port), otherwise 403: protection against web pages that would target the server from a browser |
WWW-Authenticate: Bearer realm="miraj" | 401 response | |
Cache-Control: no-store | every response |
20.11.3 Course of a session#
POST /mcp initialize → 200, Mcp-Session-Id header, capabilities, instructions
POST /mcp notifications/initialized → 202 (no body)
POST /mcp tools/list → 200, tools of the token's level
POST /mcp tools/call → 200, tool result (possible isError)
DELETE /mcp → 204, session closed, open transaction rolled backWith curl (POSIX shell syntax):
curl -i http://127.0.0.1:7008/mcp \
-H "Authorization: Bearer mjt_Xq7mPt3hKc9WbZ2rNf8sLd4v" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"test","version":"1"}}}'The result of initialize announces serverInfo (name miraj, title Miraj, version), the tools and resources capabilities (without listChanged or subscription) and an instructions text that presents to the assistant the token, the account, the level, the scope and the row limit.
Methods served: initialize, ping, tools/list, tools/call, resources/list, resources/templates/list, resources/read, and the notifications notifications/initialized and notifications/cancelled (which interrupts the current call, like KILL QUERY). Any other notification, and any response from the client, receives 202 and is ignored; any other method (prompts/list, for example) gives -32601. A session processes one request at a time: simultaneous requests on the same session run one after the other.
20.11.4 HTTP codes#
| Code | Case |
|---|---|
| 200 | JSON-RPC response (success, JSON-RPC error or tool result with isError) |
| 202 | Client notification or response acknowledged, without a body |
| 204 | DELETE: session closed |
| 400 | Unreadable JSON (-32700), invalid message (-32600), missing Mcp-Session-Id, unknown MCP-Protocol-Version, malformed HTTP request |
| 401 | Missing, unknown, expired, revoked token, locked account or refused host; session opened by another token |
| 403 | Origin foreign to the loopback; address blocked after too many failures (message of error 1129; with TLS, the connection of a blocked address is simply closed, without a response) |
| 404 | Unknown or expired session ("Session not found or expired: initialize a new session"); path other than /mcp |
| 405 | Method other than POST and DELETE (Allow: POST, DELETE header) |
| 413 | Body larger than 16 MB |
| 415 | Content-Type other than application/json |
| 431 | Headers too long (32 KB) or too many (64) |
| 501 | Transfer-Encoding other than chunked |
| 503 | max_connections reached when opening a session (message of error 1040) |
| 505 | HTTP version other than 1.0 and 1.1 |
A connection must present its first request within 10 seconds (TLS handshake included), and a started request must arrive in full within 30 seconds.
20.11.5 JSON-RPC errors#
| Code | Case |
|---|---|
| -32700 | Body that is not JSON |
| -32600 | Invalid message: batch, jsonrpc other than "2.0", id that is neither a string nor a number, missing method; repeated initialize in a session; missing Mcp-Session-Id; unknown protocol version; refused Content-Type |
| -32601 | Unknown method |
| -32602 | tools/call without a name, unknown tool or above the token's level, arguments that is not an object; resources/read without uri or with a malformed URI |
| -32002 | Resource not found or refused |
| -32000 | Server error accompanying an HTTP code 401, 403, 404 or 503 |
SQL errors and refused arguments are not JSON-RPC errors: they arrive in an isError: true result (20.7.1).
20.12 How do I secure the MCP server?#
- The lowest useful level. A
STRUCTUREtoken is enough for an assistant to write queries from the schema;READfor it to check them on the data;WRITEandADMINonly on a working or test database. - A dedicated account. The level caps, it does not replace privileges: create an account specific to the assistant, with only the necessary
GRANTs, rather than a token ofroot. - A per-database scope (
DATABASES (…)) and an expiration (EXPIRE INTERVAL n DAY): a secret forgotten in a configuration file stops opening anything. - One token per use (per assistant, per workstation, per project), to revoke one without touching the others:
DROP MCP TOKENtakes effect from the next request. - Protect the secret. It is shown only once; keep it in the secrets manager or the client's configuration, never in a source repository.
- Loopback by default. Leave
mcp_bindon127.0.0.1when the assistant runs on the server machine; outside the loopback, TLS is mandatory, and the account's host must accept the assistant's machine. - Controlled origin. A request carrying an
Originheader foreign to the loopback is refused (403): a web page cannot use a workstation's browser to reach the endpoint. - Secret-free journal. With
--log, the journal records the token name, the account, the tool, the duration, the number of rows and the error code, never the secret or the data; the SQL ofexecute_sqlis truncated there to 200 characters, passwords masked. - Timeouts.
mcp_statement_timeoutbounds each call,mcp_idle_timeoutcloses forgotten sessions and rolls back their transaction; only anADMINtoken can raisemax_statement_timefor its session, never the endpoint's call timeout. - Authentication failures. A wrong secret counts as an authentication failure for the client's address, exactly as on the main port (11.5.2): delayed response, then the address is blocked after
max_connect_errorsconsecutive failures, even with a good token (the failure registry is that of the main port). A missingAuthorizationheader is not counted. To unblock:
FLUSH HOSTS; -- RELOAD privilege- Confirmed deletion.
drop_objectrequires repeating the name;update_rowsanddelete_rowsrequire a condition unlessall_rows: true. These safeguards prevent mistakes, they do not replace a suitable level: anADMINtoken can delete everything throughexecute_sql.
20.13 Monitoring#
20.13.1 Sessions#
Each MCP session is an ordinary Miraj session:
- it appears in
SHOW PROCESSLIST(andinformation_schema.PROCESSLIST) under the user of the token's account, theHostcolumn beingmcp:<token name>@<client host>, for examplemcp:assistant@localhost;USER()andCURRENT_USER()show nothing of it and an account withoutPROCESSsees these sessions like the other sessions of its user; - it takes a
max_connectionsslot (503 beyond); KILL QUERY idinterrupts the current call (1317 for the assistant),KILL CONNECTION idcloses the session (404 on the next request, transaction rolled back);idis the session number fromSHOW PROCESSLIST, also written in square brackets in the journal.
The endpoint's housekeeping thread closes a session left without a request for mcp_idle_timeout seconds: its open transaction is rolled back and the client receives 404 on its next request (it must reopen a session through initialize). It also abandons the call that exceeds mcp_statement_timeout: the current statement stops with error 1969 (per-statement limit) or 1317 (entire call interrupted), and the session remains usable.
20.13.2 Journal#
With --log, the endpoint writes:
[12] MCP session opened from 127.0.0.1, token lecteur, account app@localhost, level READ, protocol 2025-06-18, client mon-client
[12] MCP tools/call read_rows, token lecteur, account app@localhost: 2.4 ms, 1 row(s) -- read_rows ventes.article
[12] MCP tools/call execute_sql, token lecteur, account app@localhost: 0.8 ms, 0 row(s), error 9034 -- INSERT INTO ventes.article (nom) VALUES ('gomme')
[12] MCP session closed by the client, token lecteur
[13] MCP session closed (idle or stopped), token lecteur
MCP ! 1045 (28000): Access denied for user 'mcp'@'10.0.0.5' (using password: YES) -- from 10.0.0.5
SECURITY: address 10.0.0.5 blocked after 100 consecutive authentication failures (max_connect_errors); FLUSH HOSTS unblocks itSuccessful calls and session openings are displayed on the console; failed calls, authentication refusals and SECURITY events are additionally recorded in <root>\miraj\server.log.
20.14 Known limitations#
- The
miraj-mcpstdio bridge, for clients that can only launch a process, is not yet delivered (20.6.2). - No SSE stream or server notification (
GET /mcp: 405), no JSON-RPC batch, no MCP prompts, no resource subscription. create_tablecreates neither table options, nor generated columns, norON UPDATE;drop_objectonly drops tables, views and databases: the rest goes throughexecute_sql(ADMINtoken).search_schemadoes not search trigger and event names.get_rowrequires a primary key: no key read on a view; theJSONcolumns of a view do not receive an object or array as a value.- Tool descriptions, session instructions and protocol messages are in English.
- No last-used date for tokens.
- Cluster edition: replication of tokens with accounts and replay on the secondaries of the DDL created by
create_tableremain to be validated by a cluster test.
20.15 See also#
- 10. Accounts and privileges: accounts, roles,
GRANT. - 11. Server administration:
miraj_config.xml, TLS,max_connections,max_connect_errors, journal. - 15. Error codes: 9034, 9035, 9036.
- 19. Vector search.