Lucilla is the local-business layer any agent can call.
Ask Lucilla is a Model Context Protocol server. Connect Claude, Muse, ChatGPT or your own client, and it acts for one signed-in person in their own Lucilla account: finds deals and bookable listings nearby, claims and reserves, and, for an owner, runs the shop. Every permission is a checkbox the person can untick, and anything that moves money or reaches other people describes the consequence first and needs a second explicit call.
Nothing to install. Paste one URL into the agent you already use.
Ask Lucilla is a remote MCP server. Your agent opens our sign-in page once, you choose what it may do, and from then on it can search deals, claim, book and message businesses for you.
claude mcp add --transport http ask-lucilla \
https://asklucilla.com/mcpSettings → Connectors → Add custom connector, and paste the URL below. Muse: search the connector directory for Ask Lucilla once it is listed.
https://asklucilla.com/mcp{
"mcpServers": {
"ask-lucilla": { "url": "https://asklucilla.com/mcp" }
}
}Bridge with the public mcp-remote package. No Lucilla package needed.
npx mcp-remote https://asklucilla.com/mcpSign-in is OAuth 2.1 with PKCE and dynamic client registration; no API key to copy. Business owners get the owner tools automatically when the account they sign in with owns a listing.
- OAuth 2.1
Authorization code + PKCE S256. Public clients. Tokens bound to this server.
- 8 scopes
Read-only is read-only; writing listings is never charging a card.
- 33 tools
16 for customers, 17 for business owners. All listed, each scope-guarded.
- Asks first
11 tools return a consequence and a confirm_token before they act.
Where it lives.
One MCP endpoint and one OAuth issuer, both on Google Cloud in us-east1. Everything a client needs is discoverable from the first 401.
https://asklucilla.com/mcpStreamable HTTP. JSON-RPC 2.0 over POST, one request per POST.
https://asklucilla.com/mcp/.well-known/oauth-protected-resourceRFC 9728. Names the authorization server and the baseline scopes.
https://asklucilla.comRFC 8414 / OIDC issuer identifier. Also the audience every token is bound to.
https://asklucilla.com/.well-known/oauth-authorization-serverThe same document is served at /.well-known/openid-configuration under the issuer.
https://asklucilla.com/authorizeSign-in and consent screen. Opens in the person's browser.
https://asklucilla.com/tokenauthorization_code and refresh_token grants. Public clients only (no client secret).
https://asklucilla.com/revokeRFC 7009.
https://asklucilla.com/registerRFC 7591 dynamic registration. Client ID Metadata Documents are preferred.
https://asklucilla.com/scopesThe consent wording, machine-readable. Not part of any RFC.
Protocol versions
Newest first. A client that sends none is treated as 2025-03-26. An initialize request selects the legacy handshake era; the MCP-Protocol-Version header (or _meta) selects the stateless 2026-07-28 era, where Mcp-Method and, for tools/call, Mcp-Name headers are required and must match the body.
- 2026-07-28
- 2025-11-25
- 2025-06-18
- 2025-03-26
Transport rules
- POST only. GET and DELETE answer 405: there is no standalone SSE stream and no protocol-level session.
- One JSON-RPC request per POST. Batches are refused with 400.
- Request bodies over 512 KB are refused with 413.
- Methods:
initialize,server/discover,ping,tools/list,tools/call. Capabilities: tools only. - An
Originheader, if sent, must be https or localhost, or the request is refused with 403. - Server name
ask-lucilla, version 1.0.0.
The person is always in the loop.
OAuth 2.1 as the MCP authorization spec (revision 2026-07-28) describes it. No implicit grant, no password grant, no client credentials. Every token is minted for one person, one client, and this one server.
- 01 · Discover
Start with a 401
Call the endpoint with no token. The WWW-Authenticate challenge carries the protected-resource metadata URL and the baseline scopes (profile.read deals.read). Follow it to the authorization server metadata.
- 02 · Identify
Bring a client_id
Preferred: a Client ID Metadata Document, where client_id is an https URL to your own JSON document. RFC 7591 dynamic registration at /register also works. Public clients only: there is no client secret, and token_endpoint_auth_method is none.
- 03 · Authorize
Authorization code + PKCE
Redirect the person to /authorize with response_type=code, code_challenge_method=S256 (plain is refused), the scopes you need, and resource set to the MCP endpoint (RFC 8707). Add offline_access if you want a refresh token.
- 04 · Sign in
Password never reaches us
The consent page signs the person in with the Firebase Auth web SDK, directly against Google's identity service: email and password, Google, or Apple. Our server only receives the resulting ID token, verifies it and discards it. The client never sees it.
- 05 · Consent
One checkbox per permission
The screen names your client and the host the code will be sent to, then lists each requested scope in plain words with its consequence, all ticked. The person can untick any of them; the token is genuinely narrowed. They have 10 minutes to decide.
- 06 · Exchange
Code for tokens
POST the code and code_verifier to /token within 60 seconds. You get a 30-minute access token bound to this server and, with offline_access, a refresh token that lasts 60 days of inactivity and is rotated on every use. Send it as Authorization: Bearer.
The first call
POST https://asklucilla.com/mcp
Content-Type: application/json
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list" }
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://asklucilla.com/mcp/.well-known/oauth-protected-resource", scope="profile.read deals.read"Step-up: 403 insufficient_scope
tools/list deliberately lists every tool whatever the token holds. Calling one the person did not grant returns 403 with the scope it needs. Re-run the authorization asking for just that scope; the person sees one new checkbox. Do not retry blindly; tell them which permission is missing.
HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope", scope="shop.money", resource_metadata="…"
{ "jsonrpc": "2.0", "id": 7,
"error": { "code": -32003, "message": "…", "data": { "requiredScopes": ["shop.money"] } } }Lifetimes
- Access token
- 30 minutes
- Refresh token
- 60 days of inactivity, rotated on every use
- Authorization code
- 60 seconds, single use
- Sign-in and consent window
- 10 minutes
- confirm_token
- 10 minutes, single use, bound to one tool
- availability_token
- a few minutes, re-verified when the order is written
Revoking
A client revokes its own tokens at /revoke (RFC 7009). The person can disconnect any client at any time from the You tab in the Ask Lucilla app, which deletes the grant and every token under it. Access tokens die on their own after 30 minutes either way.
Audit
Every authorization event and every tool call is written to a server-side audit log, with the tool, the outcome and, for writes, what changed (an order id, an amount, a status).
Eight permissions, in the words the person sees.
The label and consequence below are the consent screen's own text, served from one source at https://asklucilla.com/scopes. The hierarchy is deliberately shallow: shop.write does not imply shop.money, and nothing implies spend.
| Scope | Consent label | What the person is told | Implies |
|---|---|---|---|
profile.read | See who you are | Your @username, display name and city. Not your phone number or email. | — |
deals.read | Look around for you | Search deals, shops and listings near you, and read the codes and bookings you already hold. | — |
deals.claim | Claim deals and reserve things | Take a deal or hold a booking in your name. This creates a real obligation to a shop, and some holds cannot be undone without contacting them. | deals.read |
orders.write | Manage your own orders | Cancel one of your orders, or report a problem with it. Only orders you placed yourself. | deals.read |
shop.read | Read your business | Your listings, incoming orders, leads and numbers for the business you own. Customer contact details stay hidden. | — |
shop.writereal money or real people | Run your business | Create and edit listings, deals, posts, hours and branding, and message your followers. Posts and follower messages reach real people and cannot be unsent. | shop.read |
shop.moneyreal money or real people | Handle your business money | Fulfil an order, which CHARGES the customer, cancel an order, which REFUNDS them in full, and act on disputes. This moves real money. | shop.read |
spendreal money or real people | Spend your money | Buy things for you, up to the limit you set in the app. This spends real money from your wallet. | — |
offline_access is an OAuth control scope that asks for a refresh token and grants no access to anything. It is accepted at the authorization server and never appears in a challenge.
Nothing that costs money or reaches a stranger happens on the first call.
A tool marked asks first resolves and validates everything, writes nothing, and returns the consequence in plain words with any amount named, plus a single-use confirm_token. Only a second call carrying that token acts, and it acts on the payload parked by the first call, not on its own arguments. The token expires after 10 minutes, works for one tool only, and is deleted before the action runs, so a retried confirmation cannot capture or refund twice.
Every tool also carries _meta in tools/list: ca.lucilla/requiredScopes, ca.lucilla/effect and ca.lucilla/confirms, so a client can show what a call needs before the person hits a 403.
// 1. Describe. Nothing changes.
tools/call fulfil_order { "order_ref": "ABC123" }
→ { "consequence": "This will mark ABC123 fulfilled — 2 x \"Lunch special\". $24.00 CAD already held on the customer's card will be CAPTURED. …",
"confirm_token": "…", "expiresInSeconds": 600 }
// 2. Show the consequence to the person. Only if they say yes:
tools/call fulfil_order { "order_ref": "ABC123", "confirm_token": "…" }
→ { "done": true, "status": "fulfilled", "already": false, … }Every tool, grouped by who it is for.
Names are snake_case and stable. Which group works depends on who signed in: whoami says whether the account runs a business, and every shop_* tool refuses on one that does not. Limits are per person and per client, each tool in its own bucket.
Customers
Finding things, claiming and reserving, the person's own bookings, and the money their agent may spend.
| Tool | Scope | Asks first | Limit | What it does · example arguments |
|---|---|---|---|---|
ask_lucillawrite | deals.read | no | 30/min | Talk to Ask Lucilla in plain language, exactly as a person texting the agent would. Returns the agent's reply as text. The agent asks for a YES before it publishes, books or spends; pass the user's answer back through this same tool. Needs a verified phone on the account (reply carries needs_phone).
|
whoamiread | profile.read | no | 100/min | Who this session belongs to: @username, city, whether they run a business, and exactly which permissions they granted. Call it first. Returns no email or phone number.
|
get_delivery_addressread | profile.read | no | 100/min | The delivery address saved for agent purchases, or null.
|
get_spend_settingsread | profile.read | no | 100/min | The spending limits this person set for their agent and how much of the month is left. Amounts in cents. enabled: false means agent purchases are off entirely.
|
search_listingsread | deals.read | no | 100/min | Bookable and buyable listings near a point: tables, appointments, class seats, pickup orders, products. Requires lat and lng; filter by listing_type (service, product, food). Radius 0.5 to 50 km, up to 25 rows.
|
get_listing_availabilityread | deals.read | no | 100/min | Open times for one listing plus the short-lived availability_token that reserve_listing requires. Window up to 31 days.
|
my_saved_codesread | deals.read | no | 100/min | Deal codes this person holds: what for, which shop, active, redeemed or expired. The single-use burn link is never returned.
|
my_bookingsread | deals.read | no | 100/min | Everything this person has reserved, ordered or booked, with the order_id and code the counter asks for. The redeem token is never returned.
|
reserve_listingwrite | deals.claim | yes | 6/min | Reserve or order a listing in this person's name. Creates a real obligation to a shop. Needs a fresh availability_token. Takes no payment: pay-at-counter listings confirm immediately; card listings come back held and are paid in the app.
|
follow_shopwrite | deals.claim | no | 30/min | Follow a business so its posts and follower drops reach this person. Idempotent, reversible.
|
unfollow_shopwrite | deals.claim | no | 30/min | Stop following a business. Idempotent.
|
cancel_my_bookingwrite | orders.write | yes | 30/min | Cancel one of this person's own bookings. The first call says whether the deposit is refunded or forfeited, and for how much.
|
report_order_problemwrite | orders.write | yes | 30/min | Open a dispute on an order this person paid for or reserved (not_served, not_as_described, charged_wrong, business_closed, other). Holds the shop's payout while Lucilla reviews it.
|
set_delivery_addresswrite | spend | no | 10/min | Set or clear the address anything the agent buys ships to. Canada and the US only.
|
set_spend_settingswrite | spend | when widening | 10/min | Change the agent's spending limits (cents). Turning purchases on or raising any cap confirms first; turning off or lowering applies immediately.
|
approve_agent_purchasewrite | spend | when approving | 10/min | Say yes or no to a purchase parked over the approval threshold. Approving does not charge by itself; caps are re-checked at payment. Declining applies immediately.
|
Business owners
Reading the shop, running it, and the three tools that touch money. business_id is optional everywhere: omitted, it resolves to the business the person runs, including one set up over text.
| Tool | Scope | Asks first | Limit | What it does · example arguments |
|---|---|---|---|---|
my_shopread | shop.read | no | 100/min | The business this person runs: id, name, handle, category, city, live or paused, whether it can take a card, followers, trust tier. Call before any other shop tool. business_id is optional on every shop tool.
|
shop_listingsread | shop.read | no | 100/min | The business's own listings with status, price, pay mode and booked, fulfilled and cancelled counts. Includes drafts and paused.
|
shop_ordersread | shop.read | no | 100/min | Orders and bookings in a time window, with order_id, code, slot, money and whether prepaid. The customer appears as an @username only.
|
shop_calendarread | shop.read | no | 100/min | Blocked-off time, including busy time synced from Google Calendar (editable: false).
|
shop_leadsread | shop.read | no | 100/min | People who claimed a deal and consented to be contacted, as @usernames with city and what they claimed. Raw email and phone are not available here.
|
set_listing_statuswrite | shop.write | no | 30/min | Take a listing live, pause it or return it to draft. Pausing stops new bookings and leaves existing ones alone.
|
block_shop_timewrite | shop.write | no | 30/min | Block a span so nothing can be booked in it. Does not cancel bookings already inside it. At most 31 days per block.
|
unblock_shop_timewrite | shop.write | no | 30/min | Remove an owner block. Google-synced blocks are refused.
|
create_keyword_promowrite | shop.write | yes | 30/min | Set up a text-to-claim offer: customers text a keyword and get a code. Publishes a public offer the business must honour. Audience, frequency, cooldown and supply cap are enforced at claim time.
|
set_keyword_promo_activewrite | shop.write | no | 30/min | Pause or resume a keyword promo. Codes already issued stay valid.
|
set_shop_brandingwrite | shop.write | no | 30/min | Set or clear the logo or cover image by public https URL. Uploads nothing.
|
publish_shop_postwrite | shop.write | yes | 3/min | Publish a post to followers' feeds. Public, reaches real people, can be taken down but not un-seen. The first call returns the exact text and the follower count.
|
take_shop_post_downwrite | shop.write | no | 30/min | Take a post down everywhere. Already-down is a success, not an error.
|
send_follower_dropwrite | shop.write | yes | 3/min | Push a one-line message (1 to 140 characters) to every follower's phone. Cannot be unsent. One drop per shop per day. Consent and quiet hours are re-checked per person at send time.
|
fulfil_orderwrite | shop.money | yes | 10/min | Mark an order fulfilled. Captures the prepaid amount and starts the payout clock. The first call states the exact amount. A staff member cannot fulfil their own order.
|
cancel_order_as_shopwrite | shop.money | yes | 10/min | Cancel on the business's behalf. Always a full refund to whoever paid; the customer is told.
|
propose_order_timewrite | shop.money | yes | 10/min | Propose a new time for a confirmed booking and tell the customer, who may accept, ask for another time, or cancel for a full refund. Owner or admin only.
|
Booking is a three-step
search_listings finds an item_id; get_listing_availability mints a short-lived availability_token; reserve_listing will not write an order without one, and the server re-verifies it when the order is written. A token cached for an hour is refused.
Refusals are results, not transport errors
Bad input, a listing that cannot be moved, an order that is not this person's: these come back as a normal tools/call result with isError: true and a readable message in structuredContent. Only auth (401, 403) and rate limits (429) use HTTP status.
From Claude, Muse, or any MCP client.
There is nothing to install on our side. Give your client the endpoint URL; it discovers the authorization server from the first 401 and opens the sign-in page for the person.
Claude
In Claude.ai or the desktop app, add a custom connector and paste the MCP endpoint. Claude handles the OAuth flow and opens the consent page. From Claude Code:
claude mcp add --transport http ask-lucilla https://asklucilla.com/mcp
Muse and Meta AI
Ask Lucilla was submitted to the Muse connector directory and to Meta AI Connectors on 2026-09-27. Until it appears there, a Muse user can add it as a custom connector from the endpoint above. One connector serves both sides: a customer's Muse searches and claims; an owner's Muse checks leads or posts a deal. Which they get depends on who signs in.
Your own client
Any MCP client that speaks Streamable HTTP and the MCP authorization flow works: the official SDKs do both. Send one JSON-RPC request per POST, keep the
Authorization: Bearerheader on every call, and handle 401, 403 and 429 as described above. Discovery documents are public, cacheable and served with CORS for browser clients.
The simplest integration is one tool: ask_lucilla takes free text and returns the same reply a person texting Lucilla would get, with the same memory and the same yes-before-acting rules. The structured tools are there when your agent already knows the coordinates, the item or the order.
Per person, per client, per tool.
A leaked token should not be able to drain anything. Limits are ceilings on how fast something can be attempted; the underlying writes have their own harder stops. Exceeding one returns 429 with Retry-After: 60 and JSON-RPC error -32002.
- Reads100 per minute
whoami, search, availability, codes, bookings, every shop_* read.
- Ordinary writes30 per minute
ask_lucilla, follow, listing status, blocks, promos, branding, take-down, own-order cancel and problem report.
- Reservations6 per minute
reserve_listing. A hold creates an obligation to a real shop.
- Money10 per minute
fulfil, shop cancel, propose time, spend settings, delivery address, purchase approval.
- Broadcast3 per minute
publish_shop_post and send_follower_drop. A drop is also one per shop per day.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
{ "jsonrpc": "2.0", "id": 9,
"error": { "code": -32002, "message": "Money actions are limited to 10 a minute for an agent. Wait and retry." } }MCP traffic never shares a bucket with the person's own phone, so an agent listing orders cannot starve the app the person is paying from.
What never crosses this surface.
The rules below are enforced in the server, not promised in a policy. They hold for every client, including ours.
No passwords reach our server
Sign-in happens in the person's browser against Google's identity service. We receive a short-lived ID token, verify it, and discard it. The MCP client only ever sees tokens we mint.
No email or phone, ever
No tool returns an email address or a phone number, including the person's own. Codes and bookings are looked up by the server's own record of the account's verified number, never by an argument.
A business never sees identities
Orders and leads name a customer as an @username and a city, nothing more. Reaching them goes through a post or a follower drop that the server delivers, re-checking each person's consent at send time.
Lucilla does not hold funds
Card payments run through Stripe with the business as the merchant of record. A deposit is held on the customer's card and captured at fulfilment; a shop-side cancel always refunds it in full to whoever paid.
An agent cannot raise its own ceiling
Agent purchases are off until the person turns them on in the app. Widening any cap confirms first; approving a parked purchase does not charge, and caps are re-checked at the moment of paying.
Disconnect in one tap
The person can revoke any connected client from the You tab in the app. Every call is audited, so if a token leaks there is a record of exactly what it did.
Building on it? Write to us.
Integration questions, reviewer access, a tool you need that is not here. Terms · Privacy
