Add native authenticated MCP support to TrailBase - #262
Conversation
ignatz
left a comment
There was a problem hiding this comment.
Thanks jumping into the cold water - much appreciated 🙏
To reduce churn, maybe its best if first work out some of the highlevel questions.
| CLANG_PATH = { value = "./.dev-tools/libclang-18/usr/bin/clang-18", relative = true } | ||
| PKG_CONFIG_PATH = { value = "./.dev-tools/geos/usr/lib/x86_64-linux-gnu/pkgconfig", relative = true } | ||
| PKG_CONFIG_SYSROOT_DIR = { value = "./.dev-tools/geos", relative = true } | ||
| PROTOC = { value = "./.cargo/protoc-wrapper.sh", relative = true } |
There was a problem hiding this comment.
I'm going to assume that his is an artifact
| export LD_LIBRARY_PATH="${PROTOBUF_DIR}/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}" | ||
| exec "${PROTOBUF_DIR}/usr/bin/protoc" \ | ||
| -I"${PROTOBUF_DIR}/usr/include" \ | ||
| "$@" |
| ```sh | ||
| scripts/bootstrap-local-dev-tools.sh | ||
| cargo check --workspace --all-targets | ||
| ``` |
There was a problem hiding this comment.
Ok, so the above files are not artifacts.
This is a bit surprising since, I would expect most devs to have access to their machines (seems like a reasonable requirement). Otherwise, this is also very deb centric. If you don't have full access, wouldn't one rather develop inside a container? Would love to hear more about the reasoning.
| "summary": "Exchange authorization code for auth tokens.", | ||
| "mcp_support": "call_trailbase_api_operation or trailbase_request", | ||
| "requires_write_permission": True, | ||
| }, |
There was a problem hiding this comment.
Just guessing, wouldn't the MCP also need a POST request definition?
| from typing import Any | ||
| from urllib.parse import quote | ||
|
|
||
| TRAILBASE_API_OPERATIONS: tuple[dict[str, Any], ...] = ( |
There was a problem hiding this comment.
I'm a bit worried that this will get out of sync. Naively, I would have expected the MCP implementation to be part of the main binary running in some dev mode.
| from urllib.parse import quote | ||
|
|
||
| TRAILBASE_API_OPERATIONS: tuple[dict[str, Any], ...] = ( | ||
| { |
There was a problem hiding this comment.
I'm also a bit surprised over the selection of methods. Naively, I would have expected only or mostly admin APIs to be used in dev mode basically as an alternative to the dashboard. Isn't exposing only the public APIs with access protection inherently limiting maybe even useless for dev tasks.
As an example, i would have expected this to be used to drive schema changes.
|
Thanks again for the earlier feedback @ignatz. I’ve substantially redesigned this PR around your suggestions. The MCP server is now part of the main TrailBase binary and runs in the same container and process as TrailBase. It is enabled explicitly with Authentication now uses a browser-based OAuth authorization-code flow with PKCE. MCP clients open TrailBase’s existing login UI, and only current administrators are authorized. MCP access tokens are scoped and audience-bound to the instance’s MCP operations are routed through TrailBase’s existing in-process admin handlers to reduce drift and provide the administrative functionality discussed in the review, including schema changes, tables, indexes, triggers, Record APIs, configuration, users, files, backups, jobs, OAuth providers, and WASM components. Focused tools are also included for SQL, schema discovery, and configuration. I tested the complete flow with a disposable local TrailBase instance and RustRover via The PR title and description have been updated to reflect the new implementation. I’d appreciate another review when you have time, particularly around whether the native integration and admin-tool approach now align with what you had in mind. |
🙏 gave it a quick skim and it looks much closer to what I had in mind. I still have a few questions, some of it may just be my ignorance showing.
That's cool and very interesting to me. Naively I was expecting folks would trust their agents and give them the credentials to do the authentication on their behalf but arguably this would be nicer to hand the agent tokens only. Will have to look a bit more into how it's wired up.
I'm a bit confused. Originally, i thought MCP endpoints would act as actuators (i.e. mediate the action) but then based on your original proposal I think I learned that MCP servers only share metadata, which is consumed and then used to talk to the actual endpoints (which in hindsight makes sense). Maybe you can do either, maybe you could shed some light on what the flow is, i.e. after calling the MCP and learning about the "list tables" tool, which endpoint does it call. At the end of the day, I'm wondering if OpenAPI data would be enough thus allowing us to get rid of any tool duplication?
I'll definitely will need to look into how to run and validate this myself in order to squelch my confusion and be able to maintain this. FTR: I hadn't heard back so I've started some work around making OpenAPI better integrated and more complete hoping that this would also trivialize an MCP integration. Independently, I was wondering about the desired execution model, which will depend on whether the MCP only serves metadata or actual data. If it's the former, it may make sense to colocate in the same binary but not necessarily in the same process. Specifically, should it be: or i.e. run a second process that serves the metadata. I don't have experience, would be interesting to hear what others do. Thanks so much for this work. |
|
Thanks for taking another look — these are good questions, and I think the main source of confusion is that MCP supports both descriptive and executable primitives. The admin request is dispatched through the in-process Axum router using the same AppState; it does not make a second HTTP request. execute_sql similarly dispatches to the existing POST /query admin handler. The generic call_admin_api tool accepts an HTTP method, admin-relative path, and body, then invokes the matching existing admin handler in-process.
I would be happy to align this PR with the OpenAPI work. The focused tools currently contain very little independent business logic: most are convenience wrappers around existing admin handlers. The old hand-maintained operation catalog has been removed.
because the MCP tools execute real administrative operations and can reuse the existing router, application state, authorization, configuration, and schema-refresh behavior directly. A separate:
process would either need to call the running TrailBase server over HTTP and manage separate administrative credentials, or open the same depot concurrently. The former starts to recreate the sidecar architecture, while the latter seems undesirable for database ownership and consistency. The focused tests can also be run with:
I’m very open to reducing or generating the tool surface once I understand the direction of the improved admin OpenAPI integration. In particular, I’d appreciate your preference between a generic OpenAPI-backed actuator and individually generated MCP tools. |
|
Thanks for taking the time to discuss this with me - very much appreciated 🙏 - and I'm sorry I'm not already more up to speed.
You're saying the agent calls the MCP and the MCP proxies the action. My confusion also stems from browsing the landscape and stumbling over the code examples on https://crates.io/crates/rmcp-openapi, which seemed rather declarative but looking closer it does exactly what you said: "rmcp-openapi acts as a proxy between MCP clients and OpenAPI services..." 🙏
With my newfound understanding that the MCP actuates the action, this makes sense. Let me give the code a more proper look 🙏 |
| ### Public HTTPS URL | ||
|
|
||
| Use this for Cloudflare Tunnel, a reverse proxy, or another deployed TrailBase | ||
| instance: |
There was a problem hiding this comment.
For my understanding, should an MCP ever be running in production? Naively, I would expected: no
| cargo test -p trailbase --lib mcp::tests | ||
| ``` | ||
|
|
||
| For an isolated manual test: |
There was a problem hiding this comment.
? - Isn't this just the dev setup, i.e. the way you'd normally run your mcp
|
I squashed and merged all the latest changes into a single commit: #276. I'll use this as a base for the review, there's just less auxiliary changes. I'm happy to discuss here or there. Tentatively here to preserve the discussion but feel free to comment on either. |
|
If we'd focus on dev-only use-cases (at least for now), and looking at mcp-remote:
makes me wonder if (I'm also just liberally collecting my thoughts here as I'm reading up on the individual aspects) |
|
Had some more time to meditate over it today. I was contemplating what the use-cases for remote MCP are. Specifically, if a local mcp would run separately, you could still point it at a remote instance. Not that I'm advocating for unleashing agents on production instances but wouldn't splitting solve the issue? And by not actuating the action but merely proxying to the primary server there would also be no inconsistencies or multiple instances accessing the same depot. I got a lot of sun, so chances are my brain got fried or did it? As a strawman: $ trail mcp --instance=prod.example.com --admin=admin@example.com
> password: ***and then the thing would communicate locally via stdin/out EDIT: I guess it wouldn't work for oauth admins. For local instances, given access to the depot, we wouldn't need sign-in at all |
|
(sorry for sharing my ignorance and incremental discovery in such unstructured way, I partly want to document it for myself but even more so want to keep you in the loop especially if you have any insights to share). In the (still) absence of a local agent-mcp-client, I discovered https://github.com/modelcontextprotocol/inspector, which has helped me to actually exercise the code (memo to myself: debugging oauth issues the client has unfortunate caching and needs wipage of ~/.mcp-inspector). Overall the oauth works 🎉 . However, how we construct the resource uris doesn't work for local use if a "site_url" is present. The connecting client will have a mismatch and refuse to proceed. If I fix that up, it works. A few more observations:I get the tool list: {
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "call_admin_api",
"description": "Call a TrailBase admin API in-process. Paths are relative to /api/_admin. This exposes the same table, index, row, config, schema, query, user, log, backup, job, and WASM operations as the admin dashboard.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"body": {
"default": null,
"description": "Optional JSON request body."
},
"method": {
"description": "HTTP method accepted by the TrailBase admin API.",
"type": "string"
},
"path": {
"description": "Admin API path relative to /api/_admin, including an optional query string.",
"type": "string"
}
},
"required": [
"method",
"path"
],
"type": "object"
},
"outputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
},
{
"name": "execute_sql",
"description": "Execute SQL using TrailBase's admin query handler. Supports reads and writes; schema changes refresh cached metadata.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"attached_databases": {
"default": null,
"description": "Optional configured attached database names.",
"items": {
"type": "string"
},
"type": [
"array",
"null"
]
},
"query": {
"description": "One or more SQLite statements. Schema-changing statements refresh TrailBase metadata.",
"type": "string"
}
},
"required": [
"query"
],
"type": "object"
},
"outputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
},
{
"name": "get_config",
"description": "Get the complete TrailBase configuration as protobuf text. Secret values are redacted.",
"inputSchema": {
"properties": {},
"type": "object"
}
},
{
"name": "list_tables",
"description": "List TrailBase tables, views, columns, indexes, triggers, and metadata.",
"inputSchema": {
"properties": {},
"type": "object"
},
"outputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema"
}
},
{
"name": "update_config",
"description": "Validate and replace the TrailBase configuration using protobuf text from get_config. Existing secret values are preserved.",
"inputSchema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"properties": {
"config": {
"description": "Complete TrailBase config in protobuf text format, as returned by get_config.",
"type": "string"
}
},
"required": [
"config"
],
"type": "object"
}
}
]
}
}For some reason that I don't understand the inspector barfs: Thought the tool list response overall looks good to me. However, it raises the next point... ... Because the tool definitions and APIs are separate, many actions would have to fall back on Lastly (and trivial to change), with the short-lived mcp-specific auth tokens and no means to refresh, is your expectation that users would re-auth every 10mins? |
|
Just to summarize the options I see at the moment (please feel free to add):
Independent of the specific option, I think it would be wise to use OpenApi schemas that are already available to make the MCP tools more complete and useful. Then there's the open question if we want to allow the MCP to connect to a remote TrailBase server, in which case only the options (2.i) and (2.II.a) would work, with (2.i) being the most general. If remote access is not required (i.e. local-dev only), (3.) would be the most convenient to get started followed by (2.ii.b). With my questions:
I'm tentatively flip-flopping between (2.i) and (3.) (maybe even 2.ii.b or 2.ii.c, which would be the quickest to implement for a PoC or MVP. How the tokens are acquired, e.g. via CLI login, CLI arg or hijacking the depot is sort of a detail which "only" affects convenience). I don't see any clear advantage of (1.) over (2.1), if one could simply connect an MCP server to an already running server as opposed to deployinng the server with MCP always enabled. Would love to hear your thoughts. |
|
I have a basic implementation of 2.ii.c (very little code) here: trailbase/crates/cli/src/bin/trail.rs Lines 490 to 543 in 40957e6 MCP inspector is very happy and finds a long list of structured tools:
I'm basically running: TOKENS="eyJhdXR..." trail mcp --address=http://localhost:4000Could this work for you? EDIT: note that the PoC doesn't automatically refresh the auth-token yet. |
|
After a bit more of ruminating and getting my hands dirty, I think the highest priority should be: complete, correct and comprehensive MCP tools, ultimately that will determine the quality of your experience. Details such as wire-transport and authentication are important, just not as important unless you have very specific use-cases. My experiments are shaping up to the point where I have a fork of rmcp-openapi that strips out many dpeendencies and updates to current rmcp (yet the binary size increase is significant: ~7MB). I only support MCP over stdin but that seems to be what most local dev-flows and IDEs expect anyway. Authentication works either by providing tokens, e.g.: trail mcp --tokens=$(trail --depot=client/testfixture user mint admin@localhost) http://mytrailbase.org(or by looking the tokens up in the admin UI). Or with direct access to the depot: trail --depot=client/testfixture mcp --user=admin@localhost http://mytrailbase.orgI hope that's workable |

Summary
Adds optional, native MCP support directly to the TrailBase binary and existing Docker container. MCP is served from
/mcpon the TrailBase admin server and is enabled explicitly with--mcp.This replaces the earlier FastMCP sidecar design. There is no second container, second port, copied bearer token, shared depot mount, or hand-maintained public API catalog.
Authentication and security
mcp-remotemcpand audience-bound to the instance/mcpURLTools
call_admin_api: dispatches to TrailBase existing in-process admin router to avoid API driftexecute_sql: runs SQL using TrailBase administrative query handlinglist_tables: reads current tables, views, indexes, and triggersget_config: returns redacted TrailBase configurationupdate_config: validates and updates configuration while preserving secretsThis gives trusted administrators the same administrative surface used by the dashboard, including schema, table, Record API, user, file, backup, job, OAuth-provider, and WASM-component operations.
Deployment
OAuth-capable clients connect to
https://trailbase.example.com/mcp. The documentation includes localhost,mcp-remote, Docker, Portainer, Cloudflare Tunnel, reverse-proxy, and direct bearer-token examples.Validation
mcp-remoteThis redesign addresses the review feedback by moving MCP into the main binary, routing operations through existing admin handlers, avoiding a separately maintained operation catalog, and supporting administrative schema changes.