MCP server setup
@dappql/mcp is a Model Context Protocol server that turns your DappQL project into a live, typed set of agent-callable tools and resources. Any MCP-aware client, Claude Code, Cursor, Codex, Continue, Zed, can connect to it and get first-class access to your contracts.
This page shows how to wire it up. For why it exists, see Why AI-first.
Prerequisites
- Either a project with a
dap.config.js(see Getting started), or a workspace that installs one or more DappQL-packaged SDKs (e.g.@underscore-finance/sdk). In the second case, the server runs in plugin-only mode — no local config required. - An MCP-aware client.
- An RPC URL for your chain.
You do not need to install @dappql/mcp globally, npx pulls it on demand.
Claude Code
Add the server to ~/.claude.json (global) or .mcp.json at your project root (per-project):
{
"mcpServers": {
"dappql": {
"command": "npx",
"args": ["-y", "@dappql/mcp"],
"env": {
"DAPPQL_DEFAULT_RPC_URL": "https://mainnet.base.org"
}
}
}
}Restart Claude Code. In a new chat, the agent now has 13 dappql tools and 4 resources scoped to your project.
Quick sanity check, ask: "what contracts are in this project?", the agent should call projectInfo and listContracts.
DAPPQL_DEFAULT_RPC_URLis the committed default — safe to put a public RPC here. For a personal Alchemy/QuickNode key, useDAPPQL_RPC_URLin a local.env(auto-loaded,.gitignore'd). It overrides the default. See RPC config below.
Cursor
Add the same block to ~/.cursor/mcp.json:
{
"mcpServers": {
"dappql": {
"command": "npx",
"args": ["-y", "@dappql/mcp"],
"env": { "DAPPQL_DEFAULT_RPC_URL": "https://mainnet.base.org" }
}
}
}Reload Cursor. MCP servers are listed under settings → MCP.
Other clients
Any client that speaks the MCP stdio transport will work with the same invocation (npx -y @dappql/mcp). The server discovers dap.config.js by walking up from its launch directory, so launch it from inside your project (or a subdirectory of it).
Plugin-only mode
If you're an agent user who just wants to consume a published DappQL SDK (not build one), you don't need a dap.config.js. Create a folder with:
my-workspace/
├── .mcp.json
└── package.json # installs @dappql/mcp + @some-protocol/sdk// package.json
{
"dependencies": {
"@dappql/mcp": "^0.2.0",
"@underscore-finance/sdk": "^1.2.19"
}
}npm install, then open the folder in Claude Code. The server detects no dap.config.js, switches to plugin-only mode, and exposes every contract from every DappQL-packaged SDK in node_modules via their manifests.
Canonical live example: underscore-finance/mcp.
Configuration
Everything non-sensitive goes in dap.config.js. Secrets (signing keys, personal RPC URLs) go in a local .env (auto-loaded) or the env block of your MCP client config, never in the repo.
// dap.config.js
export default {
// ... your contracts, targetPath, etc.
mcp: {
rpc: 'https://mainnet.base.org', // optional, highest-priority RPC source
allowWrites: false, // default false, flip to true for callWrite
allowCodegen: false, // default false, flip to true for regenerate
},
}| Setting | Source | Purpose |
|---|---|---|
| RPC URL | mcp.rpc → DAPPQL_RPC_URL → DAPPQL_DEFAULT_RPC_URL | viem transport. See RPC configuration for precedence. |
| Signing key | DAPPQL_PRIVATE_KEY or MNEMONIC env | Required for callWrite. |
| Write permission | mcp.allowWrites: true in config | Second gate. Both this AND a key must be present. |
| Codegen permission | mcp.allowCodegen: true in config | Gates the regenerate tool. |
See Safety model for the full gating logic.
RPC configuration
The MCP server picks an RPC URL in this order, first hit wins:
mcp.rpcindap.config.js— explicit override in code.DAPPQL_RPC_URLenv — the local value. Meant for.envor shell. Your personal Alchemy / QuickNode / private node URL.DAPPQL_DEFAULT_RPC_URLenv — the committed default. Meant for theenvblock in.mcp.json/~/.claude.json. Safe for public RPCs.
Using .env
@dappql/mcp auto-loads .env from the launch directory (Node ≥20.12). Drop one next to your project:
DAPPQL_RPC_URL=https://base-mainnet.g.alchemy.com/v2/YOUR_KEY
DAPPQL_PRIVATE_KEY=0x... # optional, only if writes are enabledGitignore .env. Your teammates each keep their own; the committed DAPPQL_DEFAULT_RPC_URL is the fallback for anyone who doesn't have one.
HTTP transport (hosted / remote MCP)
By default @dappql/mcp runs over stdio — a local subprocess your MCP client spawns. For hosted scenarios where teams or external users connect to one shared MCP server (no install required), the same package also speaks streamable HTTP.
Launching in HTTP mode
# port via flag
npx @dappql/mcp --http=3737
# or via env
DAPPQL_MCP_HTTP_PORT=3737 npx @dappql/mcpEndpoints:
| Method | Path | Purpose |
|---|---|---|
POST | /mcp | Client→server JSON-RPC requests. SSE response stream. |
GET | /mcp | Server-pushed notifications (stateful only). |
DELETE | /mcp | End a session (stateful only). |
GET | /health | JSON status: chain, contract count, plugins, sessions. |
Add it to a remote-MCP-aware client by URL:
{
"mcpServers": {
"dappql": {
"url": "https://your-host.example.com/mcp"
}
}
}Stateful vs stateless
The HTTP transport supports two modes — pick one based on where you deploy.
| Stateful (default) | Stateless | |
|---|---|---|
| Trigger | --http alone | --stateless flag or DAPPQL_MCP_STATELESS=true |
| Session id | issued on initialize, sent in mcp-session-id header | none |
| Server-pushed notifications | ✅ | ❌ |
GET/DELETE /mcp | ✅ | rejected (405) |
| Per-request overhead | low (transport reused) | slightly higher (fresh transport per call) |
| Survives across instances | ❌ | ✅ |
| Right for | long-lived Node servers, single instance (Fly.io, your own VM) | serverless (Vercel, Cloudflare Workers, Lambda) |
For our read-only tool surface (contract reads, multicall, events, simulate), stateless loses no functionality — there are no notifications to push and no resources to subscribe to. Pick stateful for slightly lower per-call latency on a long-running process; pick stateless for any serverless deploy.
Deploying on Vercel
Vercel functions are stateless by nature, so use --stateless mode. Minimal sketch:
// api/mcp.ts
import { startHttpServer, loadProjectContext } from '@dappql/mcp'
// ... wire as a Vercel handler in stateless mode, with @your-org/sdk pinnedA full reference deploy lives at underscore-finance/mcp-server (coming soon) — fork that as a starting point.
Hosted-mode safety
- Never enable writes on a public/shared instance. Don't set
DAPPQL_PRIVATE_KEY/MNEMONICand don't flipmcp.allowWrites: true.simulateWriteis fine;callWriteshould be hard off. - Rate-limit at the edge. Each request is one or more chain reads. Public, unlimited = your RPC bill spirals.
- Pin one chain per host. A single deploy reads from one RPC. For multi-chain protocols, ship one host per chain.
Boot log
On startup, the server logs its state to stderr so you can verify everything loaded correctly:
[@dappql/mcp] Project: /Users/you/myapp/dap.config.js
[@dappql/mcp] Chain: 8453
[@dappql/mcp] Contracts: 12
[@dappql/mcp] Writes: disabled, writes disabled: not opted in and no signing key available
[@dappql/mcp] Codegen: disabled, codegen disabled: `mcp.allowCodegen: true` missing from dapp.config.jsIf you see No dapp.config.js found walking up from cwd, the server was launched from outside your project tree.
Enabling writes (carefully)
Writes require both a signing key in env AND an explicit config opt-in. Either alone doesn't unlock callWrite. Both together still don't skip simulation, every write is preflighted via eth_call and aborts on revert.
// dap.config.js
export default {
// ...
mcp: { allowWrites: true },
}// ~/.claude.json
{
"mcpServers": {
"dappql": {
"command": "npx",
"args": ["-y", "@dappql/mcp"],
"env": {
"DAPPQL_DEFAULT_RPC_URL": "https://mainnet.base.org",
"DAPPQL_PRIVATE_KEY": "0x..."
}
}
}
}Signing keys must stay out of the committed JSON. Keep them in a local .env (gitignored) or your shell — never in .mcp.json.
For testnets and burner-wallet workflows this is fine. For mainnet, think about it twice, an agent with write access is blast-radius-equivalent to a deploy key. The simulate-first default still protects against revertable failures, but nothing protects against intentional transfers to the wrong address.
Default position: keep allowWrites: false and rely on simulateWrite for dry-runs. See Safety model.
Verifying the connection
In Claude Code, type /mcp to see connected servers. dappql should appear with a green status and (13 tools, 4 resources) count.
If it doesn't appear:
- Restart the client after editing config.
- Check
~/.claude.jsonsyntax, JSON parse errors silently hide the server. - Launch the binary directly to read stderr:
cd your-project && npx -y @dappql/mcp. - Make sure your
dap.config.jsis valid ESM (or CJS if you're not an ESM project). - If using an env-based RPC (
DAPPQL_RPC_URLorDAPPQL_DEFAULT_RPC_URL), verify the URL is reachable withcurl.
Next
- Tools reference, every tool's purpose and signature.
- Resources reference,
dappql://docs/library, project guide, per-contract ABI. - Safety model, how writes and codegen are gated.
- Underscore case study, real agent sessions.