You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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>
Copy file name to clipboardExpand all lines: learn/developers/mcp-and-openapi-metadata.mdx
+4-66Lines changed: 4 additions & 66 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -116,7 +116,7 @@ For `search_Product`, the `conditions[].attribute` field becomes a closed `enum`
116
116
117
117
## Path B: Programmatic Resources via class-level statics
118
118
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.
120
120
121
121
```typescript
122
122
import { Resource } from'harperdb';
@@ -126,10 +126,6 @@ export class ProductInventory extends Resource {
126
126
'Aggregate inventory analytics computed over the Product catalog. '+
127
127
'Read-only; the underlying Product table is the system of record.';
128
128
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.
@@ -150,63 +146,7 @@ export class ProductInventory extends Resource {
150
146
}
151
147
```
152
148
153
-
<VersionBadgetype="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.",
`/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`.
210
150
211
151
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.
212
152
@@ -232,7 +172,7 @@ The author writes against the canonical `properties` API. Internal code paths th
232
172
233
173
## Hiding sensitive fields with `@hidden`
234
174
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:
236
176
237
177
```graphql
238
178
typeCustomer@table@export {
@@ -258,9 +198,7 @@ For OpenAPI, the document is global and not per-user filtered. Use `@hidden` (or
258
198
259
199
## Verifying the end-to-end flow
260
200
261
-
1. Add `"""docstrings"""` toa `@table@export` type— or `staticdescription` + `staticproperties` toaprogrammaticResource — andsaveyourcomponent.
201
+
1. Add `"""docstrings"""` toa `@table@export` typeandsaveyourcomponent.
262
202
2. HitMCP `tools/list` fortheapplicationprofile — confirm `get_*`, `search_*`, etc. descriptionsincludethetypedocstringandper-attributedescriptionsarepresentinthe `inputSchema` and `outputSchema`.
263
203
3. Hit `/openapi` ontheapplicationHTTPport — confirmthepath-leveldescriptionsandper-propertydescriptionsshowupinSwaggerUI / Redoc.
TwothingstocheckforaprogrammaticResourceinstep 2. Anempty `properties: {}` onthe `get_*` outputschema 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).
Copy file name to clipboardExpand all lines: reference/mcp/overview.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -46,7 +46,7 @@ Wraps Harper's operation catalog (the same set of operations the REST `/operatio
46
46
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).
47
47
48
48
- 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` <VersionBadgetype="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.
50
50
- Components can opt non-verb instance methods into the MCP surface by declaring a static `mcpTools` array on the Resource class.
51
51
- A Resource is excluded from the MCP surface when its registration sets `exportTypes.mcp = false`.
|`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 <VersionBadgetype="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).
|`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 |
62
60
63
61
`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
62
@@ -141,7 +139,7 @@ Harper also publishes a small set of synthetic resources via the MCP `resources/
141
139
|`harper://schema/{db}/{table}`| application | Per-table schema, filtered by `attribute_permissions`|
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 <VersionBadgetype="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.
Copy file name to clipboardExpand all lines: reference/mcp/tools-and-resources.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -72,15 +72,15 @@ The Resource's path is sanitized into a valid tool name: `/` and `.` become `_`.
72
72
73
73
### Input schema derivation
74
74
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) <VersionBadgetype="changed"version="v5.2.0" />, whose JSON Schema types pass through unchanged:
- Nested `Object` and `Array` attributes recurse into their `properties` / `elements`.
79
79
-`nullable: true` adds `"null"` to the type union.
80
80
- 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.
82
82
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.
0 commit comments