Hostr MCP Server

The Hostr MCP server exposes typed Hostr actions over MCP and delegates Hostr SDK work to the Dart hostr-daemon. The action catalog in hostr_cli/lib/src/actions/hostr_actions.dart is the source of truth for MCP tool names, input JSON schemas, generated TypeScript types, and workflow documentation.

For ChatGPT Apps Directory review prep, see CHATGPT_SUBMISSION.md.

One-click client install

Use the hosted endpoint for AI clients that support remote HTTP MCP with OAuth:

https://ai.hostr.development/mcp

The server advertises OAuth metadata, so clients should use their built-in OAuth/MCP login flow. Do not mint or paste tokens manually. In Codex, for example:

codex mcp add hostr https://ai.hostr.development/mcp
codex mcp login hostr

For local iteration against this checkout without rebuilding Docker:

codex mcp add hostr_local http://127.0.0.1:8787/mcp
codex mcp login hostr_local

Other clients should be pointed at the same MCP URL and allowed to complete OAuth normally.

Runtime

Start the HTTP MCP server:

npm run build
npm start

Default URL:

http://127.0.0.1:8787

For fast local iteration against real AI clients, run the source-mode server without rebuilding the Docker image:

cd ai/mcp-server
npm run dev:local

This serves:

http://127.0.0.1:8787/mcp

The Docker/proxy endpoint remains available separately at:

https://ai.hostr.development/mcp

Use the localhost URL when testing source changes. Use the Docker URL when testing the prod-like container path.

For foreground hot-reload while actively editing, run:

cd ai/mcp-server
HOSTR_MCP_WATCH=1 npm run dev:local

Health:

GET /health

MCP endpoint:

POST /mcp

In source/dev mode the server starts the daemon with:

HOSTR_DAEMON_COMMAND=dart
HOSTR_DAEMON_ARGS="bin/hostr_daemon.dart --stdio --env development"
HOSTR_DAEMON_CWD=../hostr_cli

The Docker image builds a native-asset CLI bundle with dart build cli and runs:

HOSTR_DAEMON_COMMAND=/opt/hostr-daemon/bin/hostr_daemon
HOSTR_DAEMON_CWD=/opt/hostr-daemon

Set HOSTR_DAEMON_STATE_DIR=/data/mcp (or another mounted directory) when session state must survive container restarts. Dynamic OAuth client registrations are stored atomically at MCP_OAUTH_CLIENT_STORE_PATH, defaulting to $HOSTR_DAEMON_STATE_DIR/oauth-clients.json. OAuth refresh tokens are opaque, rotating, stored hashed beside that file, and default to MCP_REFRESH_TOKEN_TTL_SECONDS=2592000. Cold Dart starts can be slow in development, so MCP waits up to HOSTR_DAEMON_TIMEOUT_MS milliseconds per daemon request. The default is 120000.

Observability

All MCP HTTP, daemon-client, daemon stderr, and tool audit logs are structured JSON lines. Keep daemon stdout reserved for newline-delimited JSON responses. Every request gets an x-trace-id; the same trace id is propagated through HTTP, MCP tool execution, the stdio daemon request, Hostr SDK trace context, and outbound SDK HTTP calls where supported. /health and /ready include image provenance (revision, created, and source) when the container was built with provenance build args.

If the MCP daemon request timeout fires, the Node bridge sends a cooperative cancel message to the daemon. Dart work checks cancellation before and around long waits such as Nostr Connect waits, book-and-pay handoff, and reservation/swap observation. Work already inside a non-cancellable external SDK call may still finish in the background, but the request path stops waiting and logs the cancellation.

Auth Model

MCP calls require a bearer token issued by the OAuth flow. The token pubkey claim selects the daemon session:

runtime.session(pubkey)

Tool inputs must not include a pubkey. The daemon rejects authenticated write calls if the active Hostr session pubkey does not match the token pubkey.

OAuth can remain valid even when the underlying NIP-46 signer has gone offline. hostr_session_status reports authenticated, signerOnline, needsReconnect, and a reconnect hint. If the access token is valid but the Nostr signer session is missing, stale, or needs bunker recovery, write tools return auth_required with sessionAction: "hostr_session_connect". The client should call hostr_session_connect with wait: false, display the returned nostrconnect URI or qrImage, then call hostr_session_connect with wait: true after the user approves the signer connection.

MCP Resources

Clients should read these resources during setup:

hostr://mcp/action-input-types
hostr://mcp/action-catalog.json
ui://widget/listing-card.html
ui://widget/payment-required.html
ui://widget/session-connect.html
ui://widget/profile-card.html
ui://widget/trip.html
ui://widget/hosting.html

The first resource contains TypeScript interfaces, JSON schemas, and multi-step workflow playbooks. The second is machine-readable catalog metadata. The ui://widget/* resources are optional lightweight MCP Apps HTML renderers for listing cards, Lightning payment QR prompts, Nostr Connect session QR prompts, profile cards, trips, and hosting cards.

Regenerate the TypeScript catalog after editing Dart actions:

cd hostr_cli
dart run bin/generate_mcp_types.dart /path/to/hostr

Tools

