import { type DomainName } from "../constants.js"; import type { JsonApiResource } from "../types.js"; /** * Entity resource templates — `boond://candidate/{id}` and friends. * * ## Why a resource and not (only) a tool * * Reading one candidate's full picture costs 2–3 tool calls today * (`boond_candidates_get`, then `_information`, then `_technical_data`), each * paying a full tool_use / tool_result round-trip. A resource read returns the * aggregate in one `resources/read`, and many MCP hosts cache a resource body * for the length of a conversation — which they do not do for tool results. * The 2026-07-28 revision adds `ttlMs` / `cacheScope` to `resources/read` * (SEP-2549, see issue #170), i.e. the resource path is the one the spec is * pushing towards caching. * * ## What this is NOT for * * It does not power Claude Desktop's `@` mention picker. That selector is fed * by `resources/list`, and a template is deliberately not enumerable here: * listing candidates would mean paginating the whole Boond database into a * `resources/list` response, on every connection, for every client. Assisted * entry goes through `completions/complete` instead (see `completeIdFor` in * `index.ts`), whose client support is uneven — hence a bonus, never a * justification. * * ## Scope: the cheap, bounded tabs only * * A resource is read whole or not at all — there is no `pageSize` and the model * cannot ask for less. So only tabs with a bounded size are aggregated. * `actions`, `positionings`, `invoices`, `times-reports`… stay tools: folding * 200 actions in here would make the read size unpredictable. * * ## URI naming: singular here, plural in the dictionaries * * Dictionary slugs are plural (`boond://dictionary/states/candidates`) because * they name a *collection* of labels. An entity template names *one* entity, so * it is singular (`boond://candidate/{id}`). The inconsistency is deliberate: * do NOT "fix" either side to match the other — both forms are published URIs * that clients may have stored. */ export interface EntityTemplate { /** Registration name (also the key of the SDK's template map). */ name: string; /** RFC 6570 URI template, exactly as advertised in `resources/templates/list`. */ uriTemplate: string; /** * Business domain this template belongs to. Entity templates ARE filtered by * the access policy: with `BOOND_MCP_PROFILE=finance`, `candidates` is gone * from `tools/list`, and leaving `boond://candidate/{id}` readable would * reopen exactly what the operator closed. Reference dictionaries stay * unfiltered — they are a lookup substrate, not business data. */ domain: DomainName; /** French entity label, used in error messages. */ entityName: string; /** Base API path of the entity (`/candidates`). */ apiPath: string; /** Tab endpoints aggregated into the body, in output order. */ tabs: readonly string[]; title: string; description: string; } export declare const ENTITY_TEMPLATES: readonly EntityTemplate[]; /** Exposed for tests and the catalogue generator; mirrors `REGISTERED_RESOURCES`. */ export declare const REGISTERED_RESOURCE_TEMPLATES: { name: string; uriTemplate: string; title: string; domain: "candidates" | "resources" | "contacts" | "companies" | "opportunities" | "actions" | "timesheets" | "projects" | "invoices" | "orders" | "deliveries" | "absences" | "expenses" | "products" | "positionings" | "payments" | "advantages" | "application" | "contracts" | "purchases" | "provider-invoices" | "accounts" | "agencies" | "business-units" | "roles" | "logs" | "notifications" | "threads" | "todolists" | "flags" | "calendars" | "webhooks" | "validations" | "poles" | "reporting" | "planning-absences" | "inactivities" | "forms" | "groupments" | "alerts" | "documents" | "workflows"; }[]; /** * Read one entity and its declared tabs, and render them as a single JSON body. * * Three properties this function exists to guarantee: * * 1. **The id never reaches the API unvalidated.** The SDK compiles `{id}` to * the RFC 6570 default pattern `([^/,]+)` — NOT to a numeric one. So * `boond://candidate/1?x=2`, `boond://candidate/1#f` and * `boond://candidate/..%2Fresources%2F9` all match the template and land * here as `variables.id`, from where they would be interpolated straight * into an API path. `EntityIdSchema` (`/^\d+$/`) is the wall; same class of * problem as the document-id path guard (#186). * 2. **A failing tab does not lose the record.** `apiRequest` throws on any * non-2xx, and a partial record (a contact with no `information` payload) * is normal in Boond. Tabs are settled independently and a failure becomes * an `_errors` entry, not a failed read. The base record is the exception: * without it there is nothing to return, so its error propagates. * 3. **The body is always parseable JSON.** The size ceiling drops whole * sections and names them in `_omitted`; it never cuts the serialised text. * A resource has no `pageSize` and cannot be asked for less, so the ceiling * has to be enforced here — but handing back a truncated JSON document * would break every client that does the one thing the mime type promises. */ export interface EntityAggregate { uri: string; entity: Pick; /** One key per successfully read tab, in declaration order. */ sections: Record; /** Tabs whose read failed, mapped to the error message. Absent when none did. */ _errors?: Record; /** Sections dropped to fit MAX_RESOURCE_BYTES. Absent when nothing was dropped. */ _omitted?: { sections: string[]; reason: string; }; } export declare function readEntityAggregate(template: EntityTemplate, rawId: unknown, uri: string, extra?: unknown): Promise; //# sourceMappingURL=templates.d.ts.map