From 22446ab7d3a14ec9b2cffe36251917e6dfbbef09 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sat, 30 May 2026 15:34:24 -0600 Subject: [PATCH 1/5] docs(reference): add MCP section Documents the v5.1 first-class built-in MCP server, covering: - Overview: protocol versions, the two profiles (operations + application), capability surface - Configuration: every knob under mcp.operations / mcp.application / mcp.session with Type/Default/Description in the established format - CLI: harper mcp bridge / print-config / doctor subcommands, UDS vs network connect modes, credential precedence including saved-JWT fallback from harper login - Tools and Resources: how operations-profile tools are generated from OPERATION_FUNCTION_MAP, why get_* is NOT a default-allowed glob, application-profile Resource walker rules including attribute_permissions narrowing and exportTypes.mcp gating, harper:// + https:// resources surface, listChanged notification semantics - Migration: stepwise migration from the deprecated HarperFast/mcp-server external addon to the built-in MCP server Wired into sidebarsReference.ts as a new top-level MCP category between HTTP and Security. Co-Authored-By: Claude Opus 4.7 --- reference/mcp/cli.md | 128 ++++++++++++++++++++ reference/mcp/configuration.md | 156 +++++++++++++++++++++++++ reference/mcp/migration.md | 90 ++++++++++++++ reference/mcp/overview.md | 68 +++++++++++ reference/mcp/tools-and-resources.md | 169 +++++++++++++++++++++++++++ sidebarsReference.ts | 33 ++++++ 6 files changed, 644 insertions(+) create mode 100644 reference/mcp/cli.md create mode 100644 reference/mcp/configuration.md create mode 100644 reference/mcp/migration.md create mode 100644 reference/mcp/overview.md create mode 100644 reference/mcp/tools-and-resources.md diff --git a/reference/mcp/cli.md b/reference/mcp/cli.md new file mode 100644 index 00000000..11df4a94 --- /dev/null +++ b/reference/mcp/cli.md @@ -0,0 +1,128 @@ +--- +title: Harper MCP CLI +--- + +# Harper MCP CLI + + + +`harper mcp` is a stdio bridge that lets MCP hosts (Claude Desktop, Cursor, Zed, or any client that speaks the [stdio MCP transport](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#stdio)) talk to a running Harper instance over Harper's Streamable HTTP MCP endpoint. + +The CLI is bundled with Harper itself; if `harper` is on your `PATH`, so is `harper mcp`. + +## Subcommands + +```bash +harper mcp [subcommand] [flags] +``` + +| Subcommand | Purpose | +| -------------- | ------------------------------------------------------------------------------------------------ | +| _(default)_ | Run the stdio bridge — JSON-RPC frames on stdin, responses on stdout, until stdin closes. | +| `print-config` | Emit a paste-ready config block for an MCP host (`--client claude-desktop`, `cursor`, or `zed`). | +| `doctor` | Connect, complete an `initialize` handshake, list tools, clean up; report each step. | +| `help` | Print the help text. | + +## Connection modes + +`harper mcp` connects in one of two modes, chosen automatically based on whether `--target` is set: + +### Local UDS (default) + +With no `--target` flag, the CLI connects to the Harper running on the same host via the operations API Unix Domain Socket — the same socket `bin/cliOperations` uses. Filesystem permissions on the socket are the access gate; no credentials are required or sent. + +The UDS path is derived from `operationsApi.network.domainSocket` in `harperdb-config.yaml` and is typically `/sockets/operations-server`. + +### Network HTTPS / HTTP + +`--target https://node.example.com:9926` connects over the network to a remote Harper. Credentials are resolved with this precedence (highest first): + +1. `--bearer ` — explicit bearer token. +2. `--username` + `--password` — explicit Basic auth. +3. URL-embedded user/pass, e.g. `--target https://alice:pw@node:9926`. +4. Saved JWT from `~/.harperdb/credentials.json` for the resolved target (populated by `harper login`). + +If none of these are present, the request goes out unauthenticated and Harper gates the response accordingly. + +Use `--insecure` to skip TLS certificate validation (network mode only) — useful for local self-signed certs during development. + +## Flags + +| Flag | Default | Purpose | +| --------------------- | ----------------------------- | --------------------------------------------------------------------------------- | +| `--profile ` | `application` | `operations` or `application`. Determines which MCP endpoint the CLI connects to. | +| `--target ` | _(local UDS)_ | Network endpoint. Switches the CLI into network mode. | +| `--mount-path ` | `/mcp` | Overrides the mount path. Match whatever `mcp..mountPath` is set to. | +| `--username ` | _(none)_ | Basic auth username (network mode). | +| `--password

