Skip to content

Commit c35e4c5

Browse files
docs: scope to shipped behavior; harper#1921 has not landed
harper#1921 (`static properties` as an MCP/OpenAPI schema source) is still open with changes requested, and v5.2.0 through v5.2.6 all shipped without it — at v5.2.6 `components/mcp/tools/application.ts` still reads `ResourceClass?.attributes ?? []`, with no `resolveAttributes` fallback. Every `<VersionBadge version="v5.2.0" />` on those sections was therefore false, so the #1921-dependent content is removed and will return, badged against the release it actually ships in, once that PR lands. What stays, each verified against v5.2.6: - REST `id.property` resolving against `static properties` (harper#1933, present at v5.2.0, absent at v5.1.26), including that the `.json` / `.cbor` / `.msgpack` / `.csv` content-type suffixes are matched first, and a new 5.2 release-notes entry for it. - `attribute_permissions` does NOT narrow MCP tool schemas. Every derivation call site passes `undefined` for permissions, so schemas are built once at registration and are identical for every caller; enforcement happens at call time, and what RBAC filters is the tool list via table-level permissions. Corrected in `mcp/overview.md`, `mcp/tools-and-resources.md`, `mcp/tool-metadata.md`, and the metadata guide. - Output-schema contracts: only `get_*` is record-shaped; `create_*` is `{ id }`, `update_*` / `patch_*` are `{ ok }`, `delete_*` is `{ deleted }` with no primary key, and `search_*` registers none — so `outputSchemas.search` has no effect. Replaces the `resource-api.md` claim that the write verbs fall back to `static properties`. - `Path B` no longer claims the two surfaces read `static properties` uniformly; it states the actual gap and points at harper#1923. - `/openapi.json` -> `/openapi`, and the access model: `GET /openapi` is super-user-only (403 otherwise) while MCP's `harper://openapi` applies no permission check at all. - Static verb handlers in the examples (gemini's review). Also merges main, which the branch was 101 commits behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 78b3df2 commit c35e4c5

6 files changed

Lines changed: 38 additions & 160 deletions

File tree

learn/developers/mcp-and-openapi-metadata.mdx

Lines changed: 4 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ For `search_Product`, the `conditions[].attribute` field becomes a closed `enum`
116116

117117
## Path B: Programmatic Resources via class-level statics
118118

119-
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.
119+
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.
120120

121121
```typescript
122122
import { Resource } from 'harperdb';
@@ -126,10 +126,6 @@ export class ProductInventory extends Resource {
126126
'Aggregate inventory analytics computed over the Product catalog. ' +
127127
'Read-only; the underlying Product table is the system of record.';
128128

129-
// The fragment flag below types MCP's `id` argument; this class static is what the
130-
// OpenAPI path parameter and the tool-description sentence read. Declare both.
131-
static primaryKey = 'sku';
132-
133129
static properties = {
134130
sku: { type: 'string', primaryKey: true, description: 'Stock keeping unit; matches Product.sku.' },
135131
onHand: { type: 'integer', description: 'Current warehouse count.' },
@@ -150,63 +146,7 @@ export class ProductInventory extends Resource {
150146
}
151147
```
152148

