Overview
Trunkville is the global directory of bespoke makers (tailors, shoemakers, shirtmakers and other artisans) and the trunk shows where they travel to see clients. The MCP server gives an AI assistant the same directory and the same appointment-request flow as the website, as structured tools.
Why use it instead of browsing
- Answers the real question in one call. “Who makes shoes and comes near Chicago, under $2,000?” combines home cities, upcoming dates and past visits; on the site that’s many pages.
- Structured, current data. Dates, cities, venues, prices and open appointment times as JSON, read live from the directory, with each time’s
slot_id. - Past visits. Makers who’ve been to a city in the last two years but have nothing scheduled are included, so an assistant can offer to ask them to come again; the site doesn’t surface this in one place.
- A real request flow, not form-filling. Requests go to the maker with your assistant named, the client gets every update by email, and the assistant can check status or withdraw later.
Request-only by design. Bespoke makers confirm every appointment personally. There is deliberately no “book” tool: nothing is an appointment until the maker accepts.
Endpoint, transport and authentication
- URL:
https://www.trunkville.com/api/mcp - Transport: Streamable HTTP (MCP over HTTP POST, JSON-RPC 2.0).
- Authentication: none. No sign-in, API key or OAuth. Requests act for the client whose name and email the assistant supplies; the
status_tokenreturned with each request is the only credential, and only for that request. - Server name:
trunkville. Tools: search_makers, get_maker, list_trunk_shows, request_appointment, ask_maker, get_request, cancel_request. - The same data is available as a JSON API, described at /openapi.json, and summarised for AI crawlers at /llms.txt.
Setup
Add Trunkville as a remote MCP server in any client that supports Streamable HTTP:
{
"mcpServers": {
"trunkville": { "url": "https://www.trunkville.com/api/mcp" }
}
}For clients that only speak stdio, use a bridge such as mcp-remote:
{
"mcpServers": {
"trunkville": { "command": "npx", "args": ["-y", "mcp-remote", "https://www.trunkville.com/api/mcp"] }
}
}To check it from a terminal:
curl -s https://www.trunkville.com/api/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'A good first call is search_makers with a city, then get_maker on a result to see their open times.
Tools
Every tool returns its result as JSON text. Failures set isError: true with { "error": "…" }. Annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) and titles are declared on every tool.
| Tool | Class | What it does |
|---|---|---|
search_makers | Read | Search bespoke makers |
get_maker | Read | Get a maker's profile and open times |
list_trunk_shows | Read | List trunk shows |
request_appointment | Sensitive write | Request an appointment (emails the maker) |
ask_maker | Sensitive write | Message a maker (emails the maker) |
get_request | Read | Check a request's status |
cancel_request | Write | Withdraw a pending request (emails the maker) |
Read tools change nothing. Sensitive write tools email a real maker on the client’s behalf, so an assistant should confirm with the client before calling them. Write changes the client’s own request.
search_makersRead
Title: Search bespoke makers · readOnlyHint: true · destructiveHint: false · idempotentHint: true · openWorldHint: false
Purpose. Find bespoke makers (tailors, shoemakers, shirtmakers…) who are based in, coming to, or have recently visited a place, filtered by craft and starting price. A place match covers makers based there, with upcoming dates there, and those who visited in the last two years with nothing scheduled; match.summary says which.
Inputs.
q | optional string. Free text: name, craft or home city |
near | optional string. A city, “Evanston, IL” or “lat,lng” |
radius_km | optional number, 1–1000 (default 150) |
city | optional string. Exact city instead of a radius |
visited | optional: any | upcoming | past (default any) |
craft | optional string, e.g. shoes, suits, shirts, knitwear |
price_min, price_max | optional numbers, in currency |
currency | optional string (default USD). Prices are compared in USD at rough fixed rates; makers without a listed price drop out when a price filter is set |
limit | optional integer, 1–200 (default 50) |
Output. count, total, the resolved place, and makers[], each with slug, name, based_in, crafts, starting_price, next_visit, match (home / upcoming / past visits near the place) and links.
Example.
search_makers({ "near": "Chicago", "craft": "shoes", "limit": 1 })
→ { "count": 1, "total": 8,
"place": { "label": "Chicago", "radius_km": 150, ... },
"makers": [{ "slug": "catella-shoemaker", "name": "Catella Shoemaker",
"based_in": "Kettering, England, UK",
"match": { "summary": "Coming to Chicago 2026-11-05", ... },
"request_url": "https://trunkville.com/book/catella-shoemaker" }] }Side effects. None.
Errors. A place that can’t be resolved returns count: 0 with a note explaining why, not an error. Invalid arguments (e.g. radius_km above 1000) are rejected by the input schema.
get_makerRead
Title: Get a maker's profile and open times · readOnlyHint: true · destructiveHint: false · idempotentHint: true · openWorldHint: false
Purpose. One maker's profile, ateliers, upcoming trunk-show visits, past visits from the last two years with a per-city summary, and the appointment times they have opened, each with a slot_id for request_appointment.
Inputs.
slug | required string, e.g. sebastian-tarek |
Output. slug, name, bio, based_in, crafts, starting_price, ateliers[], upcoming_visits[], past_visits[], cities[] (visits, last_visit, next_visit), open_times[] (slot_id, starts_at, ends_at, location) and booking (policy request_only, how to request, limits). Times are the local wall time in the visit city, written with a Z suffix; don’t convert them.
Example.
get_maker({ "slug": "trunkville-test-atelier" })
→ { "slug": "trunkville-test-atelier", "name": "Trunkville Test Atelier",
"open_times": [{ "slot_id": "…", "starts_at": "2027-01-05T10:00:00Z",
"ends_at": "2027-01-05T11:00:00Z", "city": "New York", ... }],
"booking": { "policy": "request_only", ... } }Side effects. None for the caller. Loading a maker also releases any of that maker’s holds older than 72 hours (see below).
Errors. { "error": "Maker not found." } with isError: true for an unknown slug.
list_trunk_showsRead
Title: List trunk shows · readOnlyHint: true · destructiveHint: false · idempotentHint: true · openWorldHint: false
Purpose. Which maker is in which city, when. Upcoming by default; set from in the past to see where makers have been.
Inputs.
city, country | optional strings |
near | optional string, a place name or “lat,lng” |
radius_km | optional number, 1–1000 |
maker | optional maker slug |
from, to | optional dates, YYYY-MM-DD (from may be in the past) |
Output. count and shows[], each with maker (slug, name), city, country, venue, from, to, dates_tba, hours, notes and request_url.
Example.
list_trunk_shows({ "city": "London" })
→ { "count": 1, "shows": [{ "maker": { "slug": "valstar", "name": "Valstar" },
"city": "London", "venue": "Trunk London",
"from": "2026-11-05", "to": "2026-11-08", ... }] }Side effects. None.
Errors. An empty list when nothing matches.
request_appointmentSensitive write
Title: Request an appointment (emails the maker) · readOnlyHint: false · destructiveHint: false · idempotentHint: false · openWorldHint: true
Purpose. Ask a maker for an appointment on a client’s behalf. Nothing is booked by this call. Pass a slot_id from get_maker to hold one of the maker’s open times, or describe when suits in preferred_times. To ask a maker to come back to a city they’ve visited before, pass city + preferred_times with no slot. It is a sensitive write because it emails a real maker, and the client, in the client’s name: confirm the details with the client first.
Inputs.
maker | required maker slug |
name | required, the client's name |
email | required, the client's email (they receive every update and the calendar invite) |
commission | required, what the client would like made |
agent_name | required, your assistant's name; the maker sees it |
slot_id | optional UUID from get_maker open_times |
show_uuid | optional UUID of the visit, when no slot_id |
city | optional, where the client would like to see the maker |
preferred_times | optional, when suits, in words |
phone, first_bespoke, message | optional |
Output. id, status (pending), awaiting (maker), maker, held_until (when a slot is held), thread_url (the client’s page for the request), thread_email (reply-by-email address), status_token (needed for get_request and cancel_request) and a note.
Example.
request_appointment({ "maker": "trunkville-test-atelier", "name": "Alex Reviewer",
"email": "alex@example.com", "commission": "a navy two-piece suit",
"slot_id": "<slot_id from get_maker>", "agent_name": "Muse" })
→ { "id": "…", "status": "pending", "awaiting": "maker",
"held_until": "2026-11-05T12:00:00.000Z",
"thread_url": "https://www.trunkville.com/r/…", "status_token": "…",
"note": "Nothing is booked until the maker accepts. …" }Side effects. Saves the request; holds the slot (if given) for up to 72 hours; emails the maker (or adds the request to their summary, see limits) and emails the client a receipt with their thread link.
Errors. Returned as { "error": "…" } with isError: true: missing name, invalid email or missing commission; Maker not found.; That time was just taken — please pick another. (conflict: true; call get_maker again for current times); and You already have requests waiting with this maker when the client already has 3 pending requests with that maker.
ask_makerSensitive write
Title: Message a maker (emails the maker) · readOnlyHint: false · destructiveHint: false · idempotentHint: false · openWorldHint: true
Purpose. Send a maker a plain message on a client’s behalf: a question, an introduction, or “would you come to Chicago?”. Nothing is held or booked; the maker replies by email through Trunkville’s relay and can turn it into an appointment by suggesting a time. A sensitive write because it emails a real maker in the client’s name.
Inputs.
maker | required maker slug |
name | required, the client's name |
email | required, the client's email (the reply goes there) |
message | required, at least 2 characters, in the client's words |
agent_name | required, your assistant's name |
city, phone | optional |
Output. id, kind (message), status, maker, thread_url, thread_email, status_token, note.
Example.
ask_maker({ "maker": "trunkville-test-atelier", "name": "Alex Reviewer",
"email": "alex@example.com", "message": "Do you make double-breasted overcoats?",
"agent_name": "Muse" })
→ { "id": "…", "kind": "message", "status": "pending", "status_token": "…", ... }Side effects. Saves the message as a request of kind message; emails the maker (subject to the limits below) and the client.
Errors. As for request_appointment (validation, unknown maker, 3 pending requests per client per maker).
get_requestRead
Title: Check a request's status · readOnlyHint: true · destructiveHint: false · idempotentHint: true · openWorldHint: false
Purpose. The current status of a request or message, whose move it is, the time held, proposed or confirmed, and the message thread.
Inputs.
id | required UUID from request_appointment / ask_maker |
token | required, the status_token from the same response |
Output. id, kind, status, awaiting (while pending), maker, requested, and depending on state held + held_until, proposed + confirm_url (the maker suggested a time; the client confirms at that link), or appointment (accepted); plus messages[], thread_url, created_at, responded_at.
Example.
get_request({ "id": "…", "token": "…" })
→ { "status": "pending", "awaiting": "maker",
"held": { "starts_at": "2027-01-05T10:00:00Z", ... },
"held_until": "…", "messages": [] }Side effects. None.
Errors. Missing token. or Request not found. (wrong id or token) with isError: true.
cancel_requestWrite
Title: Withdraw a pending request (emails the maker) · readOnlyHint: false · destructiveHint: true · idempotentHint: true · openWorldHint: true
Purpose. Withdraw a pending request or message for the client: the same as the client pressing Withdraw on their request page. It can’t be undone (the client would have to ask again). An accepted appointment can’t be cancelled with this tool: the client should write to the maker through the thread.
Inputs.
id | required UUID |
token | required status_token: proves the caller acts for the person who made the request |
Output. { "id", "status": "withdrawn", "maker", "thread_url", "note" }. Calling it again on a withdrawn request changes nothing and says so.
Example.
cancel_request({ "id": "…", "token": "…" })
→ { "id": "…", "status": "withdrawn",
"note": "Withdrawn. Any held time was released and the maker has been told." }Side effects. Sets the status to withdrawn, releases any held time at once, and emails the maker that the request was withdrawn.
Errors. Request not found. (wrong id or token), or This request is accepted / declined and can’t be withdrawn.
Request lifecycle
A request (from request_appointment) or message (from ask_maker) has one of these status values:
pending: open.awaitingsays whose move it is:maker(they haven’t answered yet, or the client replied) orclient(the maker suggested a time, or asked a question, and the client needs to answer). Questions and replies go back and forth on the thread while it stays pending.accepted: the maker accepted, or the client confirmed a time the maker suggested. This is the only state that is an appointment. The client gets a calendar invite andget_requestreturns theappointment.declined: the maker declined (for a message, the maker closed the conversation).withdrawn: the client withdrew it, on their request page or with cancel_request.
The status list in the API schema also includes expired, but nothing sets it today: an unanswered request stays pending and only its held time is released (below).
The 72-hour hold
When a request names one of the maker’s open times (slot_id), that time is held for that client so nobody else can take it, for up to 72 hours (held_until). The same applies to a time the maker suggests while it waits for the client. If nobody acts within 72 hours, the time is released back to the maker’s calendar, the client is emailed, and the request stays open so the maker can still answer. Releases are applied when that maker’s times are next loaded. Decline and withdrawal release the hold at once.
Duplicates and idempotency
- A client (by email address) can have at most 3 pending requests or messages with one maker. A fourth is refused with “You already have requests waiting with this maker” (HTTP 429 in the JSON API).
- A held time can only be held once: a second request for the same
slot_idis refused withconflict: true. - request_appointment and ask_maker take no idempotency key: each call creates a new request and sends new emails. Don’t retry a call that returned an id; use get_request to check it. cancel_request is safe to repeat.
Nothing is a booking until it is accepted. Assistants should describe a pending request as a request, never as a confirmed appointment.
Rate limits
These are the limits the server enforces today:
- 3 pending requests per client per maker, as above. This is the only limit that refuses a new request.
- Maker email batching. Requests are never refused because a maker is busy. When a maker gets more than 5 requests in an hour, individual emails pause and they get one summary an hour instead. Past 60 in an hour, they move to a daily summary for the next 24 hours. Answers can take longer as a result.
- Thread messages: 12 messages per hour from each side of one request’s thread; beyond that, sending is refused until the hour passes.
- Message emails to a maker: at most 20 an hour across all their threads. Further messages are saved to the thread but not emailed.
- The read tools have no per-client quota. Please keep automated traffic reasonable and follow the terms for automated access.
Testing
To try the write tools without contacting a real maker, use the test maker trunkville-test-atelier. It is hidden from search_makers, list_trunk_shows and the public directory, but get_maker returns its profile and open times, and request_appointment, ask_maker and cancel_request work on it normally. Its emails go to Trunkville staff, not to a maker. Use a real inbox you control as the client email: receipts and updates are sent to it.
Data handling
- Read tools take no personal information. Beyond our host’s standard request logs (see the Privacy Policy), we keep no record of which assistant called them.
- request_appointment and ask_maker send the client’s name, email, and optional phone and message to the maker, and store them with the request; the maker sees the
agent_nametoo. The client is emailed at every step. Requests and their threads are anonymized 24 months after their last activity. - Only send a client’s details with their permission. The
status_tokengives access to that request’s thread: keep it private to the client. - Full details: Privacy Policy and Terms of Service.
Support
Questions, problems or a maker to add: christian@trunkville.com. Trunkville is operated by Trunkville LLC.