| layout | page |
|---|---|
| title | How It Works |
| subtitle | Three moving parts — vendored skills, an MCP server, and the MariaDB Shell it runs inside. What each one is, why it is separate, and where the boundaries are. |
| permalink | /how-it-works/ |
| description | The architecture of the MariaDB AI Plugins — vendored skills, the mariadb-shell MCP server, the launcher that installs the shell, and the security model. |
Installing a plugin gives you three things that are genuinely separate. Knowing which is which explains most of what happens when something does not work.
| Part | What it is | Needs |
|---|---|---|
| Skills | {{ site.skill_count }} Markdown documents vendored into the plugin. The agent reads the ones whose description matches what you asked. |
Nothing. They work on a fresh install, offline. |
| MCP server | {{ site.tool_count }} tools that talk to a real MariaDB server, in five groups: db.*, msm.*, sandbox.*, util.*, sql.*. |
A one-time mcp setup, and a server to talk to. |
mariadb-shell |
MariaDB's command-line shell (a port of MySQL Shell). The MCP server runs inside it as a plugin. | Installed automatically on first use. |
It looks like an odd choice until you list what the shell already has: high-performance database connections, a credential store backed by the platform's secret keeping, sandbox deployment, and the schema-management engine. Re-implementing any of those in a standalone MCP server would mean a second, worse copy.
So the server is a shell plugin, and the practical consequences are:
- Passwords never touch a config file. They live in the shell's secret store. The agent asks for a connection, not for credentials.
mariadb-shellis also yours to use. Run it and type\help. It is a perfectly good SQL client, and the sandboxes the agent creates are visible to it.- The shell's config home matters. It is
~/.mariadb-shell, and it holds the plugins, the secret store and the MCP server's own settings. A relocated or unreadable config home is behind a surprising share of "the tools are gone" reports.
Every plugin ships a launcher script — mariadb-mcp-launcher.sh and its .cmd
twin — and that script is what your harness actually spawns. It resolves a shell
in this order:
$MARIADB_SHELL_BIN, if set.- A
mariadb-shellonPATHthat meets the minimum version. - A local install at
$MARIADB_SHELL_BINDIR/mariadb-shell(default~/.local/bin; on Windows%LOCALAPPDATA%\Programs\mariadb-shell\bin\). - Otherwise: fetch the official
install.sh/install.ps1and run it, then launch what it installed.
Upgrade by raising the floor, by deleting the managed install, or by running
install.sh yourself.
The launcher writes every message it produces to stderr, because stdout is the MCP JSON-RPC transport. A single stray byte on stdout breaks the protocol, which is why the installer's output is redirected too.
From mariadb-shell 26.10.3 on, the shell's package also carries mariadb-lsp,
a language server that parses .sql files with the MariaDB server's own
grammar. Where the harness supports language servers, the dev and sql
plugins wire it up, and the agent sees MariaDB's syntax errors right after it
writes a SQL file — before it runs anything:
| Harness | How it is registered |
|---|---|
| Claude Code | Automatically, by the plugin's .lsp.json. Set ENABLE_LSP_TOOL=1 to also give the agent the LSP tool (symbols, hover, definitions, references). |
| OpenCode | The lsp block of the plugin's opencode.json, merged into yours. |
| Hermes Agent | An lsp.servers entry you add to config.yaml — plugins cannot register one. |
| Codex, Pi | No language server — neither runs one. The agent gets the same checks through the MCP server's sql.* tools instead. |
It has a launcher of its own, mariadb-lsp-launcher.sh, with one rule: it
never installs anything. It finds the shell the MCP launcher would use and
runs the mariadb-lsp next to it. On a first start it simply waits for the MCP
launcher's download to finish, so starting both servers at once still fetches
the package exactly once.
The MCP server refuses everything it has not been told about. There are two
allow-lists, both set by mariadb-shell -- mcp setup, and they are the whole
model.
The agent cannot invent a host. db.connect checks its argument against the
configured connections, and db.list_connections is what an agent calls first
to find out what exists.
The matching normalizes equivalent spellings — a mariadb:// scheme, host case,
the default port, an inline password — so all of those open the stored
connection. A mysql:// scheme is not accepted. What is also refused is a URI
asking for more than was configured:
root@127.0.0.1:3310/notes_app → refused (a default schema)
root@127.0.0.1:3310?ssl-mode=REQUIRED → refused (an option)
That refusal is deliberate. Silently handing back a connection that dropped the schema or the TLS requirement would be worse than saying no.
Every file-touching tool — db.execute_sql_script with a file_path, all of
msm.*, sandbox.deploy with a sandbox_dir — is gated by an allowed-paths
list. A path that is not on it falls back to an interactive confirmation.
That fallback is why an unlisted path produces two different symptoms: for the
msm.* tools the call fails, and for sandbox.deploy it hangs, because
a headless agent cannot answer the prompt. Both mean the same thing — add the
directory with mcp setup.
sandbox.deploy registers root@127.0.0.1:<port> with its password, so the
agent can connect to the instance it just made without you re-running setup.
This is the one path by which a connection appears that you did not configure
by hand — and it points only at a throwaway server on localhost that the same
agent just created.
Each harness ships the same three variants, all built by the same
[scripts/sync-skills.sh]({{ site.repo }}/blob/main/scripts/sync-skills.sh):
| Variant | Skills | MCP server |
|---|---|---|
dev |
The full set — statements, functions, tools, connectors, topical, plus everything in additional-skills/ |
yes |
sql |
A SQL-focused subset — statements, functions, topical, plus additional-skills/sql/ |
yes |
contributor |
Skills for contributing to MariaDB tooling itself | no |
The folders are <harness>/{dev,sql,contributor}-plugin/ for claude/,
codex/, opencode/ and hermes/. pi/ ships dev only.
The content is identical; the packaging is not.
- Claude Code and Codex both read a marketplace manifest and install a
plugin directory. Codex reads
.agents/plugins/marketplace.json. - OpenCode has no marketplace, so its plugin is wired up by hand: one
mcpblock merged into your config, an env var, and a symlink for the skills. - Pi has no marketplace. A pi package is any directory with a
pifield in itspackage.json, so the repo-rootpackage.jsonis the manifest and the whole repository installs as one package. Its extension registers the MCP server with Pi's built-in MCP support (Pi 1.0+), which exposes the tools through codemode by default. - Hermes Agent has no marketplace either. Each plugin folder is a portable
Agent Plugins v1 package — a root
plugin.json,skills/and, fordevandsql, anmcp.jsondeclaring the server — that Hermes installs straight from the repository withhermes plugins install …#hermes/dev-plugin. The tools reach the model asmcp__mariadb__<tool>, and the skills sit under a namespace Hermes derives from the plugin.mcp.jsoncannot branch per OS, so native Windows points Hermes'config.yamlat the.cmdlauncher instead.
The skills are copied into each plugin rather than fetched at run time. That is a deliberate trade: it means a plugin release pins a skill version, so what you tested is what you ship, and an upstream edit never changes an agent's behaviour mid-session.
The cost is that editing additional-skills/ without re-running
scripts/sync-skills.sh silently ships stale text. There is a test for exactly
that now — every vendored additional-layer skill must be byte-identical to its
source — because it went unnoticed for four days once.
- [Get Started]({{ '/get-started/' | relative_url }}) — install and configure.
- [MCP Tool Reference]({{ '/mcp-tools/' | relative_url }}) — every tool and its arguments.
- [Skill Catalog]({{ '/skills/' | relative_url }}) — what the agent can read.
- [CONTRIBUTING.md]({{ site.repo }}/blob/main/CONTRIBUTING.md) — working on the plugins rather than with them.