Skip to content

docs(resources): document static properties as a first-class MCP/OpenAPI schema source - #605

Open
kylebernhardy wants to merge 8 commits into
mainfrom
docs/static-properties-mcp-openapi-1923
Open

docs(resources): document static properties as a first-class MCP/OpenAPI schema source#605
kylebernhardy wants to merge 8 commits into
mainfrom
docs/static-properties-mcp-openapi-1923

Conversation

@kylebernhardy

@kylebernhardy kylebernhardy commented Jul 24, 2026

Copy link
Copy Markdown
Member

Companion docs for harper#1921 — feat(mcp/openapi): drive schemas from a programmatic Resource's static properties and harper#1933 — fix(resources): URL attribute-suffix routing for programmatic static-properties Resources. Closes HarperFast/harper#1923.

What changed in core

static properties has 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 internal attributes Array, so a programmatic Resource declaring a bare static properties literal produced an empty property set. 5.2.0 fixes that, and also resolves GET /Resource/id.property against static 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. Under static 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 emitted tools/list JSON, 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 from Table.attributes.
  • reference/rest/overview.md, reference/rest/querying.md — the GET /MyTable/123.propertyName rule, 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; 6cfe041 and 556106b correct 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.
  • Only get_* has a record-shaped outputSchema. search_* has none; the write verbs advertise fixed {id} / {ok} / {deleted} envelopes. update_* and patch_* are mutually exclusive.
  • Nullability: MCP re-expands to type: ["string","null"] and never emits a nullable keyword; OpenAPI 3.0.3 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 only; inside a nested object or an array's items, MCP keeps them and OpenAPI drops them.
  • The OpenAPI path parameter and the verb-description sentence read the class-level static primaryKey (default 'id'), not the fragment's primaryKey: true. Both examples now declare both.
  • Capitalized GraphQL type names (String, Int) do map correctly; the real hazard is a name in neither vocabulary ('Text'), where MCP coerces to string and OpenAPI emits {}.

Open items for a human

  • Two pre-existing docs claims were false and are corrected here, which widens this PR beyond #1923. mcp/overview.md and the metadata guide both said attribute_permissions narrows MCP input schemas per user — every derivation call site passes undefined; 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 no release-notes/v5-lincoln/5.2.md yet — I didn't create one for a single entry. Happy to add it once that page exists.
  • Three core behaviors documented here look like bugs, not intent — filed separately if the team agrees: verb tools for table-less Resources are listed only to super-users (makeVisibleTo returns false without a db/table), hidden on a nested sub-property isn't suppressed (OpenAPI even emits hidden: true as 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 on release-notes/v5-lincoln/5.1) and npm run format:check.

PR description generated by kAIle (Claude Opus 4.8).

…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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread reference/resources/resource-api.md Outdated
@github-actions
github-actions Bot temporarily deployed to pr-605 July 24, 2026 21:23 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605

This preview will update automatically when you push new commits.

nizzlenitz and others added 2 commits July 24, 2026 15:43
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
@kylebernhardy

Copy link
Copy Markdown
Member Author

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).

@github-actions
github-actions Bot temporarily deployed to pr-605 July 24, 2026 21:49 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your 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
@github-actions
github-actions Bot temporarily deployed to pr-605 July 24, 2026 21:57 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your 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
@github-actions
github-actions Bot temporarily deployed to pr-605 July 25, 2026 00:32 Inactive
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your 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
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your preview deployment is ready!

🔗 Preview URL: https://preview.harper-documentation.harperfabric.com/pr-605

This preview will update automatically when you push new commits.

@github-actions
github-actions Bot temporarily deployed to pr-605 July 27, 2026 16:35 Inactive

@Ethan-Arrowood Ethan-Arrowood left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 constenum 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

Comment thread reference/resources/resource-api.md Outdated

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two claims in this bullet are stale against #1921's head:

  1. Top-level OpenAPI nullable is now emitted — the docs say it's dropped. As written, an author reads this after 5.2.0 ships with f34cedef and believes a working feature is missing.
  2. 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Withdrawing this — the bullet is gone rather than rewritten, and my review premise was wrong about which release we were tracking.

I checked #1921 against the tags rather than against its own head: its merge into main hasn't happened (still open, changes requested, head eaa755b4), and v5.2.0 through v5.2.6 all shipped without it. At v5.2.6 components/mcp/tools/application.ts still reads const attributes = (ResourceClass?.attributes ?? []), where #1921's head reads resolveAttributes(ResourceClass). So the entire v5.2.0-badged section these bullets lived in was describing unreleased behavior, and fact-checking the bullets against #1921's head — which is what I asked for — would have produced text that was still wrong for every shipped release.

It's removed in c35e4c5 and comes back, badged against whichever release it actually ships in, once #1921 lands. Details in the top-level comment.

sent with Claude Opus 5

