Skip to content

MCP server

dAvePi exposes its full schema-driven surface as a Model Context Protocol server, so AI agents (Claude Desktop, Claude Code, Cursor, etc.) can call into the API as native tools — no hand-written integration glue required.

The MCP server is generated from the same schema registry that powers REST and GraphQL. Add a schema, and list_<resource>, get_<resource>, create_<resource>, update_<resource>, delete_<resource>, plus per-aggregation tools, appear automatically.

TransportUse it whenEndpoint
Streamable HTTPThe agent / IDE talks to a running dAvePi server. Stateless.POST /mcp
stdioClaude Desktop / Code spawn the binary as a child process.davepi mcp

Both transports share the same tool implementations from utils/mcpServer.js. Tenant isolation, ACL projection, and soft-delete behaviour all match the REST surface exactly — the MCP tools delegate to the same Mongoose models and helpers (runAggregation, applyIncludes, etc.).

Both transports authenticate the same way: a JWT issued by /login. The token’s user_id claim becomes the tenant identity for every tool call.

  • HTTP: Authorization: Bearer <token> on every POST /mcp request.
  • stdio: DAVEPI_TOKEN env var, set when launching the process.

The CLI verifies the token at startup using TOKEN_KEY (the same secret that signs /login responses) and refuses to start with an invalid or missing token.

ToolWhenDescription
list_<path>alwaysPaginated list, with optional filter (mongo-style), sort, q (full-text), include (relations), and includeDeleted.
get_<path>alwaysFetch one record by _id. Accepts the same include set as the list tool.
create_<path>alwaysCreate a record. userId / accountId stamped from the JWT — never accepted from the caller.
update_<path>alwaysPartial update by _id. Field-level ACL filters non-writable fields out of the payload; userId/accountId are stripped from the wire so a caller can’t reassign ownership.
delete_<path>alwaysSoft-delete (or hard-delete on schemas with softDelete: false).
restore_<path>softDelete enabled (default)Clear the deletedAt tombstone so the record becomes readable again.
history_<path>audit enabled (default)Returns the audit log for a record — create / update / delete / restore actions, newest first. Field-level read-ACL applied to before/after/diff.
search_<path>any field has searchable: trueFull-text search across the framework-owned text index. Equivalent to list_<path> with sort=score:desc.
list_<path>_<rel>per hasMany relationReturns the relation’s children for a parent _id in a single batched query.
get_<path>_<rel>per hasOne / belongsTo relationReturns the populated relation (or null) for a parent _id.
upload_<path>_<field>per type: 'File' fieldUpload a base64-encoded blob. Validates against the field’s maxBytes and accept.
fetch_<path>_<field>per type: 'File' fieldReturns the public or short-lived signed URL plus the file metadata.
delete_<path>_<field>per type: 'File' fieldRemoves the blob and clears the metadata sub-doc.
aggregate_<path>_<name>per declared aggregationParams surface with their declared types; the framework prepends $match: { userId } automatically.
(state-machine transitions)per state-machine fieldUse update_<path> with { id, record: { <field>: <to> } }. The framework validates against transitions[current] and rejects undeclared moves with INVALID_TRANSITION.

Every tool result is JSON: a record (or list response) on success, or — on a typed failure — an MCP isError: true result with a structured payload:

{
"isError": true,
"content": [{ "type": "text", "text": "{ \"error\": { \"code\": \"VALIDATION\", \"message\": \"...\", \"recoverable\": true } }" }]
}

The error payload carries:

  • codeVALIDATION, NOT_FOUND, UNAUTHORIZED, FORBIDDEN, DUPLICATE, INVALID_ID, etc.
  • message — human-readable description.
  • recoverable: true on errors an agent can fix by adjusting its arguments (VALIDATION, INVALID_ID). Distinguishes “fix the call and retry” from “this won’t ever work.”
  • auth: true on UNAUTHORIZED so clients can dispatch credential refresh / re-prompting without parsing free-text codes.

Unknown errors propagate and the SDK wraps them as internal — same posture as the REST Internal server error reduction in production.

The published @davepi/mcp package collapses agent wiring to a single npx -y line. It runs in either of the two modes above depending on environment:

  • DAVEPI_URL set → HTTP-proxy mode, talks to the remote /mcp endpoint.
  • DAVEPI_URL unset → local-stdio mode, spawns davepi mcp from the project’s local install.
{
"mcpServers": {
"davepi": {
"command": "npx",
"args": ["-y", "@davepi/mcp"],
"env": {
"DAVEPI_URL": "http://localhost:5050",
"DAVEPI_TOKEN": "<long-lived-jwt>"
}
}
}
}

Claude Desktop (claude_desktop_config.json)

Section titled “Claude Desktop (claude_desktop_config.json)”

macOS path: ~/Library/Application Support/Claude/claude_desktop_config.json.

{
"mcpServers": {
"davepi": {
"command": "npx",
"args": ["-y", "@davepi/mcp"],
"env": {
"DAVEPI_URL": "https://api.example.com",
"DAVEPI_TOKEN": "<long-lived-jwt>"
}
}
}
}

Same config shape under .cursor/mcp.json or Cursor’s MCP settings.

The npx create-davepi-app scaffolder drops a working .mcp.json pre-wired with @davepi/mcp in every generated project — the easiest path to a working setup is to scaffold a template and open it in Claude Code.

Terminal window
node -e '
const jwt = require("jsonwebtoken");
console.log(jwt.sign(
{ user_id: "<your-user-id>", roles: ["user"] },
process.env.TOKEN_KEY,
{ expiresIn: "30d" }
));
'

Treat that token like any other API credential — it grants the full tool surface as that user.

Both transports respond to schema changes live:

  • HTTP: each POST /mcp builds a fresh server from the current registry.
  • stdio: subscribes to schemaLoader.onChange and rebuilds the tool list on the existing connection. The SDK emits a notifications/tools/list_changed notification so the connected client (Claude Desktop, etc.) refreshes its tool registry without reconnecting.

See Hot reload for the underlying mechanism.

Once wired, an agent can plan against the API directly:

“Create an account named ‘Acme’, then add a contact ‘Jane’ tied to it, then list contacts.”

Behind the scenes, the model calls:

  1. create_account({ record: { accountName: "Acme" } }){ _id: "abc" }
  2. create_contact({ record: { name: "Jane", accountId: "abc" } }){ _id: "xyz" }
  3. list_contact({ filter: { accountId: "abc" } }){ results: [...], totalResults: 1 }

— all under the JWT user’s tenant scope, with ACL projection and soft-delete filtering applied automatically.