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
curlandjqfor the manual JSON-RPC walkthroughs- About 30 minutes
1. Build with --features mcp-endpoint
cargo build --release --features mcp-endpoint,code-graph,graph-ragEach feature flag is additive:
| Flag | What you get |
|---|---|
mcp-endpoint (alone) | 10 DB-backed tools (heliosdb_*) + 6 in-process RAG tools |
+ code-graph | adds 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-rag | adds 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:
heliosdb-nano mcp-server --data-dir ./dataThe 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
$ 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)
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
| Tool | Purpose |
|---|---|
heliosdb_query | Run an arbitrary SQL query with optional parameters and branch override. |
heliosdb_schema | Column list of a table from the catalog. |
heliosdb_list_tables | Every user table (filters out helios_* / mv_*). |
heliosdb_create_table | Create a table with column array. |
heliosdb_insert | Insert one or more rows. |
heliosdb_branch_create | Copy-on-write branch from another. |
heliosdb_branch_list | Every branch. |
heliosdb_branch_merge | Merge source branch into target. |
heliosdb_search | Vector-similarity search via HNSW. |
heliosdb_time_travel | Read-only AS OF TIMESTAMP query. |
In-process RAG — 6 tools, no DB required
| Tool | Purpose |
|---|---|
heliosdb_bm25_index | Build a process-wide BM25 index from (doc_id, text) documents. |
heliosdb_hybrid_search | BM25 + caller-supplied vector hits, fused via RRF / MMR / weighted-linear. |
heliosdb_graph_add_edge | Append a directed edge to the in-process graph store. |
heliosdb_graph_traverse | BFS from a starting node with optional label filter and depth. |
heliosdb_graph_path | Shortest path via BFS / Dijkstra / bi-directional BFS. |
heliosdb_embed_and_store | Stash a (doc_id, text) tuple into a BM25 index. |
Code-graph extensions — 7 tools (helios_lsp_* + helios_ast_diff), via --features code-graph
| Tool | Purpose |
|---|---|
helios_lsp_definition | ”Where is name defined?” |
helios_lsp_references | ”Who uses symbol_id?” |
helios_lsp_call_hierarchy | BFS over CALLS edges, configurable direction & depth. |
helios_lsp_hover | Signature lookup. |
helios_lsp_document_symbols | File outline ordered by line. (v3.19) |
helios_lsp_rename_preview | Read-only preview of a rename’s edit list. (v3.19) |
helios_lsp_rename_apply | Identifier-boundary-aware rename with sha256 conflict check; supports dry_run. (v3.19) |
helios_lsp_references_diff | Diff a symbol’s reference set across two AS OF points. (v3.19) |
helios_lsp_body_diff | Myers-LCS body diff across two AS OF points. (v3.19) |
helios_ast_diff | File-level structural diff (added / removed / moved). (v3.19) |
Graph-RAG extension — 1 tool, via --features graph-rag
| Tool | Purpose |
|---|---|
helios_graphrag_search | Cross-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:
echo '{"jsonrpc":"2.0","id":1,"method":"helios/info"}' \ | heliosdb-nano mcp-server --data-dir ./data | jqOr over HTTP (§5):
curl -s http://localhost:8080/mcp/info | jq5. 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 initialendpointevent with the paired POST URL plus a session id.GET /mcp/info— discovery payload (same body ashelios/info).
All four transports dispatch through the same mcp::handle_rpc_with_db core, so tool/resource behaviour is identical.
POST manual call
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"} }}' | jqAuth
// 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, ”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:
- Client opens
GET /mcp/sse?session=<id>. The server replies withevent: endpointwhose body is/mcp?session=<id>. - Client copies
<id>into theMcp-Session-Idheader on the subsequentPOST /mcprequest and includes_meta.progressTokenin the body. - The server runs the tool, forwards progress events to the SSE channel keyed by
<id>, and returns the finaltools/callresponse in the POST body.
7. Resources
echo '{"jsonrpc":"2.0","id":1,"method":"resources/list"}' | heliosdb-nano mcp-server -d ./data | jqTwo static URIs are advertised; two more are URI-templated:
| URI | Body |
|---|---|
heliosdb://schema | List of every user table. |
heliosdb://branches | Every 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 bootstrapIn 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
@heliosdbslash commands. - Continue: tools appear in the
toolspanel of any agent slot. - Aider: tools are reachable via the
/mcpcommand 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
- CODE_GRAPH_TUTORIAL — populate
_hdb_code_*so the LSP tools have data. - GRAPH_RAG_TUTORIAL — wire
helios_graphrag_searchagainst a populated_hdb_graph_*. - DOCLING_INGESTION_TUTORIAL — pull external documents into the graph the MCP search hits against.
- SEMANTIC_HASH_INDEX_QUICKREF — keep re-indexing incremental as the corpus grows.
- BAAS_REST_API_TUTORIAL — the same DB also serves a PostgREST-compatible HTTP surface.