docs(resources): document static properties as a first-class MCP/OpenAPI schema source - #605
docs(resources): document static properties as a first-class MCP/OpenAPI schema source#605kylebernhardy wants to merge 6 commits into
Conversation
…API schema source
Harper 5.2.0 makes a programmatic Resource's `static properties` drive the
MCP tool schemas, the OpenAPI document, and `harper://schema` — previously
those surfaces read only the internal `attributes` Array, so a bare
`static properties` declaration produced a skeletal `{ type: 'object' }`.
5.2.0 also resolves `GET /Resource/id.property` against `static properties`.
Documents the authoring path: the JSON Schema vocabulary (lowercase types,
distinct from the capitalized GraphQL names), the full fragment key list,
how unions/item-less arrays/optional properties resolve, and a nested +
array-of-object example.
Companion to HarperFast/harper#1921 and #1933; closes HarperFast/harper#1923.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
There was a problem hiding this comment.
Code Review
This pull request updates the documentation across several files to reflect changes in version v5.2.0, specifically detailing how programmatic Resources declaring static properties now receive rich schema generation across MCP, OpenAPI, and schema introspection surfaces. It adds comprehensive explanations of JSON Schema vocabulary, an authoring rubric, and examples. Feedback on the changes suggests updating the get method in the OrderSummary example to be a static method to maintain consistency with Harper Resource documentation standards.
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605 This preview will update automatically when you push new commits. |
Adversarial fact-check of every technical claim in the previous commit
against harper core turned up 15 defects. Corrections:
- harper://schema is keyed by database/table, so a table-less programmatic
Resource never appears there; dropped it from the surface list.
- Only get_* has a record-shaped outputSchema. search_* has none; the write
verbs advertise fixed {id}/{ok}/{deleted} envelopes. update_*/patch_* are
mutually exclusive.
- Nullability: MCP re-expands to type:['string','null'] and never emits a
`nullable` keyword; OpenAPI drops it at the top level and emits it only
inside nested objects.
- enum/format/const and per-property hidden are honored at the top level;
nested/items keep them on MCP but not OpenAPI. Timestamp keys are MCP-only.
- The OpenAPI path parameter and the verb-description sentence read the
class-level `static primaryKey`, not the fragment flag — document both.
- Capitalized GraphQL type names DO map correctly; the real hazard is a name
in neither vocabulary, where the two surfaces disagree.
- Corrected the get_ProductInventory sample to the actual emitted shape
(pk description override, get_attributes description, required +
additionalProperties on the output schema).
- Added the super-user-only listing caveat for table-less Resources.
- Fixed a pre-existing claim that attribute_permissions filters tool schemas
per-user; schemas are built once at registration and are user-agnostic.
- REST: .json/.cbor/.msgpack/.csv take precedence over a same-named property.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
…contradicted it Conventions/consistency pass found three pages stating the old rule, none of which the first pass touched: - reference/mcp/overview.md said input schemas are "narrowed by your role's attribute_permissions" — they are not; schemas are built once at registration and are caller-agnostic. Corrected, plus the static properties source. - reference/mcp/tools-and-resources.md said input schemas come from Table.attributes only. - reference/rest/querying.md is the canonical page for the `id.property` URL form and still said "declared in the schema"; rest/overview.md restated the same rule 26 lines above the one this PR had updated. Also: moved the Path B version badge off the (unchanged) section heading onto the paragraph it describes, and gave the ProductInventory example the `static primaryKey` its sample output already assumed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
|
The three core-behavior gaps flagged in the description are now filed against v5.2:
These pages document current behavior, so each of those fixes will need a small follow-up edit here — the per-surface caveats in the fragment-key table collapse back to single rows once #1942 lands, the "top-level only" wording goes away with #1941, and the listing-visibility note changes with #1940. Each issue names the specific docs text to revisit. Comment generated by kAIle (Claude Opus 4.8). |
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605 This preview will update automatically when you push new commits. |
… examples The examples in this section defined `get` as an instance method while the page's own primary examples (and the Resource Static Methods section above) use `static get(target)` — the documented override point. Converts all five, not just the one added by this PR. Flagged by gemini-code-assist on documentation#605. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605 This preview will update automatically when you push new commits. |
…ing instance Verified by booting Harper with a programmatic static-properties component and hitting the endpoints: - The route is `/openapi`, not `/openapi.json` — the latter 404s. Four references corrected, two of which this PR introduced. The integration suite uses `/openapi`, which is the authority. - "OpenAPI is typically exposed to anyone reachable on the HTTP port" overstates it: a default `prod` install returns 403 for an unauthenticated GET. What is actually true, and what matters for the @hidden guidance, is that the document is global — no per-user filtering — so any authorized caller sees every docstring regardless of their own read permissions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605 This preview will update automatically when you push new commits. |
Harper's OpenAPI document declares 3.0.3, whose Schema Object is the JSON Schema draft-04 subset — `const` arrived in draft-06, so it is not a keyword there. The reference and the guide both told authors `const` surfaces on the OpenAPI side as written, which was never true and is now explicitly translated in core (harper#1921). Documents what an author actually sees: write `const`, expect `enum: [value]` in the OpenAPI output, with the same constraint semantics. Also notes that a bare `type: 'null'` becomes an untyped nullable schema for the same reason. Raised by kriszyp in review of harper#1921. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0188G62J9fZQg4J9rVuqLzjy
🚀 Preview DeploymentYour preview deployment is ready! 🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605 This preview will update automatically when you push new commits. |
Ethan-Arrowood
left a comment
There was a problem hiding this comment.
The writing here is precise and the Path-A/Path-B framing is genuinely useful — my concern is purely that the facts have moved. You refreshed the docs the same day as #1921's d1065816 (the const→enum fix), but #1921 has landed four commits since, and three of the divergence bullets are now wrong in the reader's favor — they warn authors away from shapes that work.
Requesting changes on the stale bullets (inline). Separately, a merge-ordering question that may make some of this text moot: harper#1944 removes the #1941/#1942 divergences these v5.2.0-badged sections document. If #1944 lands in 5.2.0, these passages need a same-release rewrite — so it's worth deciding now whether to write them against the unified emitter instead. (I reviewed #1944 today; it still has an outstanding union-narrowing blocker.)
Two smaller notes: this is the repo's first v5.2.0 badge and no release-notes/v5-lincoln/5.2 page exists yet — you flagged that yourself; worth tracking so the 5.2 entry lands with the feature. And an OSPO thought: the Path-B authoring rubric is agent-facing guidance, but harper-best-practices doesn't cover static properties schema authoring at all — worth a skill section once the 5.2.0 semantics are final.
Is a final fact-check pass against #1921's merge SHA already planned? That would catch this class automatically.
sent with Claude Opus 5
|
|
||
| Notes on how a few of these resolve: | ||
|
|
||
| - **Nullability is emitted differently per surface.** A `'null'` member in a union folds into the same internal flag `nullable: true` sets. MCP then re-expands it — `type: ['string', 'null']` emits `{ "type": ["string", "null"] }`, never a `nullable` keyword. OpenAPI 3.0.3 emits `{ "type": "string" }` for a top-level property (the flag isn't represented there) and `{ "type": "string", "nullable": true }` for a property inside a nested object. A union of two non-null types isn't expressible on the attribute form, so the first member wins — prefer a single type where you can. |
There was a problem hiding this comment.
Two claims in this bullet are stale against #1921's head:
- Top-level OpenAPI
nullableis now emitted — the docs say it's dropped. As written, an author reads this after 5.2.0 ships withf34cedefand believes a working feature is missing. - Multi-type unions are preserved (MCP emits a type array, OpenAPI emits
oneOf) — the docs say the first member wins, and the "prefer a single type" advice steers authors away from a shape that now works correctly.
Both need rewriting against the current head.
| - **Nullability is emitted differently per surface.** A `'null'` member in a union folds into the same internal flag `nullable: true` sets. MCP then re-expands it — `type: ['string', 'null']` emits `{ "type": ["string", "null"] }`, never a `nullable` keyword. OpenAPI 3.0.3 emits `{ "type": "string" }` for a top-level property (the flag isn't represented there) and `{ "type": "string", "nullable": true }` for a property inside a nested object. A union of two non-null types isn't expressible on the attribute form, so the first member wins — prefer a single type where you can. | ||
| - **`const` is spelled differently per surface.** Harper's OpenAPI document declares 3.0.3, whose Schema Object is the JSON Schema draft-04 subset — `const` only arrived in draft-06, so it is not a keyword there. MCP receives `const` as written; OpenAPI receives the equivalent single-value `enum`. `{ kind: { type: 'string', const: 'order' } }` reaches Swagger UI as `{ "type": "string", "enum": ["order"] }`. The constraint is identical either way; only the spelling changes. For the same reason a bare `type: 'null'` becomes an untyped `nullable` schema in the OpenAPI document, since 3.0 has no `null` type. | ||
| - `{ type: 'array' }` with no `items` is valid and means "array of anything"; it emits a bare array schema rather than guessing an element type. | ||
| - `hidden` and `assignCreatedTime` / `assignUpdatedTime` are honored only at the top level, and the timestamp keys only by MCP — OpenAPI request bodies reference one shared component schema per resource, so a Harper-assigned property still appears there. |
There was a problem hiding this comment.
Also stale: nested hidden is honored in the OpenAPI static-properties path at #1921's head. This needs a per-surface split, since MCP nested still leaks (that's #1941) — as one combined statement it's wrong for OpenAPI and right for MCP.
| Notes on how a few of these resolve: | ||
|
|
||
| - **Nullability is emitted differently per surface.** A `'null'` member in a union folds into the same internal flag `nullable: true` sets. MCP then re-expands it — `type: ['string', 'null']` emits `{ "type": ["string", "null"] }`, never a `nullable` keyword. OpenAPI 3.0.3 emits `{ "type": "string" }` for a top-level property (the flag isn't represented there) and `{ "type": "string", "nullable": true }` for a property inside a nested object. A union of two non-null types isn't expressible on the attribute form, so the first member wins — prefer a single type where you can. | ||
| - **`const` is spelled differently per surface.** Harper's OpenAPI document declares 3.0.3, whose Schema Object is the JSON Schema draft-04 subset — `const` only arrived in draft-06, so it is not a keyword there. MCP receives `const` as written; OpenAPI receives the equivalent single-value `enum`. `{ kind: { type: 'string', const: 'order' } }` reaches Swagger UI as `{ "type": "string", "enum": ["order"] }`. The constraint is identical either way; only the spelling changes. For the same reason a bare `type: 'null'` becomes an untyped `nullable` schema in the OpenAPI document, since 3.0 has no `null` type. |
There was a problem hiding this comment.
Related precision fix while you're in here: type: 'null' now emits { nullable: true, enum: [null] } at every depth (d1065816/eaa755b4), not just "an untyped nullable schema". Worth folding into the nullability refresh above.
Companion docs for harper#1921 —
feat(mcp/openapi): drive schemas from a programmatic Resource's static propertiesand harper#1933 —fix(resources): URL attribute-suffix routing for programmatic static-properties Resources. Closes HarperFast/harper#1923.What changed in core
static propertieshas been the canonical class-level metadata API since #1167, and the docs already described it — but only table-backed Resources actually got rich schemas from it. The MCP tool builder and the OpenAPI generator both read the internalattributesArray, so a programmatic Resource declaring a barestatic propertiesliteral produced an empty property set. 5.2.0 fixes that, and also resolvesGET /Resource/id.propertyagainststatic properties.So this is less "document a new API" than "the documented API now works for the case the docs implied it did" — plus the vocabulary, constraint, and per-surface details an author actually needs.
Changes
reference/resources/resource-api.md— the bulk of it. Understatic properties: which surfaces derive from it (and which explicitly don't), a JSON Schema vocabulary subsection, a table of every fragment key Harper reads with its per-surface behavior, resolution notes for unions / item-less arrays / optional properties /static primaryKey, and a nested-object + array-of-object example.learn/developers/mcp-and-openapi-metadata.mdx— Path B gets the emittedtools/listJSON, a vocabulary warning, a Path-B authoring rubric, and debugging guidance.reference/mcp/tool-metadata.md,reference/mcp/tools-and-resources.md,reference/mcp/overview.md— the three places that said input schemas come fromTable.attributes.reference/rest/overview.md,reference/rest/querying.md— theGET /MyTable/123.propertyNamerule, in both places it's stated.Review history worth knowing about
The first commit (
7fea797) was fact-checked against the implementation and 15 of its claims were wrong;6cfe041and556106bcorrect them. If you reviewed the first commit, please re-read rather than diffing. The corrections that matter most, because each is a per-surface asymmetry a reader would not guess:harper://schema/{db}/{table}is keyed by database/table, so a table-less programmatic Resource never appears there. It is not one of the surfaces.get_*has a record-shapedoutputSchema.search_*has none; the write verbs advertise fixed{id}/{ok}/{deleted}envelopes.update_*andpatch_*are mutually exclusive.type: ["string","null"]and never emits anullablekeyword; OpenAPI 3.0.3 drops it at the top level and emits it only inside nested objects.enum/format/constand per-propertyhiddenare honored at the top level only; inside a nested object or an array'sitems, MCP keeps them and OpenAPI drops them.static primaryKey(default'id'), not the fragment'sprimaryKey: true. Both examples now declare both.String,Int) do map correctly; the real hazard is a name in neither vocabulary ('Text'), where MCP coerces tostringand OpenAPI emits{}.Open items for a human
mcp/overview.mdand the metadata guide both saidattribute_permissionsnarrows MCP input schemas per user — every derivation call site passesundefined; schemas are built once at registration and are caller-agnostic (RBAC filters the tool list, and is enforced at call time). Shout if you'd rather split those out.<VersionBadge version="v5.2.0" />is the first v5.2.0 badge in the repo, and there's norelease-notes/v5-lincoln/5.2.mdyet — I didn't create one for a single entry. Happy to add it once that page exists.makeVisibleToreturnsfalsewithout a db/table),hiddenon a nested sub-property isn't suppressed (OpenAPI even emitshidden: trueas a schema key), and the two surfaces disagree on unknown type names. The docs describe current behavior; if any of these get fixed, these pages need a follow-up.Verified with
npm run build(clean; the one reported broken anchor is pre-existing onrelease-notes/v5-lincoln/5.1) andnpm run format:check.PR description generated by kAIle (Claude Opus 4.8).