.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](#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.
+
+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..c5fcae5b
--- /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..57a0ddb8
--- /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 that 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 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.
+
+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',