Skip to content

Add a read-only MCP server for SpacetimeDB#5382

Open
fishnos wants to merge 3 commits into
clockworklabs:masterfrom
fishnos:mcp-server
Open

Add a read-only MCP server for SpacetimeDB#5382
fishnos wants to merge 3 commits into
clockworklabs:masterfrom
fishnos:mcp-server

Conversation

@fishnos

@fishnos fishnos commented Jun 17, 2026

Copy link
Copy Markdown

Description of Changes

Adds spacetimedb-mcp, a new workspace crate: a Model Context
Protocol

server that lets MCP-aware agents and editors introspect a SpacetimeDB
database. Built on the official Rust MCP SDK (rmcp), it speaks JSON-RPC
over stdio.

This is an intentionally small, read-only first cut, scoped per discussion
with @bradleyshep. It exposes four tools:

  • ping — health check.
  • get_schema — full module definition (typespace, tables, reducers) as JSON.
  • list_tables — table names.
  • list_reducers — reducers with their lifecycle role.

Schema is fetched from a running host's GET /v1/database/{name}/schema
endpoint — the same path spacetime describe uses — and decoded into the
in-tree RawModuleDefV9 type, so the server stays in lockstep with the
engine rather than reparsing text. Host and optional auth token come from
SPACETIMEDB_HOST / SPACETIMEDB_TOKEN; the target database is a per-call
argument.

Deliberately out of scope for now (possible follow-ups): SQL, subscriptions,
reducer calls, logs, publish/generate, and any write operations.

Opening as a draft to get direction on whether this belongs in-tree and on
the approach before expanding scope.

API and ABI breaking changes

None. This is an additive new crate; no existing crates are modified beyond
registering the workspace member and two new workspace dependencies (rmcp,
schemars).

Expected complexity level and risk

1 — a small, self-contained, additive crate. It only reads from an existing
public HTTP endpoint and touches no engine internals. Its one coupling point
is the RawModuleDefV9 schema type, which it consumes read-only.

Testing

  • Unit tests for the schema-to-output transformations and the
    SerdeWrapper/DeserializeWrapper round trip the client relies on.
  • Integration test that serves a canned schema over a throwaway HTTP
    server and exercises the full fetch-and-decode path (no running
    instance required) — cargo test -p spacetimedb-mcp.
  • Manually verified end-to-end against a live local instance: all four
    tools return correct results.
  • Reviewer: confirm the crate's placement (crates/) and that pulling
    rmcp + reqwest into the workspace is acceptable.

fishnos added 3 commits June 15, 2026 21:51
Set up the initial `spacetimedb-mcp` crate: a Model Context Protocol
server on the official `rmcp` 1.7 SDK, communicating over stdio. The
crate is registered as a workspace member, with `rmcp` and `schemars`
added to the workspace dependencies.

Includes a single `ping` health-check tool to confirm the end-to-end
JSON-RPC flow (initialize, tools/list, tools/call) works. Logging is
routed to stderr only, so it never corrupts the stdout protocol stream.

SpacetimeDB-specific tools (schema and reducer introspection, SQL,
subscriptions, and the CLI workflow) will follow in later commits.
Add get_schema, list_tables, and list_reducers tools backed by a small
HTTP client that queries a running SpacetimeDB host's schema endpoint
(GET /v1/database/{name}/schema) and decodes it into the in-tree
RawModuleDefV9 type, reusing the same mechanism as `spacetime describe`.

The host and optional auth token are read from SPACETIMEDB_HOST and
SPACETIMEDB_TOKEN; the target database is a per-call argument, so one
server can introspect any database on the host.

Write operations, SQL, and subscriptions remain out of scope.
Split the schema-to-output shaping into pure functions (introspect
module) so it can be unit-tested without a server, and add a Client::new
constructor so the HTTP client can target an arbitrary host.

Add tests: unit tests for table/reducer shaping and the SerdeWrapper /
DeserializeWrapper round trip the client depends on, plus an integration
test that serves a canned schema over a throwaway HTTP server and
exercises the full fetch-and-decode path (no running instance required).

Improve error messages for unreachable hosts, missing databases (404),
and non-success responses.
@CLAassistant

CLAassistant commented Jun 17, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@fishnos fishnos marked this pull request as draft June 17, 2026 22:22
@fishnos fishnos marked this pull request as ready for review June 17, 2026 22:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ab09760d68

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// may be either a database name or an identity.
pub async fn module_def(&self, database: &str) -> anyhow::Result<RawModuleDefV9> {
let host = self.host.trim_end_matches('/');
let url = format!("{host}/v1/database/{database}/schema");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Escape the database path segment before issuing requests

Because the tool argument is interpolated directly into the URL, an MCP caller can include / or ? and make this authenticated request hit a different endpoint than /schema; for example database = "mydb/logs?follow=true&" becomes a GET to /v1/database/mydb/logs?... with the bearer token, and the existing logs route streams when follow=true (crates/client-api/src/routes/database.rs lines 620-652), so .json() can hang instead of returning schema. Validate the value as a SpacetimeDB name/identity or percent-encode it as a single path segment before building the URL.

Useful? React with 👍 / 👎.

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