VesperMCP docs
Open demo Star—
Docs / Getting started

Vesper MCP server

Give any AI agent a construction project's verified site record and memory — drawings with revisions, RFIs, permits, hold points, IS codes, 550 QA/QC templates and your PDFs. Vesper answers with citations, abstains when the evidence is weak, and refuses to log work that contradicts the latest drawing or an open permit.

MCP 2025-06-18Streamable HTTP11 toolsClerk JWT authPer-project scopingOpen source

Endpoint. POST {BASE_URL}/mcp — JSON-RPC 2.0. The same tools are also plain REST at GET /api/tools and POST /api/tools/{name}. The public demo on this site is static; run the backend locally or on your own host to call tools.

Quickstart

  1. Clone and start the backend
    terminal
    git clone https://github.com/atharvavdeo/vesper-ai.git
    cd vesper-ai/backend
    python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
    ollama pull bge-m3
    .venv/bin/uvicorn main:app --host 127.0.0.1 --port 8000
  2. List the tools
    terminal
    curl -s http://127.0.0.1:8000/mcp -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
  3. Ask the Nashik project a question
    terminal
    curl -s http://127.0.0.1:8000/mcp -H 'Content-Type: application/json' -d '{
      "jsonrpc":"2.0","id":2,"method":"tools/call",
      "params":{"name":"ask_memory","arguments":{"query":"Who is the structural consultant?","projectId":"NSK"}}}'

    → “The structural consultant is Sthapatya Structural Engineers – Dr. V. N. Rao …” with citations.

Connect a client

claude mcp add --transport http vesper http://127.0.0.1:8000/mcp

Replace the URL with your deployed backend. In production add Authorization: Bearer <Clerk JWT>.

Tools

Call list_projects first, then pass projectId to every project tool. Seeded projects: P1 (Pithoragarh District Hospital) and NSK (Nashik Civil Hospital).

ToolArgumentsReturnsRead-only
list_projects—Projects the caller can access, with roleyes
project_overviewprojectIdKPIs, blockers, recent activityyes
search_memoryquery, projectId?, k? 1–20Ranked passages with scores, citations, timingsyes
ask_memoryquery, projectId?Short grounded answer + citations, or abstain: trueyes
list_drawingsprojectIdEvery revision, status, latest For Constructionyes
list_rfisprojectIdRFIs with status and responsesyes
list_permitsprojectIdPermits, mandatory checks, blocking stateyes
list_observationsprojectIdLogged observations, newest firstyes
get_observationprojectId, idOne observation with evidence trailyes
memory_graphprojectId, q?, limit?Knowledge-graph nodes and edgesyes
check_site_statementprojectId, text, sessionId?, language?Engine verdict (state, spoken reply, contradictions) + sessionIdno

check_site_statement

Runs an English or Hinglish site statement through the deterministic contradiction engine: superseded revisions, tolerance breaches, unmet permit checks and unreleased hold points. Pass sessionId back for follow-ups and corrections. It may create a pending observation when the user confirms, so it is annotated readOnlyHint: false.

result (abridged)
{
  "sessionId": "VS-1a0a7062e0d-5e630a",
  "state": "blocked",
  "reply": { "text": "Logging the observation is not allowed yet. At Zone C, Level 4, the hot work permit HWP-2031 has mandatory checks still pending: Fire watcher assigned …" }
}

Protocol

Protocol version 2025-06-18 over Streamable HTTP with single JSON responses (no SSE stream, no batching).

MethodResult
initializeprotocolVersion, serverInfo, capabilities.tools, instructions
notifications/initialized202 Accepted (any message without id)
ping{}
tools/list{ tools: [...] } with JSON Schema inputSchema and annotations
tools/call{ content: [{type:"text", text}], structuredContent, isError }

REST API

Same tools without JSON-RPC — handy for webhooks, n8n, LangChain or any HTTP client.

terminal
# manifest
curl -s http://127.0.0.1:8000/api/tools

# call a tool
curl -s http://127.0.0.1:8000/api/tools/list_permits -H 'Content-Type: application/json' -d '{"projectId":"NSK"}'
# → {"ok": true, "tool": "list_permits", "result": [...]}

Authentication

ModeWhenSend
Local devCLERK_JWT_ISSUER emptyNothing, or X-Vesper-User / X-Vesper-Org / X-Vesper-Role
ProductionCLERK_JWT_ISSUER setAuthorization: Bearer <Clerk session JWT>

Isolation. The org is always read from the verified project row, never from tool arguments. A caller outside the org gets 404 project not found — it cannot probe other tenants by guessing ids.

Errors

CaseResponse
Unknown methodJSON-RPC error -32601
Unknown toolJSON-RPC error -32602
Malformed JSON-32700, HTTP 400
Tool failure (401, 403, 404, 422)isError: true, text "404: project not found"