153-
<VersionBadge type="changed" version="v5.2.0" /> — MCP `tools/list` returns the same shape a table-backed Resource
154-
produces — types, per-property descriptions, and the `enum` — instead of an empty property set:
155-
156-
```json
157-
{
158-
"name": "get_ProductInventory",
159-
"description": "Aggregate inventory analytics computed over the Product catalog. Read-only; the underlying Product table is the system of record.\n\nFetches a single ProductInventory record by sku. Runtime RBAC (allowGet) enforces per-record access at call time.",
160-
"inputSchema": {
161-
"type": "object",
162-
"properties": {
163-
"id": { "type": "string", "description": "Primary key (sku)." },
164-
"get_attributes": {
165-
"type": "array",
166-
"items": { "type": "string" },
167-
"description": "Attribute names to project; defaults to all readable attributes."
168-
}
169-
},
170-
"required": ["id"]
171-
},
172-
"outputSchema": {
173-
"type": "object",
174-
"properties": {
175-
"sku": { "type": "string", "description": "Stock keeping unit; matches Product.sku." },
176-
"onHand": { "type": "integer", "description": "Current warehouse count." },
177-
"reserved": { "type": "integer", "description": "Units allocated to open orders but not yet shipped." },
178-
"stockStatus": {
179-
"type": "string",
180-
"enum": ["in_stock", "out_of_stock", "backorder"],
181-
"description": "Derived from onHand vs reserved."
182-
}
183-
},
184-
"required": ["sku"],
185-
"additionalProperties": false
186-
},
187-
"annotations": { "readOnlyHint": true }
188-
}
189-
```
190-
191-
`/openapi` picks up the same properties. Two things this does _not_ reach: `harper://schema/{db}/{table}` is keyed by database and table name, so a Resource with no backing table never appears there; and only `get_*` carries a record-shaped `outputSchema``search_*` has none, and the write verbs advertise fixed `{ id }` / `{ ok }` / `{ deleted }` envelopes.
192-
193-
The `"record by sku"` phrasing above and the OpenAPI path parameter both come from that `static primaryKey`, not from the fragment's `primaryKey: true`. Omit it and you get `/ProductInventory/{id}` and "…record by id" while MCP's `id` argument still describes itself as the `sku`.
194-
195-
### Watch the vocabulary: lowercase JSON Schema, not GraphQL
196-
197-
`static properties` is JSON Schema. Types are lowercase — `string`, `integer`, `number`, `boolean`, `object`, `array`, `null`. Harper also recognizes the capitalized GraphQL names (`String`, `Int`, `Long`) you write in a `.graphql` schema, so those still map correctly. The hazard is a name in neither vocabulary — `'Text'`, `'Object'`, `'Array'` — where MCP quietly coerces to `{ type: 'string' }` and OpenAPI emits an untyped `{}`. Sticking to the lowercase names keeps the two surfaces in agreement.
198-
199-
Beyond `type` and `description`, the fragments carry `enum` / `format` / `const` for value constraints, `items` for arrays (including arrays of objects), and `properties` / `required` / `additionalProperties` for nested objects. Those hints reach both surfaces at the top level; inside a nested object or an array's `items`, MCP keeps them and OpenAPI drops them.
200-
201-
One spelling difference worth knowing: Harper's OpenAPI document declares 3.0.3, which predates JSON Schema's `const` keyword, so a `const` fragment reaches Swagger UI as an equivalent single-value `enum`. Write `const`; expect `enum: [value]` in the OpenAPI output. See the [Resource API reference](/reference/v5/resources/resource-api#static-properties-recordstring-jsonschemafragment) for the full key list and how unions and item-less arrays resolve.
202-
203-
### Authoring rubric, Path B edition
204-
205-
- **Describe every property.** The rubric from Path A applies verbatim — meaning over type, units and formats spelled out, short.
206-
- **Use `enum` wherever the value set is closed.** It's the single highest-leverage hint for an LLM: it turns "pass a status string" into "pass one of these four."
207-
- **Add `format`** (`date-time`, `uuid`, `email`, ...) where it applies. It reaches Swagger UI and gives the LLM a concrete shape to emit.
208-
- **Declare a `primaryKey` property _and_ `static primaryKey`.** The fragment flag types and describes the `id` argument on `get_*` / `update_*` / `delete_*`; the class static drives the OpenAPI path parameter and the tool-description sentence.
209-
- **Reach for `static outputSchemas`** when a verb returns something other than the full record — `get_*` is the only verb whose output schema is derived from `static properties`, so this is how you describe what `create_*` or a custom `search_*` actually returns.
149+
`static description`, `static hidden`, and `static outputSchemas` take effect on the MCP and OpenAPI surfaces as written. `static properties` is the canonical class-level metadata API and drives REST behavior — notably which `id.property` URL suffixes resolve — but the MCP tool builder and the OpenAPI generator still derive their schemas from the Resource's internal attributes list, which a purely programmatic Resource does not have. So the `properties` above will not by itself populate `inputSchema` / `outputSchema` or the OpenAPI component; that gap is tracked in [harper#1923](https://github.com/HarperFast/harper/issues/1923). Until it closes, describe such a Resource's result shape with `static outputSchemas`.
210150

211151
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.
212152

@@ -232,7 +172,7 @@ The author writes against the canonical `properties` API. Internal code paths th
232172

233173
## Hiding sensitive fields with `@hidden`
234174

235-
The OpenAPI document is global: there is no per-user filtering on `/openapi`, so every caller who can fetch it sees the same schemas and the same docstrings. Whether an unauthenticated caller can fetch it at all depends on your instance's authentication configuration — a default `prod` install returns `403` without credentials — but treat any field you document as visible to every user who can reach the endpoint, not just those with permission to read it. The `@hidden` directive suppresses a field (or an entire type) from both MCP and OpenAPI without affecting data access:
175+
The OpenAPI document is global: there is no per-user filtering, so every caller who can fetch it sees the same schemas and the same docstrings. `GET /openapi` itself is super-user-only — any other caller gets `403` — but the same document is also served over MCP as the `harper://openapi` resource, which applies no permission check at all. So treat any field you document as visible to every MCP client that can reach the application profile, not just to users with permission to read it. The `@hidden` directive suppresses a field (or an entire type) from both MCP and OpenAPI without affecting data access:
236176

237177
```graphql
238178
type Customer @table @export {
@@ -258,9 +198,7 @@ For OpenAPI, the document is global and not per-user filtered. Use `@hidden` (or
258198

259199
## Verifying the end-to-end flow
260200

261-
1. Add `"""docstrings"""` to a `@table @export` type or `static description` + `static properties` to a programmatic Resourceand save your component.
201+
1. Add `"""docstrings"""` to a `@table @export` type and save your component.
262202
2. Hit MCP `tools/list` for the application profileconfirm `get_*`, `search_*`, etc. descriptions include the type docstring and per-attribute descriptions are present in the `inputSchema` and `outputSchema`.
263203
3. Hit `/openapi` on the application HTTP portconfirm the path-level descriptions and per-property descriptions show up in Swagger UI / Redoc.
264204
4. Add `@hidden` to an attributeconfirm it disappears from both surfaces while remaining queryable via direct REST/SQL.
265-
266-
Two things to check for a programmatic Resource in step 2. An empty `properties: {}` on the `get_*` output schema means the fragments never resolved — most often because the class also carries a non-empty `attributes` Array, which wins. Properties named `0`, `1`, `2`… mean `static properties` was declared as an array instead of a `Record` keyed by property name. And if the tools don't appear in `tools/list` at all, check whether you're authenticated as a super-user — see the listing-visibility note in the [Resource API reference](/reference/v5/resources/resource-api#static-properties-recordstring-jsonschemafragment).

reference/mcp/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Wraps Harper's operation catalog (the same set of operations the REST `/operatio
4646
Walks your application's exported `Resource` classes and generates one MCP tool per implemented REST verb. Mounts on the **application HTTP server** (the same listener that serves your REST endpoints).
4747

4848
- For each exported Resource, Harper emits `get_<name>`, `search_<name>`, `create_<name>`, `update_<name>`, and `delete_<name>` tools when the corresponding verb is implemented on the prototype.
49-
- Input schemas are derived from `Table.attributes`, or from a programmatic Resource's `static properties` <VersionBadge type="changed" version="v5.2.0" /> when it declares no attributes. They are built once at registration and are identical for every caller — `attribute_permissions` is enforced at call time and filters which tools `tools/list` returns, not the schemas themselves.
49+
- Input schemas are derived from `Table.attributes`. They are built once at registration time and are identical for every caller — `attribute_permissions` is enforced when the tool runs, not reflected in the advertised schema. What RBAC filters is the tool _list_: `tools/list` omits a Resource's verb tools unless the caller holds the matching table-level permission.
5050
- Components can opt non-verb instance methods into the MCP surface by declaring a static `mcpTools` array on the Resource class.
5151
- A Resource is excluded from the MCP surface when its registration sets `exportTypes.mcp = false`.
5252

reference/mcp/tool-metadata.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,15 @@ Operations registered outside core (for example, `cluster_status` from harper-pr
4848

4949
For verb tools generated from exported Resources:
5050

51-
| Field | Source |
52-
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53-
| `name` | `${verb}_${sanitized-path}` (e.g. `get_Product`, `search_Customer`) |
54-
| `description` | Composed: `[ResourceClass.description \n\n] ${verb sentence} ${runtime RBAC note}` |
55-
| `inputSchema` | Derived per verb from the Resource's schema (below), once at registration time and identical for every caller. Per-property `description` propagates to `inputSchema.properties[*].description` |
56-
| `outputSchema` | The full record shape on `get_*`; fixed envelopes on `create_*` (`{ id }`), `update_*` / `patch_*` (`{ ok }`), and `delete_*` (`{ deleted }`). `search_*` deliberately omits `outputSchema` |
57-
| `annotations.readOnlyHint` | `true` on `get_*` and `search_*` |
58-
| `annotations.destructiveHint` | `true` on `delete_*` |
59-
| `annotations.idempotentHint` | `true` on `update_*` (PUT semantics); other verbs default off |
60-
61-
The schema source is the Resource's table-derived attributes when it has them, and its `static properties` declaration when it doesn't — a programmatic Resource declaring only `static properties` yields the same rich `inputSchema` / `outputSchema` as a table-backed one <VersionBadge type="changed" version="v5.2.0" />. `static properties` uses JSON Schema types (lowercase), including `enum`, `format`, `const`, arrays, and nested objects; see the [Resource API reference](/reference/v5/resources/resource-api#static-properties-recordstring-jsonschemafragment).
51+
| Field | Source |
52+
| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
53+
| `name` | `${verb}_${sanitized-path}` (e.g. `get_Product`, `search_Customer`) |
54+
| `description` | Composed: `[ResourceClass.description \n\n] ${verb sentence} ${runtime RBAC note}` |
55+
| `inputSchema` | Derived per verb from `ResourceClass.attributes`, once at registration time and identical for every caller. Per-attribute `description` propagates to `inputSchema.properties[*].description` |
56+
| `outputSchema` | The full record shape on `get_*`; fixed envelopes on `create_*` (`{ id }`), `update_*` / `patch_*` (`{ ok }`), and `delete_*` (`{ deleted }`). `search_*` deliberately omits `outputSchema` |
57+
| `annotations.readOnlyHint` | `true` on `get_*` and `search_*` |
58+
| `annotations.destructiveHint` | `true` on `delete_*` |
59+
| `annotations.idempotentHint` | `true` on `update_*` (PUT semantics); other verbs default off |
6260

6361
`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.
6462

@@ -141,7 +139,7 @@ Harper also publishes a small set of synthetic resources via the MCP `resources/
141139
| `harper://schema/{db}/{table}` | application | Per-table schema, filtered by `attribute_permissions` |
142140
| `https://{host}/{path}` | application | Application HTTP Resources, in-process |
143141

144-
For `harper://schema/{db}/{table}` and `https://{host}/{path}` entries, the descriptor description prepends `Table.description` / `ResourceClass.description` when present. `harper://schema/{db}/{table}` is enumerated by database and table name, so a Resource that identifies no table is not listed there; for one that does, the schema body falls back to its `static properties` declaration when its `attributes` Array is empty <VersionBadge type="changed" version="v5.2.0" />.
142+
For `harper://schema/{db}/{table}` and `https://{host}/{path}` entries, the descriptor description prepends `Table.description` / `ResourceClass.description` when present.
145143

146144
## See also
147145

reference/mcp/tools-and-resources.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ The Resource's path is sanitized into a valid tool name: `/` and `.` become `_`.
7272

7373
### Input schema derivation
7474

75-
Input schemas come from `Table.attributes` — or, for a programmatic Resource that declares no attributes, from its [`static properties`](/reference/v5/resources/resource-api#static-properties-recordstring-jsonschemafragment) <VersionBadge type="changed" version="v5.2.0" />, whose JSON Schema types pass through unchanged:
75+
Input schemas come from `Table.attributes`:
7676

7777
- Harper types map to JSON Schema primitive types (`Int`/`Long`/`BigInt``integer`, `Float``number`, `String`/`ID``string`, `Boolean``boolean`, `Date``[string, number]`, `Bytes`/`Blob``string` with `contentEncoding: base64`).
7878
- Nested `Object` and `Array` attributes recurse into their `properties` / `elements`.
7979
- `nullable: true` adds `"null"` to the type union.
8080
- Auto-managed columns (`assignCreatedTime`, `assignUpdatedTime`, `expiresAt`) and computed columns are stripped from write schemas (`create_*`, `update_*`) — the server fills them in.
81-
- Per-attribute `attribute_permissions` narrow the schema **per requesting user**: attributes the user cannot read are stripped from `get_*` / `search_*` schemas; attributes the user cannot insert/update are stripped from `create_*` / `update_*` schemas.
81+
- Every remaining attribute is included. Schemas are derived **once at registration time**, with no caller permissions in scope, so `attribute_permissions` does not narrow them — the descriptor a restricted user receives is identical to a super-user's.
8282

83-
The schema narrowing is a UX optimization, not a security boundary — runtime `Table.allowUpdate` / `Table.allowCreate` still enforces. The narrowing just avoids burning LLM tokens on fields the user couldn't write anyway.
83+
`attribute_permissions` is enforced when the tool runs, not when it is described: runtime `Table.allowUpdate` / `Table.allowCreate` and the per-attribute checks reject a restricted read or write regardless of what the advertised schema listed. What RBAC does filter is the tool _list_`tools/list` omits a Resource's verb tools unless the caller holds the matching table-level permission (`read` or `describe` for `get_*` / `search_*`, and `insert` / `update` / `delete` for the write verbs). Because the schema is caller-agnostic, treat every attribute name and `description` in it as visible to any authenticated caller who can see the tool, and use `@hidden` for anything that shouldn't be.
8484

8585
### Custom `mcpTools` opt-in
8686

0 commit comments

Comments
 (0)