` | _(none)_ | Basic auth password (network mode). | +| `--bearer ` | _(none)_ | Bearer token (network mode). Wins over `--username` / `--password`. | +| `--insecure` | _(off)_ | Skip TLS certificate validation (network mode only). | +| `--client ` | _(required for print-config)_ | `claude-desktop`, `cursor`, or `zed`. | +| `--help`, `-h` | _(off)_ | Print the help text and exit. | + +## `harper mcp` (the bridge) + +The default subcommand runs until stdin closes. The expected use is to invoke it from an MCP host's configuration block (see `print-config` below). Each line of stdin is parsed as a JSON-RPC frame and POSTed to the Harper MCP endpoint; each response (whether JSON or SSE-streamed) is emitted to stdout as line-delimited JSON-RPC. + +After the `initialize` handshake completes, the bridge opens a long-lived `GET /mcp` request — Harper's server-push channel — and forwards every `notifications/tools/list_changed` and `notifications/resources/list_changed` frame to stdout. The host sees one unified MCP stream. + +Logs (status, errors, dropped invalid stdin lines) go to stderr so they never collide with the JSON-RPC channel. + +## `harper mcp print-config` + +Emits a paste-ready JSON block for the requested MCP host along with a comment indicating where to put it. + +```bash +harper mcp print-config --client claude-desktop +``` + +Produces: + +```text +# Target file: ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows) +{ + "mcpServers": { + "harper": { + "command": "harper", + "args": ["mcp"] + } + } +} +# Note: Restart Claude Desktop after editing the file. +# Note: Merge into an existing `mcpServers` block if you already have one. +``` + +The generated `args` array reflects whatever flags you pass to `print-config` (other than `--client`). For instance: + +```bash +harper mcp print-config --client cursor --target https://node.example.com:9926 --profile operations +``` + +emits a block whose `args` is `["mcp", "--profile", "operations", "--target", "https://node.example.com:9926"]`. This lets you generate fully-resolved config blocks for non-default deployments without hand-editing. + +Supported clients: + +- **`claude-desktop`** — Anthropic Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS, `%APPDATA%\Claude\claude_desktop_config.json` on Windows). +- **`cursor`** — Cursor IDE (`~/.cursor/mcp.json`). +- **`zed`** — Zed editor (Zed `settings.json`, under `context_servers`). + +## `harper mcp doctor` + +Runs a three-step smoke check against the configured connection: + +1. POST `initialize` — verifies the transport works and Harper accepts the handshake. Captures the negotiated protocol version and session id. +2. POST `tools/list` — verifies the session is usable and reports how many tools are visible to the authenticated user. +3. DELETE `/mcp` — cleans up the session. This step is allowed to fail (when `mcp.session.allowClientDelete` is `false` the server returns 405); overall doctor exit is still success. + +Each step prints `[OK]` or `[FAIL]` with a short detail line. Exit code is `0` on success, `1` on any non-tolerable failure. + +```bash +$ harper mcp doctor --target https://node.example.com:9926 --bearer $TOKEN +[OK ] initialize - session=ab12... protocol=2025-06-18 +[OK ] tools/list - 14 tool(s) visible +[OK ] session cleanup + +All checks passed. +``` + +Use `doctor` as a quick "is the wire path healthy?" check before pointing a real MCP host at the server. diff --git a/reference/mcp/configuration.md b/reference/mcp/configuration.md new file mode 100644 index 00000000..b2147888 --- /dev/null +++ b/reference/mcp/configuration.md @@ -0,0 +1,156 @@ +--- +title: MCP Configuration +--- + +# MCP Configuration + + + +All MCP configuration lives under the top-level `mcp:` block in `harperdb-config.yaml`. Each profile (`operations`, `application`) is enabled by the **presence** of its sub-block — there is no separate `enabled` flag. A minimal "turn it on" config is therefore just: + +```yaml +mcp: + operations: {} + application: {} +``` + +That boots both profiles with default settings: the operations profile mounts at `/mcp` on the operations server, the application profile mounts at `/mcp` on the application HTTP server, and the default allow lists / rate limits apply. + +## `mcp.operations.*` + +Configures the operations-profile MCP endpoint that wraps Harper's operation catalog. + +### `mcp.operations.mountPath` + +Type: `string` + +Default: `/mcp` + +URL path the MCP endpoint mounts on. Change it if `/mcp` collides with another route in your application. + +### `mcp.operations.allow` + +Type: `array` (glob patterns or literal operation names) + +Default: `['describe_*', 'list_*', 'search_*', 'get_job', 'get_status', 'get_analytics', 'get_metrics', 'system_information', 'read_log', 'read_audit_log']` + +Operations exposed as MCP tools. Glob `*` matches any sequence of characters; literal names match exactly. Setting `allow` **replaces** the default list; it does not merge. To add destructive or sensitive operations to the surface (e.g. `set_configuration`, `drop_table`), include them here explicitly. + +The default list intentionally avoids `get_*` as a glob because that pulls in `get_configuration` (which can return TLS / S3 / authentication secrets), `get_components` / `get_component_file` / `get_custom_function*` (which return component source that can embed secrets), `get_backup`, and `get_deployment*`. These are all gated by `verifyPerms` at dispatch, but defaulting to "expose them to an LLM if a super_user calls them" is the wrong default — opt them in deliberately. + +### `mcp.operations.deny` + +Type: `array` (glob patterns or literal operation names) + +Default: `[]` + +Operations to filter out **after** the allow list has been applied. Useful for taking back a single operation that a broad allow glob would otherwise expose. + +### `mcp.operations.maxTools` + +Type: `integer` (minimum 1) + +Default: `200` + +Maximum number of tools returned in a single `tools/list` response page. The MCP cursor is used to page through any overflow. + +### `mcp.operations.rateLimit.*` + +See [`mcp..rateLimit.*`](#mcpprofileratelimit) below — the schema is identical for both profiles. + +Default per-profile values for `operations`: `perToolPerSecond: 10`, `perToolBurst: 20`, `sessionConcurrency: 25`, `sessionPerSecond: 100`. + +## `mcp.application.*` + +Configures the application-profile MCP endpoint that walks your exported `Resource` classes. All `mcp.operations.*` keys above also apply here; the additional knob is: + +### `mcp.application.searchMaxResults` + +Type: `integer` (minimum 1) + +Default: `1000` + +Hard cap on the number of records a generated `search_` tool can return per call. Clients still pass `limit`, but the server clamps to this ceiling regardless of what the client requests — bounded to keep a runaway agent from exhausting memory. + +Default per-profile rate-limit values for `application`: `perToolPerSecond: 25`, `perToolBurst: 50`, `sessionConcurrency: 50`, `sessionPerSecond: 200`. + +## `mcp..rateLimit.*` + +Per-session, per-tool token-bucket rate limits. A bucket exists per `(session, tool)` pair plus one per-session bucket across all tools. When either bucket is exhausted, `tools/call` returns `result.isError = true` with `kind: "rate_limited"` — **not** a JSON-RPC error — so the LLM can read the message and back off without the transport tearing down. + +### `mcp..rateLimit.perToolPerSecond` + +Type: `number` (minimum 0) + +Default: `10` (operations) / `25` (application) + +Sustained rate at which the per-tool token bucket refills. Set to `0` to disable per-tool throttling on this profile. + +### `mcp..rateLimit.perToolBurst` + +Type: `number` (minimum 0) + +Default: `20` (operations) / `50` (application) + +Burst capacity of the per-tool token bucket — how many back-to-back calls a single tool can absorb before sustained-rate refill kicks in. + +### `mcp..rateLimit.sessionConcurrency` + +Type: `integer` (minimum 0) + +Default: `25` (operations) / `50` (application) + +Maximum number of `tools/call` invocations a single session may have in flight at once. Subsequent attempts return `kind: "rate_limited"` with `scope: "concurrency"`. + +### `mcp..rateLimit.sessionPerSecond` + +Type: `number` (minimum 0) + +Default: `100` (operations) / `200` (application) + +Sustained per-session rate across **all** tools combined. Protects a worker from a single session that spreads its calls across many distinct tools (and so would otherwise dodge `perTool*`). + +## `mcp.session.*` + +Settings that apply to MCP session lifecycle on both profiles. + +### `mcp.session.idleTimeoutSeconds` + +Type: `integer` (minimum 1) + +Default: `1800` (30 minutes) + +Idle window after which a session record in `system.mcp_session` is TTL-evicted. The next request bearing the evicted session id receives HTTP 404 and the client is expected to re-`initialize`. + +### `mcp.session.allowClientDelete` + +Type: `boolean` + +Default: `false` + +When `true`, Harper accepts client-issued `DELETE /mcp` requests that explicitly terminate a session. When `false` (the default), `DELETE` returns 405 with an `Allow` header — sessions only end via idle eviction or explicit server-side cleanup. + +## Example + +A common deployment pattern that locks down the operations profile to a small explicit set, enables MCP DELETE for graceful client logout, and raises per-tool throughput for the application profile: + +```yaml +mcp: + operations: + allow: + - describe_all + - describe_database + - system_information + - get_job + rateLimit: + perToolPerSecond: 5 + perToolBurst: 10 + application: + searchMaxResults: 500 + rateLimit: + perToolPerSecond: 50 + perToolBurst: 100 + session: + idleTimeoutSeconds: 3600 + allowClientDelete: true +``` diff --git a/reference/mcp/migration.md b/reference/mcp/migration.md new file mode 100644 index 00000000..bcc7cb97 --- /dev/null +++ b/reference/mcp/migration.md @@ -0,0 +1,90 @@ +--- +title: Migrating from the External MCP Server +--- + +# Migrating from the External MCP Server + + + +Earlier deployments of Harper used the standalone [`HarperFast/mcp-server`](https://github.com/HarperFast/mcp-server) addon — a separate Node.js process that wrapped Harper's Operations API and exposed MCP over stdio. With v5.1, MCP is now a first-class **built-in** server-side surface; the external addon is deprecated and will be archived alongside this release. + +This page covers what changes for you and how to migrate. + +## What changed + +| Concern | External `mcp-server` addon | Built-in MCP (v5.1+) | +| ---------------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------- | +| Deployment | Separate Node.js process spawned by the LLM host | In-process inside Harper; nothing else to install or run | +| Transport | stdio only | Streamable HTTP (POST + GET-SSE) **plus** stdio via `harper mcp` | +| Authentication | Operations API JWT, passed via env var | Harper's native Basic / JWT / mTLS / UDS-via-filesystem-perms | +| Tool surface | Hand-curated wrapper around the Operations API | Operations profile + Application profile (Resources walker) | +| Resource exposure | None | `harper://about`, `harper://operations`, `harper://schema/*`, `harper://openapi`, `https://...` | +| `notifications/list_changed` | Not supported | Supported per-session on both profiles | +| Rate limiting | Not present | Per-session, per-tool token-bucket on both profiles | +| Audit logging | Operations API audit log only | Dedicated `mcp.audit` category with credential redaction | +| Per-attribute permissions | Not honored in the tool surface | Narrowed at schema-derivation time | +| Config | Env vars + addon's own JSON | Top-level `mcp:` block in `harperdb-config.yaml` | + +## Migration checklist + +### 1. Enable the built-in MCP surface + +Add an `mcp:` block to `harperdb-config.yaml`. The minimal "turn it on" form is: + +```yaml +mcp: + operations: {} + application: {} +``` + +If you only need the operation-wrapper functionality the external addon provided (which is roughly what `mcp.operations` does), the operations block alone is enough: + +```yaml +mcp: + operations: {} +``` + +See [MCP Configuration](./configuration.md) for the full set of knobs. + +### 2. Switch your MCP host to `harper mcp` + +The `harper mcp` CLI ships with Harper itself — see [Harper MCP CLI](./cli.md). For Claude Desktop, the new config block is: + +```json +{ + "mcpServers": { + "harper": { + "command": "harper", + "args": ["mcp"] + } + } +} +``` + +`harper mcp print-config --client claude-desktop|cursor|zed` emits paste-ready blocks for the three supported hosts. + +By default the CLI connects to the local Harper via the operations API UDS — no credentials, gated by filesystem permissions on the socket. For a remote Harper, add `--target https://node.example.com:9926` and either supply credentials with `--bearer` / `--username` + `--password`, or run `harper login https://node.example.com:9926` once and let `harper mcp` pick up the saved JWT automatically. + +### 3. Audit the operations exposed to your LLM + +The built-in default-allow list is intentionally narrower than the external addon's wrapper. If your agents rely on operations outside `describe_*` / `list_*` / `search_*` / `system_information` / `read_log` / `read_audit_log` / `get_job` / `get_status` / `get_analytics` / `get_metrics`, add them explicitly to `mcp.operations.allow`. See the [Default-allow list](./tools-and-resources.md#default-allow-list) and the rationale for excluding `get_*` as a glob (it would pull in `get_configuration`, `get_components`, etc., which can return secrets). + +### 4. Decommission the addon + +Once your MCP hosts are pointing at `harper mcp` and tools are dispatching correctly: + +- Stop the separate `mcp-server` process. +- Remove the addon's config block from your MCP host's `mcpServers` (replaced in step 2). +- Remove the `mcp-server` package from any deployment scripts. + +## Differences to watch for + +- **Operation names are unchanged.** A tool that the external addon called `search_by_value` is still `search_by_value` in the built-in MCP server. +- **Result envelopes differ slightly.** The built-in MCP server wraps operation results in MCP's `result.content[]` (with a `type: 'text'` JSON-encoded entry) per spec. Hosts that consume the raw operation JSON directly will need to parse it out of `content[0].text`. +- **Permission filtering is now enforced at `tools/list` time** — users see only operations their role can invoke. The external addon listed everything and let the dispatch fail. +- **Sessions are real now.** Each MCP host opens an `Mcp-Session-Id` session, can hold an SSE channel open for notifications, and is cleaned up on idle eviction. The external addon was stateless per-request. + +## After migration + +- Validate with `harper mcp doctor --target ` from any machine. The output gives an OK/FAIL line per handshake step. +- If your agents need richer per-resource invocation (`get_`, `search_`, etc., generated from your application's exported `Resource` classes), enable `mcp.application` as well — that surface has no equivalent in the external addon. diff --git a/reference/mcp/overview.md b/reference/mcp/overview.md new file mode 100644 index 00000000..347d69f3 --- /dev/null +++ b/reference/mcp/overview.md @@ -0,0 +1,68 @@ +--- +title: MCP Overview +--- + +# MCP Overview + + + +Harper implements the [Model Context Protocol](https://modelcontextprotocol.io) (MCP) as a first-class server-side surface, letting large-language-model hosts (Claude Desktop, Cursor, Zed, custom agents) discover and invoke Harper operations, resources, and tables through a standard wire protocol. The MCP server runs in-process inside Harper — there is no separate addon, no sidecar, and no out-of-process broker. + +## What MCP gives you + +- **Tool discovery and invocation.** LLM hosts get a typed list of operations they can call (`tools/list`) and a uniform JSON-RPC invocation envelope (`tools/call`). Per-tool input schemas come from Harper's operation catalog (operations profile) or from your `Table.attributes` and exported `Resource` classes (application profile). +- **Resource exposure.** Synthetic `harper://` URIs surface metadata (server info, OpenAPI document, table schemas, operations catalog), and `https://` URIs mirror your application's REST surface so hosts can resolve real REST endpoints in-process. +- **Server-push notifications.** `notifications/tools/list_changed` and `notifications/resources/list_changed` fire over an open Server-Sent Events channel when role mutations or schema changes alter what a session can see. +- **Per-session bookkeeping.** Sessions persist for the configured idle window; `Mcp-Session-Id` ties JSON-RPC requests, GET-SSE notifications, and the optional DELETE-session cleanup together. +- **Built-in auth and RBAC.** Harper's existing Basic, JWT, and mTLS authentication paths run unchanged on the MCP endpoint. Tool and resource visibility is filtered through your role's `permission` block (`super_user`, `structure_user`, per-operation, per-table, per-attribute). +- **Audit + rate limits.** Every `tools/call` writes to Harper's audit log (with credential redaction); per-session and per-tool token-bucket rate limits prevent a runaway agent from overwhelming a Harper worker. + +## Protocol versions supported + +| Version | Status | Notes | +| ------------ | ---------- | -------------------------------------------------------------------------------------- | +| `2025-06-18` | Preferred | The version Harper reports in the `initialize` response. | +| `2025-03-26` | Backcompat | Accepted for clients that pin to the earlier rev. | +| Other | Negotiated | Per spec, Harper responds with the preferred version; clients downgrade or disconnect. | + +The negotiation behavior follows the MCP spec's "server MUST respond with a value it does support" rule, so newer SDKs (which may default to a later protocol version Harper does not yet implement) connect cleanly by accepting the downgrade. + +## The two profiles + +Harper exposes MCP through one or two profiles, each mounted on its own endpoint and gated by its own config block. A profile is enabled if its sub-block exists in `mcp:` config — there is no separate `enabled` flag. + +### Operations profile (`mcp.operations`) + +Wraps Harper's operation catalog (the same set of operations the REST `/operation` endpoint and the legacy `OPERATIONS_API` accept). Mounts on the **operations server** (default port `9925`). + +- Default-allowed surface (read-only): `describe_*`, `list_*`, `search_*`, plus an explicit safe-getter list (`get_job`, `get_status`, `get_analytics`, `get_metrics`), `system_information`, `read_log`, `read_audit_log`. +- Operators opt destructive or sensitive operations in via `mcp.operations.allow`. Destructive operations carry `destructiveHint: true` so well-behaved MCP clients can prompt before invoking. +- Tool dispatch goes through the same `chooseOperation` + `processLocalTransaction` path as REST `/operation` — `verifyPerms` runs unchanged. + +### Application profile (`mcp.application`) + +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). + +- For each exported Resource, Harper emits `get_`, `search_`, `create_`, `update_`, and `delete_` tools when the corresponding verb is implemented on the prototype. +- Input schemas are derived from `Table.attributes` and narrowed by your role's `attribute_permissions`. +- Components can opt non-verb instance methods into the MCP surface by declaring a static `mcpTools` array on the Resource class. +- A Resource is excluded from the MCP surface when its registration sets `exportTypes.mcp = false`. + +See [MCP Tools and Resources](./tools-and-resources.md) for the full generation rules and visibility model. + +## What's next + +- **Configuration** — see [MCP Configuration](./configuration.md) for the full set of config knobs. +- **CLI** — see [Harper MCP CLI](./cli.md) for the `harper mcp` subcommand that bridges stdio MCP hosts (Claude Desktop, Cursor, Zed) to a running Harper instance. +- **Migration** — if you are coming from the `HarperFast/mcp-server` external addon, see [MCP Migration](./migration.md). + +## Out of scope for v1 + +The following items are explicitly deferred to a follow-on release: + +- OAuth 2.1 PRM (Protected Resource Metadata) authorization. +- `resources/subscribe` (per-resource change subscriptions; `list_changed` is supported). +- `Last-Event-ID` resumability for the GET-SSE channel. +- Cross-worker session sharing (each MCP session is bound to the worker that accepted the GET stream). +- TypeScript-type reflection into JSON Schema for custom `mcpTools` entries (schemas are hand-authored in v1). +- Global REST/operations rate limiting — only per-session/per-tool limits apply on the MCP surface. diff --git a/reference/mcp/tools-and-resources.md b/reference/mcp/tools-and-resources.md new file mode 100644 index 00000000..b7cfe4ff --- /dev/null +++ b/reference/mcp/tools-and-resources.md @@ -0,0 +1,169 @@ +--- +title: MCP Tools and Resources +--- + +# MCP Tools and Resources + + + +This page documents what the MCP server actually exposes — which tools land on `tools/list` for which user, how their input schemas are built, and what shows up in `resources/list` for each profile. Configuration knobs that gate this surface are documented in [MCP Configuration](./configuration.md). + +## Operations profile — tool generation + +Tools are generated by walking Harper's `OPERATION_FUNCTION_MAP` and filtering through the configured allow/deny lists. Each tool is named for its operation (`describe_all`, `search_by_value`, `system_information`, …) and dispatches through the same `chooseOperation` + `processLocalTransaction` path the REST `/operation` endpoint uses, so existing `verifyPerms` enforcement runs unchanged. + +### Default-allow list + +The default `mcp.operations.allow` list is intentionally narrow and read-only: + +- `describe_*` — schema / database / table descriptions. +- `list_*` — enumerations (users, roles, databases). +- `search_*` — search operations. +- `get_job`, `get_status`, `get_analytics`, `get_metrics` — explicit safe getters. +- `system_information` — server-level information. +- `read_log`, `read_audit_log` — log readers. + +`get_*` is deliberately **not** a wildcard. That glob would otherwise pull in: + +- `get_configuration` — returns TLS, S3, and authentication secrets. +- `get_components`, `get_component_file`, `get_custom_function`, `get_custom_functions` — return component source code, which can embed secrets. +- `get_backup` — backup metadata / payload. +- `get_deployment`, `get_deployment_payload` — deployment artifacts. + +These are all gated by `verifyPerms`, but defaulting to "expose them to the LLM if a super_user invokes them" is the wrong posture for MCP — the LLM provider sees and may log every input/output. Operators who want any of them on the surface opt them in via `mcp.operations.allow`. + +### Tool annotations + +Each generated tool carries MCP annotations the client can use to decide how to surface it: + +- `readOnlyHint: true` — operations matching the read-only set (`describe_*`, `list_*`, `search_*`, `get_*`, `read_*`, `system_information`, `status`). MCP hosts can render these as "safe to call without confirmation". +- `destructiveHint: true` — operations Harper knows are destructive (`drop_*`, `delete*`, `restart*`, `set_configuration`, `remove_node`). MCP hosts SHOULD prompt before invoking. + +Neither hint is an authorization check — `verifyPerms` runs at dispatch. + +### Per-user filtering + +`tools/list` is filtered through `canRoleInvokeOperation` so each session sees only the operations its user can actually call: + +- `super_user` sees everything in the allow list. +- A user with `structure_user: true` sees schema-structure operations (`create_schema`, `drop_table`, `create_attribute`, etc.) in addition to anything in `permission.operations`. +- Other users see only operations listed in `permission.operations`. + +The list is cached per session and recomputed when a `notifications/tools/list_changed` event would fire. + +## Application profile — tool generation + +The application profile walks Harper's `Resources` registry. For each exported `Resource` whose registration does not set `exportTypes.mcp = false`, Harper emits one MCP tool per implemented REST verb: + +| Verb on Resource prototype | Tool name | Schema source | +| -------------------------- | ------------------------ | ------------------------------------------------------------- | +| `get(target, request)` | `get_` | Primary key + optional `get_attributes` | +| `search(target, request)` | `search_` | `conditions`, `operator`, `get_attributes`, `limit`, `cursor` | +| `post(target, data)` | `create_` | All writable attributes; non-nullable non-PK fields required | +| `put(target, data)` | `update_` (`put`) | PK + writable attributes | +| `patch(target, data)` | `patch_` (`patch`) | PK + writable attributes | +| `delete(target, request)` | `delete_` | Primary key | + +A Resource that implements both `put` and `patch` emits `update_` (favoring `put`). + +### Tool-name sanitization + +The Resource's path is sanitized into a valid tool name: `/` and `.` become `_`. If two Resources sanitize to the same name, Harper disambiguates by prefixing the database name; if a collision still occurs, a 6-character hash suffix is appended. + +### Input schema derivation + +Input schemas come from `Table.attributes`: + +- 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`). +- Nested `Object` and `Array` attributes recurse into their `properties` / `elements`. +- `nullable: true` adds `"null"` to the type union. +- Auto-managed columns (`assignCreatedTime`, `assignUpdatedTime`, `expiresAt`) and computed columns are stripped from write schemas (`create_*`, `update_*`) — the server fills them in. +- 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. + +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. + +### Custom `mcpTools` opt-in + +A component author can expose non-verb instance methods as MCP tools by declaring a static `mcpTools` array on the Resource class: + +```ts +class Orders extends Tables.orders { + static mcpTools = [ + { + name: 'reconcile_unsettled', + method: 'reconcileUnsettled', + description: 'Reconcile all orders flagged as unsettled and emit a summary', + inputSchema: { + type: 'object', + properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } }, + }, + }, + ]; + + async reconcileUnsettled({ since }) { + /* ... */ + } +} +``` + +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. + +### `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`: + +```ts +server.http(Resource, { name: 'internal-thing', exportTypes: { mcp: false } }); +``` + +This is independent of the `http` exportType — the only switch operators set to scope MCP visibility is `mcp`. + +## Resources surface + +Both profiles serve `resources/list`, `resources/read`, and `resources/templates/list`. The resources are static (no `resources/subscribe` in v1). + +### `harper://` URIs + +| URI | Profile | Content | +| ------------------------------------ | ----------- | -------------------------------------------------------------- | +| `harper://about` | both | Server version, profile name, protocol versions, capabilities. | +| `harper://operations` | operations | User-filtered list of allowed operation names. | +| `harper://openapi` | application | The OpenAPI 3.0.3 document for the application's REST surface. | +| `harper://schema/{database}/{table}` | application | Per-table attribute definitions, RBAC-filtered at read time. | + +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 + +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: + +```json +{ + "uri": "https://node.example.com:9926/Product", + "path": "Product", + "database": "data", + "table": "product", + "hint": "Use the corresponding `get_*` or `search_*` tool from `tools/list` to fetch records." +} +``` + +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. + +## `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: + +1. Walks the per-worker session registry. +2. For each session on that profile, re-resolves the bound user (so a role-perm mutation between handshake and the event fires against current permissions, not the frozen snapshot). +3. Recomputes the session's `tools/list` and `resources/list` against the fresh user. +4. Compares to the snapshot taken at session start (or after the last fire). +5. Emits `notifications/tools/list_changed` and / or `notifications/resources/list_changed` if and only if the visible set actually changed. + +Sessions whose visible surface is unchanged see nothing — there is no broadcast. The notification carries no diff payload; clients call `tools/list` and `resources/list` again to fetch the new state. + +The GET-SSE channel itself closes on: + +- Explicit `DELETE /mcp` (when `mcp.session.allowClientDelete` is `true`). +- The session being TTL-evicted from `system.mcp_session` after `mcp.session.idleTimeoutSeconds`. +- The client dropping the underlying TCP connection (Harper's HTTP server propagates that to the iterator's `return()`, which the registry's on-close listener catches). +- An idle-prune sweep (belt-and-braces against the cases above missing — see [Configuration / `mcp.session.idleTimeoutSeconds`](./configuration.md#mcpsessionidletimeoutseconds)). diff --git a/sidebarsReference.ts b/sidebarsReference.ts index c7457f84..8bb069ba 100644 --- a/sidebarsReference.ts +++ b/sidebarsReference.ts @@ -188,6 +188,39 @@ const sidebars: SidebarsConfig = { }, ], }, + { + type: 'category', + label: 'MCP', + collapsible: false, + className: 'reference-category-header', + items: [ + { + type: 'doc', + id: 'mcp/overview', + label: 'Overview', + }, + { + type: 'doc', + id: 'mcp/configuration', + label: 'Configuration', + }, + { + type: 'doc', + id: 'mcp/cli', + label: 'CLI', + }, + { + type: 'doc', + id: 'mcp/tools-and-resources', + label: 'Tools and Resources', + }, + { + type: 'doc', + id: 'mcp/migration', + label: 'Migration', + }, + ], + }, { type: 'category', label: 'Security', From 1ac8f8263b87b09e5fc6e514c01176697286b9ba Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 31 May 2026 16:44:41 -0600 Subject: [PATCH 2/5] Update cli.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- reference/mcp/cli.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/mcp/cli.md b/reference/mcp/cli.md index 11df4a94..fe8c8b76 100644 --- a/reference/mcp/cli.md +++ b/reference/mcp/cli.md @@ -62,7 +62,7 @@ Use `--insecure` to skip TLS certificate validation (network mode only) — usef ## `harper mcp` (the bridge) -The default subcommand runs until stdin closes. The expected use is to invoke it from an MCP host's configuration block (see `print-config` below). Each line of stdin is parsed as a JSON-RPC frame and POSTed to the Harper MCP endpoint; each response (whether JSON or SSE-streamed) is emitted to stdout as line-delimited JSON-RPC. +The default subcommand runs until stdin closes. The expected use is to invoke it from an MCP host's configuration block (see [print-config](#harper-mcp-print-config) below). Each line of stdin is parsed as a JSON-RPC frame and POSTed to the Harper MCP endpoint; each response (whether JSON or SSE-streamed) is emitted to stdout as line-delimited JSON-RPC. After the `initialize` handshake completes, the bridge opens a long-lived `GET /mcp` request — Harper's server-push channel — and forwards every `notifications/tools/list_changed` and `notifications/resources/list_changed` frame to stdout. The host sees one unified MCP stream. From e01b9c161de94b4545c3fc08fd80c9e6152192c3 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 31 May 2026 16:44:54 -0600 Subject: [PATCH 3/5] Update tools-and-resources.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- 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 b7cfe4ff..1ae098e0 100644 --- a/reference/mcp/tools-and-resources.md +++ b/reference/mcp/tools-and-resources.md @@ -116,7 +116,7 @@ The MCP surface mirrors the public REST surface. A Resource is filtered out of M server.http(Resource, { name: 'internal-thing', exportTypes: { mcp: false } }); ``` -This is independent of the `http` exportType — the only switch operators set to scope MCP visibility is `mcp`. +This is independent of the `http` exportType — the only switch that operators set to scope MCP visibility is `mcp`. ## Resources surface From 0b12c43a00116b310c2a821678172930f2e78e74 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 31 May 2026 16:45:06 -0600 Subject: [PATCH 4/5] Update tools-and-resources.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- 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 1ae098e0..57a0ddb8 100644 --- a/reference/mcp/tools-and-resources.md +++ b/reference/mcp/tools-and-resources.md @@ -154,7 +154,7 @@ Per-record reads go through the tools surface, where each Resource's `allow{Read 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: 1. Walks the per-worker session registry. -2. For each session on that profile, re-resolves the bound user (so a role-perm mutation between handshake and the event fires against current permissions, not the frozen snapshot). +2. For each session on that profile, re-resolves the bound user (so any role/permission mutations occurring between the handshake and the event are evaluated against current permissions, rather than a frozen snapshot). 3. Recomputes the session's `tools/list` and `resources/list` against the fresh user. 4. Compares to the snapshot taken at session start (or after the last fire). 5. Emits `notifications/tools/list_changed` and / or `notifications/resources/list_changed` if and only if the visible set actually changed. From b1fa951f2683aa46535e16240030f2bf06d801e5 Mon Sep 17 00:00:00 2001 From: Kyle Bernhardy Date: Sun, 31 May 2026 16:45:16 -0600 Subject: [PATCH 5/5] Update overview.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- reference/mcp/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reference/mcp/overview.md b/reference/mcp/overview.md index 347d69f3..c5fcae5b 100644 --- a/reference/mcp/overview.md +++ b/reference/mcp/overview.md @@ -64,5 +64,5 @@ The following items are explicitly deferred to a follow-on release: - `resources/subscribe` (per-resource change subscriptions; `list_changed` is supported). - `Last-Event-ID` resumability for the GET-SSE channel. - Cross-worker session sharing (each MCP session is bound to the worker that accepted the GET stream). -- TypeScript-type reflection into JSON Schema for custom `mcpTools` entries (schemas are hand-authored in v1). +- TypeScript type reflection into JSON Schema for custom `mcpTools` entries (schemas are hand-authored in v1). - Global REST/operations rate limiting — only per-session/per-tool limits apply on the MCP surface.