DOCS NAV · BUILD AN MCP SERVER ▾
BUILD AN MCP SERVER
How to write, host, publish, and monetize an MCP server through mcpmeter — from a hello-world handler to your first payout.
WHAT YOU'RE BUILDING
An MCP server is just an HTTP endpoint that speaks JSON-RPC 2.0. Agents (Claude, Cursor, Cline, Codex…) POST requests to it; you respond with tools the agent can use. mcpmeter sits between the two: it authenticates the consumer's bearer key, charges their balance, forwards the call to your server, and returns your response unchanged.
This guide targets the Streamable HTTP transport from the 2025-03-26 MCP spec. Single endpoint, JSON-RPC over HTTP POST, optional SSE for long-running tools. Stdio MCPs work with mcpmeter too via mcp-remote as a bridge, but native HTTP is simpler.
Three methods you must handle:
| Method | When called | What you return |
|---|---|---|
initialize | First contact | protocol version + your server name |
tools/list | Agent discovers your tools | array of tools with input schemas |
tools/call | Agent invokes a tool | the tool's result |
THE MCPMETER CONTRACT
When a consumer calls https://proxy.mcpmeter.com/your-slug, the proxy:
- Validates the bearer key (
mcpm_live_…), looks up the listing. - Enforces rate limit + free-tier quota; debits credit if billable.
- Forwards the JSON-RPC body unchanged to your
upstream_url. - Streams your response back, adds
X-Mcpmeter-Billed+X-Mcpmeter-Duration-Msheaders. - Writes a usage event (mcp_id, tool_name, status, duration, billed, client).
What the proxy strips from the request before forwarding:
Authorization— that's the consumer's mcpmeter key, not for you.Cookie— session-shaped data shouldn't reach upstream MCPs.Host,Content-Length,X-Forwarded-Host— rebuilt by the proxy.
What the proxy preserves and forwards:
- Body (JSON-RPC) bit-for-bit.
Content-Type,Accept,User-Agent, MCP session headers.- Any header named
X-Forward-{Name}— rewritten as{Name}on the upstream request. Lets a consumer pass extra auth (your own API key, an OAuth token, etc.) without us knowing about it.
# Consumer's request to mcpmeter curl https://proxy.mcpmeter.com/their-mcp \ -H "Authorization: Bearer mcpm_live_…" \ -H "X-Forward-Authorization: Bearer their_upstream_token" \ -H "X-Forward-X-API-Version: 2" # Your upstream receives POST / # your endpoint Authorization: Bearer their_upstream_token # rewritten X-API-Version: 2 # rewritten Content-Type: application/json User-Agent: …
HELLO WORLD — MINIMAL SERVER (NODE)
A complete MCP server in 50 lines. Run it on :3000, point an mcpmeter listing at http://your-host:3000:
import Fastify from 'fastify'; const tools = [{ name: 'reverse', description: 'Reverse a string.', inputSchema: { type: 'object', required: ['text'], properties: { text: { type: 'string' } } }, annotations: { readOnlyHint: true }, }]; const app = Fastify(); app.post('/', async (req, reply) => { const { id, method, params } = req.body || {}; if (method === 'initialize') return ok(id, { protocolVersion: '2025-03-26', capabilities: { tools: {} }, serverInfo: { name: 'reverse-mcp', version: '0.1.0' }, }); if (method === 'notifications/initialized') { reply.code(202).send(); return; } if (method === 'tools/list') return ok(id, { tools }); if (method === 'tools/call') { const { name, arguments: args } = params; if (name === 'reverse') { return ok(id, { content: [{ type: 'text', text: [...args.text].reverse().join('') }] }); } return err(id, -32601, `Unknown tool: ${name}`); } return err(id, -32601, `Unknown method: ${method}`); }); function ok(id, result) { return { jsonrpc: '2.0', id, result }; } function err(id, code, message) { return { jsonrpc: '2.0', id, error: { code, message } }; } app.listen({ port: 3000, host: '0.0.0.0' });
Same shape in any language. See our reference handlers ↗ for 23 working examples.
TOOL SCHEMAS — WHAT MAKES AGENTS SUCCESSFUL
The inputSchema on every tool is JSON Schema. Agents read it to figure out what arguments to pass. Three rules that drastically improve LLM tool-call accuracy:
- Mark every required field in the
requiredarray. Agents will try to omit fields that look optional. - Write descriptions for every property, not just the tool. The LLM sees property descriptions when picking values.
- Constrain enums + min/max wherever possible.
"difficulty": {"type": "string", "enum": ["easy","medium","hard"]}is better than free-form strings.
Add annotations: { readOnlyHint: true } to read-only tools (queries, lookups). mcpmeter uses this to enable the public "Try it" widget on read tools, and clients use it to show consent prompts before write tools mutate state. Without the hint we assume write.
ERROR CONTRACT
JSON-RPC errors use a numeric code + message. mcpmeter passes them through unchanged but will refund the consumer's debit on HTTP 5xx. Use these conventions:
| Situation | HTTP | JSON-RPC code |
|---|---|---|
| Bad arguments (missing required, wrong type) | 200 | -32602 |
| Unknown tool name | 200 | -32601 |
| Upstream API failed (rate-limit, outage) | 502 or 503 | n/a (use HTTP) |
| Generic tool failure (your bug) | 200 | -32000 |
Returning a 5xx triggers an automatic refund. Use it for your failures, not the agent's bad input.
PUBLISH YOUR LISTING
Once your server runs publicly:
- Sign up as a publisher: mcpmeter.com/register
- From the publisher dashboard, hit Submit MCP.
- Fill in name, slug, description, category, transport (
streamablefor HTTP), and your publicupstream_url. - Pick a logo emoji or upload an image; add a cover banner if you want.
- Add 3-5 sample prompts under "Try asking your agent" — improves discoverability.
- Submit. We test the URL with a
tools/listcall; if it responds we mark the listing live.
Your upstream_url is hidden from consumers. They only ever see proxy.mcpmeter.com/your-slug. We never expose it in API responses or page HTML — it's stored in the $hidden attribute on the listing model.
PRICING MODELS
Three pricing modes per listing, set when you submit (changeable any time):
| Model | How it works | Use when |
|---|---|---|
per_call | Flat rate per tool call. Default $0.0002. | Most cases. |
tiered | Different rates per tool. Set tier_config JSON. | One expensive tool (e.g., LLM-backed) + cheap reads. |
free | $0/call. Rate-limit alone bounds use. | Trying to build adoption, or your upstream is free anyway. |
You also pick a monthly free tier (default 30 calls per consumer) so people can test before we charge them. Their first 30 calls in a month don't bill against either party.
Platform fee: 20%. You keep 80% of every billed call. So at $0.0002/call you net $0.00016/call — about $1.60 per 10k calls.
GET PAID
Payouts go through Stripe Connect Express:
- From your publisher dashboard, click Connect Stripe.
- Stripe walks you through identity + bank-account verification (5-15 min).
- Your dashboard flips from PAYOUTS NOT CONFIGURED to PAYOUTS ENABLED when KYC completes.
- On the 1st of each month at 04:00 UTC, our scheduled job aggregates the prior month's earnings and triggers the transfer.
- Stripe pays out to your bank within 1-3 business days.
You can verify the payout history any time in Dashboard → Payouts. Each row links to the Stripe transfer object for full audit.
BEST PRACTICES
Latency
Aim for sub-300ms p95. Agents are interactive; calls slower than 1s feel broken. If you're wrapping a slow upstream, cache aggressively at your layer.
Idempotency
Every request the proxy sends has a unique X-Mcpmeter-Request-Id header. Use it as your idempotency key for write operations — the same request id within 24h should produce the same result.
Output size
Keep tool responses under 100 KB of text. Bigger payloads burn the agent's context window and discourage adoption. If you need to return more, paginate (cursor, next_page_token) or return a URL the agent can re-fetch.
Don't log payloads
Consumers expect that what they send to a tool stays private. Log call counts, latency, and error rates; don't log argument values (especially for write tools that may carry PII).
Versioning
Breaking-change a tool? Don't rename it — add a v2: convert_v2. Old agents keep working; new ones can adopt the new shape. We don't currently surface tool deprecation in the UI; mark deprecated tools clearly in their description.
SHIP CHECKLIST
- Server responds correctly to
initialize,tools/list,tools/call - Every tool has
description,inputSchemawithrequired+ property descriptions - Read-only tools are marked
readOnlyHint: true - p95 latency under 500ms (measure under load, not on a warm cache)
- 5xx triggers refund — you've tested with a deliberately broken call
- Stripe Connect KYC complete (so payouts can flow on day-1)
Ready? Sign up as a publisher ↗ and submit your first MCP. We typically approve listings within 1 business day.