From 4240e0ae9b93d22c57e9b55d527c289466f80dbf Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Mon, 6 Jul 2026 08:21:27 -0600 Subject: [PATCH 1/3] Document MCP custom content resources and the harper+rest descriptor scheme (harper#1613) Companion to HarperFast/harper#1613 (fixes HarperFast/harper#1609): static mcpResources opt-in (fixed URIs + templates + completions + reserved-scheme rule), and exported-Resource descriptors moving from http(s):// to harper+rest:// with back-compat. Co-Authored-By: Claude Fable 5 --- reference/mcp/tools-and-resources.md | 53 ++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 3 deletions(-) diff --git a/reference/mcp/tools-and-resources.md b/reference/mcp/tools-and-resources.md index 4b050447..e465ffd3 100644 --- a/reference/mcp/tools-and-resources.md +++ b/reference/mcp/tools-and-resources.md @@ -108,6 +108,49 @@ class Orders extends Tables.orders { The corresponding instance method runs through Harper's normal `transactional()` envelope, so per-record `allow*` predicates and audit logging behave the same way as regular verb dispatch. Authentication is "is the user logged in" only — finer-grained gating is the method's responsibility. +### Custom `mcpResources` opt-in + +A component author can expose arbitrary content — documentation pages, rendered reports, any `text` or `blob` payload — as MCP resources under author-chosen URIs by declaring a static `mcpResources` array (5.1.16+): + +```ts +class DocsPages extends Resource { + static mcpResources = [ + { + uri: 'docs:///index', + name: 'docs index', + description: 'List of all documentation pages', + mimeType: 'text/markdown', + method: 'readIndex', + }, + { + uriTemplate: 'docs:///{+path}', + name: 'docs page', + description: 'A documentation page by path', + mimeType: 'text/markdown', + method: 'readPage', + completions: { path: ['guides/install.md', 'guides/deploy.md'] }, + }, + ]; + + async readIndex() { + return { text: '- docs:///guides/install.md\n…', mimeType: 'text/markdown' }; + } + + async readPage(params) { + return { text: loadPage(params.path), mimeType: 'text/markdown' }; + } +} +``` + +Each entry declares exactly one of `uri` (fixed — listed by `resources/list`) or `uriTemplate` (listed by `resources/templates/list`). Templates use `{name}` to match a single path segment and `{+name}` to match across segments; `resources/read` extracts the parameters and passes them to the named instance method as its first argument. The method returns a string (text content), `{ text, mimeType? }`, `{ blob, mimeType? }` (base64 binary), or any other object (serialized as JSON). + +Notes: + +- Reads dispatch on the **live** registry class, so an exported `resources.js` subclass's method (and its access control) always wins — the same rule as custom `mcpTools`. Authentication is "is the user logged in"; finer-grained gating is the method's responsibility. +- `completions` optionally declares candidate values per template parameter, served by `completion/complete`. +- Custom URIs must use an author-chosen scheme (`docs:///…` above). The reserved schemes — `harper:`, `harper+rest:`, `http:`, `https:` — are rejected at registration so custom entries cannot shadow the built-in surfaces. +- A read error from the method surfaces to the client as a sanitized JSON-RPC error; the raw error is written to the server log. + ### `exportTypes` gating The MCP surface mirrors the public REST surface. A Resource is filtered out of MCP enumeration entirely when its registration sets `exportTypes.mcp = false`: @@ -133,13 +176,13 @@ Both profiles serve `resources/list`, `resources/read`, and `resources/templates The schema URIs honor each user's `permission[db].tables[table]` walk — a user with no `read` or `describe` perm on a table gets a "permission denied" response from `resources/read`. -### `https://` URIs +### `harper+rest://` URIs -The application profile additionally exposes every exported `Resource` (that passes the `exportTypes.mcp` gate **and** the `hasRestVerbs` check) as an `https://:/` URI. These resolve in-process via `Resources.getMatch(path, 'mcp')` — there is no outbound HTTP request. The body returned by `resources/read` is a small descriptor: +The application profile additionally exposes every exported `Resource` (that passes the `exportTypes.mcp` gate **and** the `hasRestVerbs` check) as a `harper+rest://:/` URI (5.1.16+ — earlier releases listed these under `http(s)://`, which the MCP spec reserves for resources a client can fetch directly from the web; legacy `http(s)://` URIs continue to work for `resources/read` and `resources/subscribe`). These resolve in-process via `Resources.getMatch(path, 'mcp')` — there is no outbound HTTP request. The body returned by `resources/read` is a small descriptor: ```json { - "uri": "https://node.example.com:9926/Product", + "uri": "harper+rest://node.example.com:9926/Product", "path": "Product", "database": "data", "table": "product", @@ -149,6 +192,10 @@ The application profile additionally exposes every exported `Resource` (that pas Per-record reads go through the tools surface, where each Resource's `allow{Read,…}` predicates run. The `resources/read` descriptor itself is a fast, side-effect-free hint — not a capability. +### Custom content URIs + +Author-declared `mcpResources` entries (see [Custom `mcpResources` opt-in](#custom-mcpresources-opt-in)) appear alongside the built-in surfaces: fixed URIs in `resources/list`, templates in `resources/templates/list`. A registered custom URI always wins over the discovered surfaces on `resources/read`. + ## `notifications/*/list_changed` After the `initialize` handshake, an MCP client opens `GET /mcp` to keep an SSE channel open for server-push frames. Harper subscribes to its existing role-cache and schema-reload event channels and, whenever one fires: From 0af6b44d4dd0fb051bf15ae11cd6aff7c2bee827 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 8 Jul 2026 08:39:17 -0600 Subject: [PATCH 2/3] Fix version refs to 5.1.18 (harper#1613 merged after 5.1.17 shipped) Co-Authored-By: Claude Fable 5 --- reference/mcp/tools-and-resources.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reference/mcp/tools-and-resources.md b/reference/mcp/tools-and-resources.md index e465ffd3..8a969c79 100644 --- a/reference/mcp/tools-and-resources.md +++ b/reference/mcp/tools-and-resources.md @@ -110,7 +110,7 @@ The corresponding instance method runs through Harper's normal `transactional()` ### Custom `mcpResources` opt-in -A component author can expose arbitrary content — documentation pages, rendered reports, any `text` or `blob` payload — as MCP resources under author-chosen URIs by declaring a static `mcpResources` array (5.1.16+): +A component author can expose arbitrary content — documentation pages, rendered reports, any `text` or `blob` payload — as MCP resources under author-chosen URIs by declaring a static `mcpResources` array (5.1.18+): ```ts class DocsPages extends Resource { @@ -178,7 +178,7 @@ The schema URIs honor each user's `permission[db].tables[table]` walk — a user ### `harper+rest://` URIs -The application profile additionally exposes every exported `Resource` (that passes the `exportTypes.mcp` gate **and** the `hasRestVerbs` check) as a `harper+rest://:/` URI (5.1.16+ — earlier releases listed these under `http(s)://`, which the MCP spec reserves for resources a client can fetch directly from the web; legacy `http(s)://` URIs continue to work for `resources/read` and `resources/subscribe`). These resolve in-process via `Resources.getMatch(path, 'mcp')` — there is no outbound HTTP request. The body returned by `resources/read` is a small descriptor: +The application profile additionally exposes every exported `Resource` (that passes the `exportTypes.mcp` gate **and** the `hasRestVerbs` check) as a `harper+rest://:/` URI (5.1.18+ — earlier releases listed these under `http(s)://`, which the MCP spec reserves for resources a client can fetch directly from the web; legacy `http(s)://` URIs continue to work for `resources/read` and `resources/subscribe`). These resolve in-process via `Resources.getMatch(path, 'mcp')` — there is no outbound HTTP request. The body returned by `resources/read` is a small descriptor: ```json { From d175c6f705f02a4f30e228a87d44121ba499f0e2 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Wed, 8 Jul 2026 08:57:21 -0600 Subject: [PATCH 3/3] Align mcpResources auth wording with docs#566: anonymous sessions served, no login gate Co-Authored-By: Claude Fable 5 --- reference/mcp/tools-and-resources.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/mcp/tools-and-resources.md b/reference/mcp/tools-and-resources.md index 8a969c79..578b0a1e 100644 --- a/reference/mcp/tools-and-resources.md +++ b/reference/mcp/tools-and-resources.md @@ -146,7 +146,7 @@ Each entry declares exactly one of `uri` (fixed — listed by `resources/list`) Notes: -- Reads dispatch on the **live** registry class, so an exported `resources.js` subclass's method (and its access control) always wins — the same rule as custom `mcpTools`. Authentication is "is the user logged in"; finer-grained gating is the method's responsibility. +- Reads dispatch on the **live** registry class, so an exported `resources.js` subclass's method (and its access control) always wins — the same rule as custom `mcpTools`. Custom resources are served to **any** MCP session, including anonymous, unauthenticated ones (the public-docs case this feature targets) — the MCP layer performs no auth check for them; to restrict one, check `context.user` in the `read` method and throw. - `completions` optionally declares candidate values per template parameter, served by `completion/complete`. - Custom URIs must use an author-chosen scheme (`docs:///…` above). The reserved schemes — `harper:`, `harper+rest:`, `http:`, `https:` — are rejected at registration so custom entries cannot shadow the built-in surfaces. - A read error from the method surfaces to the client as a sanitized JSON-RPC error; the raw error is written to the server log.