Skip to content

Authoring with Claude Code

StayFn ships an MCP server (StayFn.Mcp, plan Phase 6) so an AI coding session can write functions against the real OHIP surface: the committed OpenAPI specs (tools/ohip-specs/26.3.0.0), the generated clients, the live-verified OHIP facts and the runtime’s own registry rules. The one rule the server enforces and repeats: never invent OHIP endpoint paths, headers, event names or model members — if a tool does not return it, it does not exist.

.mcp.json at the repository root registers the server for Claude Code:

{ "mcpServers": { "stayfn": { "command": "bash", "args": ["tools/mcp/stayfn-mcp.sh"] } } }

Open Claude Code in the repository, approve the project server when asked, and the tools appear as mcp__stayfn__…. The script builds src/StayFn.Cli quietly (output on stderr — stdout is the protocol stream) and runs stayfn mcp stdio --root <repo>. Requirements: the .NET 8 SDK (global.json) and Docker only if you also run the integration tests.

Manual registration, or another checkout:

Terminal window
claude mcp add stayfn -- bash /path/to/stayfn/tools/mcp/stayfn-mcp.sh
# or, without the build step and with a tenant assembly in the catalog:
claude mcp add stayfn -- dotnet /path/to/StayFn.Cli.dll mcp stdio --root /path/to/repo --assemblies /path/to/Tenant.Functions.dll

stayfn mcp stdio options: --root <dir> (the only directory function.scaffold writes under and function.validate reads from; default: the repository root), --specs <dir> (default <repo>/tools/ohip-specs/26.3.0.0), --assemblies <path>… (extra function assemblies in the catalog; the shipped reference set is always there). Configuration also comes from STAYFN__Mcp__* variables. The stdio server has no database and no tenant: invocation.query and sandbox.invoke answer with an explanation and point at the host.

Remote (Streamable HTTP) — a running host

Section titled “Remote (Streamable HTTP) — a running host”

The host exposes the same server at /mcp behind the admin JWT: any admin role opens a session, read tools work for a viewer, function.scaffold and sandbox.invoke need operator (or owner). The pre-2025 HTTP-with-SSE transport is not enabled; Claude Code uses Streamable HTTP:

Terminal window
TOKEN=$(dotnet run --project src/StayFn.Cli -- dev token --role operator --tenant <tenant id>) # Development only
claude mcp add --transport http stayfn-host http://localhost:5080/mcp --header "Authorization: Bearer $TOKEN"

Host settings (section Mcp, appsettings.json / STAYFN__Mcp__*):

Setting Default Meaning
Mcp:AllowSandboxInvoke false Lets sandbox.invoke run functions through the invoker against the tenant’s environment. Off unless an operator turns it on.
Mcp:ScaffoldRoot empty (none) The only directory function.scaffold writes under / function.validate reads from. Empty = both tools refuse.
Mcp:SpecsPath empty → repo or /app/ohip-specs/26.3.0.0 in the image The committed spec snapshot ohip.operation reads.
Mcp:MaxSearchResults 25 Cap of catalog.search.

An owner token without a tenant claim (system scope) must pass tenantId to the tenant-scoped tools; a bound token may only name its own tenant. All database reads run under the tenant’s TenantScope, so row-level security — not filters — keeps tenants apart.

Tool What it does
catalog.search — (query, area?, kind?, limit?) Word search over functions, handlers and the ≈2,850 OHIP operations of the generated modules (ohip.<module>.<operationId>); functions come with their JSON schemas, operations with method and path. kind: function, scheduled, handler, ohip-op.
catalog.describe — (name) One entry in full: metadata, budget, schemas; ohip.* names are described like ohip.operation.
ohip.operation — (module, operationId, depth?) From the committed spec: method, path, hotel scoping, path/query parameters (the runtime adds the headers), request/response schemas with $refs inlined (depth 1–6, 600-node budget), a synthetic example, the generated C# model types, and the call. csharp.call is the flat call ctx.Ohip.Api.<Module>.<Op>Async(<required arguments>, ct: ctx.Cancellation), with its full signature (flatSignature), the [Obsolete] message of a deprecated operation, and for *async operations the polled result type and the Start…Async call. csharp.kiotaCall is the same request as a ctx.Ohip.… Kiota chain. Both are verified by reflection against the generated code. The typed helpers documented for the operation are listed too. Unknown ids are errors, never guesses.
function.scaffold — (name, kind, area?, description?, inputs?, outputs?, operation?, eventName?, schedule?, directory?, testsDirectory?, docsDirectory?, rootNamespace?, overwrite?) Writes the class (records, [OhipFunction] with the area, [OhipBudget], hotel guard, the flat call with path parameters taken from the input and the Kiota chain as a comment, a mapper), a test file (WireMock happy path + OHIP error + registration, over StayFn.Testing.FunctionTestHost) and a doc stub — under the root only, never over an existing file unless overwrite. In this repository the files land in src/StayFn.Functions.Reference/<Area>/, tests/StayFn.UnitTests/Functions/ and docs/functions/<area>/; elsewhere in functions/<Area>/, tests/ and docs/functions/.
function.validate — (path, additionalPaths?) Compiles the file against the StayFn assemblies (no host), runs StayFn.Analyzers (STAYFN001–006) and the registry’s start-up rules, and lists the functions it would register — with file and line for every diagnostic.
invocation.query — (from?, to?, function?, hotel?, status?, correlationId?, limit?, tenantId?) Host only: recent invocations of the caller’s tenant (newest first), masked error text, no payloads.
sandbox.invoke — (name, input, hotel?, idempotencyKey?, tenantId?) Host only, operator + Mcp:AllowSandboxInvoke: runs a function through the normal pipeline (trigger internal) and returns status, output (PII fields masked at field level) and error. Synthetic inputs only.

