---
title: "Your MCP server"
url: https://services.dev/docs/mcp
---
# Your MCP server

URL: https://services.dev/docs/mcp

> Every brand serves an MCP server written from its operation rows. Its tools, hints, output schemas, prompts and instructions come from the same file as its API, so the two never disagree.

Every brand serves MCP at `/mcp`, and over stdio from its folder. Nobody writes a tool. Each one is a row of the [operation registry](https://services.dev/docs/reference/operations.md), rendered for an agent: the same row gives the API its route, the CLI its command and the SDK its call. `check` fails `surface_drift` when a tool has no row, so the tools and the routes can't drift apart.

Over it, an agent prices, orders, waits, accepts, sends back and cancels. It never signs or consents for anyone, because a row only a person may call renders no tool ([What an agent can never do](https://services.dev/docs/mcp/never.md)).

## Connect

```txt
# a client that launches its MCP servers runs the brand over stdio, in its folder:
$ npx services.dev mcp

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}
← {"id":1,"serverInfo":{"name":"lingua","version":"2026-10-25"},"protocolVersion":"2025-06-18"}
→ {"jsonrpc":"2.0","id":2,"method":"tools/list"}
← order_translation · get_job · list_jobs · accept_job · send_back_job · cancel_job · end_job · reorder_job · set_job_policy · send_job_signal · send_job_message · list_job_messages · reissue_ask

# or, while dev runs, it relays to the running brand:
$ npx services.dev mcp --url http://localhost:8787
```

Over HTTP it's JSON-RPC 2.0 at `POST /mcp`, one message or a batch. A notification gets `202`.

| Request | What it answers |
|---|---|
| `initialize` | the brand's name and version, its `tools`, `resources` and `prompts` capabilities, and the instructions below. It speaks `2025-06-18`, `2025-03-26` and `2024-11-05`, and answers any other version with the newest |
| `tools/list` | the buyer toolset; a test key at `?toolset=sandbox` adds the sandbox's |
| `tools/call` | the row's answer, as `content` (the words, then the JSON) and `structuredContent`. `isError` is true for a refusal, and false for an OFFER |
| `resources/templates/list` | `job://{id}{?wait}` |
| `resources/list` | the brand's jobs, each as `job://<id>` |
| `resources/read` | one job, now or after `?wait=<seconds>` |
| `prompts/list`, `prompts/get` | one `buy_<service>` prompt per Service |
| `ping` | `{}` |

## What an agent reads first

`initialize` answers with instructions written from your Services: how to order, when each one charges, which ones wait on a party, and what test mode means. Change a price or add an ask, and the instructions change with it. `check` holds them to 120 words (`instructions_too_long`).

```txt
→ initialize {"protocolVersion":"2025-06-18"}
← {"protocolVersion":"2025-06-18","serverInfo":{"name":"lingua","version":"2026-10-25"},"capabilities":{"tools":{},"resources":{},"prompts":{}}}

  instructions, written from the brand:
  order_job takes service, the input, max_usd and a key.
  Without max_usd you get an OFFER: send its next call unchanged to order.
  The same key returns the same job.
  Services charge on delivery, except filed-translation (on file's receipt).
  Canceling before then costs $0.
  get_job with wait: 50 waits through any person's line; then accept_job, or send_back_job with a note.
  filed-translation waits on a party: next hand_to_person is a link only that person can use; no tool acts for them.
  This is test mode: a sk_test_ key, simulated AI and money; metadata.sandbox picks branches.

  93 of 120 words
```

## Tools are rows

```txt
| Tool | Toolset | readOnlyHint | destructiveHint | idempotentHint | Operation |
|---|---|---|---|---|---|
| `order_engagement` | buyer |  | yes | yes | `service.order` |
| `order_translation` | buyer |  | yes | yes | `service.order` |
| `get_job` | buyer | yes |  | yes | `job.get` |
| `list_jobs` | buyer | yes |  | yes | `job.list` |
| `accept_job` | buyer |  | yes | yes | `job.accept` |
| `send_back_job` | buyer |  | yes |  | `job.send_back` |
| `cancel_job` | buyer |  | yes | yes | `job.cancel` |
| `end_job` | buyer |  | yes | yes | `job.end` |
| `reorder_job` | buyer |  | yes |  | `job.again` |
| `set_job_policy` | buyer |  |  | yes | `job.policy` |
| `send_job_signal` | buyer |  |  |  | `job.signal` |
| `send_job_message` | buyer |  |  |  | `job.message` |
| `list_job_messages` | buyer | yes |  | yes | `job.thread` |
| `list_clocks` | sandbox | yes |  | yes | `clock.list` |
| `get_clock` | sandbox | yes |  | yes | `clock.get` |
| `list_subscriptions` | sandbox | yes |  | yes | `subscription.list` |
| `get_subscription` | sandbox | yes |  | yes | `subscription.get` |
| `cancel_subscription` | sandbox |  | yes | yes | `subscription.cancel` |
| `reissue_ask` | buyer |  |  |  | `ask.reissue` |
| `create_test_clock` | sandbox |  |  |  | `test_clock.create` |
| `advance_test_clock` | sandbox |  | yes | yes | `test_clock.advance` |
| `list_events` | sandbox | yes |  | yes | `event.list` |
| `send_event` | sandbox |  |  |  | `event.inbound` |
| `job://{id}{?wait}` | resource | | | | `job.wait` |

tools/list lists the buyer toolset: 14 tools, about 5,156 tokens. A test key adds the sandbox's with http://localhost:8787/mcp?toolset=sandbox.
```

A tool's hints come from its row's effect. A `read` is read-only, an `act` is destructive, and a `record`, such as `set_job_policy` or `reissue_ask`, is neither. A tool is idempotent when the same call twice changes nothing more. An order tool is too, because the offer's next call carries a `key`, and the same key returns the same job.

Tools come in two toolsets. The buyer's is what every key lists. A test key that connects with `?toolset=sandbox` adds test clocks, the event feed and synthetic events. `check` fails `toolset_too_large` when the buyer's toolset passes 8,000 tokens, so your brand stays cheap to load into an agent's context.

## One order tool per Service

Each Service gets `order_<service>`, its input taken straight from the Service's. Its output schema names the three things it can answer: a job, an OFFER, or a refusal with the exact codes this row returns.

```txt
→ tools/list
← order_translation
  description   Translation: send a document and a language; get a document back. $49 per job, within 48 hours, charged on delivery. AI delivers every job.
  annotations   {"readOnlyHint":false,"idempotentHint":true,"destructiveHint":true}
  inputSchema
    document    file      required
    to          language  required  a BCP 47 tag, such as fr-CA
    max_usd     number    optional  The most you will pay, in dollars. Missing or below the price, you get an offer instead of a job.
    key         string    optional  Any string you choose. Retrying with the same key returns the same job.
    metadata    object    optional  Your own keys, echoed on the job and its events.
    test_clock  string    optional  A test clock the job runs on: tc_….
  outputSchema  oneOf
    job         status: working | waiting | delivered | accepted | canceled | ended
    OFFER       type, object, price, next
    refusal     code: input_invalid | policy_invalid | key_reused | not_found | not_live | key_missing | key_invalid | body_invalid | version_unknown | api_error
```

`max_usd` and `key` aren't required. Without `max_usd`, or below the price, the answer is an OFFER, returned as a result and not an error. `isError` is false, the first text block is what a model reads, and `structuredContent` is the same answer to the schema. Its `next` is the exact call that orders, with `max_usd` and a `key` filled in, so sending it twice never orders twice.

```txt
→ tools/call order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA"}
← isError: false
# content[0]: the text an agent reads first
← OFFER · $49 per job, within 48 hours, charged on delivery.
←   terms Charged on delivery. Cancel at $0 until it ships. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.
←   next  order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"order-49736585"}
# structuredContent: the same answer, to the outputSchema
← {
  "type": "OFFER",
  "object": "offer",
  "service": "translation",
  "title": "Translation",
  "price": {
    "usd": 49,
    "sentence": "$49 per job, within 48 hours, charged on delivery."
  },
  "eta": "48h",
  "terms": "Charged on delivery. Cancel at $0 until it ships. Send it back with a note and the AI redoes it, or you pay $0. Final 7 days after delivery.",
  "next": {
    "do": "order",
    "service": "translation",
    "arguments": {
      "document": {
        "url": "https://example.com/lease.pdf",
        "name": "lease.pdf"
      },
      "to": "fr-CA",
      "max_usd": 49,
      "key": "order-49736585"
    },
    "tool": "order_translation",
    "http": "POST http://localhost:8787/jobs {\"service\":\"translation\"}"
  },
  "simulated": true
}

# the offer's next call, sent unchanged
→ tools/call order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"order-49736585"}
← job_7Hq2 · working · $49.00 held

→ resources/read job://job_7Hq2?wait=50
← job_7Hq2 · delivered · lease.fr-CA.pdf · $49.00 charged
  next    accept_job {"id":"job_7Hq2"}
```

Past 12 Services, or three quarters of the token budget, the brand serves one `order_job` tool instead, the Service named in `service`:

```txt
$ ls services   # 13 Services
arabic.ts chinese.ts dutch.ts french.ts german.ts hindi.ts italian.ts japanese.ts korean.ts polish.ts portuguese.ts spanish.ts turkish.ts

→ tools/list
← order_job · get_job · list_jobs · accept_job · send_back_job · cancel_job · end_job · reorder_job · set_job_policy · send_job_signal · send_job_message · list_job_messages · reissue_ask
  order_job.description               Order any of lingua's 13 Services: name it in service, with its own fields. Without max_usd, the offer: the price, the terms and the exact next call.
  order_job.inputSchema.service.enum  arabic · chinese · dutch · french · german · hindi · italian · japanese · korean · polish · portuguese · spanish · turkish

→ tools/call order_job {"service":"german","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"max_usd":49,"key":"contract-de"}
← job_7Hq2 · working · $49.00 held · german
```

## Refusals

A refusal is `isError: true` with the one error shape every surface returns, so an agent reads the same `code`, `reason` and `next` it would over HTTP ([Answers](https://services.dev/docs/api/answers.md)). Arguments are checked against the tool's input schema before anything runs. A misfit is `input_invalid`: its `next` is the same call, each missing field set to null and named in `fill`. A call the server can't route, such as an unknown tool or a sandbox tool without the sandbox toolset, is a JSON-RPC `-32602` that says what to do.

```txt
→ tools/call order_translation {"to":"fr-CA"}
← isError: true
# content[0]: the text an agent reads first
← input_invalid: order_translation needs document.
←   next  order_translation {"to":"fr-CA","document":null}
←   docs  http://localhost:8787/docs/errors/input_invalid
# structuredContent: the one error shape every surface returns
← {
  "type": "BLOCKED",
  "category": "invalid_request",
  "code": "input_invalid",
  "reason": "order_translation needs document.",
  "param": "document",
  "next": {
    "do": "service.order",
    "tool": "order_translation",
    "arguments": {
      "to": "fr-CA",
      "document": null
    },
    "fill": [
      "document"
    ],
    "http": "POST http://localhost:8787/jobs"
  },
  "doc_url": "http://localhost:8787/docs/errors/input_invalid",
  "request_id": "req_8FYaUE",
  "simulated": true
}

→ tools/call create_test_clock {}   # at /mcp, without ?toolset=sandbox
← {"code":-32602,"message":"create_test_clock is a sandbox tool: connect to http://localhost:8787/mcp?toolset=sandbox with a test key."}
```

## Resources and prompts

`job://{id}{?wait}` reads one job, and waits on it like `get_job` with `wait`. `subject://{id}` reads a subject, with its clocks and who is of record.

Each Service gets one prompt that walks an agent through a careful purchase: read the offer, ask the person, order, wait, review, then accept or send back. When a Service waits on a party, its fourth step also tells the agent to pass the link on.

```txt
→ prompts/get buy_translation {"document":"lease.pdf","to":"fr-CA"}
← Buy Translation from lingua for me. Input: document: lease.pdf; to: fr-CA.
← 1. Call order_job, naming service "translation", with the input and no max_usd. It answers an OFFER: the price, the terms and the exact next call.
← 2. Show me the price and terms ($49 per job, within 48 hours, charged on delivery) and ask me to agree. Do nothing more until I say yes.
← 3. Send the offer's next call unchanged: it carries max_usd and a key, so sending it twice never orders twice.
← 4. Call get_job with wait: 50 until the job is delivered.
← 5. Show me the output, then call accept_job if I accept it, or send_back_job with my note and the AI redoes it.
```

## In the example brands

The legal example's five Services show it. Every one of them waits on a party, and the instructions say so. Four have a submit step, so each charges on that step's receipt, and the instructions name the receipt:

```txt
→ tools/list
← order_matter · get_matter · list_matters · accept_matter · send_back_matter · cancel_matter · end_matter · reorder_matter · set_matter_policy · send_matter_signal · send_matter_message · list_matter_messages · reissue_ask
  13 tools, about 4,569 tokens

→ initialize
  order_matter takes service, the input, max_usd and a key.
  Without max_usd you get an OFFER: send its next call unchanged to order.
  The same key returns the same matter.
  Services charge on file's receipt, except contract-review (on acceptance), demand-letter (on send's receipt), docket-watch (monthly) and formation (on form_entity's receipt).
  Canceling before then costs $0.
  get_matter with wait: 50 waits through any person's line; then accept_matter, or send_back_matter with a note.
  contract-review, demand-letter, formation, provisional-patent and trademark-response wait on a party: next hand_to_person is a link only that person can use; no tool acts for them.
  This is test mode: a sk_test_ key, simulated AI and money; metadata.sandbox picks branches.
```

[The legal example](https://services.dev/docs/examples/legal.md) runs all five. [The sales example](https://services.dev/docs/examples/sales/agent.md) runs a deal.

## Streaming sessions

A streaming session adds progress notifications, resource subscriptions, and a party's ask elicited as a URL. Over plain HTTP the ask arrives as `next.hand_to_person`. What no tool will ever do is on the next page, [What an agent can never do](https://services.dev/docs/mcp/never.md).

---

- Up: [Your first paid job](https://services.dev/docs.md)
- Previous: [Listening locally](https://services.dev/docs/cli/listen.md)
- Next: [What an agent can never do](https://services.dev/docs/mcp/never.md)
