SYSTEM ACTIVE
MENU
HOME / DOCS / BUILD AN MCP SERVER
DOCS NAV · BUILD AN MCP SERVER
DOC PUBLISH

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.

v0.1.0 UPDATED 2026-05-10 ~12 MIN READ

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.

PROTOCOL VERSION

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:

MethodWhen calledWhat you return
initializeFirst contactprotocol version + your server name
tools/listAgent discovers your toolsarray of tools with input schemas
tools/callAgent invokes a toolthe tool's result

THE MCPMETER CONTRACT

When a consumer calls https://proxy.mcpmeter.com/your-slug, the proxy:

  1. Validates the bearer key (mcpm_live_…), looks up the listing.
  2. Enforces rate limit + free-tier quota; debits credit if billable.
  3. Forwards the JSON-RPC body unchanged to your upstream_url.
  4. Streams your response back, adds X-Mcpmeter-Billed + X-Mcpmeter-Duration-Ms headers.
  5. 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.
EXAMPLE — CONSUMER FORWARDS A SECONDARY TOKEN
# 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:

server.jsNODE
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 required array. 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.
READ vs WRITE — TELL US WHICH

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:

SituationHTTPJSON-RPC code
Bad arguments (missing required, wrong type)200-32602
Unknown tool name200-32601
Upstream API failed (rate-limit, outage)502 or 503n/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:

  1. Sign up as a publisher: mcpmeter.com/register
  2. From the publisher dashboard, hit Submit MCP.
  3. Fill in name, slug, description, category, transport (streamable for HTTP), and your public upstream_url.
  4. Pick a logo emoji or upload an image; add a cover banner if you want.
  5. Add 3-5 sample prompts under "Try asking your agent" — improves discoverability.
  6. Submit. We test the URL with a tools/list call; if it responds we mark the listing live.
UPSTREAM URL IS SECRET

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):

ModelHow it worksUse when
per_callFlat rate per tool call. Default $0.0002.Most cases.
tieredDifferent 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:

  1. From your publisher dashboard, click Connect Stripe.
  2. Stripe walks you through identity + bank-account verification (5-15 min).
  3. Your dashboard flips from PAYOUTS NOT CONFIGURED to PAYOUTS ENABLED when KYC completes.
  4. On the 1st of each month at 04:00 UTC, our scheduled job aggregates the prior month's earnings and triggers the transfer.
  5. 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

BEFORE YOU SUBMIT
  • Server responds correctly to initialize, tools/list, tools/call
  • Every tool has description, inputSchema with required + 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.