Text or WhatsApp the agent:+1 (314) 237-4046WhatsApp
Ask LucillaText Lucilla
For developers · MCP

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.

Connect in 30 seconds

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 Code
claude mcp add --transport http ask-lucilla \
  https://asklucilla.com/mcp
Claude.ai, ChatGPT, Muse

Settings → 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
Cursor, Windsurf, any mcp.json
{
  "mcpServers": {
    "ask-lucilla": { "url": "https://asklucilla.com/mcp" }
  }
}
Clients that only speak stdio

Bridge with the public mcp-remote package. No Lucilla package needed.

npx mcp-remote https://asklucilla.com/mcp

Sign-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.

Endpoints

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.

MCP endpoint
https://asklucilla.com/mcp

Streamable HTTP. JSON-RPC 2.0 over POST, one request per POST.

Protected resource metadata
https://asklucilla.com/mcp/.well-known/oauth-protected-resource

RFC 9728. Names the authorization server and the baseline scopes.

OAuth issuer
https://asklucilla.com

RFC 8414 / OIDC issuer identifier. Also the audience every token is bound to.

Authorization server metadata
https://asklucilla.com/.well-known/oauth-authorization-server

The same document is served at /.well-known/openid-configuration under the issuer.

Authorize
https://asklucilla.com/authorize

Sign-in and consent screen. Opens in the person's browser.

Token
https://asklucilla.com/token

authorization_code and refresh_token grants. Public clients only (no client secret).

Revoke
https://asklucilla.com/revoke

RFC 7009.

Register
https://asklucilla.com/register

RFC 7591 dynamic registration. Client ID Metadata Documents are preferred.

Scope catalog
https://asklucilla.com/scopes

The 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 Origin header, if sent, must be https or localhost, or the request is refused with 403.
  • Server name ask-lucilla, version 1.0.0.
Authorization

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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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).

Scopes

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.

ScopeConsent labelWhat the person is toldImplies
profile.readSee who you areYour @username, display name and city. Not your phone number or email.—
deals.readLook around for youSearch deals, shops and listings near you, and read the codes and bookings you already hold.—
deals.claimClaim deals and reserve thingsTake 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.writeManage your own ordersCancel one of your orders, or report a problem with it. Only orders you placed yourself.deals.read
shop.readRead your businessYour listings, incoming orders, leads and numbers for the business you own. Customer contact details stay hidden.—
shop.write
real money or real people
Run your businessCreate 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.money
real money or real people
Handle your business moneyFulfil an order, which CHARGES the customer, cancel an order, which REFUNDS them in full, and act on disputes. This moves real money.shop.read
spend
real money or real people
Spend your moneyBuy 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.

Confirm, then act

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, … }
Tool reference

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.

ToolScopeAsks firstLimitWhat it does · example arguments
ask_lucilla
write
deals.readno30/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).

{ "text": "tacos near me under $15", "lat": 43.65, "lng": -79.38 }

whoami
read
profile.readno100/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_address
read
profile.readno100/min

The delivery address saved for agent purchases, or null.

{}

get_spend_settings
read
profile.readno100/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_listings
read
deals.readno100/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.

{ "lat": 43.65, "lng": -79.38, "radius_km": 3, "listing_type": "food" }

get_listing_availability
read
deals.readno100/min

Open times for one listing plus the short-lived availability_token that reserve_listing requires. Window up to 31 days.

{ "item_id": "itm_…", "quantity": 2 }

my_saved_codes
read
deals.readno100/min

Deal codes this person holds: what for, which shop, active, redeemed or expired. The single-use burn link is never returned.

{ "status": "active" }

my_bookings
read
deals.readno100/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.

{ "active_only": true }

reserve_listing
write
deals.claimyes6/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.

{ "item_id": "itm_…", "availability_token": "…", "slot_start_ms": 1790000000000, "quantity": 2 }

follow_shop
write
deals.claimno30/min

Follow a business so its posts and follower drops reach this person. Idempotent, reversible.

{ "business_id": "conn_…" }

unfollow_shop
write
deals.claimno30/min

Stop following a business. Idempotent.

{ "business_id": "conn_…" }

