Every surface, from one contract
One Service() becomes an API, a CLI, an MCP server, an SDK, a Site, Docs and an App. Each one renders the same contract, so no surface can say something the others don't.
You write the Service. Everything else is generated from it: your API, CLI, MCP server, SDK, Site and Docs, and an App with a portal for buyers, a portal for the people you place in the loop, and a console for you. Each renders the same contract, so an order from a terminal, from an agent's MCP client or from your own code is one job, with one id, one price and one set of answers.
Four lines are a whole Service:
import { Service } from 'services.dev'
export default Service({
name: 'Translation',
input: { document: 'file', to: 'language: a BCP 47 tag, such as fr-CA' },
output: { document: 'file' },
price: 49,
})npx services.dev serves it on localhost, and the same order works on four surfaces. Captured from one run:
magic values: none
# CLI
$ npx services.dev translation document=https://example.com/lease.pdf to=fr-CA --max 49 --key lease
job_7Hq2 · working · $49.00 held
# API
$ curl -X POST http://localhost:8787/jobs -H 'content-type: application/json' -d '{"service":"translation","document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"}'
← 202 · job_7Hq2 · working · $49.00 held
# MCP
→ tools/call order_translation {"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"}
← job_7Hq2 · working · $49.00 held
# SDK
await lingua.translation.order({"document":{"url":"https://example.com/lease.pdf","name":"lease.pdf"},"to":"fr-CA","max_usd":49,"key":"lease"})
← job_7Hq2 · working · $49.00 held
same job, job_7Hq2, and the same body on all four surfaces, request ids asideThe capture fails unless all four surfaces answer with the same body.
The docs come in two layers, written for two different readers.
- Layer A is what you're reading: services.dev's own docs, for you as the builder. What each surface derives from, how to shape it, and how to run the business.
- Layer B is what your brand serves. Your buyers, their agents, your integrators and your members read it on your domain, in your words, with no services.dev name. Every page whose reader is a buyer, an integrator, an agent or a member is Layer B.
Layer A never re-explains a Layer B page. Layer B serves on localhost as you build, and on your own domain once you deploy. Everything lingua serves with no key, from one run:
magic values: none
$ curl http://localhost:8787<path> # no key
GET /.well-known/agents.json 200 · application/json brand.card
GET /openapi.json 200 · application/json brand.openapi
GET /llms.txt 200 · text/markdown brand.llms
GET /manifest.json 200 · application/json brand.manifest
GET /pricing 200 · application/json brand.pricing
GET /verify/suite.json 200 · application/json brand.verify
GET /translation 200 · text/html the Site's page for Translation
GET /console 200 · text/html the owner consoleEach surface is the operations registry read in one column, plus what that surface adds.
| Surface | Renders | Rows | Adds |
|---|---|---|---|
| API | the HTTP column | 88 rows | /openapi.json with webhooks · one error shape · idempotency keys · /verify/suite.json |
| CLI | the CLI column | 67 rows | listen · test clocks · npx <brand> · login · a member's commands |
| MCP | the MCP column, minus every row only a person's session calls | 23 rows | annotations from each effect · a prompt per Service · stdio · the job:// resource |
| SDK | the SDK column, typed from the manifest | 35 rows | literal unions and typed errors · webhooks.verify() · published under your name |
| Site | each Service's name, description, price sentence and human sentence; its steps as a timeline; the boundary | the manifest | a page per Service · /pricing · /terms · .md twins |
| Docs | every registry | all five registries | /llms.txt · a page per Service and per operation · /llms-full.txt · a changelog per deploy |
| App: customer portal | every operation a buyer, a client or a party calls | 55 rows | its screens, each control an operation |
| App: delivery portal | every operation a member's session calls | 30 rows | its screens, each control an operation |
| App: owner console | every operation | 88 rows | its screens, each control an operation |
A call only a person's session or a party's own link can make, such as approving a step or signing through an ask, has no MCP tool and no SDK method, so an agent gets the link to hand to the person instead. The contract shows it row by row, and What an agent can never do explains why.
Adding a signer to Translation is two lines in one file. diff prints what that did to every surface at once:
$ npx services.dev diff grow/ask.ts grow/role.ts
grow/ask.ts (9 lines) → grow/role.ts (13 lines)
Translation · changed description, deliver, human, price, portal, roles, rules · adds a person: the human line, or a step a role does; a role from a Pool: its members, and the Tasks they claim
API ~ operation service.order.translation POST /translation item.description
~ operation service.quote.translation GET /translation.json answers.description, answers.deliver, answers.human.sentence
+ operation job.approve POST /{portal}/jobs/{id}/approve
+ operation member.join POST /{portal}/join
+ operation member.verify POST /{portal}/members/{id}/verify
+ operation member.availability POST /me/availability
+ operation member.ledger.get GET /me/ledger
+ operation member.ledger.add POST /me/ledger
+ operation member.ledger.checks GET /me/ledger/checks
+ operation member.missed GET /me/missed
+ operation task.list GET /tasks
+ operation task.get GET /tasks/{id}
+ operation task.sheet GET /tasks/{id}/sheet
+ operation task.pass POST /tasks/{id}/pass
+ operation task.claim POST /tasks/{id}/claim
+ operation task.clear POST /tasks/{id}/clearThe approval became a route, a CLI command and a delivery portal at /sworn-translators, never an MCP tool. The new price reached every surface in the same save.
In this order, everywhere they're listed. The table above gives each its status.
- API: your routes, answers and events. Your API.
- CLI:
services.devfor building, and the same calls for buyers. The CLIs. - MCP: a tool per call an agent may make. Your MCP server.
- SDK: each call as a typed SDK method. The SDKs.
- Site: a page per Service, with its price and who delivers it.
- Docs: a page per Service and per operation, and
/llms.txtfor agents. - App: one shell with a view for everyone who takes part in a job.
- The customer portal, for buyers, clients and parties, on every Service.
- The delivery portal, for the people you place in the loop.
- The owner console, for you: the ledger and the line.
The API, the CLI, the MCP server, the SDK, the site, the docs and the apps name only your brand. Your buyers run npx <brand>, never npx services.dev, and every surface serves on your own domain.
Every surface renders the same contract, and no surface renames it. A brand may use its own words, such as "matter" for job, but only by declaring them in Brand.words. check fails surface_drift the moment a surface serves a call the contract doesn't have. The contract shows both.