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)

@ignatz

ignatz commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

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

@ignatz

ignatz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

(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:

Rejected by the Inspector
Invalid result for tools/list: [ { "code": "invalid_value", "values": [ "object" ], "path": [ "tools", 0, "outputSchema", "type" ], "message": "Invalid input" }, { "code": "invalid_value", "values": [ "object" ], "path": [ "tools", 1, "outputSchema", "type" ], "message": "Invalid input" }, { "code": "invalid_value", "values": [ "object" ], "path": [ "tools", 3, "outputSchema", "type" ], "message": "Invalid input" } ]

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 call_admin_api, which doesn't have a schema, which makes me wonder how an agent would reason about it. Is the expectation that it would reason about the entire code-base and figure out available APIs, IO schemas, ... by itself?

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?

@ignatz

ignatz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Just to summarize the options I see at the moment (please feel free to add):

  1. As proposed: mcp-over-http co-located in the primary server process, custom oauth with custom auth/tokens, and custom tool definitions
    1. We could try to update the implementation to get the tools from OpenApi.
  2. Use rmcp-openapi, which requires HTTP proxying, i.e. all interactions would be carried out by the primary server. In which case we need valid admin (auth,refresh,csrf) tokens. To sign-in we could:
    1. Use oauth as above but minting "standard" tokens rather than custom tokens. This would require MCP-over-http.
    2. If we wanted to use MCP-over-stdin:
      1. Login via CLI - would only work for non-oauth-admin users
      2. Privileged auto-login requiring access to depot (only at startup to get user and keys to mint tokens. Notably, this would require two arguments, server address and depot path. For a dev setup we could probably default to localhost:4000 🤷‍♀️ ).
      3. We could also just require the user to provide tokens, e.g. make them easy C&P from admin UI.
  3. Standalone MCP (likely using stdin since everything would have to be local to depot anyway) that doesn't require a primary server to be up. I.e. point it at the depot and oneshot requests. Ideally tool definitions would still come from OpenApi and we'd have to convert ourselves.

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:

  • Do we want to support non-local workflows, i.e. run MCP-over-http in-process or have an MCP connect to a remote server?
  • Is MCP-over-http broadly used/supported by clients?

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.

@ignatz

ignatz commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

I have a basic implementation of 2.ii.c (very little code) here:

SubCommands::Mcp { address, tokens } => {
#[derive(Deserialize)]
struct Tokens {
auth_token: String,
refresh_token: String,
csrf_token: String,
}
let tokens = BASE64_STANDARD
.decode(&tokens)
.map_or(tokens, |b| String::from_utf8_lossy(&b).to_string());
let Tokens {
auth_token,
refresh_token,
csrf_token,
} = serde_json::from_str(&tokens)?;
let json = trailbase::openapi::build_api_definitions(
/* config= */ None, /* include_admin= */ true,
)
.to_json()?;
let headers = {
use reqwest::header::{AUTHORIZATION, HeaderMap, HeaderValue};
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {auth_token}"))?,
);
headers.insert("Refresh-Token", HeaderValue::from_str(&refresh_token)?);
headers.insert("CSRF-Token", HeaderValue::from_str(&csrf_token)?);
headers
};
let mut server = rmcp_openapi::Server::builder()
// QUESTION: Can we avoid the serialization/deserialization detour?
.openapi_spec(serde_json::from_str(&json)?)
.default_headers(headers)
.base_url(url::Url::parse(
address.as_deref().unwrap_or("http://localhost:4000"),
)?)
.build();
server.load_openapi_spec()?;
// Serve over stdio instead of HTTP
eprintln!("start listening on stdio");
let service = rmcp::service::serve_server(server, rmcp::transport::stdio()).await?;
// QUESTION: is this needed?
service.waiting().await?;
}
}

MCP inspector is very happy and finds a long list of structured tools:

Screenshot From 2026-08-14 17-44-07

I'm basically running:

TOKENS="eyJhdXR..." trail mcp --address=http://localhost:4000

Could this work for you?

EDIT: note that the PoC doesn't automatically refresh the auth-token yet.

@ignatz

ignatz commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

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

I hope that's workable

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