Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions reference/mcp/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
---
title: Harper MCP CLI
---

# Harper MCP CLI

<VersionBadge version="v5.1.0" />

`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 `<rootPath>/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 <token>` — 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 <name>` | `application` | `operations` or `application`. Determines which MCP endpoint the CLI connects to. |
| `--target <url>` | _(local UDS)_ | Network endpoint. Switches the CLI into network mode. |
| `--mount-path <path>` | `/mcp` | Overrides the mount path. Match whatever `mcp.<profile>.mountPath` is set to. |
| `--username <u>` | _(none)_ | Basic auth username (network mode). |
| `--password <p>` | _(none)_ | Basic auth password (network mode). |
| `--bearer <token>` | _(none)_ | Bearer token (network mode). Wins over `--username` / `--password`. |
| `--insecure` | _(off)_ | Skip TLS certificate validation (network mode only). |
| `--client <name>` | _(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.
156 changes: 156 additions & 0 deletions reference/mcp/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
---
title: MCP Configuration
---

# MCP Configuration

<VersionBadge version="v5.1.0" />

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<string>` (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<string>` (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.<profile>.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_<resource>` 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.<profile>.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.<profile>.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.<profile>.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.<profile>.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.<profile>.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
```
90 changes: 90 additions & 0 deletions reference/mcp/migration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: Migrating from the External MCP Server
---

# Migrating from the External MCP Server

<VersionBadge version="v5.1.0" />

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 <your-endpoint>` from any machine. The output gives an OK/FAIL line per handshake step.
- If your agents need richer per-resource invocation (`get_<resource>`, `search_<resource>`, etc., generated from your application's exported `Resource` classes), enable `mcp.application` as well — that surface has no equivalent in the external addon.
Loading