hostr_session_status
hostr_session_connect
hostr_session_accounts
hostr_session_switch
hostr_session_logout
hostr_listings_search
hostr_listings_list
hostr_listings_create
hostr_listings_edit
hostr_listings_availability
hostr_listings_reviews
hostr_listings_reservationGroups
hostr_reservations_negotiateOffer
hostr_reservations_negotiateAccept
hostr_reservations_pay
hostr_reservations_commit
hostr_reservations_cancel
hostr_updates
hostr_thread_view
hostr_thread_message
hostr_escrow_involve
hostr_profile_show
hostr_profile_edit
hostr_trips_list
hostr_bookings_list
hostr_escrow_methods
hostr_swaps_watch
hostr_swaps_recoverAll
hostr_swaps_list

Write tools default to preview mode. The app or AI client should call the tool once with dryRun: true, show the returned preview to the user, then call again with dryRun: false only after explicit approval.

There are no legacy publish or broadcast write parameters. Every write-style MCP action uses dryRun.

Money units

When a user or listing says sats, it means satoshis, not dollars, cents, or whole bitcoin. One sat is exactly 1/100,000,000 BTC.

For Hostr MCP monetary inputs in sats, use:

{
  "value": "50000",
  "currency": "BTC",
  "unit": "sats",
  "decimals": 0
}

The value is the satoshi count as a string. Do not convert 50000 sats to 50000 BTC, 50000 USD, or 0.0005 unless the receiving field explicitly asks for BTC decimal notation instead of unit: "sats".

Listing specifications

hostr_listings_create.specifications and hostr_listings_edit.patch.specifications are the canonical listing amenities/specifications map. Use the snake_case keys below, not display labels or arbitrary amenity names. In particular, Wi-Fi/wifi/WIFI is wireless_internet.

Example:

{
  "specifications": {
    "wireless_internet": true,
    "kitchen": true,
    "free_parking": true,
    "max_guests": 4,
    "beds": 2,
    "bedrooms": 1,
    "bathrooms": 1
  }
}

Boolean keys: airconditioning, allows_pets, crib, tumble_dryer, washer, elevator, free_parking, gym, hair_dryer, heating, high_chair, wireless_internet, iron, jacuzzi, kitchen, outlet_covers, pool, private_entrance, smoking_allowed, breakfast, fireplace, smoke_detector, essentials, shampoo, infants_allowed, children_allowed, hangers, flat_smooth_pathway_to_front_door, grab_rails_in_shower_and_toilet, oven, bbq, balcony, patio, dishwasher, refrigerator, garden_or_backyard, microwave, coffee_maker, dishes_and_silverware, stove, fire_extinguisher, carbon_monoxide_detector, luggage_dropoff_allowed, beach_essentials, beachfront, baby_monitor, babysitter_recommendations, childrens_books_and_toys, game_console, street_parking, paid_parking, hot_water, lake_access, single_level_home, waterfront, first_aid_kit, handheld_shower_head, home_step_free_access, lock_on_bedroom_door, mobile_hoist, path_to_entrance_lit_at_night, pool_hoist, ev_charger, rollin_shower, shower_chair, tub_with_shower_bench, wide_clearance_to_bed, wide_clearance_to_shower_and_toilet, wide_hallway_clearance, baby_bath, changing_table, room_darkening_shades, stair_gates, table_corner_guards, extra_pillows_and_blankets, ski_in_ski_out, window_guards, disabled_parking_spot, grab_rails_in_toilet, events_allowed, common_spaces_shared, bathroom_shared, security_cameras.

Numeric keys: max_guests, beds, bedrooms, bathrooms, bathtub, tv. On create/edit, the top-level fields guests, beds, bedrooms, and bathrooms are also accepted and are folded into specifications; inside the specifications map, prefer max_guests rather than guests.

Image uploads

Remote clients that need to attach user-provided images must upload the original image bytes beside MCP, not inside the JSON-RPC /mcp request:

Preferred MCP tool flow:

hostr_images_upload({ file: <file-typed uploaded image> })
  -> structuredContent.usage.image.url

hostr_listings_create({ images: [{ url: structuredContent.usage.image.url }] })

hostr_profile_edit({ image: structuredContent.usage.image.url })

The upload response is intentionally generic. hostr_images_upload does not know whether the image will be used for a listing, profile, badge, or another image field; callers should use structuredContent.usage.image.url or structuredContent.upload.url wherever a durable image URL is required.

The hostr_images_upload schema marks file as type: "file" and advertises _meta["openai/fileParams"] = ["file"] so clients that support file rewrite/upload handling can stream the original attached file. If a client represents uploaded files as local references such as /mnt/data/photo.jpg, those references belong only in the file-typed hostr_images_upload.file argument so Hostr can read/download the original bytes and re-upload them to Blossom; never put them directly in listing images[].url or profile image/picture.

Fallback raw HTTP flow for clients that can make HTTP requests:

POST /mcp/uploads/images
Content-Type: multipart/form-data
field: file=<original image file>