Tool names carry a dot as the plan spells them; Claude Code shows them as mcp__stayfn__catalog.search etc.

  • stayfn://catalog — the whole catalog as JSON (no schemas).
  • stayfn://adr/ohip-facts — the verified OHIP facts, embedded at build time so the container image carries it.
  • stayfn://templates/{Function|FunctionTests|Handler|HandlerTests|Doc} — the raw scaffolding templates ({{Name}} placeholders).

Prompts: author-function (goal, area?, operation?), author-event-handler (eventName, goal), write-tests-for-function (function). Each returns the workflow below as one user message, with the typed event contract named when one exists.

The workflow (worked example: “create a function that returns tomorrow’s arrivals with VIP flag”)

Section titled “The workflow (worked example: “create a function that returns tomorrow’s arrivals with VIP flag”)”
  1. Read stayfn://adr/ohip-facts (§2 modules, §3 headers and hotel scoping, §8 errors, §9 live response shapes).
  2. catalog.search {"query": "search hotel reservations", "area": "rsv", "kind": "ohip-op"} → ohip.rsv.searchHotelReservations (POST /rsv/v1/hotels/{hotelId}/reservations/searches); the deprecated getHotelReservations is flagged with its replacement.
  3. ohip.operation {"module": "rsv", "operationId": "searchHotelReservations"} → SearchHotelReservationsRequest has arrivalStartDate/arrivalEndDate, the 200 body is ReservationsDetails (reservations.reservationInfo[] with roomStay, reservationGuest.vip, reservationIdList). The call is ctx.Ohip.Api.Rsv.SearchHotelReservationsAsync(body, ct: ctx.Cancellation), where hotelId defaults to the request’s hotel. The same request as a Kiota chain is ctx.Ohip.Reservations.Client.Hotels[hotel].Reservations.Searches.PostAsync(body, cancellationToken: ctx.Cancellation). The typed helper ctx.Ohip.Reservations.SearchByArrivalDateAsync uses the same operation.
  4. function.scaffold {"name": "tomorrows-arrivals", "kind": "function", "area": "rsv", "description": "…", "inputs": [{"name": "vipOnly", "type": "bool", "optional": true}], "outputs": [{"name": "confirmationNumber", "type": "string"}, {"name": "vip", "type": "bool"}, …], "operation": "rsv.searchHotelReservations"} → class, tests and doc stub.
  5. Implement Run (business date = the hotel’s local tomorrow: ctx.Hotel, the hotel’s IANA zone comes with the schedule tick or a variable) and the mapper from the generated models; keep everything behind IFunctionContext; log counts, never guest names.
  6. function.validate on the class file until it reports ok: true with no STAYFN diagnostics.
  7. Complete the scaffolded tests (synthetic data, TEST01), run dotnet test tests/StayFn.UnitTests --filter TomorrowsArrivals.

tests/StayFn.IntegrationTests/Mcp/McpAuthoringExitTests.cs runs exactly this sequence over the real stdio transport and asserts the result compiles and its generated tests pass.

  • Functions use IFunctionContext only; function.validate fails on HttpClient, DbContext or IConfiguration (STAYFN001–003).
  • A function may take typed parameters instead of an input record: function.scaffold writes Run(DateOnly date, string? roomType, IFunctionContext ctx) for one to three simple inputs without an operation, and an input record otherwise. Such functions default to Derived idempotency unless the attribute sets Idempotency; the templates set None for reads.
  • Call other functions through the generated typed client (using StayFn.Functions.Client; then ctx.Functions.Rsv().GetReservationAsync("12345678")) instead of a name string; function.validate runs the generator, so the call compiles there as in a build.
  • The scaffolded tests need a test project with Xunit and FluentAssertions as global usings (as tests/Directory.Build.props declares) and a reference to StayFn.Testing.
  • Synthetic data only: no real guest data, confirmation numbers or the sandbox hotel code in code, tests or fixtures.
  • sandbox.invoke output is masked at field level (names, e-mails, phones, addresses, documents); error messages are masked by the runtime. Use it to check shapes and statuses, not to read guest data.
  • The stdio server trusts the local developer (no roles); the HTTP server trusts the admin JWT and confines file access to Mcp:ScaffoldRoot.