cancel_my_booking
write
orders.writeyes30/min

Cancel one of this person's own bookings. The first call says whether the deposit is refunded or forfeited, and for how much.

{ "order_id": "ABC123", "reason": "Can't make it" }

report_order_problem
write
orders.writeyes30/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.

{ "order_id": "ABC123", "reason": "not_served" }

set_delivery_address
write
spendno10/min

Set or clear the address anything the agent buys ships to. Canada and the US only.

{ "name": "…", "line1": "12 King St W", "city": "Hamilton", "region": "ON", "postal_code": "L8P 1A1", "country_code": "CA" }

set_spend_settings
write
spendwhen widening10/min

Change the agent's spending limits (cents). Turning purchases on or raising any cap confirms first; turning off or lowering applies immediately.

{ "monthly_cap_cents": 5000 }

approve_agent_purchase
write
spendwhen approving10/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.

{ "purchase_id": "pur_…", "approve": true }

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.

ToolScopeAsks firstLimitWhat it does · example arguments
my_shop
read
shop.readno100/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_listings
read
shop.readno100/min

The business's own listings with status, price, pay mode and booked, fulfilled and cancelled counts. Includes drafts and paused.

{ "status": "live" }

shop_orders
read
shop.readno100/min

Orders and bookings in a time window, with order_id, code, slot, money and whether prepaid. The customer appears as an @username only.

{ "from_ms": 1790000000000, "to_ms": 1790086400000, "status": "confirmed" }

shop_calendar
read
shop.readno100/min

Blocked-off time, including busy time synced from Google Calendar (editable: false).

{}

shop_leads
read
shop.readno100/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_status
write
shop.writeno30/min

Take a listing live, pause it or return it to draft. Pausing stops new bookings and leaves existing ones alone.

{ "item_id": "itm_…", "status": "paused" }

block_shop_time
write
shop.writeno30/min

Block a span so nothing can be booked in it. Does not cancel bookings already inside it. At most 31 days per block.

{ "start_ms": 1790000000000, "end_ms": 1790086400000, "reason": "Closed Monday" }

unblock_shop_time
write
shop.writeno30/min

Remove an owner block. Google-synced blocks are refused.

{ "block_id": "blk_…" }

create_keyword_promo
write
shop.writeyes30/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.

{ "deal_label": "free coffee with any bagel", "keyword": "BAGEL", "duration_days": 7 }

set_keyword_promo_active
write
shop.writeno30/min

Pause or resume a keyword promo. Codes already issued stay valid.

{ "promo_id": "prm_…", "active": false }

set_shop_branding
write
shop.writeno30/min

Set or clear the logo or cover image by public https URL. Uploads nothing.

{ "logo_url": "https://…/logo.png" }

publish_shop_post
write
shop.writeyes3/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.

{ "title": "New lunch menu", "content": "…", "type": "announcement" }

take_shop_post_down
write
shop.writeno30/min

Take a post down everywhere. Already-down is a success, not an error.

{ "post_id": "pst_…" }

send_follower_drop
write
shop.writeyes3/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.

{ "text": "Fresh bagels until 2pm, 20% off with code BAGEL" }

fulfil_order
write
shop.moneyyes10/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.

{ "order_ref": "ABC123" }

cancel_order_as_shop
write
shop.moneyyes10/min

Cancel on the business's behalf. Always a full refund to whoever paid; the customer is told.

{ "order_ref": "ABC123", "reason": "Kitchen closed" }

propose_order_time
write
shop.moneyyes10/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.

{ "order_ref": "ABC123", "new_start_ms": 1790010000000 }

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.

Connect

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: Bearer header 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.

Rate limits

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.

  • Reads
    100 per minute

    whoami, search, availability, codes, bookings, every shop_* read.

  • Ordinary writes
    30 per minute

    ask_lucilla, follow, listing status, blocks, promos, branding, take-down, own-order cancel and problem report.

  • Reservations
    6 per minute

    reserve_listing. A hold creates an obligation to a real shop.

  • Money
    10 per minute

    fulfil, shop cancel, propose time, spend settings, delivery address, purchase approval.

  • Broadcast
    3 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.

Privacy and money

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

support@lucilla.ca