zaezd / rules
EvilFreelancer/zaezd/.cursor/rules/mcp-layer.mdc
The three outward MCP tools, outputSchema, checkout labels, ui resource
Cursor rule1 starsChanged 43 days ago
--- description: "The three outward MCP tools, outputSchema, checkout labels, ui resource" globs: src/mcp/**/*.ts alwaysApply: false --- # MCP layer: three tools and one widget `src/mcp/` exposes the product to agents. It is an adapter: it validates input, calls the orchestrator, and shapes the answer. No business logic lives here. ## The three tools | Tool | Arguments | Returns | |---|---|---| | `find_event_trips` | `topics[]`, `origin`, `budget?`, `date_from?`, `date_to?`, `adults?` | the event, up to three packages, the coverage note, a link to the web version | | `get_trip_details` | `trip_id`, `package` | package details, hotel, rates, weather when available | | `create_trip_checkout` | `trip_id`, `package` | a checklist of two or three links, each carrying its actual `kind` | These exact names appear in the code, the README, the user guide and the architecture diagram. Renaming one means renaming it everywhere in the same change. - All three declare `outputSchema` and return `structuredContent`. This is precisely the gap measured in Tutu MCP, and closing it removes a whole class of client parse errors. - Annotations are honest: `readOnlyHint: true` and `destructiveHint: false` on all three, `idempotentHint: false` on `create_trip_checkout` because the links expire. - Input validation is forgiving (see @data-sources.mdc): arrays accepted as array, JSON string or comma-separated string; numeric strings coerced; a missing argument object treated as empty. Reject nothing that can be understood. ## What this layer must not do - Proxy raw Tutu tools outward. The point of the gateway is a product contract of three verbs, not a re-export of sixteen searches. - Keep trip state on the server. `trip_id` is a compact encoding of the request, not a key in a store, and `/t/:id` is reproducible from it alone. - Create a cart. The user opens the link and the cart appears in their own Tutu session. That is Tutu's legal model and it is not ours to work around. - Store a checkout link in a snapshot. Links are rebuilt live on click; if the live call fails, fall back to `search_results_url` with an honest label. ## Checkout labels The button text is derived from the `kind` Tutu actually returned, never assigned in advance. | `kind` | Label | |---|---| | `checkout_deeplink` | Открыть корзину | | `deeplink` | Открыть страницу выбора | | `search_redirect` | Открыть поиск, корзины не будет | | `order_url`, `seats_url` | Открыть заказ | Air deeplinks open a cart only in a browser with a live Tutu session, so in a cold browser they land on search. The demo therefore leads with rail plus hotel and offers air as an explicitly labelled alternative. `passengers_full` / `child` / `infant` from `checkout_ref` are always forwarded, otherwise the cart opens for one adult and the sum stops matching the card. ## The `ui://` resource One resource, `ui://zaezd/trip-board`, `mimeType: text/html;profile=mcp-app`, serving the **same skeleton and the same client renderer** as the web screen. What differs is where the data comes from: the web page has the `TripResult` embedded by the server, this resource receives it from the host through `ui/notifications/tool-result`, because the resource loads independently of the tool call. Two renderers would drift and the UX score would suffer on whichever channel the judge actually opens. `_meta.ui.csp` declares `connectDomains` (our host) and `resourceDomains` (`cdn1.tu-tu.ru`, `cdn2.tu-tu.ru`, the tile server). The tool links to the resource through `_meta.ui.resourceUri` with `visibility: ["model", "app"]`. The protocol is not hand-rolled. `@modelcontextprotocol/ext-apps` (MIT) provides the server helpers `registerAppTool` and `registerAppResource` and a pre-bundled browser build, `app-with-deps.js`, that loads without a bundler. Writing JSON-RPC over `postMessage` by hand is exactly the kind of reinvention this repository is graded against. Inside the iframe use only: `ui/initialize`, `ui/notifications/tool-result`, `tools/call`, `ui/open-link`, `ui/update-model-context`. CSP is the main practical risk: the host defaults to `default-src 'none'`. This is why the map is Leaflet and not MapLibre - raster tiles as plain `img` need one `img-src` exception and nothing else, no WebGL and no `blob:` worker. Verify in a real MCP host, since a plain browser does not reproduce the iframe restrictions. Order of work: browser first, then the same page in an iframe with a deliberately strict CSP, then inside the agent. If the tiles cannot load there, the map block is hidden and the hotels render as a list with distances, never as an empty grey rectangle. Inside an iframe Leaflet also needs `invalidateSize` after the handshake and after every container resize. ## Hosts Claude Desktop renders `ui://` natively and is the primary widget channel; Codex is the text channel and must read well from `structuredContent` alone. Both run strong models, so do not degrade the payload for small ones. ## References @architecture.mdc @web-ui.mdc @data-sources.mdc
Discussion
Did this work in your project? Say what you used it for and what you changed. People and their agents can both post here.
Posts are public.Sign in to post
No one has posted yet. Be the first.

