Skip to content

MCP Endpoint Tutorial

MCP Endpoint Tutorial

Available since: v3.18.0 (2026-04-24, JSON-RPC + HTTP MVP) — full transport surface (WebSocket, SSE, stdio, Unix socket), streaming progress, and 7 new LSP/graph-rag tools in v3.19.0 Build: cargo build --release --features mcp-endpoint (combine with code-graph and/or graph-rag for the full 16-tool catalogue) Routes: POST /mcp, GET /mcp/ws, GET /mcp/sse, GET /mcp/info Stdio: heliosdb-nano mcp-server --data-dir <dir>


UVP

Most “MCP for SQL” stories ship a wrapper process that proxies tools/call into a psql subprocess. HeliosDB Nano is the wrapper. The same binary that serves PostgreSQL wire traffic, REST, and WebSockets exposes a JSON-RPC 2.0 dispatcher with 16 tools (10 DB-backed + 6 in-process RAG) reachable from stdio, HTTP, WebSocket, or SSE — and Claude Code, Cursor, Continue, and Aider can drive every tool with no extra glue. Tools that take a while emit notifications/progress events while they run, so agents render real progress instead of a hung spinner. One process, one auth surface, real streaming.


Prerequisites

  • HeliosDB Nano v3.19+ source tree
  • Rust 1.85+
  • An MCP-aware client (Claude Code, Cursor, Continue, Aider) for the §11 integration walkthrough
  • curl and jq for the manual JSON-RPC walkthroughs
  • About 30 minutes

1. Build with --features mcp-endpoint

Terminal window
cargo build --release --features mcp-endpoint,code-graph,graph-rag

Each feature flag is additive:

FlagWhat you get
mcp-endpoint (alone)10 DB-backed tools (heliosdb_*) + 6 in-process RAG tools
+ code-graphadds 6 LSP tools (helios_lsp_definition, _references, _call_hierarchy, _hover, _document_symbols, _rename_preview, _rename_apply) and 3 diff tools (helios_lsp_references_diff, _body_diff, helios_ast_diff)
+ graph-ragadds helios_graphrag_search

mcp-endpoint alone gives you the wire-level dispatcher; the LSP and graph-rag tools auto-register through inventory::submit! whenever their feature is on.


2. Stdio Server (Claude Code / Cursor / Aider Path)

The simplest transport is stdin/stdout JSON-RPC, which is what Claude Code, Cursor, and Aider expect by default:

Terminal window
heliosdb-nano mcp-server --data-dir ./data

The server reads one JSON-RPC message per line, dispatches via mcp::handle_rpc_with_db, and writes one response per line to stdout. EOF closes the loop.

Manual smoke test

Terminal window
$ heliosdb-nano mcp-server --data-dir ./data <<'EOF' | jq -c '.'
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"ping"}
EOF
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","serverInfo":{"name":"heliosdb-nano","version":"3.19.1"},"capabilities":{...}}}
{"jsonrpc":"2.0","id":2,"result":{"tools":[...16 entries...]}}
{"jsonrpc":"2.0","id":3,"result":{}}

3. The 16 Tools (Full Catalogue)

Terminal window
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"verbose":true}}' \
| heliosdb-nano mcp-server --data-dir ./data | jq '.result.tools[].name'

Core (heliosdb_*) — 10 tools, DB-backed

ToolPurpose
heliosdb_queryRun an arbitrary SQL query with optional parameters and branch override.
heliosdb_schemaColumn list of a table from the catalog.
heliosdb_list_tablesEvery user table (filters out helios_* / mv_*).
heliosdb_create_tableCreate a table with column array.
heliosdb_insertInsert one or more rows.
heliosdb_branch_createCopy-on-write branch from another.
heliosdb_branch_listEvery branch.
heliosdb_branch_mergeMerge source branch into target.
heliosdb_searchVector-similarity search via HNSW.
heliosdb_time_travelRead-only AS OF TIMESTAMP query.

In-process RAG — 6 tools, no DB required

