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.
Local (stdio) — this repository
Section titled “Local (stdio) — this repository”.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:
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.dllstayfn 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:
TOKEN=$(dotnet run --project src/StayFn.Cli -- dev token --role operator --tenant <tenant id>) # Development onlyclaude 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.
Resources and prompts
Section titled “Resources and prompts”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”)”- Read
stayfn://adr/ohip-facts(§2 modules, §3 headers and hotel scoping, §8 errors, §9 live response shapes). catalog.search{"query": "search hotel reservations", "area": "rsv", "kind": "ohip-op"}→ohip.rsv.searchHotelReservations(POST /rsv/v1/hotels/{hotelId}/reservations/searches); the deprecatedgetHotelReservationsis flagged with its replacement.ohip.operation{"module": "rsv", "operationId": "searchHotelReservations"}→SearchHotelReservationsRequesthasarrivalStartDate/arrivalEndDate, the 200 body isReservationsDetails(reservations.reservationInfo[]withroomStay,reservationGuest.vip,reservationIdList). The call isctx.Ohip.Api.Rsv.SearchHotelReservationsAsync(body, ct: ctx.Cancellation), wherehotelIddefaults to the request’s hotel. The same request as a Kiota chain isctx.Ohip.Reservations.Client.Hotels[hotel].Reservations.Searches.PostAsync(body, cancellationToken: ctx.Cancellation). The typed helperctx.Ohip.Reservations.SearchByArrivalDateAsyncuses the same operation.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.- 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 behindIFunctionContext; log counts, never guest names. function.validateon the class file until it reportsok: truewith noSTAYFNdiagnostics.- Complete the scaffolded tests (synthetic data,
TEST01), rundotnet 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.
Guard rails to keep in mind
Section titled “Guard rails to keep in mind”- Functions use
IFunctionContextonly;function.validatefails onHttpClient,DbContextorIConfiguration(STAYFN001–003). - A function may take typed parameters instead of an input record:
function.scaffoldwritesRun(DateOnly date, string? roomType, IFunctionContext ctx)for one to three simple inputs without an operation, and an input record otherwise. Such functions default toDerivedidempotency unless the attribute setsIdempotency; the templates setNonefor reads. - Call other functions through the generated typed client (
using StayFn.Functions.Client;thenctx.Functions.Rsv().GetReservationAsync("12345678")) instead of a name string;function.validateruns the generator, so the call compiles there as in a build. - The scaffolded tests need a test project with
XunitandFluentAssertionsas global usings (astests/Directory.Build.propsdeclares) and a reference toStayFn.Testing. - Synthetic data only: no real guest data, confirmation numbers or the sandbox hotel code in code, tests or fixtures.
sandbox.invokeoutput 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.