Skip to content

Add native authenticated MCP support to TrailBase - #262

Open
brigon-dev wants to merge 32 commits into
trailbaseio:mainfrom
brigon-dev:trailbase-mcp
Open

Add native authenticated MCP support to TrailBase#262
brigon-dev wants to merge 32 commits into
trailbaseio:mainfrom
brigon-dev:trailbase-mcp

Conversation

@brigon-dev

@brigon-dev brigon-dev commented Jul 15, 2026

Copy link
Copy Markdown

Summary

Adds optional, native MCP support directly to the TrailBase binary and existing Docker container. MCP is served from /mcp on 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

  • Browser-based OAuth authorization code flow with PKCE S256
  • OAuth protected-resource and authorization-server discovery metadata
  • Dynamic client registration for clients such as mcp-remote
  • Access-token refresh
  • MCP tokens scoped to mcp and audience-bound to the instance /mcp URL
  • Live administrator-status lookup on every MCP request
  • Existing administrator bearer tokens supported as an explicit compatibility mode
  • MCP disabled by default

Tools

  • call_admin_api: dispatches to TrailBase existing in-process admin router to avoid API drift
  • execute_sql: runs SQL using TrailBase administrative query handling
  • list_tables: reads current tables, views, indexes, and triggers
  • get_config: returns redacted TrailBase configuration
  • update_config: validates and updates configuration while preserving secrets

This 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

trail --public-url https://trailbase.example.com run --mcp

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

  • Native MCP unit tests pass
  • TrailBase CLI build and checks pass
  • Browser OAuth, PKCE callback, token exchange, and refresh flow tested against a disposable local depot
  • MCP initialization and tool discovery tested with RustRover through mcp-remote
  • Table creation, row insertion, schema listing, and SQL reads tested through MCP
  • No personal or production TrailBase data was modified during destructive testing

This 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.

@ignatz ignatz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks jumping into the cold water - much appreciated 🙏

To reduce churn, maybe its best if first work out some of the highlevel questions.

Comment thread .cargo/config.toml
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 }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to assume that his is an artifact

Comment thread .cargo/protoc-wrapper.sh
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" \
"$@"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above?

