# MCP direct-booking specification for a small business Version: November 7, 2026. This specification describes the released behavior of a small lodging group, generalized for a developer or AI builder. Direct booking is new. It does not establish guest adoption, completed stays or business results. A unit means the inventory being reserved, such as a room, appointment slot or piece of equipment. Rooms and nights remain the worked example below. An appointment business must replace date arithmetic and occupancy rules deliberately. ## Configuration worksheet Resolve every double-brace placeholder before building. The tables distinguish shipped defaults from decisions that have no transferable default. Numbers in schemas and quoted sentences below show the shipped settings. If a setting changes, change its wording, tests and documentation together. A value described as fixed in the implementation needs a reviewed code change, not just a settings edit. In the shipped code the per-mailbox limits, the per-reservation cancellation limit and the two-per-email pending limit are fixed constants; only the limits named in section 10 are settings. | Placeholder | Worked default or required decision | Purpose | |---|---|---| | `{{BUSINESS_NAME}}` | Your public business name | Public identity; no sample business identity is supplied | | `{{LOCATION_COUNT}}` | 12 in the worked implementation; use your actual count, including 1 | Deployment coverage and degraded pending bound | | `{{LOCATION_NAME}}`, `{{LOCATION_LIST}}` | Your location name and public list | Route requests without guessing an ambiguous location | | `{{HUB_ORIGIN}}`, `{{LOCATION_ORIGIN}}` | Your HTTPS custom origins | Published-host guards and authenticated routing | | `{{FORM_URL}}` | Existing form with only requested room type and dates prefilled | Preserve the normal booking route | | `{{BARE_FORM_URL}}`, `{{LOCATIONS_URL}}` | Existing form without query values and public locations page | Preserve inherited quote fallbacks | | `{{PHONE}}`, `{{BUSINESS_PHONE}}` | Public location and business fallback numbers | Every refusal has a human route | | `{{STAFF_EMAIL}}`, `{{FROM_EMAIL}}` | Approved recipient and authenticated sender | Deliver notices without exposing internal configuration | | `{{PRIVACY_URL}}`, `{{POLICY_URL}}` | Your existing public privacy and cancellation pages | Explain collection and the applicable policy | | `{{PROPERTY_PREFIX}}`, `{{REFERENCE}}` | Your reference namespace and the current reservation reference | Route opaque references; never reuse a real example | | `{{CONTEXT}}`, `{{FIELD}}`, `{{ALLOWED_FIELDS}}`, `{{STATUS}}` | Detected context, invalid field, permitted field list, current reservation state | Dynamic text slots in the refusal catalogue | | `{{CHECKIN}}`, `{{CHECKOUT}}`, `{{NIGHTS}}`, `{{LAST_CHECKOUT}}` | Requested dates, calculated nights and latest allowed checkout | Request-only stay-cap messages | | `{{ROOM_TYPE}}`, `{{ROOM_TYPES}}`, `{{MINIMUM_NIGHTS}}` | Requested type, types sold at the location and current live minimum | Inherited quote failures | | `{{WEEKLY_PRICE}}`, `{{DAILY_EQUIVALENT}}`, `{{TAX_LABEL}}` | Existing business price text, weekly price divided by 7 rounded to 2 decimal places, and tax wording | No sample price or tax is imported | | `{{STAY_POLICY_REASON}}` | Owner-approved explanation for the maximum stay | Do not transfer another jurisdiction's legal claim | | `{{QUOTE_ERROR}}`, `{{HTTP_STATUS}}` | Non-sensitive quote failure detail and actual HTTP status | Describe an unavailable quote without inventing a price | | `{{CONFIRM_HOURS}}` | 24 hours | Staff's written confirmation promise, not automatic acceptance | | `{{MAX_STAY_NIGHTS}}` | 30 | Maximum span, including connected stays | | `{{ADJACENCY_GAP_NIGHTS}}` | 1 | A one-night gap joins stays for the continuous-span rule | | `{{MIN_NIGHTS}}` | Single 1, double 1, studio 7, subject to live location policy | Retain the existing pricing and minimum-stay rules | | `{{ADVANCE_DAYS}}` | 550 | Existing pricing horizon; its lower bound also accepts yesterday, as noted below | | `{{MAX_GUESTS}}` | 4; omitted guest count becomes 1 | Existing form's ceiling; inventory restrictions still apply | | `{{CHECKIN_HOUR}}`, `{{TIME_ZONE}}` | Your arrival hour and IANA time zone; the worked example used 15:00, America/Denver | Cancellation cutoff uses local arrival time and daylight saving changes | | `{{OFFICE_HOURS}}` | Monday to Friday 08:00 to 18:00; Saturday 10:00 to 18:00; Sunday closed | Local cancellation gate, also subject to central office status | | `{{CANCEL_BEFORE_HOURS}}` | Strictly more than 48 hours | Chat cancellation cutoff, checked at both steps | | `{{MIN_AGE}}` | 18 | Worked check-in statement; owner must approve local agreement requirements | | `{{CODE_MINUTES}}`, `{{CODE_DIGITS}}`, `{{WRONG_TRIES}}` | 15 minutes, 6 digits, 5 wrong tries | Limit the unproved operation's lifetime and guessing | | `{{RESEND_COUNT}}`, `{{RESEND_SECONDS}}` | 2 resends, at least 60 seconds apart | Limit mail without extending the original expiry | | `{{EMAIL_HOURLY}}`, `{{EMAIL_DAILY}}` | 3 per hour, 6 per rolling 24 hours | Booking-code limits per normalized mailbox, including resends | | `{{PORTFOLIO_EMAIL_HOURLY}}`, `{{PORTFOLIO_EMAIL_DAILY}}` | 8 per hour, 20 per rolling 24 hours | Adjustable group booking-code ceilings | | `{{LOCATION_EMAIL_HOURLY}}` | 3 | Adjustable second check at each location; no location daily fallback | | `{{CANCEL_REFERENCE_DAILY}}`, `{{CANCEL_PORTFOLIO_DAILY}}` | 3 per reservation, 5 across the group per rolling 24 hours | Separate cancellation-code capacity | | `{{CONTACT_PENDING}}`, `{{LOCATION_PENDING}}`, `{{PORTFOLIO_PENDING}}` | 2 per proved email, 3 per location, 8 group pending chat holds | Bound reservations awaiting staff confirmation | | `{{MAIL_PLAN}}`, `{{MAIL_WORKING}}`, `{{MAIL_HEADROOM}}`, `{{NOTICE_FORECAST}}` | 100, 80, 20, 55 messages per day | Owner-supplied plan, working total, buffer and ordinary-mail forecast; not four software counters | | `{{PROOF_DAYS}}`, `{{INDEX_DAYS}}` | 400 days each | Separate replay-receipt lifetime and index checkout cutoff | | `{{COMMIT_RECOVERY_HOURS}}`, `{{UNINDEXED_PENDING_HOURS}}` | 24 hours each | Strip old committing intent details; clean abandoned pending records | | `{{RESERVATION_RETENTION}}`, `{{LOG_RETENTION}}` | Owner must set; no complete deletion policy shipped here | Full records and provider logs outlive index trimming unless separately deleted | | `{{NOTICE_RETRY_HOURS}}`, `{{PROVIDER_DEDUP_HOURS}}` | At least 36 hours; provider observation was 24 hours | Make the duplicate-notice risk explicit | | `{{SWEEP_MINUTES}}` | 15 | Scheduled cleanup and retry cadence | | Other implementation number to confirm | Shipped value and reason | |---|---| | Schema string lengths | Property/name 120, room type 20, dates 10, email 254, phone 40, notes 500, identifiers/references 100 | | Validation details | Email local part at most 64; domain labels at most 63; a 10-digit phone gets a default country prefix, otherwise normalized phone has 11 to 15 digits after removing international 00; last-four lookup exactly 4 digits. The public phone sentence says 10 to 15 even though raw international input can be longer | | Identifier entropy | Intent and cancellation identifiers use 24 random bytes, rendered as 48 hexadecimal characters after the location prefix; new reference suffixes use 8 bytes, rendered as 16 uppercase hex characters | | Legacy references | Existing 5-character suffix format remains accepted; backend lookup rejects references longer than 80 characters | | Lookup protection | 10 failed lookups per reference for 1 hour; at most 500 nonlocked counter entries retained; successful lookup clears its failure counter | | Configurable limit range | Positive integers 1 to 1000 for the chat settings; keep cross-limit capacity checks outside this range validation | | Code-budget acknowledgement | 2 seconds for the shared read and conditional write; no acknowledged allowance means no send | | Request deadlines | 44-second hub deadline, below the 45-second acceptance target and observed 60-second host limit; 25-second location deadline | | Subrequest limits | Target mutation relay up to 40 seconds; contact reads 4 seconds in parallel; ordinary lookup relay 5 seconds; inventory read 3 seconds; inherited location quote 6 seconds; office-status read 2 seconds; mail send 3.5 seconds | | Public routing and batches | Unique short-name matching starts at 3 characters; at most 1 booking-tool call per batch, including lookup | | Conditional storage timing | Location requests at most 4 seconds within its deadline; ordinary hub request 650 ms, mutation loop normally 1.5 seconds; mail-counter operations share their 2-second deadline | | Coordination leases | 30 seconds for contact markers, local operation locks and notice claims; location deadline ends 500 ms before a supplied contact-marker deadline | | Admission uncertainty | Hub mail admission reserves through a 60-second cushion; local pending send timestamps use a 5-second cushion; retain uncertain charges | | Sweep bounds | 24-second work deadline within an observed 30-second scheduled host limit; up to 12 selected rows, up to 9 priority rows, work in groups of 3 | | Recovery grace | 3 minutes before rebuilding absent notice work; 10 minutes before releasing an indexed hold with no reservation record | | Notice backoff | 15 minutes, then 30 minutes, then hourly; setup failure retries hourly; failures after the retry window flag staff | | Bounded polling | Local locks: up to 240 attempts spaced 50 ms; identical sending-intent wait: up to 100 reads spaced 50 ms; hub mutations: up to 400 attempts within their deadline | | Price comparison coverage | Every supported room type over 1 through 30 nights; preserve existing currency, tax and rounding, without importing this example's prices | ## 1. Purpose and scope Build four tools that let a guest request a reservation, prove control of an email address, look up minimal status and cancel within the business's rules. No online payment, card collection, refund processing, identity verification or automatic final staff acceptance is included. Proof establishes control of the destination mailbox only. It does not establish the person's identity, ownership of a supplied phone or authority over another mailbox. The public connector is the hub. Existing read tools continue to find locations, quote prices and show current availability. The new tools are `start_booking`, `confirm_booking`, `get_booking` and `cancel_booking`. A one-location business can deploy the same separation with one backend. Never add a booking operation to a public location read server simply because its backend supports authenticated staff operations. Use the shipped behavior as a tested starting point, not a claim that every limitation was solved. Sections 22 and 23 distinguish accepted risks and deferred repairs. The developer must return code, repeatable tests, a release procedure, a refusal inventory, a data-retention statement and a changed-file list. The owner approves business settings before a live booking test. ## 2. What stays exactly as it is Keep the existing form, phone route, signature gate, pricing functions, inventory source and staff confirmation process. Preserve existing read-tool names, inputs and response bytes on a fixed call set. Keep taxes, rate tiers, minimum stays and currency rounding in the existing price function. Do not duplicate pricing in a new chat helper. The signed web form keeps its direct-send email path. Chat notice bookkeeping must not delay, prevent or duplicate those sends. A late storage failure must have the same effect on the form as before. Even mail bytes for unusual web names containing links, dots, slashes or newlines remain unchanged. A broader repair of that old behavior is separate work. The chat route explicitly defers the agreement and identification steps to check-in. It does not weaken the web signature gate. A guest-supplied source flag cannot grant access to the authenticated chat submission path. Date changes use cancellation and rebooking, subject to cancellation hours and cutoff, with phone support when that cannot finish. There is no change tool. Do not create an extra reservation silently. ## 3. Owner decisions to make first Complete the worksheet, name the staff role that resolves interrupted operations and agree on the exact confirmation promise. Identify the authoritative inventory, pricing and policy sources. Decide which room types each location sells. Agree on whether staff can confirm an unsigned web request through the existing desk process; do not change that rule as a side effect of chat work. Confirm the mail plan with the account owner. The initial figure changed during the worked release. The final working total deliberately stayed below the plan. State whether every recipient copy counts, what ordinary business mail shares the account, and how staff react when the forecast no longer fits. Choose retention for full records, mail copies and host logs. Approve the mailbox normalization policy. The implementation lowercases addresses, removes plus suffixes across providers and applies dot folding and domain-alias folding to one provider family. This is a grouping rule, not proof that every provider treats those addresses identically. Preserve the delivery address separately. Changing it requires compatibility tests and a migration plan for counters and open intents. Approve office hours, arrival time, time zone and cancellation policy separately. The shipped cancellation code checks both central office availability and a local weekly-hours rule. A central override alone cannot extend the local window. Keep that behavior or replace both checks together with explicit tests. No legal rule for all businesses is inferred from the worked stay or age limits. ## 4. Architecture The hub validates public calls and relays to an authenticated operation at the selected location. Raw guest inputs pass through memory for that call. They are not stored as guest records at the hub and must not enter request logs. The location owns intents, reservations, room holds, cancellation state and notices. There is no shared inventory lock across locations. Each location uses conditional writes on its own inventory index so two callers cannot sell its last room. Do not confuse this with absence of all coordination: the hub keeps aggregate pending counts, timestamped mail admissions, keyed email hashes and short-lived keyed contact markers. These are pseudonymous control data. Describe them in privacy documentation instead of claiming that absolutely no guest-related data exists at the hub. The backend authenticates the relay using a shared operational secret, verifies deployment context and published status, and checks its own custom hostname. Public arguments cannot select an arbitrary backend origin or supply authentication, production context, budget approval, test markers or staff-only contact results. These fields are internal operations only. Use conditional create and compare-and-swap operations. A conditional conflict is contention, not an unavailable store. Retrying a conflict must re-read current state. A stale writer must never overwrite a newer counter or inventory row. Shared mail-counter failure stops code sending. Shared pending-counter or contact-marker unavailability has a different shipped behavior: local operations can continue under local limits, with incomplete cross-location checks flagged to staff. Preserve that distinction and disclose the degraded bound. ## 5. Data and retention An unproved intent contains `id`, `data`, `contact`, `price`, `salt`, `digest`, `expires`, `wrong`, `resends`, `sentAt`, `state` and `fingerprint`. `data` holds the supplied name, delivery email, formatted phone, notes, guest count, room type and dates. `contact` contains normalized matching values. Never store the six-digit code itself. Salt its digest and compare equal-length digests with a constant-time comparison. After proof, replace the code digest with a secret-derived replay `proof`, allocate `ref` and a staff-action token, and enter `committing`. A finished receipt retains the identifier, reference, result, replay proof, expiry and `keepUntil`. It does not need the original name or phone. A stale committing intent becomes `recovery` after its original expiry plus 24 hours; remove `data` and `contact`, keep a reference and proof for recovery, and retain that receipt for another 400 days. Successful receipts also last 400 days. Expired unproved intents are purged on state access or sweep. Booking and cancellation send history uses rolling 24-hour windows. Lookup counters expire after an hour. Contact markers and locks expire after 30 seconds. These are cleanup predicates, not guarantees that storage deletes bytes at the exact wall-clock expiry if no job runs. Cancellation intents retain proof receipts independently of booking intents. A reservation includes `ref`, `token`, `source: "assistant"`, `agreementAtCheckin: true`, contact fields, dates, `nights`, `guests`, `quotedTotal`, `chatPrice`, `rateBasis`, `notes`, `deskNotes`, `uncheckedProperties`, `roomTaken`, `state` and `createdAt`. Notice state includes `assistantNotices`, delivery and setup failure flags, retry times and cancellation repair markers. Staff-only notes may include other reservation references; they must stay behind staff authentication. The inventory index trims rows by checkout older than 400 days when updated. Full reservation records survive under their own keys. Do not publish "all guest data is deleted after 400 days." Implement the owner-approved full-record and log retention separately, including backups, provider mail history and recovery records. Preserve unresolved held reservations and operational evidence until staff have reconciled them. ## 6. Tools, schemas, annotations and effects Use ordinary object schemas with `additionalProperties: false`. No top-level `oneOf`, `anyOf`, `allOf` or conditional composition is needed. The two mode-switching tools deliberately have an empty `required` array; the handler enforces complete, mutually exclusive modes. Export the actual schema, not a simplified table, and test it in a real client. A JSON-RPC batch may contain at most one booking-tool call, including `get_booking`. If more occur, reject all booking calls before running any or charging mail. Other read calls in the batch retain their ordinary behavior. This transport rule is distinct from each operation's validation. ### start_booking Description: "Validates and prices a stay, checks current availability without holding a room, and emails a six-digit code. The intent and code last 15 minutes. An identical open start returns the same booking_id without another email. Resend takes only booking_id, allows two resends at least 60 seconds apart, and keeps the original expiry. Mailbox, location and group code limits apply. A room is held only after confirm_booking accepts the code. Unavailable chat booking returns the requested form link and public phone." ```json { "type": "object", "additionalProperties": false, "properties": { "property": {"type": "string", "maxLength": 120, "description": "Property name, slug, domain or unique short name."}, "roomtype": {"type": "string", "maxLength": 20, "description": "Room type: single, double or studio; case-insensitive."}, "checkin": {"type": "string", "maxLength": 10, "description": "Arrival date, YYYY-MM-DD."}, "checkout": {"type": "string", "maxLength": 10, "description": "Departure date, YYYY-MM-DD."}, "guest_name": {"type": "string", "maxLength": 120, "description": "Guest name."}, "email": {"type": "string", "maxLength": 254, "description": "Email address for the verification code."}, "phone": {"type": "string", "maxLength": 40, "description": "Contact phone number, with country code for international numbers."}, "guests": {"type": "integer", "minimum": 1, "description": "Guest count, subject to the room limit."}, "notes": {"type": "string", "maxLength": 500, "description": "Optional plain-text arrival details or requests."}, "booking_id": {"type": "string", "maxLength": 100, "description": "Opaque identifier returned by the first step."} }, "required": [] } ``` New mode requires `property`, `roomtype`, `checkin`, `checkout`, `guest_name`, `email` and `phone`. Optional `guests` defaults to one and is checked against the handler's maximum of four. Resend mode accepts only `booking_id`. Empty optional notes and guest count, and empty identifier mode selectors, are normalized consistently before validating the selected mode. Malformed optional fields still fail. Success returns `booking_id`, `summary` with property, room type, dates, nights, subtotal, tax and total, `code_expires_at`, `code_expires_local` with the same instant in plain local wording, masked `code_sent_to`, `channel: "email"` and a message stating the expiry and saying no room is held. Internal `verification_sent` and `verification_uncertain` bookkeeping is stripped at the hub. It must not read stored stays to decide an unproved caller's answer or reserve inventory. ### confirm_booking Description: "Verifies the emailed code for a booking_id, then holds an available room and submits the reservation for staff confirmation within 24 hours. Repeating the valid pair returns the same reservation. Proved-email rules cover pending reservations, overlap and the 30-day continuous span, including a one-night gap. The guest brings photo ID and signs the agreement at check-in. No payment is taken." ```json { "type": "object", "additionalProperties": false, "properties": { "booking_id": {"type": "string", "maxLength": 100, "description": "Opaque identifier returned by the first step."}, "code": {"type": "string", "pattern": "^\\d{6}$", "description": "The six-digit code received by the guest by email."} }, "required": ["booking_id", "code"] } ``` Return `reservation_number`, `status`, `property`, `roomtype`, `checkin`, `checkout`, `total`, `phone` and `message`. Initial held status is `reserved`, not `confirmed` and not an unsigned `pending` request. Do not claim a hold from a mere durable record. Replays re-read the current reservation so cancellation, completion and moves are not reported as fresh reservations. ### get_booking Description: "Reads reservation status, property, room type, dates and total using the reservation number and the last four digits of its contact phone. It returns no guest name, email or full phone. Incorrect details produce the same no-reservation answer. Failed lookups count toward a per-reference limit." ```json { "type": "object", "additionalProperties": false, "properties": { "reservation_number": {"type": "string", "maxLength": 100, "description": "Reservation number."}, "phone_last4": {"type": "string", "pattern": "^\\d{4}$", "description": "Last four digits of the booking contact phone number."} }, "required": ["reservation_number", "phone_last4"] } ``` Return only `ok`, `reservation_number`, `status`, `property`, `roomtype`, `checkin`, `checkout` and numeric `total`. The second fact is a lookup check, not phone verification. Strip a stored phone extension before extracting its last four digits. An unsigned web request that holds no room is not a reservation for this tool. A storage outage is not "no reservation found." ### cancel_booking Description: "Cancels in two steps. Reservation number and phone last four start an email-code challenge. cancel_id and code complete cancellation, release the room and notify staff and guest. Automated cancellation is available more than 48 hours before check-in during office hours. Other cases return the property phone. Codes have separate reservation and group budgets. No payment or refund is processed." ```json { "type": "object", "additionalProperties": false, "properties": { "reservation_number": {"type": "string", "maxLength": 100, "description": "Reservation number."}, "phone_last4": {"type": "string", "pattern": "^\\d{4}$", "description": "Last four digits of the booking contact phone number."}, "cancel_id": {"type": "string", "maxLength": 100, "description": "Opaque identifier returned by the first step."}, "code": {"type": "string", "pattern": "^\\d{6}$", "description": "The six-digit code received by the guest by email."} }, "required": [] } ``` Mode one requires exactly `reservation_number` and `phone_last4`; mode two requires exactly `cancel_id` and `code`. Code delivery uses the stored reservation address, never a replacement supplied by the caller. Return `cancel_id`, `code_expires_at`, `code_expires_local`, a message stating the expiry and that the reservation stays in place until the code is confirmed, `code_sent_to: "the booking email address"` and `channel: "email"`. An existing open cancellation returns its identifier without another mail. Completion returns current reservation state. No cancellation refusal includes a new-booking link. | Tool | readOnlyHint | destructiveHint | idempotentHint | openWorldHint | |---|---|---|---|---| | start_booking | false | true | false | true | | confirm_booking | false | true | true | true | | get_booking | true | false | true | false | | cancel_booking | false | true | true | true | Include a human title in each tool and its annotations. Descriptions describe behavior; they never direct the assistant to ignore instructions, disclose data or make another call. Server validation enforces permissions regardless of annotations. `get_booking` is read-only in business effect even though failed lookups update defensive counters. ## 7. Intent and reservation state machines An intent begins as `sending`, becomes `open` after accepted code delivery, and becomes `committing` only after proof. Expiry, replacement or five wrong attempts removes an unproved intent. A failed send removes the new intent; an uncertain send retains its budget charge. An identical concurrent start waits for the same sending intent rather than creating another message. After a completed confirmation, retain a compact result and proof receipt. A retry with its valid code loads the actual reservation. A retry without valid proof cannot recover it. A long-interrupted commit enters `recovery` as described in retention. The original code expiry does not make an already proved, partly committed reservation disappear. The reservation begins as `pending` with `roomTaken: false`. Register its reference for cleanup before saving it. A successful conditional inventory write changes the authoritative index to `reserved`. Update the full record to `reserved` and `roomTaken: true`. If that last record update fails, recovery uses the index to repair the record. Never sell the indexed room a second time because a record write lagged. Staff can move a held reservation through the existing `confirmed`, `completed`, `cancelled` or `moved` states. A successful chat submission promises staff confirmation; it does not impersonate a staff decision. Cancellation first saves its durable decision and repair markers, then updates the index to release the room and queues notices. A sweep repairs an interrupted release. Keep cancellation proof receipts so retries report the same outcome. ## 8. Booking flow, in order 1. Validate deployment evidence and the argument object before making a business call. Select only a configured location. Reject unknown fields, invalid dates, unsupported room types and malformed contact values before code-budget work. Names lose control, formatting and zero-width characters and collapse whitespace; otherwise preserve what the guest typed. Empty cleaned names fail. Notes receive separate plain-text and link treatment. 2. Call existing pricing and availability logic. Enforce the live minimum stay and requested-stay cap without reading anyone's stored stays. The worked price function accepts an arrival offset from minus one through 550 days; do not silently change that inherited boundary while promising unchanged form behavior. A business that wants no past arrivals needs a separately reviewed pricing change. 3. Read the location switch and aggregate pending capacity. Read only the specific unproved intent needed for identical-start reuse. If an identical `sending` or `open` fingerprint exists, return its identifier without another email. The fingerprint includes normalized email and phone, cleaned booking fields and original notes. Changed details produce a new intent; successful delivery then replaces earlier open intents for that normalized email at that location. 4. Atomically record shared mail permission within two seconds. Check group limits before mailbox limits. Recheck the location hourly limit at the actual send boundary. Store the salted code digest, send the email and mark the intent open. No reservation or inventory hold exists yet. Resend checks the addressed intent before charging anything, replaces the code, increments its send count and keeps the original expiry and wrong-try count. 5. On confirmation, prove the code at the location before reading contact stays or taking a contact marker. Allocate the recoverable reference once. An already completed intent returns its current reservation. A competing confirmation either reaches that same result or returns the in-progress sentence. 6. After proof, coordinate on the normalized email, re-prove in case another call completed, and query location histories in parallel. First reuse an existing held chat reservation for the same proved email, location, room type and dates. Otherwise apply two pending reservations per proved email, overlap refusal and the maximum connected stay span. Include reserved, confirmed and completed rows in the continuous-span calculation. Ignore cancelled rows and unsigned requests that hold no room. 7. A one-night gap connects intervals. Repeatedly extend the outer start and end until no matching interval joins. Compare the entire outer span, including gaps, with the maximum. Use the same calculation on the signed-in staff screen. Phone-only matches have no role in any of these decisions. 8. Admit the group pending slot, then recheck policy and local pending capacity at the location. Save the recoverable record before attempting the atomic hold. The inventory conditional write checks actual vacancy and the local pending ceiling together. A lost last room produces no success answer; clean the abandoned pending record and give the requested form and phone. 9. Queue and attempt reservation notices after the hold. A failed notice does not undo the reservation. Return the reference and actual state with truthful delivery information. Preserve recovery state if any answer is uncertain. Staff review unread locations and phone matches on the signed-in screen before final confirmation. ## 9. Privacy rules and their reasons Before proof, no response or code-send decision may depend on stored stays associated with the supplied contact. Otherwise a stranger can discover a stay by comparing answers. Public vacancy, aggregate pending capacity and prior code-send limits may affect a response; guest stay history may not. Tests hold those public and defensive inputs constant when comparing private worlds. Whether an unproved phone matches stored stays never changes acceptance, refusal wording, response links or email content. Requiring valid phone text is different from treating the number as authority. A caller can enter another person's number. After email proof, only the proved mailbox can trigger person-based reservation rules. All contact-rule refusals use one sentence, regardless of overlap, continuous-span or pending-count cause: "This stay cannot be booked in chat. Use the booking form or call {{PHONE}}." Append only the requested stay's form URL. A mailbox over its own code limit receives this same sentence, not the pause sentence. Never reveal prior dates, counts, properties, references or whether a phone matched. Phone-match information appears only on the authenticated staff screen. No owner email may contain a match indicator, another guest's reference or differing text that implies a match. Reply-To can send a quoted staff message back to a guest, so owner mail is not a safe place for cross-guest notes. Keep staff access scoped and label unread locations rather than pretending all locations were checked. Do not log raw contacts, codes, mail-provider replies or whole request bodies. Fixed diagnostics and aggregate outcomes suffice for most operations. Keep proof receipts secret-derived and rotate the operational secret coherently. Rotation invalidates replay proofs and per-email keyed history, while reservations and aggregate counts remain. Staff must reconcile interrupted work before a new start after rotation. ## 10. Limits and mail budgets The worked settings are `chatPortfolioEmailHourly: 8`, `chatPortfolioEmailDaily: 20`, `chatPortfolioCancelDaily: 5`, `chatPortfolioPendingLimit: 8`, `chatEmailHourly: 3` and `chatPendingLimit: 3`. The first four belong to the hub settings; the last two belong to each location. Editing a ceiling preserves existing admission history. A valid settings range does not establish that the mail plan or inventory can support the chosen value. Use `W = P - H` and require `R + B + C <= W`. With the observed owner-approved figures, working mail is 80 of a 100-message plan, leaving 20 headroom. About 55 covers ordinary business mail and reservation notices, 20 covers booking codes and 5 covers cancellation codes. These are planning allowances. There is no software cap of 55 on genuine reservation mail and no reservation of those messages at the provider. Each normalized mailbox receives at most three booking codes per hour and six per rolling day, including resends. Cancellation has its own three codes per reservation and five group codes per rolling day. A stranger exhausting booking traffic must not exhaust the cancellation allowance. The location hourly limit remains an additional check, not an independent daily fallback allowance. Shared admission must be acknowledged before sending. Retain a charge after a lost send answer because delivery may have occurred. Refund only a known rejected or unused admission. Do not restart a budget when changing settings or refund an uncertain send and immediately repeat it. The group ceiling is checked first so a globally exhausted budget cannot expose a mailbox's recent activity through a different answer. The normal pending chat ceiling is eight across the group and three at one location. When the shared pending store is unavailable, local limits still apply and the possible bound becomes three times the location count, or 36 in the worked deployment. Contention is refused rather than treated as outage. Cross-location overlap and per-email limits can also weaken when remote histories or coordination are unavailable. Record that limitation and require staff checks; do not claim an unconditional group cap. ## 11. Idempotence and retries Retry a confirmation with the same `booking_id` and code. Allocate one reference, preserve proof and reconcile against the authoritative inventory row. A retry after confirmation, cancellation or completion must report the current outcome. Starting over with the same proved email and identical location, room type and dates reuses an existing held chat reservation rather than making another one. Identical unproved starts reuse their identifier and code email. A changed start replaces the previous open intent only after its own successful send. A resend does not extend the lifetime or reset wrong attempts. The normalized-address fingerprint can retain the first delivery address even if a later plus-address spelling changes; that accepted limitation is not a new successful delivery. Notice records use stable identifiers, claims and provider idempotency keys. Retry at 15 minutes, 30 minutes and then hourly. Keep retrying for at least 36 hours before a failed attempt marks exhaustion. The observed provider suppresses duplicates for only 24 hours. A lost answer followed by a retry after that protection expires can duplicate mail. Staff need a visible reference and delivery history; never promise exactly-once email delivery. ## 12. Refusal catalogue The following is the generalized shipped wording, including differences between layers. Preserve exact strings within a case so tests can compare bytes. Operation-handler refusals carry a public phone. Use the business phone when no location resolves. Lookup and cancellation never carry a form link. Transport rejections occur before the handler and are the exception documented below. Three assembly rules avoid hiding suffix differences in this table. **P** appends ` Call {{PHONE}}.` when the sentence lacks that phone. **F** appends ` The booking form is available, or call {{PHONE}}.` and then a newline plus `{{FORM_URL}}`. **L** appends just a newline plus `{{FORM_URL}}`. A start-call wrapper additionally supplies L if the location and both real dates are known and no form URL is already present. Every FORM below is based solely on the addressed request or intent, never a different guest's stay. "Known" means that this information is still available, not inferred from someone else's reservation. | Literal sentence or template | Assembly | Form | Phone | |---|---|---|---| | This stay cannot be booked in chat. Use the booking form or call {{PHONE}}. | L | Yes | Yes | | Chat booking is briefly paused. Use the booking form or call {{PHONE}}. | L | Yes | Yes | | Chat booking is briefly paused. | F | Yes | Yes | | Chat booking is briefly paused while staff confirm existing reservations. | F | Yes | Yes | | Chat booking is switched off for this property. Call {{PHONE}}. | L | Yes | Yes | | Chat booking is switched off for this property. | F | Yes | Yes | | This booking cannot be checked right now. Try again in a few minutes or call {{PHONE}}. | L for known start stay | Known start | Yes | | The live booking policy could not be read. Please try again. | F | Yes | Yes | | The live booking policy cannot be checked right now. Try again in a few minutes or call {{PHONE}}. | F | Yes | Yes | | No room is free for those nights. Use find_rooms to look again. | F | Yes | Yes | | The last room went while you were verifying. Use find_rooms to look again. | F | Yes | Yes | | No reservation was made. | F | Yes | Yes | | Booking intent expired or unavailable. | F if data survives, otherwise P | Known intent only | Yes | | Incorrect or expired verification code. | F for known booking data, otherwise P | Known booking only | Yes | | Only two code resends are allowed per booking intent. | F | Yes | Yes | | Wait 60 seconds between verification emails. | F | Yes | Yes | | The email address was not accepted. Check the address and start again. | F for addressed stay | Known stay | Yes | | This booking confirmation is still in progress. Ask again in a minute with the same booking_id and code, or call {{PHONE}}. | None | No | Yes | | The booking outcome could not be checked right now. The booking may have gone through; a confirmation email is the proof. Ask again in a minute with the same booking_id and code, or call {{PHONE}}. | None | No | Yes | | no reservation found. Call {{PHONE}}. | None | No | Yes | | This reservation cannot be checked right now. Try again in a few minutes or call {{PHONE}}. | None | No | Yes | | To cancel this reservation by phone, call {{PHONE}} during office hours. The published refund and cancellation policy applies. | None | No | Yes | | To cancel this reservation, call {{PHONE}} during office hours. Cancellation cannot be completed in chat right now. | None | No | Yes | | The cancellation outcome could not be checked right now. The cancellation may have gone through. Ask again in a minute with the same cancel_id and code, or call {{PHONE}}. | None | No | Yes | | The cancellation may have gone through. Ask again in a minute with the same cancel_id and code, or call {{PHONE}}. | None | No | Yes | | Reservation {{REFERENCE}} is canceled. No room is currently reserved under this reference. Call {{PHONE}} with any questions. | L | Yes, confirmation replay | Yes | | Reservation {{REFERENCE}} has ended. No room is currently reserved under this reference. Call {{PHONE}} with any questions. | L | Yes, confirmation replay | Yes | | Reservation {{REFERENCE}} was moved. No room is currently reserved under this reference. Call {{PHONE}} with any questions. | L | Yes, confirmation replay | Yes | Expired, replaced or exhausted whole booking intents share the unavailable-intent response. A wrong code, including a code replaced by resend on an existing intent, uses the incorrect-code response until the intent is removed. These classes are not identical bytes. The privacy invariant compares the same call and intent state with and without private stays. It does not assert that every lifecycle or validation error has identical text. Field errors below receive P, plus the start-wrapper form link when its request has a resolved location and real dates. They reflect caller input, not stored guest history. Literal sample address text uses a reserved example domain. | Validation sentence | Form | Phone | |---|---|---| | Booking arguments must be an object. | No | Yes | | Unknown field "{{FIELD}}". Allowed fields: {{ALLOWED_FIELDS}}. | Known start only | Yes | | Resending takes only booking_id. Unknown field "{{FIELD}}". Allowed fields: booking_id. | Known start only | Yes | | Cancellation takes either reservation_number and phone_last4, or cancel_id and code. Unknown field "{{FIELD}}". Allowed fields: cancel_id, code. | No | Yes | | Invalid reservation_number. The full reference from the reservation email is required, including its {{PROPERTY_PREFIX}} property prefix. | No | Yes | | phone_last4 must be exactly four digits. | No | Yes | | Invalid booking_id. The complete identifier returned by start_booking is required. | Known start only | Yes | | Invalid cancel_id. The complete identifier returned by cancel_booking is required. | No | Yes | | property must be a property name, slug or domain of at most 120 characters. | Known start only | Yes | | guest_name is required and must be at most 120 characters. | Known start only | Yes | | email must be a valid address such as name@example.com, with a dot in the domain and no spaces. | Known start only | Yes | | phone must be text containing 10 to 15 digits; spaces, parentheses, dashes and a leading country code are allowed. | Known start only | Yes | | notes must be text no longer than 500 characters. | Known start only | Yes | | guests must be a whole-number count from 1 to 4. | Known start only | Yes | | roomtype must be single, double or studio (case-insensitive). | Known start only | Yes | | checkin must be a real date in YYYY-MM-DD format. | No | Yes | | checkout must be a real YYYY-MM-DD date after checkin. | Only if both dates are real | Yes | | property must identify one property by name, slug or domain. Available properties: {{LOCATION_LIST}}. | No | Yes | | code must be the six digits received by email. | No | Yes | | Give a guest name, a valid email address, a contact phone number with country code, and notes of at most 500 characters. | Known start only | Yes | | The reservation form allows 1 to 4 guests per room. | Known start only | Yes | | Give real checkin and checkout dates as YYYY-MM-DD. | Known start only | Yes | | Unknown booking tool. | No | Yes | Deployment errors also receive P and the start-wrapper link when available. The hub wording is: "Booking tools are disabled because the deployment context is unknown."; "Booking tools are disabled on this draft (context: {{CONTEXT}}). No code was sent and no reservation was changed."; "Booking tools are disabled because this is not the current published production deploy (context: {{CONTEXT}})."; and "Booking tools are disabled on this non-custom hostname (context: {{CONTEXT}})." The authenticated location layer has three distinct variants: "Booking tools are disabled because the deploy context is unknown."; "Booking tools are disabled on this draft (context: {{CONTEXT}})."; and "Booking tools are disabled because this production deploy is not currently published." Its non-custom-host sentence matches the hub. Each carries P; only the public start wrapper adds a known request's form. Internal-only "Invalid contact details" and "Unknown assistant booking operation" also receive P. They are not additional public tools. The first rejected booking call in a batch returns: "A JSON-RPC batch may contain at most one booking-tool call. No booking calls were run and no emails were charged. Send one booking-tool call in its own request." Later rejected booking calls return: "A JSON-RPC batch may contain at most one booking-tool call. No booking calls were run." Neither includes a phone or form. Protocol failures such as malformed JSON are also transport errors, not reservation outcomes. Existing quote and stay-cap refusals remain the existing read tools' exact sentences. The booking wrapper adds a phone, but adds a prefilled form only when the existing answer lacks the form path. A quote outage or minimum-stay reply that already contains a bare form can therefore retain that bare link. Do not claim every inherited failure is prefilled. The business-specific sentence inventory must remain byte-tested alongside booking refusals. These are the generalized inherited templates. Substitute the business's existing price and policy text; the stay-policy explanation is deliberately not a copied legal claim. Adjust singular and plural for one location. The request-only cap is separate from the fixed post-proof contact refusal: ```text {{BUSINESS_NAME}}: {{CHECKIN}} to {{CHECKOUT}}, {{NIGHTS}} nights Every stay is capped at {{MAX_STAY_NIGHTS}} days, and {{CHECKIN}} to {{CHECKOUT}} is {{NIGHTS}} nights, so none of the {{LOCATION_COUNT}} properties can take it as one booking. The longest stay from {{CHECKIN}} checks out on {{LAST_CHECKOUT}}. Ask again with a check out no more than {{MAX_STAY_NIGHTS}} days after the check in. {{STAY_POLICY_REASON}} Questions: call {{BUSINESS_PHONE}}. ``` The wrapper adds the requested location's phone if different and the requested form. The quote-outage template already has both a phone and a bare form: > {{LOCATION_NAME}} could not be asked for a quote just now ({{QUOTE_ERROR}}), so there is no figure here and none should be guessed. Call {{PHONE}} or see {{BARE_FORM_URL}}, which prices the stay on the page. The shipped helper can insert `HTTP {{HTTP_STATUS}}`, `no quote in its answer`, `did not answer in time` or a caught error message. A new adaptation must constrain that last value to non-sensitive diagnostics; the source does not establish universal error sanitization. For minimum-stay refusal, preserve the line order: `No quote: minimum stay`; the optional nightly-pause sentence below; `At {{LOCATION_NAME}}, a {{ROOM_TYPE}} is booked for {{MINIMUM_NIGHTS}} nights or more.`; the studio sentence when applicable; the weekly-price sentence when a rate exists; and `Book at {{BARE_FORM_URL}}, or call {{PHONE}}.` The optional nightly-pause sentence applies to a nonstudio minimum of at least seven: "Nightly stays are temporarily paused so we can make room for guests staying a week or longer, up to {{MAX_STAY_NIGHTS}} days." The studio sentence is: "A studio here is sold by the week only: it has no nightly rate, so it is never booked for under a week." It continues: "A single or a double here has a nightly rate, subject to availability: ask quote_stay with roomtype single or double for the same dates." The weekly-price sentence is: "A week in a {{ROOM_TYPE}} is {{WEEKLY_PRICE}}, before {{TAX_LABEL}}." For nonstudio rooms insert ", which is {{DAILY_EQUIVALENT}} a day" before the tax clause. This path keeps its bare form. | Other inherited refusal header | Following line | Form and phone after booking wrapper | |---|---|---| | No quote: weekly-only room booked for under a week | A studio here is sold by the week only: it has no nightly rate, so there is no price for a stay of under a week in one. | Both for known request | | No quote: over the {{MAX_STAY_NIGHTS}}-day cap | Every stay is capped at {{MAX_STAY_NIGHTS}} days. {{STAY_POLICY_REASON}} The latest allowed checkout from {{CHECKIN}} is {{LAST_CHECKOUT}}. | Both for known request | | No quote: unknown room type | The argument is roomtype (roomType, room_type, type and room are read the same way). {{LOCATION_NAME}} sells: {{ROOM_TYPES}}. | Both for known request | | No quote: check in is in the past | That arrival date has already passed. | Both for known request | | No quote: check in is too far ahead | That is further ahead than this business books. | Both for known request | | No quote: check out is not after check in | Check out has to be after check in. | Normally caught by hub validation first | | No quote: unreadable dates | No additional sentence | Phone; form only when both dates resolve | The unsupported-type answer can also say: "Our sister properties nearby have studios by the week: find_rooms on {{HUB_ORIGIN}}/mcp checks all {{LOCATION_COUNT}} properties, or see {{LOCATIONS_URL}}." Include that only when those alternatives exist. The four new-tool schemas still accept `roomtype` only; inherited read-tool aliases do not relax their validation. The confirmation message is also contractual: "Bring photo ID. The guest agreement is signed at check-in before you receive a key. Guests under 18 years of age must be accompanied by a parent or legal guardian. A parent or guardian must agree in writing to accept liability for all costs and damages and provide acceptable payment. Staff will confirm in writing within 24 hours." Replace age, agreement and timing only with owner-approved policy. For a confirmed reservation, replace the last sentence with "Staff has confirmed this reservation." Other returned states must name their actual status, not imply a new hold. ## 13. Failure table | Failure | State and guest response | Recovery obligation | |---|---|---| | Verification mail rejected | No hold; refund only the known failed allowance; invalid-address or operational fallback | New start or valid resend, without resetting limits | | Verification send answer lost | No hold yet; delivery uncertain and budget retained | Never assume that no email arrived | | Reservation notice mail down | Hold remains; return reference and actual status with delivery warning | Queue notice work and expose pending or exhausted delivery to staff | | Target location unreachable | No invented vacancy, lookup absence or cancellation success | Return outage or uncertain wording appropriate to the operation; retry same identifier | | Another location unreachable after proof | Target can proceed; remote contact history is incomplete | Staff screen identifies unchecked locations before confirmation | | Shared code-counter write fails | No code sent without acknowledged admission | Fixed pause response; no local daily fallback | | Reservation write fails before hold | No success response; preserve or clean registered pending work | Retry/reconcile; say no reservation was made only when no operation can still finish | | Inventory conditional write loses a race | No oversale or second reference | Re-read current inventory; return last-room or pending-cap refusal | | Record write fails after hold | Inventory remains authoritative | Recover the record from the held row; do not create another hold | | Cancellation record saves but index update fails | Cancellation may have gone through; inventory release can still be pending | Sweep repairs the index and notices using durable cancellation markers | | Slow store | Clip each awaited call to remaining operation deadline | Return uncertainty by the hub deadline; retain proof and pending work | | Client gives up or loses the answer | Server work might already have committed | Same identifier and code, or phone; never infer failure from client silence | The hub's 44-second budget is shared, not the sum of every maximum subrequest. Proof reads, contact coordination and parallel location reads consume it before the target mutation. The target call is clipped to remaining time and its own 25-second deadline, shortened further by a contact-marker deadline. Test the worst awaited path and the response race. A timeout does not roll back a remote write already accepted. ## 14. Scheduled sweep Run every 15 minutes with a 24-second work deadline. Purge expired intents, age recovery receipts and remove old unindexed pending records after 24 hours. Repair cancelled-record/index mismatches and unfinished notice setup. Retry due notices. A held index row with no record becomes eligible for release after ten minutes. A newly held record gets three minutes before missing-notice reconstruction, so a sweep does not race ordinary submission work immediately. Rotate both priority and ordinary audit cursors. Process up to nine priority rows and fill the remaining part of a twelve-row batch with ordinary audits, in groups of three. Old completed rows must not starve active retries. A failed notice after the 36-hour window marks staff attention; exhausted notices do not retry forever without intervention. A cleanup exception on one row must not erase evidence for other work. Use the host's actual scheduled invocation contract. A non-production build marker always refuses. A nonempty runtime context must be production and explicitly published. An empty runtime context is treated like a missing one, not like a failed published check. Without runtime context, require the production build marker and a parseable scheduler `next_run` value. The marker is a non-secret file written into the bundle during the build, never an environment variable read at run time. Draft manual calls must fail before any write. Log one fixed success diagnostic and fixed refusals for non-production build, non-production runtime, missing published evidence, missing build/runtime evidence and missing scheduler evidence. Do not include guest data. Host summary lines can arrive before program log text. Observe the eventual success line at every location; if one is absent from a window, inspect its next run. Never turn a blank first line into a claim that the job refused. ## 15. Staff screen and notices Add the chat source label, agreement-at-check-in reminder, unread-location warning, pending delivery and exhausted delivery status. Phone-match details belong only here. Show reference, location, state, arrival, departure, nights and total connected span on every relevant row. Match stored web phones with extensions too. Enforce the existing staff scope before opening another location's reservation; an old note can name a reference that a limited clerk cannot open, which remains an accepted limitation. Keep staff action links and Reply-To behavior as before. No cross-guest match information enters owner mail, guest mail, cancellation mail, staff-decision mail or nightly mail. Treat guest text as data. Chat names keep normal punctuation but have control and formatting characters removed; mail applies bounded link neutralization. Chat notes use broader link and path removal. Do not apply those repairs to the web path in this change. Provide settings for the location booking switch and chat limits. Audit who changed them. Expose interrupted holds, cancellation repairs and notice failures so staff can check the actual state before taking another action. A failed email does not mean the reservation is absent. Staff need a reference they can act on even when the guest's email is delayed. ## 16. Production detection and draft safety Interactive booking tools require a nonempty production context, `published: true` and the configured custom hostname. A named draft context, unknown context, stale production deployment or non-custom hostname refuses. Host evidence must come from runtime configuration, never a tool argument or a caller-controlled forwarded header. Test how the host constructs request URLs instead of trusting an environment variable available only during builds. Drafts may lack production-only secrets. The worked staff screen therefore showed "not configured" and its authenticated storage diagnostic could not run. Do not weaken the booking guard or copy production guest data to make a draft test pass. Use an isolated, token-guarded diagnostic with throwaway keys if a separately authorized real-store check is necessary, and remove it afterwards. The per-location `bookingEnabled` setting is the operational off switch and preserves the form route. In the shipped code a missing policy defaults to enabled; an unreadable policy is an error, not evidence that the owner switched booking off. For rollout, explicitly set any desired disabled state rather than relying on missing settings. Keep recovery records and existing reservations when turning chat off. ## 17. Deployment order and gates Freeze reviewed code and checker bytes. Record the suite result against those bytes and bind deployment to a commit and the expected published deployment. Recheck the fingerprint immediately before publishing. A moved branch or changed checker invalidates the release evidence. Keep one production writer per site and one owner of a shared clone. Deploy locations first and check every one. Pass the unchanged-read-tool, pricing, signature, form and staff regressions. Observe a scheduled completion at each location. Complete the real-client draft test. Only then expose booking through the hub. The guest booking capability changes at that final step. For a single location, the same sequence still applies. Prepare listing edits before the switch. Keep the normal form and phone as fallback and retain the per-location off switch. A complete rollback may need a reviewed production-context deployment; never publish a draft as an improvised rollback. Preserve receipts and interrupted-operation data. If a deployment answer is lost, stop and identify what actually published using read-only hosting records. Verify byte provenance and current published status before resuming. Do not invent a receipt, relax guards or redeploy blindly. Resume only remaining locations after checked receipts establish the partial state. A completed deployment with an interrupted check needs a check, not another deployment. Run the full reservation-to-email end-to-end check immediately after release. The worked early run passed 33 of 33 checks; this is recorded release evidence, not a test rerun by this document. Real bookings hold inventory and notify staff. Inform staff, choose dates well ahead, record how cancellation will work, and finish inside the permitted hours or use the agreed phone route. ### Owner test script 1. [ ] Agree the test with staff, choose suitable dates and record the starting availability count. Expect a staff cancellation to use staff-decline wording, which can read oddly for a test. 2. [ ] Ask the assistant for a quote in plain language. Compare the total with the booking form for the same dates, unit and guests. 3. [ ] Give your name, email and phone, with a note marking the reservation as a test. 4. [ ] Read what the client shows before it sends anything. Choose **allow once**, not **always allow**, even if the latter is highlighted. Allow once is the safer test choice. 5. [ ] Confirm the answer says nothing is held yet. 6. [ ] Receive the code by email. The owner gives the code personally. Keep a person in the loop; an assistant driving a browser should not type a credential on a live site. 7. [ ] Confirm a reservation email arrives with a reference. Record the timing privately without copying a real reference into public notes. 8. [ ] Confirm the owner notice arrives. In the signed-in staff screen, check that the booking is labelled as coming from chat. 9. [ ] Cancel the test reservation from the staff screen. 10. [ ] Confirm the cancellation email arrives and availability returns to the count recorded before the test. 11. [ ] Check guest mail for local time. Our code email showed a raw UTC timestamp even though assistants can convert times for guests. Show local time in guest mail. 12. [ ] Verify the outcome from emails and availability. Another AI session or a person may close tabs or delete chats while you work, so the chat is not the final record. In our first real owner attempt, the code email arrived within seconds. The reservation email followed about forty seconds later. Staff cancellation followed about two minutes after that, and availability returned to its starting count. Roughly two and a half minutes passed from code to cancellation. This was one owner test, not guest results or a delivery-time promise. ## 18. Tests that must exist Use isolated synthetic stores, fixed clocks, controlled randomness and fake mail for destructive or failure testing. Keep test files and fixtures on disk. A new reviewer reruns the artifacts they rely on rather than accepting another session's summary. Each repaired defect needs a negative control: demonstrate the test fails on the old implementation and passes on the repaired one. A test that passes both does not establish the repair. Compare full response bytes in paired worlds with and without matching stays. Cover start, identical start, resend, wrong code, expired intent, exhausted intent and operational failures. Keep requested dates, inventory, random identifiers and code-send history identical. Before proof, email or phone stay history cannot alter responses, mail or holds. After proof, compare the caller's own email with somebody else's phone in matching and nonmatching worlds. Include links, recipients, subjects, Reply-To, bodies and persisted effects. Do not normalize away the leak under test. After proof of the matching email, exercise pending, overlap, completed-adjacent and chained intervals, including one-night gaps. All contact refusals use the fixed sentence. Test invalid references, valid-shaped absent references, wrong last four and recognized wrong location prefixes. Invalid syntax may produce a field error; well-formed private lookup failures must remain indistinguishable. Quote the complete owner email into a simulated reply to prove no other guest's information escapes. Run old and new web handlers side by side with identical clocks, stores and mail. Include valid and missing signatures, empty signature images, forged chat source markers, invalid stays and unusual names. Fail each store read and write in turn, including before notices, after the hold and between record updates. Compare responses, held rooms, records and every mail byte. Test missing mail configuration and failed guest delivery during staff actions. Include bursts and slow operations; the original path must remain equal. For concurrency, test many starts at, below and above every mail ceiling, including resends, reset boundaries and settings edits. Compare admission counts with actual fake-mail calls. Exercise hub down, up and down again and a 700 ms store. A 700 ms latency is a test scenario, not a supported performance guarantee. Confirm the 20 group booking, 5 group cancellation and 6 per-address daily ceilings. Race the last room and both pending ceilings. Normal operation must respect three local and eight group pending holds. Simulate the documented outage mode separately and measure its bound rather than claiming eight there. Run identical starts in parallel and prove one identifier and one email. The repaired release demonstrated 60 consecutive parallel rounds. Race identical confirms with delayed cross-location reads; losers must get in-progress wording and later recover the same reference. Test every cancellation boundary on both steps: office opening and closing, Sunday, exact 48-hour cutoff, daylight saving transitions, wrong code, expired code, exhausted code, replay and lost index update. Cancellation cannot spend booking-code budget or return a new-booking link. Verify lookup lockout counts only failures and successful lookup clears the counter. Inject interruption before and after every durable boundary: pending registration, record creation, inventory hold, proof receipt, notice claim, cancellation record and index release. Run the sweep after each and assert recovery or a visible staff action. Check expiry without future guest traffic. Exercise more rows than a sweep batch to prove cursor fairness. Test worst-case awaited durations against the 44-second response deadline and scheduler budget. Retain fixed read-tool calls and compare prices for all room types over 1 through 30 nights. Require tests for every tool name a check script calls against the location's actual tools list. Rerun a previously flaky suite on clean copies until its repaired repeatability claim is supported. Historical repeated runs are evidence of that release only, not an exemption from testing your adaptation. ## 19. Host facts to verify on a draft Run every checker against a real draft early. If a checker reports a block or refusal, call the server directly to distinguish a checker bug from a server failure. For JSON-RPC, use HTTP status and content type to classify transport blockage; words inside a valid response are not evidence of a blocked connection. The worked checker accidentally called a tool absent from location servers after passing fake-server tests. Verify runtime deployment shape, published status, custom-host detection, absent secrets, cold starts, scheduled invocation and conditional storage behavior. Use only throwaway records for storage proof. The observed diagnostic rejected stale compare-and-swap and create-only overwrite on five of five runs. Twenty parallel writes to one version yielded exactly one winner. Those samples prove the tested behavior at that time, not universal latency or availability. Add the draft as a temporary connector in a real client. Confirm all eight hub tools load, inspect their sort order, run one read tool and one booking tool that must refuse, then remove the connector. Location servers in the worked implementation expose three read tools, not the hub's eight. Enumerate the actual server rather than copying a tool count from a runbook. Client loading alone does not test a complete booking. ## 20. Listing and registry steps Prepare descriptions, examples, privacy text, authentication setting, tool list and reviewer instructions before exposing booking. A live listing saying "never books a room" becomes false at the switch. The worked mismatch lasted about twenty-five minutes. Save the prepared changes immediately afterwards and verify the public page itself. Use one editor tab. Refresh your own connector's tool list first, reopen the editor and then sync. A previously opened page can retain the old list and count; a second stale tab can overwrite good edits. Existing users may need refresh or reconnection. Do not promise that reconnect behavior is identical across clients. Run every tool before attesting to self-testing. An earlier checked box can remain checked after the tool set changes. Reviewer instructions must identify calls that hold real inventory, how to release it, office hours, cutoff rules and what a rate-limit answer means. State whether sign-in is required truthfully. Check destination rules for emailed codes before submitting this design there. Update registry and discovery records alongside public instructions. Public text may clear before an authentication change, and the public page may lag the editor. The observed timings are not an approval promise. Keep the existing form route available while reviews or client refreshes are outstanding. ### Community and Verified checklist - [ ] Read the [verification guide](https://claude.com/docs/connectors/verification.md). There is no application for Verified and no action to take. Submissions are scanned automatically and listed as Community by default. Escalation is assessed automatically for listings flagged as highly useful to Claude users. Verified review is slower and involves closer review, with a functional test of each tool. - [ ] Explain the label accurately. It affects display and discovery, not capabilities once connected. Verified shows a checkmark. Community shows a label and a reminder before connecting. Verified is not a security audit. - [ ] Meet the same [review criteria](https://claude.com/docs/connectors/building/review-criteria.md) whichever label the listing has. The [submission rules](https://claude.com/docs/connectors/building/submission.md) require Verified before a listing where users connect to different URLs can be published. - [ ] Use mcp-review@anthropic.com for review escalations, as the submission page directs. It is not a way to request the label. This applies per listing; a business with several connectors has nothing to file across all of them at once. - [ ] Treat the following as our inference, not Anthropic's published escalation criteria: link the listing from your documentation and website to encourage real use. Keep tool errors low and all fields accurate. A wrong sign-in setting can stop connections and hold usage down. Keep reviewer instructions current for every tool, including releasing test reservations. Keep data-handling answers true. None of this guarantees Verified status. - [ ] Watch [portal usage, tool calls and error rate](https://claude.com/docs/connectors/building/managing-your-listing.md). A new listing starts with no data. Daily metrics can lag by 24 hours and omit low-volume results. ### Registration tracker Checked November 7, 2026. Copy this table and keep one row for every actual listing or account connection. Replace the blanks. Use statuses such as not started, testing, submitted, live, update pending or paused. If a directory shows no version, write "not shown" and record the tools you actually checked. A submitted form is not a live listing. | Place | Status | URL | Version listed | Tools listed | Last checked | What to update when tools change | |---|---|---|---|---|---|---| | Official MCP Registry | [status] | [record URL] | [version] | [tools] | [UTC date/time] | New server.json version, endpoint and short description | | mcptry | [status] | [record URL] | [version] | [tools] | [UTC date/time] | Registry source or direct record, then check imported tools and findings | | Claude directory | [status] | [listing URL] | [version] | [tools] | [UTC date/time] | Description, examples, tools, privacy, sign-in and reviewer instructions | | Claude custom connector | [status] | [server URL] | [version] | [tools] | [UTC date/time] | Refresh and test the account connection; check authentication changes | | ChatGPT custom MCP | [status] | [server URL] | [version] | [tools] | [UTC date/time] | Refresh metadata, start a new chat and test | | ChatGPT public plugin | [status] | [listing URL] | [version] | [tools] | [UTC date/time] | Server checks; new ZIP for changed package metadata or skills | | Gemini custom connection | [status] | [server URL] | [version] | [tools] | [UTC date/time] | Sync schemas and rerun customer prompts | | Google Connected Apps preview | [status] | [app URL] | [version] | [tools] | [UTC date/time] | New version, synced tools, review and publication | | Google lodging partner route | [status] | [partner or application URL] | [version] | [capabilities] | [UTC date/time] | Confirm requirements and changes with the admitted partner | | PulseMCP | [status] | [listing URL] | [version] | [tools] | [UTC date/time] | Check intake pause and Registry source; record any pending correction | | Glama | [status] | [listing URL] | [version] | [tools] | [UTC date/time] | Registry source or owner-managed details, then check the visible entry | | mcp.so | [status] | [listing URL] | [version] | [tools] | [UTC date/time] | Confirm edit route; update description, endpoint and tools | | MCP Market | [status] | [listing URL] | [version] | [tools] | [UTC date/time] | Confirm edit route; update description, endpoint and tools | ### Per-place checks - [ ] **Official Registry:** follow the [publisher instructions](https://modelcontextprotocol.io/registry/quickstart). Prepare server.json, prove the namespace, validate and publish a new version. Coordinate the publisher's single saved login for the computer account. Confirm the public record. - [ ] **mcptry:** check the [automatic Registry import or optional direct submission](https://mcptry.com/submit). Claim only if useful. Compare imported tools and dates, investigate findings and recheck after changes. A scanner grade does not prove a completed booking. - [ ] **Claude directory:** follow the [listing kit](https://jwatte.com/downloads/mcp-connector-listing-kit.md) and its ten steps and [current publishing guide](https://claude.com/docs/directory/publish). Confirm paid submission access and the owning account. Refresh the custom connection, reopen one editor, then sync. Check sign-in, locked answers and the public page. Ask the review team to change fields the editor locks; these were issues in our test. - [ ] **Claude custom:** use the [unlisted-connector route](https://claude.com/docs/connectors/custom). Free allows one; workspace setup needs appropriate permissions. Run the customer prompts. A connection alone is not a directory listing. - [ ] **ChatGPT custom:** check plan and workspace access in the [registration comparison](https://jwatte.com/blog/mcp-connector-listing-small-business/#where-to-register-and-what-each-place-gives-you). Follow the [connection guide](https://developers.openai.com/plugins/deploy/connect-chatgpt), refresh after tool changes and retest in a new chat. - [ ] **ChatGPT public plugin:** prepare [verified publisher identity, domain proof, ZIP, five success cases, three negative cases, video and support/privacy links](https://developers.openai.com/plugins/deploy/submission). Check [commerce and restricted inputs](https://developers.openai.com/plugins/plugin-guidelines), including one-time codes. Do not copy this booking flow into a submission before resolving eligibility. - [ ] **Gemini custom:** connect in [Developer Mode](https://developers.google.com/connected-apps/mcp/dev-mode?hl=en), sync schemas and record actual tool calls and results. In our test, connection succeeded but two chats stalled on web search. That session did not prove execution or establish a product-wide failure. - [ ] **Google Connected Apps:** confirm [preview access](https://developers.google.com/connected-apps?hl=en), then follow [domain proof, tool sync, testing, review and versioned publication](https://developers.google.com/connected-apps/gpc/perform-the-steps). Do not label a private test connection as published. - [ ] **Google lodging:** check [programme admission](https://developers.google.com/hotels/ucp) and the partner route first. The small lodging group's declined application is its own outcome. Do not budget on acceptance or confuse this with Connected Apps preview. - [ ] **PulseMCP:** check its [paused submissions and edits](https://www.pulsemcp.com/submit) before relying on a listing. Publish accurate Registry data and check again when intake resumes. - [ ] **Glama:** follow its [submission and ownership checks](https://glama.ai/mcp/faq). Record whether Registry data or owner-managed details control the listing. Check the visible tools after an update. - [ ] **mcp.so:** inspect the [current remote-server form](https://mcp.so/submit?type=remote-server). Confirm the editing route before paying. Its paid badge does not prove security review. - [ ] **MCP Market:** inspect the [submission options](https://mcpmarket.com/submit). Confirm ownership and editing arrangements before relying on the entry. A paid badge is not assistant approval. Assign one person to this tracker. Update every entry the same day the tools change. Record pending review or delayed import and check again until the public entry matches. Saying "read-only" after the server starts taking bookings is a false statement to customers. Keep support, privacy, sign-in, examples and tool effects in step too. ## 21. Reviewer brief Give the reviewer code, fixtures and the owner's decisions, not a conclusion that it is safe. Use a separate reviewer session or model. Define an ownership list for folders, clones, sites and shared logins. Search any copied harness for absolute paths before executing it, and fingerprint protected material before and after. A script can write outside its apparent working folder. The reviewer checks every number and refusal against actual handlers, independently runs privacy and old/new comparisons, and identifies what only the host can prove. Keep review artifacts available across sessions, but rerun relied-on tests. A narrow repair brief lists exactly permitted changes. Its result lists every changed file and reason; compare against saved original bytes to prove that the repair stayed narrow. Coordinate shared publisher credentials explicitly. Announce taking a shared login, serialize shared clone work and never share browser tabs. Do not infer that another session closed a tab. Pin production operations to reviewed bytes and require the deployment to refuse when those bytes or the expected production state moved. ## 22. Accepted risks One person with several mailboxes can reserve up to configured caps. Staff confirmation remains the backstop. A supplied phone is not authority to refuse a booking. A caller holding every detail of an open request can discover its in-progress intent and exhaust wrong attempts, forcing the guest to start again. Mailbox proof does not fix that denial-of-service risk. Owner mail carries token staff links and its Reply-To is the guest, so a quoted staff reply can hand a guest the Confirm and Cancel links for their own booking. Tell staff not to quote it, or require sign-in on those links. Shared-control outages weaken group pending and contact coordination as described above. Mail delivery can be delayed or duplicated beyond the provider's suppression window. Secret rotation invalidates replay proofs and resets secret-derived identity history. Full reservation records need a separately specified deletion policy. None of these limitations should disappear from the operator notes merely because the happy-path test passed. ## 23. Open items that may be left for later The shipped name filter still lets a link behind one leading character survive in staff mail. Five blank-looking characters also pass name validation: U+3164, U+FFA0, U+115F, U+1160 and U+2800. A corrected plus-address at a provider outside the dot-folding family may retain the first destination until the open intent expires, up to 15 minutes. Limited staff accounts can see a thin or stale cross-location match note. Record these as deferred repairs; do not claim the filters or address replacement are complete. A separately reviewed change tool, alternative verification channels, stricter past-date rules, improved text filtering and a stronger group-wide admission policy can follow later. They must not be silently enabled by this specification. Real-device signature checks, translated policy review and first approved delivery tests remain business-specific acceptance work. Any item essential to the owner's promised behavior must be finished before launch rather than placed in this optional list.