Python

# pip install mcp
import asyncio, os
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

URL = os.getenv("VESPER_MCP_URL", "http://127.0.0.1:8000/mcp")
HEADERS = {"Authorization": f"Bearer {os.environ['VESPER_TOKEN']}"} if os.getenv("VESPER_TOKEN") else {}

async def main():
    async with streamablehttp_client(URL, headers=HEADERS) as (read, write, _):
        async with ClientSession(read, write) as s:
            await s.initialize()
            tools = await s.list_tools()
            print([t.name for t in tools.tools])
            r = await s.call_tool("ask_memory", {"query": "Which permits are blocking work today?", "projectId": "NSK"})
            print(r.structuredContent["answer"])

asyncio.run(main())

TypeScript / Node

// npm i @modelcontextprotocol/sdk
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(new URL(process.env.VESPER_MCP_URL ?? "http://127.0.0.1:8000/mcp"), {
  requestInit: { headers: process.env.VESPER_TOKEN ? { Authorization: `Bearer ${process.env.VESPER_TOKEN}` } : {} },
});
const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(transport);

const { tools } = await client.listTools();
console.log(tools.map((t) => t.name));

const res = await client.callTool({ name: "list_permits", arguments: { projectId: "NSK" } });
console.log(res.structuredContent);

Go

main.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

func call(name string, args map[string]any) (map[string]any, error) {
	body, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
		"params": map[string]any{"name": name, "arguments": args}})
	req, _ := http.NewRequest("POST", "http://127.0.0.1:8000/mcp", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	if t := os.Getenv("VESPER_TOKEN"); t != "" {
		req.Header.Set("Authorization", "Bearer "+t)
	}
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	var out struct {
		Result struct {
			StructuredContent map[string]any `json:"structuredContent"`
			IsError           bool           `json:"isError"`
		} `json:"result"`
	}
	return out.Result.StructuredContent, json.NewDecoder(resp.Body).Decode(&out)
}

func main() {
	r, err := call("ask_memory", map[string]any{"query": "What cover does IS 456 require for columns?", "projectId": "P1"})
	fmt.Println(r["answer"], err)
}

cURL

curl -s $VESPER_MCP_URL -H 'Content-Type: application/json' -H "Authorization: Bearer $VESPER_TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'

Add a tool (server side)

Tools live in backend/routes/tools.py. Each is a JSON Schema entry plus a branch in run_tool() that calls an existing REST endpoint, so auth and scoping come for free.

backend/routes/tools.py
TOOLS.append({
    "name": "list_submittals",
    "title": "List submittals",
    "description": "Submittals with review status and spec references.",
    "inputSchema": {"type": "object", "properties": {"projectId": _PID}, "required": ["projectId"]},
    "annotations": _RO,
})
_BY_NAME["list_submittals"] = TOOLS[-1]

# in run_tool():
if name == "list_submittals":
    return await _call(request, "GET", "/api/submittals", params={"projectId": pid})

Need SSE or batching? Wrap run_tool() with the official mcp Python SDK (FastMCP); names, schemas and handlers stay the same.

Self-host

ServicePortNeeded for
FastAPI backend (backend/)8000MCP, REST, engine, memory
Ollama bge-m311434Query embeddings
Next.js dashboard (app/)3000Optional UI
LiveKit agent (agent/)—Optional voice
backend/.env.local
GROQ_API_KEY=            # answer phrasing (Cerebras fallback: CEREBRAS_API_KEY)
CLERK_JWT_ISSUER=        # empty = local header auth; set in production
MEMORY_COGNEE_ENABLED=1  # knowledge graph (optional)

Production checklist

  1. Deploy the backend behind HTTPS — Fly.io, Render, Railway or a VPS. The Cloudflare site is static and has no /mcp.
  2. Set CLERK_JWT_ISSUER so every call needs a Clerk JWT; front it with an OAuth 2.1 proxy for MCP-spec authorization if clients need it.
  3. Allow your client origins in CORS for browser-based agents.
  4. Rate-limit /mcp and /api/tools/* — reranking is the main cost (~0.4 s per uncached search).
  5. Smoke test with tools/list and one ask_memory call, then register the public URL in your client or an MCP directory.

FAQ

Does Vesper hallucinate answers?

Answers are built only from retrieved passages. If the reranked evidence is weak, ask_memory returns abstain: true, and sentences quoting numbers absent from the sources are removed.

Can an agent write bad data?

Only check_site_statement can lead to a write, and the deterministic engine blocks logs that contradict the latest drawing revision, tolerances, open permits or hold points.

Which languages are supported?

English, Hindi and Hinglish statements; answers are returned in English with citations.