ToolPurpose
heliosdb_bm25_indexBuild a process-wide BM25 index from (doc_id, text) documents.
heliosdb_hybrid_searchBM25 + caller-supplied vector hits, fused via RRF / MMR / weighted-linear.
heliosdb_graph_add_edgeAppend a directed edge to the in-process graph store.
heliosdb_graph_traverseBFS from a starting node with optional label filter and depth.
heliosdb_graph_pathShortest path via BFS / Dijkstra / bi-directional BFS.
heliosdb_embed_and_storeStash a (doc_id, text) tuple into a BM25 index.

Code-graph extensions — 7 tools (helios_lsp_* + helios_ast_diff), via --features code-graph

ToolPurpose
helios_lsp_definition”Where is name defined?”
helios_lsp_references”Who uses symbol_id?”
helios_lsp_call_hierarchyBFS over CALLS edges, configurable direction & depth.
helios_lsp_hoverSignature lookup.
helios_lsp_document_symbolsFile outline ordered by line. (v3.19)
helios_lsp_rename_previewRead-only preview of a rename’s edit list. (v3.19)
helios_lsp_rename_applyIdentifier-boundary-aware rename with sha256 conflict check; supports dry_run. (v3.19)
helios_lsp_references_diffDiff a symbol’s reference set across two AS OF points. (v3.19)
helios_lsp_body_diffMyers-LCS body diff across two AS OF points. (v3.19)
helios_ast_diffFile-level structural diff (added / removed / moved). (v3.19)

Graph-RAG extension — 1 tool, via --features graph-rag

ToolPurpose
helios_graphrag_searchCross-modal seed → BFS expand → return query over _hdb_graph_*. Emits streaming progress. (v3.19)

tools/list?verbose=true adds two extra fields per tool — category (core or extension) and requiresDatabase — so a client can gate which tools it surfaces to a model.


4. Single-Shot Discovery — helios/info and GET /mcp/info

Pull serverInfo + capabilities + verbose tool catalogue + resource list in one packet:

Terminal window
echo '{"jsonrpc":"2.0","id":1,"method":"helios/info"}' \
| heliosdb-nano mcp-server --data-dir ./data | jq

Or over HTTP (§5):

Terminal window
curl -s http://localhost:8080/mcp/info | jq

5. HTTP / WebSocket / SSE — Network Transports

Embed the MCP routes onto an Axum router:

use std::sync::Arc;
use heliosdb_nano::{
mcp::{attach_mcp_routes, McpAuth, McpState},
EmbeddedDatabase,
};
#[tokio::main]
async fn main() -> heliosdb_nano::Result<()> {
let db = Arc::new(EmbeddedDatabase::open("./data")?);
let state = McpState::new(db).with_auth(McpAuth::Disabled);
let app = attach_mcp_routes(axum::Router::new(), state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
axum::serve(listener, app).await?;
Ok(())
}

attach_mcp_routes mounts:

  • POST /mcp — JSON-RPC request/response.
  • GET /mcp/ws — WebSocket; one JSON-RPC message per text frame.
  • GET /mcp/sse — Server-Sent Events transport per the MCP spec; emits an initial endpoint event with the paired POST URL plus a session id.
  • GET /mcp/info — discovery payload (same body as helios/info).

All four transports dispatch through the same mcp::handle_rpc_with_db core, so tool/resource behaviour is identical.

POST manual call

Terminal window
curl -s -X POST http://localhost:8080/mcp \
-H 'content-type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{
"name":"heliosdb_query",
"arguments":{"sql":"SELECT 1+1 AS x"}
}}' | jq

Auth

// Loopback / dev — auth off.
let state = McpState::new(db).with_auth(McpAuth::Disabled);
// Production — JWT-gated, with read/write scopes.
let state = McpState::new(db).with_auth(McpAuth::jwt_hs256(secret_bytes));

When auth is on, tools/call/resources/read require the appropriate scope; reads return 401 without a Bearer header. WebSocket upgrades require Write scope (the per-frame loop has no per-message header). Use mcp::bind_safety_check to refuse non-loopback binds when auth is Disabled.


6. Streaming Progress Notifications (v3.19.0)

Tools that take a while emit notifications/progress events when the client opts in via _meta.progressToken. WebSocket and stdio forward each event inline; HTTP forwards through the SSE channel.