The endpoint also accepts raw image bytes with an image/* or application/octet-stream content type. It does not require MCP OAuth, Nostr auth, or a Hostr foreground session, but when a valid MCP bearer token is present it first tries the logged-in Hostr session's Blossom upload path before falling back to the server's direct Blossom upload endpoint. The fallback uploads the original bytes to Blossom with no Authorization header, so the configured Blossom PUT /upload endpoint must also allow unauthenticated uploads. The response includes upload.url, sha256, size, and MIME metadata. Pass the returned upload.url as images[].url to hostr_listings_create, or as image/picture to hostr_profile_edit; the MCP listing and profile tools advertise image URLs only.

Do not base64-encode user-uploaded images into hostr_listings_create or hostr_profile_edit. Do not serve a temporary localhost URL for Hostr to fetch; localhost points at the wrong machine/container for remote MCP. Do not pass client-local paths such as /mnt/data, /mnt/shared, or file:// URLs to listing images[].url or profile image/picture. Do not resize, downscale, crop, recompress, transcode, or create thumbnails unless the user explicitly asks for that. If neither hostr_images_upload nor the upload POST can be used, stop and ask for a public image URL or for the client to expose an upload capability.

Workflow driving

Agents should use the workflow docs from hostr://mcp/action-input-types, but these are the intended command sequences:

  • New listing: call hostr_profile_edit if profile details need updating, then hostr_listings_create with dryRun: true, show the preview, and repeat with dryRun: false only after approval. The live listing path ensures seller config is published.
  • Edit listing: call hostr_listings_edit with dryRun: true, review the returned listing/event preview, then repeat with dryRun: false.
  • Search and reserve: call hostr_listings_search, then hostr_listings_availability, then hostr_reservations_negotiateOffer with dryRun: true; repeat with dryRun: false to send the private negotiate-stage reservation DM.
  • Negotiation: call hostr_updates to inspect thread/trade ids. Use hostr_reservations_negotiateOffer with tradeId and amount to send a follow-up offer, hostr_reservations_negotiateAccept to accept the latest offer, or hostr_reservations_cancel to cancel the private negotiation or committed reservation.
  • Payment: for normal instant-book payment, call hostr_reservations_bookAndPay. After showing the returned QR/invoice, call the read-only hostr_swaps_watch with swapId, tradeId, and reservationWaitSeconds; it has no dryRun parameter and does not require approval. Use hostr_swaps_recoverAll only for explicit manual recovery/debug flows.
  • Messaging: call hostr_updates, choose recipient pubkeys from the thread/trade, call hostr_thread_message with dryRun: true, then repeat with dryRun: false.
  • Listing management/profile/trips/bookings: call hostr_listings_list to inspect listing inventory, hostr_profile_show to inspect the current profile, hostr_profile_edit to preview/publish profile changes, hostr_trips_list for guest-side reservations, and hostr_bookings_list for reservations on listings authored by the authenticated user.
  • Escrow compatibility: call hostr_escrow_methods with a seller pubkey before payment when the agent needs to explain compatible escrow services or ask the user to choose a non-default service.
  • Swaps: call hostr_swaps_list, then hostr_swaps_watch for a specific swap id, and hostr_swaps_recoverAll when stale operations need recovery.

Chat presentation

MCP cannot force every AI client to render native UI cards. The server returns both structured data and Markdown text content. Listing search responses include image-carousel Markdown for each listing:

### Listing Title

**Image carousel:** 3 images

![Listing image 1](https://...)
![Listing image 2](https://...)
![Listing image 3](https://...)

Clients that support rich MCP cards can build cards from structuredContent.listingCards[*]. Clients that only render Markdown will still show listing sections with inline images.

Listing tools also advertise ui://widget/listing-card.html through _meta.ui.resourceUri, with _meta["openai/outputTemplate"] as the ChatGPT compatibility alias. The widget is intentionally just HTML/CSS/vanilla JS; it reads window.openai.toolOutput.listingCards or window.openai.toolOutput.display.cards and renders cards. Do not put business logic in it. Keep structuredContent.listingCards and displayMarkdown as the portable source of truth.

The same optional widget pattern is used for:

  • ui://widget/payment-required.html: reads structuredContent.paymentDisplays or structuredContent.display.cards for external Lightning payment QR prompts.
  • ui://widget/session-connect.html: reads structuredContent.display for Nostr Connect login QR prompts.
  • ui://widget/profile-card.html: reads structuredContent.profileCards or structuredContent.display.cards for profile show/edit results.
  • ui://widget/trip.html: reads structuredContent.tripCards, structuredContent.reservationCards, or structuredContent.display.cards for guest trips. Cancelled trips render a bold Cancelled marker.
  • ui://widget/hosting.html: reads structuredContent.hostingCards, structuredContent.reservationCards, or structuredContent.display.cards for host-side reservations, including Hosting {guest} at: {stay} text.

All widgets are examples. Generic MCP clients can ignore the UI resources and continue rendering displayMarkdown or structured JSON.

Verification

Run these checks before deploying:

cd hostr_cli
dart test test/unit/hostr_actions_test.dart
dart analyze lib/src/actions/hostr_actions.dart lib/src/daemon/hostr_daemon.dart

cd ../ai/mcp-server
npm run build