Add MCP server (beacon-mcp): expose tables to Claude over streamable HTTP - #323
Merged
Merged
Conversation
Tables can now carry consumer-facing metadata, decoupled from their storage
definition: an MCP descriptor (how a downstream MCP server should surface the
table) and named query presets (predefined filter sets). Extensions are typed
and validated against the live table schema.
Storage is a `tables://<name>/extensions.json` sidecar, separate from
`table.json`, so extensions apply uniformly to every table type, can be edited
without rebuilding the provider, survive provider re-registration (MV refresh,
Iceberg alter), and are removed automatically on DROP TABLE.
Manage via SQL:
SET EXTENSION '<kind>' FOR <table> TO '<json>'
DROP EXTENSION '<kind>' FOR <table>
SHOW EXTENSIONS FOR <table>
or REST: public GET /api/table-extensions; admin PUT/DELETE
/api/admin/table-extensions/{name}. All OpenAPI-documented.
Validation rejects unknown columns, unsupported operators, malformed
between/in values, and duplicate preset names.
Tests: extension validation + parser unit tests, a persistence round-trip and
cleanup test, and an end-to-end runtime test (CREATE TABLE -> SET/SHOW/DROP
EXTENSION -> validation rejection).
…le HTTP Introduces a `beacon-mcp` crate that turns beacon into a Model Context Protocol server, mounted at `/mcp` in beacon-api via rmcp's streamable-HTTP transport. MCP clients (e.g. Claude) can discover tables and run read-only queries. Tools are generated from the runtime: - generic: `list_tables`, `describe_table`, `run_sql` (SELECT-only) - one tool per table whose `mcp` table extension is enabled, with inputs derived from the extension metadata (exposed_columns -> `select`, presets -> a `preset` enum that expands to the stored filters), built on the table-extensions feature. Execution runs through Runtime::run_query as a non-super-user (read-only), with results serialized to JSON and capped to 1000 rows. Identifiers are quoted and preset values rendered as SQL literals. Pinned rmcp =1.8.0 (2.0.0 was one day old at time of writing). Tests: catalog/SQL-builder unit tests (preset expansion, column-exposure enforcement, value escaping). Verified end to end against a running server: initialize -> tools/list -> tools/call run_sql returns JSON rows. Docs: docs/mcp.md (tools, exposing a table, connecting Claude).
# Conflicts: # Cargo.lock # Cargo.toml # beacon-api/Cargo.toml # beacon-api/src/axum/admin/mod.rs # beacon-api/src/axum/client/mod.rs # beacon-api/src/axum/client/tables.rs # beacon-core/src/lib.rs # beacon-core/src/parser/beacon_parser.rs # beacon-core/src/parser/statement.rs # beacon-core/src/runtime.rs # beacon-core/src/statement_plan/mod.rs # beacon-core/src/statement_plan/physical.rs # beacon-core/src/statement_plan/query_planner.rs
MCP tool execution now always runs with is_super_user cleared, so the query planner rejects DDL/DML for any caller (defense-in-depth, independent of how the identity was resolved). The caller's roles are preserved so per-user read grants still apply. Docs updated.
Preset/MCP extension payloads now parse against a strict structure: - PresetFilter.op is a typed PresetOp enum (= != < <= > >= between in), serialized with the symbolic spelling; any other operator is rejected at parse time with a clear error instead of only at schema-validation. - All extension structs use serde(deny_unknown_fields), so typos/extra keys are rejected rather than silently dropped. This guarantees stored extension JSON conforms to a known shape that consumers can reliably parse. Updated tests + API re-export.
- Validate mcp tool_name against MCP/Anthropic rules (1-64 chars [A-Za-z0-9_-]) so a bad name can't break a client's tools/list; sanitize generated defaults. - Add optional `title` (maps to Tool.title) and emit annotations.readOnlyHint on every generated tool, matching the MCP Tool descriptor. - McpExtension already parses strictly (deny_unknown_fields from prior change). - Tests + docs updated.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #323 +/- ##
==========================================
- Coverage 74.71% 74.53% -0.19%
==========================================
Files 284 290 +6
Lines 37089 38448 +1359
==========================================
+ Hits 27712 28656 +944
- Misses 9377 9792 +415
🚀 New features to boost your workflow:
|
exposed_columns entries may now be either a bare name or {name, description},
so curators can describe what each column means. Descriptions are folded into
the generated tool's 'select' help and returned by describe_table; the table's
own meaning continues via 'description' -> Tool.description. New ExposedColumn/
ColumnDoc types (strict-parsed), validation by column name, tests + docs.
describe_table and each per-table tool now present a merged per-column view — name + data_type + nullable + description — scoped to exposed_columns (in order) when set, or all columns otherwise. Descriptions come from the extension, with a fallback to the Arrow field's description/comment metadata. The select parameter help lists each column as 'name (type): meaning'. Adds resolve_columns() + test.
- docs/mcp.md: config/env vars, agent authentication (create user, header examples for Claude Code / Desktop / SDKs), Tool-standard mapping table, quick check. - docs/mcp-architecture.md: request lifecycle, tool generation, identity flow + read-only enforcement, tool-call->SQL, result handling, extension points.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds an in-tree MCP server so MCP clients (e.g. Claude) can discover and query beacon's tables over the Model Context Protocol. New
beacon-mcpcrate, mounted as a streamable-HTTP route (/mcp) inbeacon-apiusing the officialrmcpSDK (pinned=1.8.0).How it works
The MCP tool surface is generated from data, not hard-coded:
list_tables,describe_table,run_sql(read-only SELECT → JSON rows).mcpextension hasenabled: truebecomes its own tool, generated from the extension metadata —tool_name→Tool.name,title→Tool.title,description→Tool.description,exposed_columnsconstrain the generatedinputSchema, andpresetnames become an enum the model can pick (expanded to the stored filters at query time).This is the payoff of the table-extensions work: curators add or shape MCP tools via
SET EXTENSION/ the admin REST API, with no code changes.Auth — MCP is just another user, and strictly read-only
/mcprides the sameresolve_identitymiddleware as the client API: each request authenticates viaAuthorization(resolving to that user's roles), the anonymous principal when enabled, or a role-less identity otherwise. Per-user read RBAC applies.is_super_usercleared, so the query planner rejects all DDL/DML for any caller (defense-in-depth) — MCP is read-only by design. Verified live:SELECTreturns rows;CREATE TABLEis rejected.BEACON_MCP_ENABLED(default on; setfalseto disable mounting).Standards & validation
mcpdescriptor maps onto the MCPToolstandard, and every generated tool carriesannotations.readOnlyHint: true.tool_nameis validated to MCP/Anthropic rules (1–64 chars[A-Za-z0-9_-]) so a bad name can't break a client'stools/list; generated defaults are sanitized.PresetOpenum (= != < <= > >= between in) andserde(deny_unknown_fields)on all extension structs — unknown keys/operators are rejected with clear errors, not silently dropped.Files
beacon-mcpcrate:server.rs(rmcpServerHandler: dynamiclist_tools/call_tool),catalog.rs(tool generation + preset→SQL),result.rs(query→JSON).beacon-api/src/axum/router.rs: gated/mcpmount +resolve_identitylayer.docs/mcp.md: connection + behavior.Tests
beacon-mcpunit tests (tool generation, preset/SQL rendering, escaping),beacon-coreextension tests (strict parsing, schema + tool_name validation), and an end-to-end runtime test. Full workspace builds; affected-crate tests pass. Smoke-tested the live/mcphandshake →tools/list→tools/callfor both read (allowed) and write (rejected) under authenticated and unauthenticated requests.