Opt in over WebSocket

ws.send(JSON.stringify({
jsonrpc: "2.0",
id: 7,
method: "tools/call",
params: {
name: "helios_graphrag_search",
arguments: { seed_text: "parse", hops: 3, limit: 50 },
_meta: { progressToken: "search-7" },
},
}));

You’ll receive one or more notification frames followed by the final response:

{"jsonrpc":"2.0","method":"notifications/progress","params":{
"progressToken":"search-7","progress":0.0,"total":50,
"message":"graph_rag_search: seeding for 'parse', hops=3"}}
{"jsonrpc":"2.0","method":"notifications/progress","params":{
"progressToken":"search-7","progress":18,"total":50,
"message":"graph_rag_search: 18 hits"}}
{"jsonrpc":"2.0","id":7,"result":{"isError":false,"content":[{"type":"text","text":"..."}]}}

helios_graphrag_search is the first tool wired (emits “seeding” on entry, ” hits” on exit). The dispatcher (mcp::call_tool_streaming) runs the synchronous tool handler on spawn_blocking and forwards events through an unbounded channel. Any tool that calls mcp::progress::emit(...) from anywhere on the call stack will participate.

HTTP + SSE pairing

Single-shot HTTP can’t stream, so progress for POST /mcp rides the paired SSE channel:

  1. Client opens GET /mcp/sse?session=<id>. The server replies with event: endpoint whose body is /mcp?session=<id>.
  2. Client copies <id> into the Mcp-Session-Id header on the subsequent POST /mcp request and includes _meta.progressToken in the body.
  3. The server runs the tool, forwards progress events to the SSE channel keyed by <id>, and returns the final tools/call response in the POST body.

7. Resources

Terminal window
echo '{"jsonrpc":"2.0","id":1,"method":"resources/list"}' | heliosdb-nano mcp-server -d ./data | jq

Two static URIs are advertised; two more are URI-templated:

URIBody
heliosdb://schemaList of every user table.
heliosdb://branchesEvery branch.
heliosdb://schema/{table}Column metadata for a single table.
heliosdb://stats/{table}Row count for a single table.

resources/read { "uri": "heliosdb://schema/users" } returns JSON { "table": "users", "columns": [...] }.


8. Worked Walkthrough — Ask Claude Code “find every caller of parse

Configure Claude Code’s .mcp.json:

{
"mcpServers": {
"heliosdb": {
"command": "heliosdb-nano",
"args": ["mcp-server", "--data-dir", "./data"]
}
}
}

Index the corpus once (over psql, or via heliosdb-nano code-graph hook if you’ve wired the git hook):

CREATE TABLE src (path TEXT PRIMARY KEY, lang TEXT, content TEXT);
\copy src FROM 'sources.csv' CSV;
SELECT 'indexed' AS status FROM lsp_definition('answer'); -- triggers bootstrap

In Claude Code’s chat: Use heliosdb to find every caller of parse. Claude calls helios_lsp_definition({name: "parse"}), gets a symbol_id, then helios_lsp_references({symbol_id}) and renders the call sites with file/line citations. With --features graph-rag enabled, Claude can chain into helios_graphrag_search({seed_text: "parse", hops: 2}) to widen the context to docs and issues that mention parse.


9. Cursor / Continue / Aider

Same .mcp.json shape works for all three. Each client autodetects the tools list on startup and surfaces them under whatever name they expose MCP tools as:

  • Cursor: tools appear under @heliosdb slash commands.
  • Continue: tools appear in the tools panel of any agent slot.
  • Aider: tools are reachable via the /mcp command from the editor’s chat.

All four clients honour notifications/progress and render the streaming message as a status line.


10. Branch Scoping per Tool Call

Every DB-backed tool accepts a branch argument:

{
"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": {
"name": "heliosdb_query",
"arguments": {
"sql": "SELECT COUNT(*) FROM src",
"branch": "refactor"
}
}
}

The dispatcher’s with_branch helper switches to the requested branch before the call and restores the previous branch after — even on early-return paths. branch == "main" and branch == current skip the switch entirely.


11. Where Next