From 2b743e1019d29b17a1fe4e495ea300ea05141f6d Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 8 Jun 2026 13:08:01 -0600 Subject: [PATCH 1/4] docs: MCP/OpenAPI metadata authoring for #1095 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document Harper's class-level metadata surface that drives both the MCP tool descriptors and the OpenAPI document from a single source of truth. - reference/database/schema.md: - New "Documenting Types and Fields" section covering GraphQL "" "" docstring conventions on @table @export types and their fields, with notes on which MCP/OpenAPI surfaces they reach. - @hidden directive documented at both type level (under Type Directives) and field level (under Field Directives), with explicit framing as metadata-visibility — not access control. - Trust-model note: docstrings reach LLMs and public OpenAPI consumers verbatim; treat them as code. - reference/resources/resource-api.md: - New "Class-level metadata for MCP and OpenAPI" section covering static description, static properties (Record), static outputSchemas (per-verb overrides), static hidden, static mcp.annotations (narrow MCP override), and static mcpTools (custom non-verb methods). - Inheritance pattern: extending @table @export Resources composes via spread (static properties = { ...Parent.properties, foo: {...} }). - Under-annotate-before-mis-annotate note for idempotentHint. - learn/developers/mcp-and-openapi-metadata.mdx (new): - Full how-to with before/after MCP tools/list output. - GraphQL-first path for @table @export Resources. - Programmatic Resource path for class-statics authoring. - Inheritance example, @hidden example, RBAC notes, verification steps. - reference/mcp/tool-metadata.md (new): - Per-profile sourcing reference for every tool-descriptor field (operations vs application, verb tools vs custom mcpTools). - Sample descriptors for get_Product (with docstrings) and add_user. - Catalog of harper://* synthetic resources. Companion to HarperFast/harper feat-mcp-metadata-1095 (the implementation PR). Co-Authored-By: Claude Opus 4.7 --- learn/developers/mcp-and-openapi-metadata.mdx | 188 ++++++++++++++++++ reference/database/schema.md | 57 ++++++ reference/mcp/tool-metadata.md | 148 ++++++++++++++ reference/resources/resource-api.md | 153 ++++++++++++++ 4 files changed, 546 insertions(+) create mode 100644 learn/developers/mcp-and-openapi-metadata.mdx create mode 100644 reference/mcp/tool-metadata.md diff --git a/learn/developers/mcp-and-openapi-metadata.mdx b/learn/developers/mcp-and-openapi-metadata.mdx new file mode 100644 index 00000000..13ca28c0 --- /dev/null +++ b/learn/developers/mcp-and-openapi-metadata.mdx @@ -0,0 +1,188 @@ +--- +title: Writing quality MCP and OpenAPI descriptions +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +When an MCP client connects to Harper, the LLM on the other side sees your application as a list of tools. The text it reads to pick the right tool — the tool description, the per-attribute property descriptions, the output schema shape — is the dominant signal for tool selection. The same metadata also drives Harper's OpenAPI document, which any HTTP API consumer (Swagger UI, Redoc, generated SDKs, machine clients) reads. + +This guide shows how to author that metadata once and have it flow to both surfaces — via GraphQL docstrings for `@table @export` Resources, and via class-level statics for programmatic Resource subclasses. + +## Why descriptions matter + +Harper auto-generates MCP tools for every exported Resource. Without descriptions, every tool gets a generic template: `"get on resource '/Product' (table Product). Runtime RBAC enforces per-record access at call time."` An LLM picking between `get_Product`, `get_Order`, `get_Customer` sees three near-identical descriptions. Tool selection becomes guesswork. + +Add a one-line docstring to your `@table @export` type and the picture changes: each tool's description includes a sentence about what the resource actually represents, and every searchable attribute has a per-attribute description the LLM can use to form queries. + +## Path A: `@table @export` Resources via GraphQL docstrings + +For table-backed Resources, the natural authoring locus is the GraphQL schema. Triple-quoted docstrings on types and fields are picked up by Harper's parser and flow through to both MCP and OpenAPI automatically — no JavaScript code changes required. + +### Before + +```graphql +type Product @table @export { + sku: String! @primaryKey + name: String! + priceCents: Int! + inStock: Int! +} +``` + +MCP `tools/list` returns: + +```json +{ + "name": "get_Product", + "description": "get on resource '/Product' (table Product). Runtime RBAC (allowGet) enforces per-record access at call time.", + "inputSchema": { + "type": "object", + "properties": { "id": { "type": "string", "description": "Primary key (sku)." } }, + "required": ["id"] + } +} +``` + +### After + +```graphql +""" +Product catalog row — what shows up in the storefront listing, +search, and inventory feeds. One row per SKU. +""" +type Product @table @export { + """Stock keeping unit — globally unique across catalogs.""" + sku: String! @primaryKey + + """Display name shown in the storefront. 100 chars max.""" + name: String! + + """Retail price in cents (USD).""" + priceCents: Int! + + """Current inventory level. Decremented by orders; reconciled nightly.""" + inStock: Int! +} +``` + +MCP `tools/list` now returns: + +```json +{ + "name": "get_Product", + "description": "Product catalog row — what shows up in the storefront listing, search, and inventory feeds. One row per SKU.\n\nFetches a single Product record by sku. Runtime RBAC (allowGet) enforces per-record access at call time.", + "inputSchema": { + "type": "object", + "properties": { + "id": { "type": "string", "description": "Primary key (sku)." } + }, + "required": ["id"] + }, + "outputSchema": { + "type": "object", + "properties": { + "sku": { "type": "string", "description": "Stock keeping unit — globally unique across catalogs." }, + "name": { "type": "string", "description": "Display name shown in the storefront. 100 chars max." }, + "priceCents": { "type": "integer", "description": "Retail price in cents (USD)." }, + "inStock": { "type": "integer", "description": "Current inventory level. Decremented by orders; reconciled nightly." } + }, + "required": ["sku", "name", "priceCents", "inStock"], + "additionalProperties": false + } +} +``` + +And `/openapi.json` picks up the same data: schema-level `description`, per-property `description`, and prepended path-level descriptions for every verb on `/Product`. + +### `search_*` gets typed and described too + +For `search_Product`, the `conditions[].attribute` field becomes a closed `enum` of the readable attributes, and each per-property description threads through. The LLM goes from "an attribute name (string)" to "one of these specific attribute names, with this meaning each." + +### Authoring rubric + +- **Lead with a verb-led sentence on the type:** "Product catalog row…", "Customer profile and order history…". Skip the trivia ("This is the Product table"); the LLM already knows it's a table. +- **Field docstrings should explain meaning, not type.** Saying "Integer." adds nothing — the schema already says `Int!`. Saying "Retail price in cents (USD)" lets the LLM construct sensible queries. +- **Mention units, formats, and edge cases.** "ISO 8601 timestamp", "cents not dollars", "null for SKUs that have never been counted". +- **Keep docstrings short.** Long descriptions waste LLM context and clutter the OpenAPI UI. + +## Path B: Programmatic Resources via class-level statics + +For Resources without `@table @export` backing — Resource subclasses that override `get`/`post`/`put`/`delete` directly, or that aggregate across multiple tables — there's no GraphQL schema to derive from. Declare the same metadata directly on the class as JSON-Schema-shaped statics. The MCP and OpenAPI layers read both surfaces uniformly. + +```typescript +import { Resource } from 'harperdb'; + +export class ProductInventory extends Resource { + static description = + 'Aggregate inventory analytics computed over the Product catalog. ' + + 'Read-only; the underlying Product table is the system of record.'; + + static properties = { + sku: { type: 'string', primaryKey: true, + description: 'Stock keeping unit; matches Product.sku.' }, + onHand: { type: 'integer', + description: 'Current warehouse count.' }, + reserved: { type: 'integer', + description: 'Units allocated to open orders but not yet shipped.' }, + stockStatus: { type: 'string', enum: ['in_stock', 'out_of_stock', 'backorder'], + description: 'Derived from onHand vs reserved.' }, + }; + + async get(id) { /* returns { sku, onHand, reserved, stockStatus } */ } + async search(query) { /* ... */ } +} +``` + +See the [Resource API reference](../../reference/resources/resource-api#class-level-metadata-for-mcp-and-openapi) for the full surface, including `static outputSchemas` for per-verb projection overrides, `static hidden` for full suppression, and `static mcp` for narrow MCP-only annotation overrides. + +## Inheritance: extending a table + +Resources extending a `@table @export` Resource inherit the auto-derived metadata. Override individual entries with spread: + +```typescript +const { Product } = tables; + +class CustomProduct extends Product { + static properties = { + ...Product.properties, + priceCents: { + ...Product.properties.priceCents, + description: 'Retail price in cents, including any per-customer adjustments.', + }, + }; +} +``` + +The author writes against the canonical `properties` API. Internal code paths that need ordered iteration continue to read `Class.attributes` (the Array form), preserved through inheritance. + +## Hiding sensitive fields with `@hidden` + +OpenAPI is typically exposed to anyone reachable on the HTTP port — there's no per-user filtering on `/openapi.json`. A docstring on a sensitive field publishes that text to anyone who can hit the endpoint. The `@hidden` directive suppresses a field (or an entire type) from both MCP and OpenAPI without affecting data access: + +```graphql +type Customer @table @export { + id: Long @primaryKey + name: String + + """Internal — used by the pricing engine; not for external consumers.""" + creditScore: Int @hidden +} +``` + +`creditScore` is still queryable via direct Harper interfaces under the caller's `attribute_permissions` — `@hidden` is a metadata-visibility directive, not access control. For programmatic Resources, the equivalent is `static hidden = true` on the class (or `hidden: true` on a per-property entry in `static properties`). + +> **Trust model.** Docstrings reach LLMs and public OpenAPI consumers verbatim. Treat them as code: don't put secrets, internal-only commentary, or speculative prose in them. Use `@hidden` to suppress fields that shouldn't surface publicly. + +## RBAC and per-user filtering + +For MCP tool descriptors, `attribute_permissions` already filters the schema per-user — an attribute the caller cannot read is dropped from that user's view of the tool descriptor, along with its description. The new metadata flows through the existing pipeline. + +For OpenAPI, the document is global and not per-user filtered. Use `@hidden` (or `static hidden`) to control what surfaces there. + +## Verifying the end-to-end flow + +1. Add `"""docstrings"""` to a `@table @export` type and save your component. +2. Hit MCP `tools/list` for the application profile — confirm `get_*`, `search_*`, etc. descriptions include the type docstring and per-attribute descriptions are present in the `inputSchema` and `outputSchema`. +3. Hit `/openapi.json` on the application HTTP port — confirm the path-level descriptions and per-property descriptions show up in Swagger UI / Redoc. +4. Add `@hidden` to an attribute — confirm it disappears from both surfaces while remaining queryable via direct REST/SQL. diff --git a/reference/database/schema.md b/reference/database/schema.md index cb1792d8..03c30260 100644 --- a/reference/database/schema.md +++ b/reference/database/schema.md @@ -185,6 +185,47 @@ type StrictRecord @table @sealed { } ``` +### `@hidden` + +Suppresses the type from introspectable surfaces — MCP tool descriptors and the OpenAPI document. The table still exists; data is still queryable through Harper's other interfaces subject to RBAC. `@hidden` is a **metadata-visibility** directive, not an access-control mechanism: use `attribute_permissions` on roles to control data access. + +```graphql +type InternalConfig @table @hidden { + id: Long @primaryKey + value: String +} +``` + +`@hidden` is also available as a [field directive](#hidden-1) to suppress individual attributes. + +## Documenting Types and Fields + +Harper picks up GraphQL's standard triple-quoted docstrings on type and field definitions. Docstrings flow through to: + +- **MCP** — `Table.description` (consumed as a prefix on every verb-tool description) and `inputSchema.properties[*].description` on derived tool schemas +- **OpenAPI** — `components.schemas[*].description`, per-property `description`, and the path-level `description` for every verb on the resource + +```graphql +""" +Product catalog row — what shows up in the storefront listing, +search, and inventory feeds. One row per SKU. +""" +type Product @table @export { + """Stock keeping unit — globally unique across catalogs.""" + sku: String! @primaryKey + + """Display name shown in the storefront.""" + name: String! + + """Retail price in cents (USD).""" + priceCents: Int! +} +``` + +Docstrings on `@hidden` fields are dropped from the descriptive surfaces alongside the field itself. + +> **Trust model.** Docstrings reach LLMs and public OpenAPI consumers verbatim. Treat them as code: don't put secrets, internal-only commentary, or speculative prose in them. Use `@hidden` to suppress fields that shouldn't surface publicly. + ## Field Directives Field directives apply to individual attributes in a type definition. @@ -249,6 +290,22 @@ type Event @table { } ``` +### `@hidden` + +Suppresses the field from MCP tool descriptors and the OpenAPI document. The attribute still exists in the table; data is still queryable through other interfaces subject to RBAC. Use this for fields that should not appear in introspectable surfaces. + +```graphql +type Customer @table { + id: Long @primaryKey + name: String + + """Internal — do not surface to external consumers.""" + creditScore: Int @hidden +} +``` + +`@hidden` is a metadata-visibility directive, not access control: `attribute_permissions` on roles remains the data-access enforcement mechanism. + ## Relationships diff --git a/reference/mcp/tool-metadata.md b/reference/mcp/tool-metadata.md new file mode 100644 index 00000000..24642e2e --- /dev/null +++ b/reference/mcp/tool-metadata.md @@ -0,0 +1,148 @@ +--- +title: MCP tool payload sourcing +--- + +# MCP tool payload sourcing + +Harper's MCP server publishes tools via the Model Context Protocol (rev 2025-06-18, Streamable HTTP transport). This page is a reference for what fields appear on each generated tool descriptor and where the data comes from. For authoring guidance, see the [Writing quality MCP and OpenAPI descriptions](../../learn/developers/mcp-and-openapi-metadata) how-to. + +## Tool descriptor fields + +Per the MCP spec, every tool descriptor returned from `tools/list` carries: + +| Field | Required | Purpose | +|---|---|---| +| `name` | yes | Machine identifier used by `tools/call` | +| `description` | yes | LLM-facing prose explaining what the tool does and when to pick it over siblings | +| `inputSchema` | yes | JSON Schema for the arguments the tool accepts | +| `outputSchema` | no | JSON Schema for the data the tool returns (added in spec rev 2025-06-18) | +| `annotations` | no | Hints for clients: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title` | + +Harper populates each from a different source depending on the tool's profile and origin. + +## Profiles + +Harper exposes two MCP profiles, each with its own port and registration loop: + +- **Operations profile** — registers one MCP tool per Harper operation that survives the `mcp.operations.allow` / `deny` filter. Walks `OPERATION_FUNCTION_MAP`. The default allow list is read-only and intentionally narrow: `describe_*`, `list_*`, `search_*`, plus an explicit list of safe getters. +- **Application profile** — registers verb tools (`get_*`, `search_*`, `create_*`, `update_*`, `patch_*`, `delete_*`) for every exported Resource, plus any custom tools declared via `static mcpTools`. + +## Sourcing per profile + +### Operations profile + +For operations tools, the descriptor fields are sourced from: + +| Field | Source | +|---|---| +| `name` | Operation name (from `OPERATION_FUNCTION_MAP` key) | +| `description` | Hand-authored entry in the operations descriptions catalog; falls back to a generic template when an operator opts in a non-cataloged operation | +| `inputSchema` | Hand-curated JSON Schema in the operations input-schemas catalog; falls back to `{ type: 'object', additionalProperties: true }` | +| `annotations.readOnlyHint` | `true` if the operation matches a read-only prefix (`describe_`, `list_`, `search_`, `get_`, `read_`) or is the literal `system_information` | +| `annotations.destructiveHint` | `true` for operations in the curated destructive set (`drop_*`, `delete_*`, `restart`, `set_configuration`, etc.) | +| `annotations.idempotentHint` | Default empty; opt-in per operation after end-to-end verification that the second call produces the same observable outcome | + +Operations registered outside core (for example, `cluster_status` from harper-pro) don't have catalog entries; they fall back to the generic description template until the per-operation metadata registry lands. + +### Application profile — verb tools + +For verb tools generated from exported Resources: + +| Field | Source | +|---|---| +| `name` | `${verb}_${sanitized-path}` (e.g. `get_Product`, `search_Customer`) | +| `description` | Composed: `[ResourceClass.description \n\n] ${verb sentence} ${runtime RBAC note}` | +| `inputSchema` | Derived per verb from `ResourceClass.attributes` and the caller's `attribute_permissions`. Per-attribute `description` propagates to `inputSchema.properties[*].description` | +| `outputSchema` | Derived per verb from `ResourceClass.attributes` for `get_*` / `create_*` / `update_*` / `patch_*`. `delete_*` returns `{ deleted: true, }`. `search_*` deliberately omits `outputSchema` | +| `annotations.readOnlyHint` | `true` on `get_*` and `search_*` | +| `annotations.destructiveHint` | `true` on `delete_*` | +| `annotations.idempotentHint` | `true` on `update_*` (PUT semantics); other verbs default off | + +`static description` and `static properties` on the Resource class override the auto-derived values. `static outputSchemas[verb]` overrides per-verb output schemas. `static mcp.annotations[verb]` overrides annotations per verb. `static hidden === true` suppresses the entire Resource from MCP listing. + +### Application profile — custom `mcpTools` + +For tools declared via `static mcpTools`: + +| Field | Source | +|---|---| +| `name` | `def.name` from the `mcpTools` entry | +| `description` | `def.description` from the entry; falls back to a generic template if omitted (with a warn-once log at registration) | +| `inputSchema` | `def.inputSchema` from the entry; falls back to `{ type: 'object', additionalProperties: true }` if omitted (with a warn-once log at registration) | +| `outputSchema` | `def.outputSchema` from the entry (optional; no fallback) | +| `annotations` | `def.annotations` from the entry (optional pass-through) | + +Custom tools have no per-user listing filter beyond authentication — the Resource's instance method is responsible for whatever RBAC it needs to enforce. + +## Sample descriptors + +### `get_Product` (with docstrings) + +Given a `Product` table with type-level and field-level docstrings: + +```json +{ + "name": "get_Product", + "description": "Product catalog row — what shows up in the storefront listing, search, and inventory feeds. One row per SKU.\n\nFetches a single Product record by sku. Runtime RBAC (allowGet) enforces per-record access at call time.", + "inputSchema": { + "type": "object", + "properties": { + "id": { "type": "string", "description": "Primary key (sku)." }, + "get_attributes": { "type": "array", "items": { "type": "string" } } + }, + "required": ["id"] + }, + "outputSchema": { + "type": "object", + "properties": { + "sku": { "type": "string", "description": "Stock keeping unit — globally unique across catalogs." }, + "name": { "type": "string", "description": "Display name shown in the storefront." }, + "priceCents": { "type": "integer", "description": "Retail price in cents (USD)." } + }, + "required": ["sku", "name", "priceCents"], + "additionalProperties": false + }, + "annotations": { "readOnlyHint": true } +} +``` + +### `add_user` (operations profile) + +```json +{ + "name": "add_user", + "description": "Creates a new Harper user with username, password, and role. Requires super_user. Username is immutable after creation — use drop_user + add_user to rename.", + "inputSchema": { + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string" }, + "role": { "type": "string", "description": "Role name. Use list_roles to discover available roles." }, + "active": { "type": "boolean", "default": true } + }, + "required": ["username", "password", "role"] + } +} +``` + +Note that `add_user` does NOT carry `idempotentHint: true`. Under MCP semantics this would claim the second call produces the same observable outcome — but `add_user("bob")` on first call returns the created user and on second call returns an "already exists" error. Different observable outcomes → not idempotent. + +## `harper://*` resources + +Harper also publishes a small set of synthetic resources via the MCP `resources/list` endpoint: + +| URI | Profile | Description | +|---|---|---| +| `harper://about` | both | Server version, profile, MCP protocol versions | +| `harper://operations` | operations | User-filtered operations catalog | +| `harper://openapi` | application | Full OpenAPI 3.0.3 document | +| `harper://schema/{db}/{table}` | application | Per-table schema, filtered by `attribute_permissions` | +| `https://{host}/{path}` | application | Application HTTP Resources, in-process | + +For `harper://schema/{db}/{table}` and `https://{host}/{path}` entries, the descriptor description prepends `Table.description` / `ResourceClass.description` when present. + +## See also + +- [Writing quality MCP and OpenAPI descriptions](../../learn/developers/mcp-and-openapi-metadata) — authoring how-to +- [Schema reference: docstrings and `@hidden`](../database/schema) — GraphQL surface +- [Resource API reference: class-level metadata](../resources/resource-api#class-level-metadata-for-mcp-and-openapi) — programmatic surface diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 71a0297b..530df1fa 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -439,6 +439,159 @@ The name of the primary key attribute for the table. --- +## Class-level metadata for MCP and OpenAPI + +Resource classes — both `@table @export` Resources and programmatic Resource subclasses — can declare class-level static fields that drive the MCP tool descriptors and OpenAPI document. These statics are JSON-Schema-aligned and tool-agnostic; the same data feeds every introspectable surface. + +For `@table @export` Resources, `static description` and `static properties` are auto-derived from the GraphQL schema (docstrings and field types). For programmatic Resources, you declare them directly. + +### `static description?: string` + +Class-level docstring. Consumed by: + +- **MCP** — prefixed onto every verb-tool description (`get_X`, `search_X`, etc.) and onto the `harper://schema/{db}/{table}` resource description +- **OpenAPI** — set as the schema-level `description` on the resource's component schema; also prepended to the `description` of every path operation (POST/GET/PUT/PATCH/DELETE) + +```typescript +import { Resource } from 'harperdb'; + +export class ProductInventory extends Resource { + static description = + 'Aggregate inventory analytics computed over the Product catalog. ' + + 'Read-only; the underlying Product table is the system of record.'; + + async get(id) { /* ... */ } +} +``` + +### `static properties?: Record` + +JSON-Schema-shaped attribute map keyed by name. This is the canonical public API for class-level metadata. For `@table @export` Resources it's auto-derived from the GraphQL schema. For programmatic Resources, declare it directly: + +```typescript +export class ProductInventory extends Resource { + static description = '...'; + static properties = { + sku: { type: 'string', primaryKey: true, + description: 'Stock keeping unit; matches Product.sku.' }, + onHand: { type: 'integer', + description: 'Current warehouse count.' }, + reserved: { type: 'integer', + description: 'Units allocated to open orders but not yet shipped.' }, + stockStatus: { type: 'string', enum: ['in_stock', 'out_of_stock', 'backorder'], + description: 'Derived from onHand vs reserved.' }, + }; + + async get(id) { /* ... */ } +} +``` + +For complex types and nested structures, JSON Schema vocabulary applies (`type`, `enum`, `required`, `additionalProperties`, etc.). Per-property `description` flows into both MCP `inputSchema.properties[*].description` and OpenAPI `components.schemas[*].properties[*].description`. + +**Inheritance composes naturally.** Extend a `@table @export` Resource and override individual entries with spread: + +```typescript +const { Product } = tables; + +class CustomProduct extends Product { + static properties = { + ...Product.properties, + priceCents: { + ...Product.properties.priceCents, + description: 'Retail price in cents, including any per-customer adjustments.', + }, + }; +} +``` + +The author writes against `properties` (the public API). Internal code that needs ordered iteration / index metadata continues to read `Class.attributes` (the internal Array form, also inherited). + +### `static outputSchemas?: { [verb: string]: JsonSchemaFragment }` + +Per-verb output schema overrides for programmatic Resources whose verb methods return a projection rather than the full record. When omitted, the MCP deriver falls back to `static properties` for the cheap verbs (`get`/`create`/`update`/`patch`) and a synthesized `{deleted: true, }` envelope for `delete`. `search_*` deliberately has no output schema. + +```typescript +export class ProductInventory extends Resource { + static description = '...'; + static properties = { /* full record shape */ }; + + static outputSchemas = { + get: { + type: 'object', + properties: { + sku: { type: 'string' }, + onHand: { type: 'integer' }, + stockStatus: { type: 'string', enum: ['in_stock', 'out_of_stock', 'backorder'] }, + }, + required: ['sku', 'onHand', 'stockStatus'], + }, + }; + + async get(id) { /* returns the projection above */ } +} +``` + +### `static hidden?: boolean` + +When `true`, the Resource is dropped from MCP tool registration and OpenAPI path generation entirely. Data remains accessible via direct Harper interfaces subject to RBAC. Equivalent to applying `@hidden` to the `@table @export` declaration. + +```typescript +export class InternalDiagnostics extends Resource { + static hidden = true; + async get() { /* ... */ } +} +``` + +### `static mcp?: { annotations?: { [verb: string]: Annotations } }` + +Narrow, MCP-only override for annotation hints that don't fit JSON Schema (such as `idempotentHint` per verb). Documented as discouraged — most authors should only need `static description` + `static properties`. Use this when you need to claim, for example, that your custom `update` semantics are observably idempotent on repeat calls. + +```typescript +export class ProductInventory extends Resource { + static description = '...'; + static properties = { /* ... */ }; + + static mcp = { + annotations: { + update: { idempotentHint: true }, + }, + }; +} +``` + +> **Under-annotate before mis-annotate.** Under MCP semantics, `idempotentHint: true` is a strong claim: the second call must produce the same observable outcome as the first. `add_*`-style operations that return "already exists" on the second call are NOT idempotent in this sense, even though they don't crash. Verify repeat-call behavior end-to-end before annotating. + +### `static mcpTools?: ReadonlyArray<...>` + +Component-author opt-in for exposing non-verb instance methods as MCP tools. Each entry maps an instance-method name to an MCP tool descriptor. RBAC is enforced by the Resource method itself; the MCP layer does not invent new ACLs. + +```typescript +export class ProductInventory extends Resource { + static mcpTools = [ + { + name: 'reconcile_inventory', + method: 'reconcileInventory', + description: + 'Triggers an immediate reconciliation against the warehouse system. ' + + 'Returns the diff applied. Heavy — do not call in a loop.', + inputSchema: { + type: 'object', + properties: { + sku: { type: 'string', description: 'SKU to reconcile, or omit for full sweep.' }, + }, + }, + annotations: { destructiveHint: false, idempotentHint: false }, + }, + ]; + + async reconcileInventory(args) { /* ... */ } +} +``` + +Custom `mcpTools` declarations without a `description` or `inputSchema` are still registered, but Harper emits a warn-once log at registration time — LLM tool selection degrades without these fields. + +--- + ### `setComputedAttribute(name: string, computeFunction: (record) => any)` Define the compute function for a `@computed` schema attribute. From 2de09ee33a007b3314bfb9665efa53af2623169c Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 8 Jun 2026 13:16:12 -0600 Subject: [PATCH 2/4] docs: apply prettier formatting to MCP/OpenAPI metadata docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prettier --check on Docusaurus' configured profile flagged style issues across all four files in the docs PR. No content changes — just spacing, indentation, and line-break conventions to match the existing repo style. Co-Authored-By: Claude Opus 4.7 --- learn/developers/mcp-and-openapi-metadata.mdx | 49 ++++++++---- reference/database/schema.md | 16 +++- reference/mcp/tool-metadata.md | 74 +++++++++---------- reference/resources/resource-api.md | 44 +++++++---- 4 files changed, 111 insertions(+), 72 deletions(-) diff --git a/learn/developers/mcp-and-openapi-metadata.mdx b/learn/developers/mcp-and-openapi-metadata.mdx index 13ca28c0..b8b13020 100644 --- a/learn/developers/mcp-and-openapi-metadata.mdx +++ b/learn/developers/mcp-and-openapi-metadata.mdx @@ -52,16 +52,24 @@ Product catalog row — what shows up in the storefront listing, search, and inventory feeds. One row per SKU. """ type Product @table @export { - """Stock keeping unit — globally unique across catalogs.""" + """ + Stock keeping unit — globally unique across catalogs. + """ sku: String! @primaryKey - """Display name shown in the storefront. 100 chars max.""" + """ + Display name shown in the storefront. 100 chars max. + """ name: String! - """Retail price in cents (USD).""" + """ + Retail price in cents (USD). + """ priceCents: Int! - """Current inventory level. Decremented by orders; reconciled nightly.""" + """ + Current inventory level. Decremented by orders; reconciled nightly. + """ inStock: Int! } ``` @@ -85,7 +93,10 @@ MCP `tools/list` now returns: "sku": { "type": "string", "description": "Stock keeping unit — globally unique across catalogs." }, "name": { "type": "string", "description": "Display name shown in the storefront. 100 chars max." }, "priceCents": { "type": "integer", "description": "Retail price in cents (USD)." }, - "inStock": { "type": "integer", "description": "Current inventory level. Decremented by orders; reconciled nightly." } + "inStock": { + "type": "integer", + "description": "Current inventory level. Decremented by orders; reconciled nightly." + } }, "required": ["sku", "name", "priceCents", "inStock"], "additionalProperties": false @@ -119,18 +130,22 @@ export class ProductInventory extends Resource { 'Read-only; the underlying Product table is the system of record.'; static properties = { - sku: { type: 'string', primaryKey: true, - description: 'Stock keeping unit; matches Product.sku.' }, - onHand: { type: 'integer', - description: 'Current warehouse count.' }, - reserved: { type: 'integer', - description: 'Units allocated to open orders but not yet shipped.' }, - stockStatus: { type: 'string', enum: ['in_stock', 'out_of_stock', 'backorder'], - description: 'Derived from onHand vs reserved.' }, + sku: { type: 'string', primaryKey: true, description: 'Stock keeping unit; matches Product.sku.' }, + onHand: { type: 'integer', description: 'Current warehouse count.' }, + reserved: { type: 'integer', description: 'Units allocated to open orders but not yet shipped.' }, + stockStatus: { + type: 'string', + enum: ['in_stock', 'out_of_stock', 'backorder'], + description: 'Derived from onHand vs reserved.', + }, }; - async get(id) { /* returns { sku, onHand, reserved, stockStatus } */ } - async search(query) { /* ... */ } + async get(id) { + /* returns { sku, onHand, reserved, stockStatus } */ + } + async search(query) { + /* ... */ + } } ``` @@ -165,7 +180,9 @@ type Customer @table @export { id: Long @primaryKey name: String - """Internal — used by the pricing engine; not for external consumers.""" + """ + Internal — used by the pricing engine; not for external consumers. + """ creditScore: Int @hidden } ``` diff --git a/reference/database/schema.md b/reference/database/schema.md index 03c30260..422d8705 100644 --- a/reference/database/schema.md +++ b/reference/database/schema.md @@ -211,13 +211,19 @@ Product catalog row — what shows up in the storefront listing, search, and inventory feeds. One row per SKU. """ type Product @table @export { - """Stock keeping unit — globally unique across catalogs.""" + """ + Stock keeping unit — globally unique across catalogs. + """ sku: String! @primaryKey - """Display name shown in the storefront.""" + """ + Display name shown in the storefront. + """ name: String! - """Retail price in cents (USD).""" + """ + Retail price in cents (USD). + """ priceCents: Int! } ``` @@ -299,7 +305,9 @@ type Customer @table { id: Long @primaryKey name: String - """Internal — do not surface to external consumers.""" + """ + Internal — do not surface to external consumers. + """ creditScore: Int @hidden } ``` diff --git a/reference/mcp/tool-metadata.md b/reference/mcp/tool-metadata.md index 24642e2e..8f95c7bf 100644 --- a/reference/mcp/tool-metadata.md +++ b/reference/mcp/tool-metadata.md @@ -10,13 +10,13 @@ Harper's MCP server publishes tools via the Model Context Protocol (rev 2025-06- Per the MCP spec, every tool descriptor returned from `tools/list` carries: -| Field | Required | Purpose | -|---|---|---| -| `name` | yes | Machine identifier used by `tools/call` | -| `description` | yes | LLM-facing prose explaining what the tool does and when to pick it over siblings | -| `inputSchema` | yes | JSON Schema for the arguments the tool accepts | -| `outputSchema` | no | JSON Schema for the data the tool returns (added in spec rev 2025-06-18) | -| `annotations` | no | Hints for clients: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title` | +| Field | Required | Purpose | +| -------------- | -------- | ------------------------------------------------------------------------------------------------ | +| `name` | yes | Machine identifier used by `tools/call` | +| `description` | yes | LLM-facing prose explaining what the tool does and when to pick it over siblings | +| `inputSchema` | yes | JSON Schema for the arguments the tool accepts | +| `outputSchema` | no | JSON Schema for the data the tool returns (added in spec rev 2025-06-18) | +| `annotations` | no | Hints for clients: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title` | Harper populates each from a different source depending on the tool's profile and origin. @@ -33,14 +33,14 @@ Harper exposes two MCP profiles, each with its own port and registration loop: For operations tools, the descriptor fields are sourced from: -| Field | Source | -|---|---| -| `name` | Operation name (from `OPERATION_FUNCTION_MAP` key) | -| `description` | Hand-authored entry in the operations descriptions catalog; falls back to a generic template when an operator opts in a non-cataloged operation | -| `inputSchema` | Hand-curated JSON Schema in the operations input-schemas catalog; falls back to `{ type: 'object', additionalProperties: true }` | -| `annotations.readOnlyHint` | `true` if the operation matches a read-only prefix (`describe_`, `list_`, `search_`, `get_`, `read_`) or is the literal `system_information` | -| `annotations.destructiveHint` | `true` for operations in the curated destructive set (`drop_*`, `delete_*`, `restart`, `set_configuration`, etc.) | -| `annotations.idempotentHint` | Default empty; opt-in per operation after end-to-end verification that the second call produces the same observable outcome | +| Field | Source | +| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | Operation name (from `OPERATION_FUNCTION_MAP` key) | +| `description` | Hand-authored entry in the operations descriptions catalog; falls back to a generic template when an operator opts in a non-cataloged operation | +| `inputSchema` | Hand-curated JSON Schema in the operations input-schemas catalog; falls back to `{ type: 'object', additionalProperties: true }` | +| `annotations.readOnlyHint` | `true` if the operation matches a read-only prefix (`describe_`, `list_`, `search_`, `get_`, `read_`) or is the literal `system_information` | +| `annotations.destructiveHint` | `true` for operations in the curated destructive set (`drop_*`, `delete_*`, `restart`, `set_configuration`, etc.) | +| `annotations.idempotentHint` | Default empty; opt-in per operation after end-to-end verification that the second call produces the same observable outcome | Operations registered outside core (for example, `cluster_status` from harper-pro) don't have catalog entries; they fall back to the generic description template until the per-operation metadata registry lands. @@ -48,15 +48,15 @@ Operations registered outside core (for example, `cluster_status` from harper-pr For verb tools generated from exported Resources: -| Field | Source | -|---|---| -| `name` | `${verb}_${sanitized-path}` (e.g. `get_Product`, `search_Customer`) | -| `description` | Composed: `[ResourceClass.description \n\n] ${verb sentence} ${runtime RBAC note}` | -| `inputSchema` | Derived per verb from `ResourceClass.attributes` and the caller's `attribute_permissions`. Per-attribute `description` propagates to `inputSchema.properties[*].description` | -| `outputSchema` | Derived per verb from `ResourceClass.attributes` for `get_*` / `create_*` / `update_*` / `patch_*`. `delete_*` returns `{ deleted: true, }`. `search_*` deliberately omits `outputSchema` | -| `annotations.readOnlyHint` | `true` on `get_*` and `search_*` | -| `annotations.destructiveHint` | `true` on `delete_*` | -| `annotations.idempotentHint` | `true` on `update_*` (PUT semantics); other verbs default off | +| Field | Source | +| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `${verb}_${sanitized-path}` (e.g. `get_Product`, `search_Customer`) | +| `description` | Composed: `[ResourceClass.description \n\n] ${verb sentence} ${runtime RBAC note}` | +| `inputSchema` | Derived per verb from `ResourceClass.attributes` and the caller's `attribute_permissions`. Per-attribute `description` propagates to `inputSchema.properties[*].description` | +| `outputSchema` | Derived per verb from `ResourceClass.attributes` for `get_*` / `create_*` / `update_*` / `patch_*`. `delete_*` returns `{ deleted: true, }`. `search_*` deliberately omits `outputSchema` | +| `annotations.readOnlyHint` | `true` on `get_*` and `search_*` | +| `annotations.destructiveHint` | `true` on `delete_*` | +| `annotations.idempotentHint` | `true` on `update_*` (PUT semantics); other verbs default off | `static description` and `static properties` on the Resource class override the auto-derived values. `static outputSchemas[verb]` overrides per-verb output schemas. `static mcp.annotations[verb]` overrides annotations per verb. `static hidden === true` suppresses the entire Resource from MCP listing. @@ -64,13 +64,13 @@ For verb tools generated from exported Resources: For tools declared via `static mcpTools`: -| Field | Source | -|---|---| -| `name` | `def.name` from the `mcpTools` entry | -| `description` | `def.description` from the entry; falls back to a generic template if omitted (with a warn-once log at registration) | -| `inputSchema` | `def.inputSchema` from the entry; falls back to `{ type: 'object', additionalProperties: true }` if omitted (with a warn-once log at registration) | -| `outputSchema` | `def.outputSchema` from the entry (optional; no fallback) | -| `annotations` | `def.annotations` from the entry (optional pass-through) | +| Field | Source | +| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `name` | `def.name` from the `mcpTools` entry | +| `description` | `def.description` from the entry; falls back to a generic template if omitted (with a warn-once log at registration) | +| `inputSchema` | `def.inputSchema` from the entry; falls back to `{ type: 'object', additionalProperties: true }` if omitted (with a warn-once log at registration) | +| `outputSchema` | `def.outputSchema` from the entry (optional; no fallback) | +| `annotations` | `def.annotations` from the entry (optional pass-through) | Custom tools have no per-user listing filter beyond authentication — the Resource's instance method is responsible for whatever RBAC it needs to enforce. @@ -131,13 +131,13 @@ Note that `add_user` does NOT carry `idempotentHint: true`. Under MCP semantics Harper also publishes a small set of synthetic resources via the MCP `resources/list` endpoint: -| URI | Profile | Description | -|---|---|---| -| `harper://about` | both | Server version, profile, MCP protocol versions | -| `harper://operations` | operations | User-filtered operations catalog | -| `harper://openapi` | application | Full OpenAPI 3.0.3 document | +| URI | Profile | Description | +| ------------------------------ | ----------- | ----------------------------------------------------- | +| `harper://about` | both | Server version, profile, MCP protocol versions | +| `harper://operations` | operations | User-filtered operations catalog | +| `harper://openapi` | application | Full OpenAPI 3.0.3 document | | `harper://schema/{db}/{table}` | application | Per-table schema, filtered by `attribute_permissions` | -| `https://{host}/{path}` | application | Application HTTP Resources, in-process | +| `https://{host}/{path}` | application | Application HTTP Resources, in-process | For `harper://schema/{db}/{table}` and `https://{host}/{path}` entries, the descriptor description prepends `Table.description` / `ResourceClass.description` when present. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 530df1fa..5116d9a7 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -460,7 +460,9 @@ export class ProductInventory extends Resource { 'Aggregate inventory analytics computed over the Product catalog. ' + 'Read-only; the underlying Product table is the system of record.'; - async get(id) { /* ... */ } + async get(id) { + /* ... */ + } } ``` @@ -472,17 +474,19 @@ JSON-Schema-shaped attribute map keyed by name. This is the canonical public API export class ProductInventory extends Resource { static description = '...'; static properties = { - sku: { type: 'string', primaryKey: true, - description: 'Stock keeping unit; matches Product.sku.' }, - onHand: { type: 'integer', - description: 'Current warehouse count.' }, - reserved: { type: 'integer', - description: 'Units allocated to open orders but not yet shipped.' }, - stockStatus: { type: 'string', enum: ['in_stock', 'out_of_stock', 'backorder'], - description: 'Derived from onHand vs reserved.' }, + sku: { type: 'string', primaryKey: true, description: 'Stock keeping unit; matches Product.sku.' }, + onHand: { type: 'integer', description: 'Current warehouse count.' }, + reserved: { type: 'integer', description: 'Units allocated to open orders but not yet shipped.' }, + stockStatus: { + type: 'string', + enum: ['in_stock', 'out_of_stock', 'backorder'], + description: 'Derived from onHand vs reserved.', + }, }; - async get(id) { /* ... */ } + async get(id) { + /* ... */ + } } ``` @@ -513,7 +517,9 @@ Per-verb output schema overrides for programmatic Resources whose verb methods r ```typescript export class ProductInventory extends Resource { static description = '...'; - static properties = { /* full record shape */ }; + static properties = { + /* full record shape */ + }; static outputSchemas = { get: { @@ -527,7 +533,9 @@ export class ProductInventory extends Resource { }, }; - async get(id) { /* returns the projection above */ } + async get(id) { + /* returns the projection above */ + } } ``` @@ -538,7 +546,9 @@ When `true`, the Resource is dropped from MCP tool registration and OpenAPI path ```typescript export class InternalDiagnostics extends Resource { static hidden = true; - async get() { /* ... */ } + async get() { + /* ... */ + } } ``` @@ -549,7 +559,9 @@ Narrow, MCP-only override for annotation hints that don't fit JSON Schema (such ```typescript export class ProductInventory extends Resource { static description = '...'; - static properties = { /* ... */ }; + static properties = { + /* ... */ + }; static mcp = { annotations: { @@ -584,7 +596,9 @@ export class ProductInventory extends Resource { }, ]; - async reconcileInventory(args) { /* ... */ } + async reconcileInventory(args) { + /* ... */ + } } ``` From e82668a2dfa04e471b17ff8edff12fb8c87b553d Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 8 Jun 2026 13:33:48 -0600 Subject: [PATCH 3/4] docs: fix broken cross-references to use absolute /reference/v5/ paths Docusaurus's strict broken-link check rejected relative paths that resolved without the /v5/ versioning prefix the existing docs use. Switched the two inter-doc links to absolute paths matching the established pattern. Co-Authored-By: Claude Opus 4.7 --- learn/developers/mcp-and-openapi-metadata.mdx | 2 +- reference/mcp/tool-metadata.md | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/learn/developers/mcp-and-openapi-metadata.mdx b/learn/developers/mcp-and-openapi-metadata.mdx index b8b13020..8e547c2f 100644 --- a/learn/developers/mcp-and-openapi-metadata.mdx +++ b/learn/developers/mcp-and-openapi-metadata.mdx @@ -149,7 +149,7 @@ export class ProductInventory extends Resource { } ``` -See the [Resource API reference](../../reference/resources/resource-api#class-level-metadata-for-mcp-and-openapi) for the full surface, including `static outputSchemas` for per-verb projection overrides, `static hidden` for full suppression, and `static mcp` for narrow MCP-only annotation overrides. +See the [Resource API reference](/reference/v5/resources/resource-api#class-level-metadata-for-mcp-and-openapi) for the full surface, including `static outputSchemas` for per-verb projection overrides, `static hidden` for full suppression, and `static mcp` for narrow MCP-only annotation overrides. ## Inheritance: extending a table diff --git a/reference/mcp/tool-metadata.md b/reference/mcp/tool-metadata.md index 8f95c7bf..0ee2dbb8 100644 --- a/reference/mcp/tool-metadata.md +++ b/reference/mcp/tool-metadata.md @@ -4,7 +4,7 @@ title: MCP tool payload sourcing # MCP tool payload sourcing -Harper's MCP server publishes tools via the Model Context Protocol (rev 2025-06-18, Streamable HTTP transport). This page is a reference for what fields appear on each generated tool descriptor and where the data comes from. For authoring guidance, see the [Writing quality MCP and OpenAPI descriptions](../../learn/developers/mcp-and-openapi-metadata) how-to. +Harper's MCP server publishes tools via the Model Context Protocol (rev 2025-06-18, Streamable HTTP transport). This page is a reference for what fields appear on each generated tool descriptor and where the data comes from. For authoring guidance, see the [Writing quality MCP and OpenAPI descriptions](/learn/developers/mcp-and-openapi-metadata) how-to. ## Tool descriptor fields @@ -143,6 +143,6 @@ For `harper://schema/{db}/{table}` and `https://{host}/{path}` entries, the desc ## See also -- [Writing quality MCP and OpenAPI descriptions](../../learn/developers/mcp-and-openapi-metadata) — authoring how-to -- [Schema reference: docstrings and `@hidden`](../database/schema) — GraphQL surface -- [Resource API reference: class-level metadata](../resources/resource-api#class-level-metadata-for-mcp-and-openapi) — programmatic surface +- [Writing quality MCP and OpenAPI descriptions](/learn/developers/mcp-and-openapi-metadata) — authoring how-to +- [Schema reference: docstrings and `@hidden`](/reference/v5/database/schema) — GraphQL surface +- [Resource API reference: class-level metadata](/reference/v5/resources/resource-api#class-level-metadata-for-mcp-and-openapi) — programmatic surface From 9b7e6f14627c0b3903be548d751201ebe41ad984 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 8 Jun 2026 14:34:33 -0600 Subject: [PATCH 4/4] docs: address gemini-code-assist review feedback on #516 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five medium-priority polish findings: 1. Removed unused Tabs / TabItem imports from learn/developers/mcp-and-openapi-metadata.mdx — no usage in the guide. 2. Disambiguated the type-level @hidden header from the field-level one in reference/database/schema.md to prevent the duplicate TOC entries and fragile #hidden / #hidden-1 anchors Docusaurus auto-generates. Now `@hidden (Type Directive)` and `@hidden (Field Directive)`. 3. Updated the intra-doc link from #hidden-1 to #hidden-field-directive to match the new explicit anchor. 4. Replaced the placeholder `ReadonlyArray<...>` type signature for `static mcpTools` in reference/resources/resource-api.md with a descriptive type name `McpToolDefinition[]`. Co-Authored-By: Claude Opus 4.7 --- learn/developers/mcp-and-openapi-metadata.mdx | 3 --- reference/database/schema.md | 6 +++--- reference/resources/resource-api.md | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/learn/developers/mcp-and-openapi-metadata.mdx b/learn/developers/mcp-and-openapi-metadata.mdx index 8e547c2f..aea29a39 100644 --- a/learn/developers/mcp-and-openapi-metadata.mdx +++ b/learn/developers/mcp-and-openapi-metadata.mdx @@ -2,9 +2,6 @@ title: Writing quality MCP and OpenAPI descriptions --- -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - When an MCP client connects to Harper, the LLM on the other side sees your application as a list of tools. The text it reads to pick the right tool — the tool description, the per-attribute property descriptions, the output schema shape — is the dominant signal for tool selection. The same metadata also drives Harper's OpenAPI document, which any HTTP API consumer (Swagger UI, Redoc, generated SDKs, machine clients) reads. This guide shows how to author that metadata once and have it flow to both surfaces — via GraphQL docstrings for `@table @export` Resources, and via class-level statics for programmatic Resource subclasses. diff --git a/reference/database/schema.md b/reference/database/schema.md index 422d8705..cb6ff58f 100644 --- a/reference/database/schema.md +++ b/reference/database/schema.md @@ -185,7 +185,7 @@ type StrictRecord @table @sealed { } ``` -### `@hidden` +### `@hidden` (Type Directive) Suppresses the type from introspectable surfaces — MCP tool descriptors and the OpenAPI document. The table still exists; data is still queryable through Harper's other interfaces subject to RBAC. `@hidden` is a **metadata-visibility** directive, not an access-control mechanism: use `attribute_permissions` on roles to control data access. @@ -196,7 +196,7 @@ type InternalConfig @table @hidden { } ``` -`@hidden` is also available as a [field directive](#hidden-1) to suppress individual attributes. +`@hidden` is also available as a [field directive](#hidden-field-directive) to suppress individual attributes. ## Documenting Types and Fields @@ -296,7 +296,7 @@ type Event @table { } ``` -### `@hidden` +### `@hidden` (Field Directive) Suppresses the field from MCP tool descriptors and the OpenAPI document. The attribute still exists in the table; data is still queryable through other interfaces subject to RBAC. Use this for fields that should not appear in introspectable surfaces. diff --git a/reference/resources/resource-api.md b/reference/resources/resource-api.md index 5116d9a7..6124efca 100644 --- a/reference/resources/resource-api.md +++ b/reference/resources/resource-api.md @@ -573,7 +573,7 @@ export class ProductInventory extends Resource { > **Under-annotate before mis-annotate.** Under MCP semantics, `idempotentHint: true` is a strong claim: the second call must produce the same observable outcome as the first. `add_*`-style operations that return "already exists" on the second call are NOT idempotent in this sense, even though they don't crash. Verify repeat-call behavior end-to-end before annotating. -### `static mcpTools?: ReadonlyArray<...>` +### `static mcpTools?: McpToolDefinition[]` Component-author opt-in for exposing non-verb instance methods as MCP tools. Each entry maps an instance-method name to an MCP tool descriptor. RBAC is enforced by the Resource method itself; the MCP layer does not invent new ACLs.