Comment thread reference/resources/resource-api.md Outdated
- **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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same disposition as the nullability thread — removed rather than split, since the passage documented unreleased behavior. Once #1921 lands, this needs the per-surface split you described (or none at all, if harper#1944 lands with it and unifies the emitters). Worth re-deriving from the merged code at that point rather than from either PR's head.

sent with Claude Opus 5

Comment thread reference/resources/resource-api.md Outdated
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also removed with the surrounding section. Noting the fact here so it isn't lost when this content is rewritten: at #1921's head, type: 'null' emits { nullable: true, enum: [null] } at every depth (d1065816 / eaa755b4), not the untyped nullable schema the old text described.

sent with Claude Opus 5

@kriszyp kriszyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, some fixes to apply.

🤖 Reviewed with GPT 5.6

Comment thread reference/mcp/tools-and-resources.md Outdated
### Input schema derivation

Input schemas come from `Table.attributes`:
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:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bullets immediately below still say attribute_permissions narrows these schemas per requesting user, which contradicts this PR's corrected model. Registration derives every descriptor with no caller permissions, so a user whose role excludes (for example) secret still receives the same schema containing secret; the restriction is enforced when the tool runs. Only table/verb permissions affect whether the tool itself appears in tools/list. Please update lines 81-83 accordingly, and also reword reference/mcp/overview.md:49, where the current grammar still says attribute_permissions filters the tool list.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c35e4c5, and you were right about the scope — it needed a third page.