Comment thread README.md
```sh
scripts/bootstrap-local-dev-tools.sh
cargo check --workspace --all-targets
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mcp/src/trailbase_mcp/endpoints.py Outdated
"summary": "Exchange authorization code for auth tokens.",
"mcp_support": "call_trailbase_api_operation or trailbase_request",
"requires_write_permission": True,
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just guessing, wouldn't the MCP also need a POST request definition?

Comment thread mcp/src/trailbase_mcp/endpoints.py Outdated
from typing import Any
from urllib.parse import quote

TRAILBASE_API_OPERATIONS: tuple[dict[str, Any], ...] = (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread mcp/src/trailbase_mcp/endpoints.py Outdated
from urllib.parse import quote

TRAILBASE_API_OPERATIONS: tuple[dict[str, Any], ...] = (
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@brigon-dev brigon-dev changed the title Adds an MCP sidecar for TrailBase with Docker/Portainer support and a documented release flow. Add native authenticated MCP support to TrailBase Aug 9, 2026
@brigon-dev

Copy link
Copy Markdown
Author

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 --mcp and served from /mcp on the existing admin server, so there is no longer a FastMCP sidecar, second container, second port, shared depot mount, copied bearer token, or separately maintained API-operation catalog.

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 URL.

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 mcp-remote: browser login, PKCE callback, token exchange, MCP initialization, tool discovery, table creation, row insertion, and reads all succeeded.

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.

@ignatz

ignatz commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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 --mcp and served from /mcp on the existing admin server, so there is no longer a FastMCP sidecar, second container, second port, shared depot mount, copied bearer token, or separately maintained API-operation catalog.

🙏 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.

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 URL.

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.

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'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 tested the complete flow with a disposable local TrailBase instance and RustRover via mcp-remote: browser login, PKCE callback, token exchange, MCP initialization, tool discovery, table creation, row insertion, and reads all succeeded.

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:

# Run MCP in the same proces
trail run --mcp

or

trail mcp

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.

@brigon-dev

Copy link
Copy Markdown
Author

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.
An MCP server can expose resources/prompts that are primarily contextual metadata, but MCP tools are actuators. A client first calls tools/list to learn each tool’s name and JSON input schema. When the model uses one, the client sends a tools/call JSON-RPC request to /mcp; the MCP server executes the operation and returns its result.
In the current implementation, the flow for list_tables is:
IDE/agent

  -> POST /mcp with tools/call("list_tables")
  -> TrailBaseMcp::list_tables()
  -> existing admin router GET /tables
  -> existing list_tables_handler
  -> result returned through MCP

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.
So the MCP server is not only advertising endpoint metadata. It mediates and executes the actions.
I agree that OpenAPI should ideally be the source of truth. OpenAPI and MCP solve slightly different parts of the problem: OpenAPI can describe the HTTP operations and their schemas, while an MCP client still needs MCP tools—or a generic bridge tool—to invoke them. If the admin OpenAPI coverage becomes complete, I see a few possible ways to reduce duplication:

  1. Keep one generic call_admin_api actuator and expose the OpenAPI document as context.
  2. Generate MCP tool definitions from the admin OpenAPI document.
  3. Keep only a small number of purpose-built tools where MCP needs behavior beyond a normal HTTP call, such as preserving/redacting configuration secrets.

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.
On authentication: browser OAuth was intentional so the administrator’s password is entered only into TrailBase’s existing login UI. The agent receives a short-lived token scoped and audience-bound to that TrailBase /mcp resource. TrailBase also checks current administrator status from the database on every MCP request. A trusted client can still supply an existing admin bearer token as a compatibility option, but there is deliberately no MCP tool that asks the agent to handle an administrator password.
Regarding the execution model, I currently prefer:

trail run --mcp

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:

trail mcp

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.
If MCP ultimately becomes only an OpenAPI/metadata adapter, a separate process would make more sense. With the current actuator model, colocating it in the running process seems simpler and less error-prone. It remains disabled unless --mcp is explicitly supplied.
To validate locally:

trail --depot /tmp/trailbase-mcp components add trailbase/auth_ui

trail \
  --depot /tmp/trailbase-mcp \
  --public-url http://localhost:4000 \
  run \
  --address 0.0.0.0:4000 \
  --mcp
Then a command-based MCP client can connect with:
{
  "mcpServers": {
    "trailbase-local": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "http://localhost:4000/mcp",
        "--allow-http",
        "--static-oauth-client-metadata",
        "{\"scope\":\"mcp\"}"
      ]
    }
  }
}

The focused tests can also be run with:

cargo test -p trailbase --lib mcp::tests

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.

@ignatz

ignatz commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

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.

I think the main source of confusion is that MCP supports both descriptive and executable primitives. An MCP server can expose resources/prompts that are primarily contextual metadata, but MCP tools are actuators. A client first calls tools/list to learn each tool’s name and JSON input schema. When the model uses one, the client sends a tools/call JSON-RPC request to /mcp; the MCP server executes the operation and returns its result. In the current implementation, the flow for list_tables is: IDE/agent

  -> POST /mcp with tools/call("list_tables")
  -> TrailBaseMcp::list_tables()
  -> existing admin router GET /tables
  -> existing list_tables_handler
  -> result returned through MCP

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..." 🙏

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:

trail mcp

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. If MCP ultimately becomes only an OpenAPI/metadata adapter, a separate process would make more sense. With the current actuator model, colocating it in the running process seems simpler and less error-prone. It remains disabled unless --mcp is explicitly supplied. To validate locally:

With my newfound understanding that the MCP actuates the action, this makes sense. Let me give the code a more proper look 🙏

Comment thread mcp/README.md
### Public HTTPS URL

Use this for Cloudflare Tunnel, a reverse proxy, or another deployed TrailBase
instance:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my understanding, should an MCP ever be running in production? Naively, I would expected: no

Comment thread mcp/README.md
cargo test -p trailbase --lib mcp::tests
```

For an isolated manual test:

@ignatz ignatz Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

? - Isn't this just the dev setup, i.e. the way you'd normally run your mcp

@ignatz

ignatz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

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.

@ignatz

ignatz commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

If we'd focus on dev-only use-cases (at least for now), and looking at mcp-remote:

So far, the majority of MCP servers in the wild are installed locally, using the stdio transport. This has some benefits: both the client and the server can implicitly trust each other as the user has granted them both permission to run.

makes me wonder if trail mcp may make sense (on the side, I'm not too concerned about to processes sharing the depot, it may be a nuisance when the config is changed, we'd have to watch the file or SIGHUP the server). Presumably, not needing mcp-remote would at least make initial setup easier (something I'm still lacking :hide:)

(I'm also just liberally collecting my thoughts here as I'm reading up on the individual aspects)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants