Treatyway MCP contract
Version 1 is a draft standard a seller's MCP server can implement, so a buyer's agent knows what to call. It covers services and goods.
Published November 2026. Status: draft. Open for feedback. Charles Azam publishes this contract. Write to charles@treatyway.com.
This page is the contract. The machine-readable copy is /contract/v1.json. If they disagree, this page wins, and the disagreement is a bug. Old versions stay published. This one stays at /contract/v1.
Treatyway is optional. A buyer's agent may call a seller's MCP directly. In direct mode the conversation never passes through Treatyway. Getting a pass tells Treatyway which seller you asked a pass for, nothing else.
What is live, and what is not, is listed once at the end of this page.
Versioning
A seller declares this contract in two places. Either one is enough. Both is better.
The tool treatyway_contract takes no arguments and returns the standard tools that server implements:
{"contract":"treatyway","version":"1","tools":["describe_offer","check_availability","request_quote","get_status","message"]}
tools lists implemented standard tools only. Leave out a tool that is not implemented. place_order is left out until the seller turns it on. Custom tools are not listed here. Agents discover those with MCP tools/list.
The server may also put this line in its MCP instructions or metadata:
treatyway-contract: 1
That line is the major version. A breaking change bumps it, and the tool's version, to the next whole number. The new contract is published at /contract/v2, then /contract/v3. This page stays here.
An additive change is 1.x. New optional fields and new tools may appear. The instructions line stays treatyway-contract: 1. The tool may return "version":"1.1". A client written for version 1 must keep working: servers ignore unknown input fields, and clients ignore unknown output fields. Removing a field, changing a type, or changing what a field means is a breaking change.
This publication's version is 1.
Tools
Six tools. Each one takes JSON and returns JSON. Names:
describe_offer— what the seller sells, services or goods, and how to order.check_availability— a date or time for a service, or stock for a good.request_quote— a described job or a basket. Returnsquote_idand a price, orneeds_human.place_order— books a service or orders goods. Off unless the seller enables it. Requires trust level 2.get_status— status of a quote or an order.message— free text.
The minimum trust level is a rule of this standard. Treatyway's hosted tools enforce it. Level 0 cannot call request_quote or place_order. check_availability and message record an information request, so level 0 may call them within today's caps. A seller's own server enforces the same minimums when the caller presents a pass.
| Tool | Minimum level | Default |
|---|---|---|
describe_offer | 0 | On |
message | 0 | On |
check_availability | 0 | On. Records an information request. |
request_quote | 1 | On |
get_status | 0 | On |
place_order | 2 | Off. Treatyway's hosted tool returns not_supported. |
Level 0 is also limited in how many requests the seller accepts. The seller chooses the numbers. Level 0 gets fewer requests than level 1. Treatyway's own caps are in agents.md. This standard does not freeze those numbers.
Every answer
Every response names who answered. answered_by.name is required. answered_by.role is optional: business, person, or agent.
Every response may include needs_human when a person still has to supply or decide something. needs_human.missing is an array of short strings saying what is missing. A shop that cannot answer is not in error. The call succeeded, and the body says what is missing.
Unknown input fields are ignored. Unknown output fields are ignored.
describe_offer
What the seller sells. Use it for a service, a good, or both.
Input fields are all optional.
| Field | Required | Meaning |
|---|---|---|
kind | no | service, good, or any. Filters the list. |
Output:
| Field | Required | Meaning |
|---|---|---|
offers | yes | The list. Empty only when needs_human is set. |
offers[].name | yes | Short name. |
offers[].kind | yes | service or good. |
offers[].id | no | Stable id for later calls. |
offers[].description | no | What the buyer gets. |
offers[].area | no | Where this offer is served or shipped. |
offers[].hours | no | When the seller takes it. |
offers[].lead_time | no | How long until the work or the delivery. |
offers[].how_to_order | no | How to order this offer. |
area, hours, lead_time, how_to_order | no | The same facts for the whole shop, when they are not per offer. |
answered_by | yes | Who answered. |
needs_human | no | What is missing. |
Example, a service and a good. The prices and hours are an illustration, not a live listing.
{
"offers": [
{
"id": "site-visit",
"name": "Site visit",
"kind": "service",
"area": "Oakland and Berkeley",
"hours": "Weekdays 08:00–16:00",
"lead_time": "3 business days",
"how_to_order": "request_quote, then place_order if the seller has enabled it"
},
{
"id": "brass-rod",
"name": "Brass rod, 10 mm",
"kind": "good",
"lead_time": "Ships in 2 business days",
"how_to_order": "request_quote with a quantity"
}
],
"how_to_order": "Describe the job or the basket with request_quote.",
"answered_by": {"name": "Example Metals", "role": "business"}
}
check_availability
A date or time for a service. A quantity in stock for a good.
| Field | Required | Meaning |
|---|---|---|
kind | yes | service or good. |
start | when kind is service | ISO-8601 date or date-time. |
end | no | End, for a service. |
quantity | when kind is good | Whole number, 1 or more. |
offer_id | no | From describe_offer. |
sku | no | Seller's own stock code, for a good. |
Output: available (boolean) when the seller can answer. quantity_available for a good. Echo start and end when useful. note is optional. answered_by is required. Omit available and set needs_human when the seller cannot answer.
{
"available": true,
"start": "2026-11-20T15:00:00-07:00",
"answered_by": {"name": "Example Metals", "role": "business"}
}
{
"available": true,
"quantity_available": 40,
"answered_by": {"name": "Example Metals", "role": "agent"}
}
request_quote
A described job, or a basket of goods. Returns quote_id and a price, or the string needs_human in place of a price.
Treatyway's hosted request_quote requires business_id, subject, message, quantity, destination, and idempotency_key. Level 0 cannot call it. On a seller's own server, request_quote requires summary. destination is optional there.
The fields below are the seller's own server.
| Field | Required | Meaning |
|---|---|---|
summary | yes | The job or the basket, in plain text. |
kind | no | service or good. |
lines | no | Structured lines. Each line needs description. quantity, offer_id, sku, and when are optional. |
when | no | Date or time for a service. |
destination | no | Where the work happens, or where goods should go. |
currency | no | ISO 4217 code the buyer prefers. |
Output:
| Field | Required | Meaning |
|---|---|---|
quote_id | yes | Id for get_status and place_order. Returned even when the price needs a person. |
price | yes | An object with amount and currency, or the string needs_human. |
price.amount | with a price object | Decimal string, such as "1380.00". Not a float. |
price.currency | with a price object | ISO 4217 code. |
expires_at | no | ISO-8601 time the price stops being valid. |
note | no | A short condition. |
answered_by | yes | Who answered. |
needs_human | no | What is missing. Set it when price is needs_human. |
{
"quote_id": "q_example",
"price": {"amount": "1380.00", "currency": "USD"},
"expires_at": "2026-11-27T00:00:00Z",
"answered_by": {"name": "Example Metals", "role": "person"}
}
{
"quote_id": "q_example",
"price": "needs_human",
"needs_human": {"missing": ["the alloy, and whether the rods are cut to length"]},
"answered_by": {"name": "Example Metals", "role": "business"}
}
The first example is a priced job. The second is a basket the seller will not price without a person. Neither example is a live quote.
place_order
Books a service, or orders goods, against a quote. It does not take payment. Treatyway has no payment code, and this standard does not add one.
Off by default. The seller enables it by listing place_order in treatyway_contract and in MCP tools/list. Until then, agents do not call it. A call to a tool that is not listed is refused. Nothing is booked.
Requires trust level 2. Below that, refuse the call and do not book. The error text says that place_order requires trust level 2. This is a refusal, not a needs_human answer.
A direct caller with no pass has no Treatyway level. The seller must not treat a missing pass as level 2. It may refuse place_order, or use its own authentication. A call that went through Treatyway carries the trust label. Only Level 2, Owner proven, may place an order.
| Field | Required | Meaning |
|---|---|---|
quote_id | yes | From request_quote. |
confirm | yes | Must be true. The agent is committing. |
note | no | A short note to the seller. |
Output: order_id and status when an order exists. status is accepted, rejected, or needs_human. quote_id may be echoed. answered_by is required. When nothing was booked, status and needs_human are enough, and order_id is omitted.
{
"order_id": "o_example",
"quote_id": "q_example",
"status": "accepted",
"answered_by": {"name": "Example Metals", "role": "person"}
}
get_status
Status of one quote or one order. Send quote_id, order_id, or both.
status is the seller's word. Common values are open, quoted, needs_human, accepted, in_progress, completed, cancelled, no_show, and rejected. Others are allowed. summary is optional. answered_by is required.
{
"quote_id": "q_example",
"order_id": "o_example",
"status": "in_progress",
"summary": "Cutting Thursday.",
"answered_by": {"name": "Example Metals", "role": "business"}
}
message
Free text. text is required. quote_id and order_id are optional context.
The output may include text as the reply. answered_by is required. needs_human is optional.
{
"text": "The visit includes measuring the doorway.",
"answered_by": {"name": "Ada", "role": "person"}
}
Level 0 may send a message. The business is told the agent is not authenticated. The seller may cap how many.
Tools a seller adds
A seller may add tools. get_menu and configure_product are examples. Agents discover them with MCP tools/list. They are not part of this contract, and they are not listed in treatyway_contract.
Reserved names:
treatyway_contractand the six standard tool names. A seller must not reuse them with a different meaning.- Any name that starts with
treatyway_. - Any name that starts with a standard tool name and an underscore, such as
request_quote_bulk.
Those names are reserved so a later version of this contract can use them.
Identity pass
Issued when signing is configured. If it is not, get_pass returns not configured and /.well-known/jwks.json has an empty keys array. A seller must not require a pass.
The shape:
- The buyer's agent asks Treatyway for a pass scoped to one seller. Treatyway learns only that agent X wants to reach seller Y. Treatyway does not learn the message, the quote, or the order.
- The agent calls the seller's MCP directly and sends the pass as
Authorization: Bearer. - The seller checks the pass locally with Treatyway's public keys. There is no call back to Treatyway.
The pass is a signed JWT. Proposed claims:
| Claim | Meaning |
|---|---|
iss | https://treatyway.com. |
sub | The agent id. |
owner | { "name", "proof" }. proof is declared, social, or domain. Level 0 cannot get a pass. |
level | 1 or 2. |
aud | The seller's domain. A pass for one domain is refused at another. |
exp | 10 minutes from issue. iat is included. |
jti | A unique id. Treatyway stores the agent id, aud, jti, and the time. It does not store the conversation. |
The algorithm is ES256 (ECDSA P-256, SHA-256). The public keys are at https://treatyway.com/.well-known/jwks.json. When signing is not configured that document is { "keys": [] }.
get_pass takes seller_domain and needs a level 1 or 2 workspace key. POST /api/pass/verify with { "token" } checks the signature locally and returns the claims. A seller should still verify locally and not depend on that call.
Local check:
const token = process.argv[2];
const jwks = await fetch("https://treatyway.com/.well-known/jwks.json").then((response) => response.json());
const [headerPart, payloadPart, signaturePart] = token.split(".");
const header = JSON.parse(Buffer.from(headerPart, "base64url").toString());
const key = jwks.keys.find((item) => item.kid === header.kid);
const cryptoKey = await crypto.subtle.importKey("jwk", key, { name: "ECDSA", namedCurve: "P-256" }, false, ["verify"]);
const ok = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
cryptoKey,
Buffer.from(signaturePart, "base64url"),
new TextEncoder().encode(`${headerPart}.${payloadPart}`),
);
const claims = JSON.parse(Buffer.from(payloadPart, "base64url").toString());
if (!ok || claims.iss !== "https://treatyway.com" || claims.exp * 1000 < Date.now()) throw new Error("pass rejected");
A Bearer workspace key and the trust label still apply when the call goes through Treatyway. Direct calls use the pass, or whatever authentication the seller already has. Level 0 has no pass.
Trust levels
A request shows one of three labels, with a level number.
Agents
| Level | Label | What the business is told |
|---|---|---|
| 0 | Owner unknown | This agent is not authenticated. Treatyway does not know who operates it. |
| 1 | Owner named | The name, and that it was not checked. Treatyway's words today are "Declared by agent, not verified". |
| 2 | Owner proven | That the owner controls a website or account. Level 2 proves the operator controls a website or account. It does not mean a specific person approved the request. |
A level 1 name can be typed by anyone. Never present it as checked.
What each level may call is in the tools table above. place_order is level 2 only, and only after the seller enables it.
Businesses
These are the business's own levels. They are not the agent levels above.
| Level | Meaning |
|---|---|
| Business level 1 | The business gives its website. Declared, not checked. Treatyway has no declared-only state. Starting a claim checks the domain immediately. |
| Business level 2 | The business proves its website. A claim proves control of the listed website, by a DNS TXT record or a file on that site. A listing with no website cannot reach Business level 2. The company name is not proved. A separate registry match exists for some San Francisco listings. That match is not Business level 2. |
An unclaimed listing is neither Business level. Email to that listing is not live.
Direct, or through Treatyway
Both are allowed. Treatyway is not a mandatory middleman.
In direct mode the conversation never passes through Treatyway. Getting a pass tells Treatyway which seller you asked a pass for, nothing else. A direct call does not appear on a Treatyway dashboard, and it does not feed reputation.
Going through Treatyway is the path for a seller that has no MCP of its own. It is also a path a seller with an MCP may choose. Treatyway records the request. When a channel is connected, it can deliver a signed webhook. Email to a business is not live.
Reputation, when it exists, counts only what Treatyway can see or what a party confirms. A direct deal counts only after an outcome report. A fast reply counts toward ranking only when the reply went through Treatyway. The reports are stored. The ranking rule is not in force.
Outcome reports
POST /api/outcome and the MCP tool report_outcome. Send pass_jti or request_id, and outcome of completed, no_show, or cancelled. No message, no price, and no description. A workspace key reports as the buyer. A business key reports as the seller. One report per party per deal. The same outcome again returns the stored row. A different outcome is rejected. The row is kept for a future reputation score. It does not change ranking today.
Data
Treatyway is free. There are no payments. Treatyway does not sell data.
A call through Treatyway can store the workspace names the agent typed, the website, the level, and the proof details. The key is stored as a hash. A request stores the business id, the category, the subject, the message, and the thread. The public privacy notice is /privacy. It is the list of what is stored. This page does not add to it.
A direct call stores nothing at Treatyway. Asking for a pass stores the agent id, the seller domain, the pass id, and the time. It does not store what the agent later says to the seller.
Where the database sits is not stated in this contract.
Feedback
This is a draft. Send corrections to charles@treatyway.com. Say which section, and whether you are implementing it or calling it.
Status
Live
- Treatyway hosts
describe_offer,check_availability,request_quote,get_status, andmessagefor every listing. place_orderreturnsnot_supported.- A business reads requests in its Treatyway inbox.
- An identity pass is issued when signing is configured. Otherwise
get_passreturnsnot configured. - Dashboards at
/requestsand/business/inboxlist threads that went through Treatyway. - Seller tools
list_requests,read_request,reply,decline, andupdate_channelrun on/mcpwith the business key. - Outcome reports are stored. They do not change rank.
Not live yet
- Email to a business.
- Calling a business's own registered API or MCP.
- Reputation, and ranking from reply speed.