Verified against v5.2.6: every derivation call site in components/mcp/tools/application.ts passes undefined for permissions (deriveGetSchema(attributes, undefined) and so on for all six verbs), so the schema a restricted user receives is byte-identical to a super-user's.

  • tools-and-resources.md — the "narrow the schema per requesting user" bullet is gone, along with the "schema narrowing is a UX optimization" paragraph that followed it. Replaced with: schemas are derived once at registration with no caller permissions in scope; attribute_permissions is enforced when the tool runs; and what RBAC filters is the tool list, via table-level permissions (read/describe for get_*/search_*, insert/update/delete for the writes — that's makeVisibleTo).
  • overview.md — same correction on the one-line version. I also dropped the "attribute_permissions ... filters which tools tools/list returns" phrasing the earlier commit introduced, since that attributes the list filtering to the wrong permission type.
  • tool-metadata.md — the inputSchema row said "and the caller's attribute_permissions"; now "once at registration time and identical for every caller".
  • mcp-and-openapi-metadata.mdx — the RBAC section said attribute_permissions already filters the schema per-user. Rewritten.

Since the schema is caller-agnostic, each page now also says to treat attribute names and descriptions in a descriptor as visible to any authenticated caller who can see the tool.

sent with Claude Opus 5

- **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."
- **Add `format`** (`date-time`, `uuid`, `email`, ...) where it applies. It reaches Swagger UI and gives the LLM a concrete shape to emit.
- **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.
- **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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static outputSchemas cannot currently describe a custom search_* result: the search registration path deliberately omits outputSchema and never reads outputSchemas.search. An author following this advice will add an override that has no effect in tools/list. Please limit this recommendation to supported verbs (such as get/create), or explicitly state that search_* output schemas are not supported yet.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and it's now stated as a contract rather than a recommendation.

The Path-B rubric bullet that gave this advice is gone — see the top-level comment for why the whole Path-B section came out (c35e4c5) — but the underlying fact needed to live somewhere, so resource-api.md's static outputSchemas section now carries a per-verb table, and it says outright that outputSchemas.search is not read at all and a search entry has no effect on tools/list. Confirmed against v5.2.6: the search_* registration passes only inputSchema, with no outputSchema key and no overrideOutput('search') call.

sent with Claude Opus 5

Comment thread reference/resources/resource-api.md Outdated
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`.
As of v5.2.0, a Resource that declares `static properties` with no table backing drives the same schema derivation a table-backed Resource gets:

- **MCP verb tools** — the `inputSchema` on every registered verb tool, and the `outputSchema` on `get_*` (the full record shape). `search_*` has no `outputSchema`; `create_*` / `update_*` / `patch_*` / `delete_*` advertise fixed result envelopes (`{ id }`, `{ ok }`, `{ deleted }`) that don't vary with your properties. A Resource implementing both `put` and `patch` registers `update_*` only — `patch_*` appears when it implements `patch` without `put`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixed-envelope description is correct, but the static outputSchemas section later says that, when no override is supplied, create/update/patch fall back to static properties. They do not: only get_* derives its output from the record properties; the write verbs default to { id } or { ok } (and delete to { deleted }). Please update that later fallback sentence so this page gives authors one consistent response contract.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c35e4c5. The sentence was wrong on both halves, so it's now a table:

Verb Output schema with no override
get_* record shape from the Resource's attributes
create_* { id }, typed by the primary-key attribute
update_*, patch_* { ok }
delete_* { deleted }
search_* none

Two corrections beyond what you flagged: delete_* is { deleted } alone — deriveDeleteOutputSchema builds a single boolean property with additionalProperties: false, so the <pk> the old text promised is not there. And since only get_* is record-shaped, the section now says static outputSchemas is the only way to describe what a write verb returns.

I also added the case this PR was originally about: a programmatic Resource with no attributes gets an empty get_* record schema, because the derivation reads the internal attributes Array rather than static properties (harper#1923). Until that closes, the guidance is to describe the result with static outputSchemas.get.

sent with Claude Opus 5

Ethan-Arrowood and others added 2 commits August 26, 2026 17:00
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>
@github-actions

Copy link
Copy Markdown

🚀 Preview Deployment

Your 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

Copy link
Copy Markdown
Member

Rescoped to shipped behavior — harper#1921 has not landed

Picking this up to finish it, I fact-checked the version claims against harper's tags rather than against the feature branches, and the premise had moved out from under the PR.

harper#1921 is still open (changes requested, head eaa755b4, last touched 2026-07-30), and v5.2.0 through v5.2.6 have all shipped without it. The decisive check, per the repo's badge guidance:

$ git show v5.2.6:components/mcp/tools/application.ts | grep 'const attributes'
    const attributes = (ResourceClass?.attributes ?? []) as HarperAttribute[];

$ git show pr1921:components/mcp/tools/application.ts | grep 'const attributes'
    const attributes = resolveAttributes(ResourceClass) as HarperAttribute[];

resolveAttributes is absent from both components/mcp/tools/application.ts and resources/openApi.ts at v5.2.6. So every <VersionBadge version="v5.2.0" /> on the static-properties sections was false, and had this merged, the docs would have promised a feature no release contains.

What I did

Removed the #1921-dependent content and kept everything the PR got right about shipped behavior — which turned out to be most of it. Each surviving claim is verified against v5.2.6.

Kept:

  • The REST change, which did ship. harper#1933 is present at v5.2.0 and absent at v5.1.26, so the v5.2.0 badge on rest/overview.md and rest/querying.md is correct. Verified the details too: the own-key check in Resource.parsePath, and that EXTENSION_TYPES (json, cbor, msgpack, csv) is matched before the property check.
  • The attribute_permissions corrections @kriszyp flagged, across four pages. Every derivation call site passes undefined for permissions, so schemas are caller-agnostic; RBAC filters the tool list via table-level permissions.
  • The output-schema contracts. Only get_* is record-shaped; create_* is { id }, update_*/patch_* are { ok }, delete_* is { deleted } with no primary key, search_* has none.
  • /openapi.json -> /openapi, plus the actual access model: GET /openapi is super-user-only (403 otherwise), while MCP's harper://openapi applies no permission check at all. The old text's "exposed to anyone reachable on the HTTP port" was wrong about REST and understated the MCP surface.
  • The static verb handlers in the examples (@gemini-code-assist's catch, Kyle's 10bc616).

Also corrected, pre-existing on main: Path B claimed "the MCP and OpenAPI layers read both surfaces uniformly." They don't — that's the harper#1923 gap this PR was going to close. It now states the gap and points authors at static outputSchemas in the meantime. That claim being live in published docs is arguably the most user-visible thing in this diff.

Added: a 5.2 release-notes entry for the id.property change, which had none.

Removed (returns when #1921 lands, badged against the release it ships in): the static properties derivation sections in resource-api.md, the Path-B tools/list JSON block, the JSON Schema vocabulary subsection and fragment-key table, the Path-B authoring rubric, and the per-surface divergence bullets.

Answering the open questions from review

  • @Ethan-Arrowood's merge-ordering question — decided by default. #1944 is also still open, so there is no unified emitter to write against either. Both PRs need to land before any of this text can be written accurately, and whoever writes it should derive it from the merged code rather than from either PR's head.
  • My three stale-fact threads — withdrawn rather than fixed; the passages are gone. I left the underlying facts in the threads so they aren't lost.
  • The first-v5.2.0-badge and missing-5.2-release-notes concerns Kyle raised — both resolved by main: release-notes/v5-lincoln/5.2.md and 5.3.md now exist, and v5.3 is the open line.
  • harper#1940 is fixed on main: table-less Resources now list verb tools to any authenticated user, with anonymous sessions excluded. The "super-users only" note this PR added was already stale, and came out with the rest.

Housekeeping

Merged main — the branch was 101 commits behind. npm run build is clean (the two reported broken anchors are both pre-existing on main: backups/overview -> cli/commands#backing-up-with-volume-snapshots, and 5.1#deployment-operations) and npm run format:check passes under main's @harperfast/code-guidelines.

This should be mergeable as-is now. Someone should track re-adding the removed sections against #1921 — happy to file that if it isn't already covered by harper#1923.

sent with Claude Opus 5

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[docs] Document programmatic Resource static properties for MCP/OpenAPI schema authoring

5 participants