@harperfast/template-react-ts-studio 1.12.8 → 1.12.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +514 -445
  2. package/.agents/skills/harper-best-practices/rules/automatic-apis.md +96 -93
  3. package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +84 -58
  4. package/.agents/skills/harper-best-practices/rules/extending-tables.md +27 -35
  5. package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +40 -39
  6. package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +93 -37
  7. package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +68 -66
  8. package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +24 -28
  9. package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +98 -105
  10. package/.agents/skills/harper-best-practices/rules.manifest.yaml +28 -3
  11. package/.agents/skills/harper-mcp/AGENTS.md +138 -26
  12. package/.agents/skills/harper-mcp/rules/enabling-mcp.md +140 -28
  13. package/agent/skills/harper-best-practices/AGENTS.md +514 -445
  14. package/agent/skills/harper-best-practices/rules/automatic-apis.md +96 -93
  15. package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +84 -58
  16. package/agent/skills/harper-best-practices/rules/extending-tables.md +27 -35
  17. package/agent/skills/harper-best-practices/rules/handling-binary-data.md +40 -39
  18. package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +93 -37
  19. package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +68 -66
  20. package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +24 -28
  21. package/agent/skills/harper-best-practices/rules/v5-upgrade.md +98 -105
  22. package/agent/skills/harper-best-practices/rules.manifest.yaml +28 -3
  23. package/agent/skills/harper-mcp/AGENTS.md +138 -26
  24. package/agent/skills/harper-mcp/rules/enabling-mcp.md +140 -28
  25. package/package.json +1 -1
  26. package/skills-lock.json +2 -2
@@ -204,6 +204,11 @@ rules:
204
204
  # @updatedTime (1×) → the three exceptions PUT always applies
205
205
  - 'Location'
206
206
  - '@updatedTime'
207
+ # One-line Notes facts that regeneration kept compressing away;
208
+ # each occurs once in the sources.
209
+ - 'Sec-WebSocket-Protocol: mqtt'
210
+ - '@hidden'
211
+ - 'static hidden = true'
207
212
  cross_links:
208
213
  - querying-rest-apis
209
214
  - real-time-apps
@@ -220,9 +225,22 @@ rules:
220
225
  must_cover:
221
226
  - '=gt='
222
227
  - '=lt='
223
- - 'select('
224
- - 'sort('
225
- - 'limit('
228
+ # The signatures, verbatim, in place of the bare `select(`/`sort(`/
229
+ # `limit(`: any one example satisfied those, and the signatures under
230
+ # them kept being dropped. Each contains the anchor it replaces.
231
+ - 'select()'
232
+ - 'sort(property)'
233
+ - 'sort(+property,-property,...)'
234
+ - 'limit(end)'
235
+ # One anchor per type-conversion row: each row is the only statement of
236
+ # its rule (`null`, untyped coercion, the explicit prefixes).
237
+ - 'name==null'
238
+ - 'name==123'
239
+ - 'name==true'
240
+ - 'name==number:123'
241
+ - 'name==boolean:true'
242
+ - 'name==string:some%20text'
243
+ - 'name==date:2024-01-05T20%3A07%3A27.955Z'
226
244
  cross_links:
227
245
  - automatic-apis
228
246
 
@@ -693,6 +711,13 @@ rules:
693
711
  # `## Response Objects` had no anchor and was dropped in full. Every
694
712
  # occurrence of `headers` in the whole-file source is in that section.
695
713
  - 'headers'
714
+ # The `moduleLoader` trade-offs, which the `moduleLoader` anchor alone
715
+ # did not hold: a separate context breaks `instanceof` across the
716
+ # boundary, and `native` is a plain `import()` without the app-scoped
717
+ # `logger`.
718
+ - 'instanceof'
719
+ - 'import()'
720
+ - 'logger'
696
721
 
697
722
  - rule: delegating-to-the-built-in-agent
698
723
  description: How to delegate tasks to Harper's built-in agent via the CLI and the agent operations API.
@@ -4,55 +4,167 @@ Guidelines for exposing a Harper instance as a Model Context Protocol (MCP) serv
4
4
 
5
5
  ## 1. Setup & Connection
6
6
 
7
- ### 1.1 Enabling MCP
7
+ ### 1.1 Enabling and Configuring Harper MCP Server Profiles
8
8
 
9
- Instructions for exposing a Harper instance as a Model Context Protocol (MCP) server.
9
+ Instructions for the agent to enable and configure Harper's MCP server profiles (`operations` and `application`) using the `mcp:` block in `harper-config.yaml`.
10
10
 
11
11
  #### When to Use
12
12
 
13
- Use this skill when an AI client (Claude, an agent framework, or any MCP-capable tool) should talk to Harper directly — calling tools backed by your tables and Resources, reading data as MCP resources, or driving Harper operations.
13
+ Apply this rule when adding MCP support to a Harper deployment so that LLM hosts (Claude Desktop, Cursor, Zed, custom agents) can discover and invoke Harper operations and resources. Use it when configuring rate limits, session lifecycle, allow/deny lists, or durable quota policies for either MCP profile. See [connecting-clients.md](connecting-clients.md) for wiring an MCP client to a running Harper instance.
14
14
 
15
15
  #### How It Works
16
16
 
17
- Harper ships two independent MCP **profiles**, each a Streamable HTTP endpoint (MCP spec rev 2025-06-18):
17
+ 1. **Enable profiles via presence of their sub-block.** A profile is enabled by the **presence** of its sub-block under `mcp:` — there is no separate `enabled` flag. Add an empty map to enable a profile with all defaults:
18
18
 
19
- 1. **`application` profile** — the profile most apps want. Exposes your application's surface: auto-generated CRUD verb tools for `@export`ed tables, plus anything components opt in via `static mcpTools` / `static mcpPrompts` / `static mcpResources`. Mounts on the application HTTP port (default `9926`).
20
- 2. **`operations` profile** — exposes Harper's operations API as tools (user-filtered by operation permissions). Mounts on the operations port (default `9925`). Intended for administrative agents, not application clients.
19
+ ```yaml
20
+ mcp:
21
+ operations: {}
22
+ application: {}
23
+ ```
21
24
 
22
- Enable them in `harper-config.yaml` (or the equivalent `HARPER_SET_CONFIG` process env var):
25
+ - The `operations` profile mounts at `/mcp` on the **operations server** (default port `9925`).
26
+ - The `application` profile mounts at `/mcp` on the **application HTTP server** (default port `9926`).
27
+
28
+ 2. **Configure the operations profile (`mcp.operations`).** This profile wraps Harper's operation catalog.
29
+
30
+ | Key | Type | Default | Purpose |
31
+ | ----------- | ----------------- | --------- | -------------------------------------------------------------------------------- |
32
+ | `mountPath` | `string` | `/mcp` | URL path the endpoint mounts on |
33
+ | `allow` | `array<string>` | See below | Operations exposed as MCP tools (glob or literal); **replaces** the default list |
34
+ | `deny` | `array<string>` | `[]` | Operations filtered out after `allow` is applied |
35
+ | `maxTools` | `integer` (min 1) | `200` | Max tools per `tools/list` response page |
36
+
37
+ Default `allow` list:
38
+
39
+ ```yaml
40
+ allow:
41
+ - describe_*
42
+ - list_*
43
+ - search_*
44
+ - get_job
45
+ - get_status
46
+ - get_analytics
47
+ - get_metrics
48
+ - system_information
49
+ - read_log
50
+ - read_audit_log
51
+ ```
23
52
 
24
- ```yaml
25
- mcp:
26
- application:
27
- mountPath: /mcp # path on the application HTTP port
28
- operations:
29
- mountPath: /mcp # path on the operations port
30
- ```
53
+ Setting `allow` **replaces** the default — it does not merge. To expose destructive operations (e.g. `set_configuration`, `drop_table`), add them explicitly.
31
54
 
32
- A profile is enabled by the **presence** of its config sub-block — there is no separate `enabled` flag, so `mcp: { application: {} }` alone turns the application profile on with defaults. Each profile is independent; enable only what you need. Key per-profile options:
55
+ 3. **Configure the application profile (`mcp.application`).** This profile walks your exported `Resource` classes and generates one MCP tool per implemented REST verb. It accepts all `mcp.operations.*` keys plus:
33
56
 
34
- - `mountPath` — path the endpoint mounts on (default `/mcp`).
35
- - `allow` / `deny` (operations profile only) — glob patterns or literal operation names selecting which operations become tools. The default is a deliberately read-only list; setting `allow` **replaces** it (no merge), so destructive operations like `set_configuration` must be opted in explicitly.
36
- - `maxTools` — page size for `tools/list` responses (default 200); overflow pages via the MCP cursor.
37
- - `rateLimit.*` — see the [Rate Limiting](rate-limiting.md) skill.
38
- - durable quotas — registered in code with `server.setMcpQuotaHandler`, not a config key; see the [Durable Quotas](durable-quotas.md) skill.
57
+ | Key | Type | Default | Purpose |
58
+ | ------------------ | ----------------- | ------- | ------------------------------------------------------------------ |
59
+ | `searchMaxResults` | `integer` (min 1) | `1000` | Hard cap on records a `search_<resource>` tool can return per call |
39
60
 
40
- Version notes: the transport and tool surface shipped across 5.1.x (complete protocol surface — prompts, resources, subscriptions, completions, cancellation, progress — in 5.1.10+). Custom content resources (`mcpResources`) are 5.1.18+. Per-client rate limiting and durable quotas are 5.2.0+.
61
+ 4. **Set rate limits for each profile (`mcp.<profile>.rateLimit.*`).** Token-bucket limits apply per `(session, tool)` pair and per session overall. Exhausted buckets return `result.isError = true` with `kind: "rate_limited"` — not a JSON-RPC error. See [rate-limiting.md](rate-limiting.md) for broader context.
41
62
 
42
- **Verify the version before relying on gated features.** Unsupported config keys (such as the `rateLimit.perClient*` security controls) are **accepted and silently ignored** by older versions — nothing errors, the feature just doesn't run. Check `serverInfo.version` in the `initialize` response (or read `harper://about`) first, and after configuring a limit, prove it denies at least once before trusting it.
63
+ | Key | Type | Default (operations) | Default (application) | Purpose |
64
+ | -------------------- | ----------------- | ----------------------------------- | --------------------- | ----------------------------------------------------------------- |
65
+ | `perToolPerSecond` | `number` (min 0) | `10` | `25` | Sustained per-tool refill rate; `0` disables |
66
+ | `perToolBurst` | `number` (min 0) | `20` | `50` | Burst capacity of the per-tool bucket |
67
+ | `sessionConcurrency` | `integer` (min 0) | `25` | `50` | Max concurrent `tools/call` per session |
68
+ | `sessionPerSecond` | `number` (min 0) | `100` | `200` | Sustained per-session rate across all tools |
69
+ | `perClientPerSecond` | `number` (min 0) | `0` (disabled) | `0` (disabled) | Sustained rate keyed on client identity (5.2.0+) |
70
+ | `perClientBurst` | `number` (min 0) | `perClientPerSecond` floored at `1` | same | Burst capacity of the per-client bucket |
71
+ | `identityHeader` | `string` | unset (socket IP) | unset | Trusted header supplying client identity (e.g. `x-forwarded-for`) |
72
+
73
+ `perClientPerSecond` (5.2.0+) survives session cycling. Only set `identityHeader` when a trusted proxy strips or replaces it on untrusted traffic — a client-controlled header bypasses per-client limits entirely.
74
+
75
+ 5. **Configure session lifecycle (`mcp.session.*`).** These settings apply to both profiles. A dropped session causes the next request bearing that session id to receive HTTP 404; the client is expected to re-`initialize`.
76
+
77
+ | Key | Type | Default | Purpose |
78
+ | -------------------- | ----------------- | ------- | ----------------------------------------------------------------------- |
79
+ | `idleTimeoutSeconds` | `integer` (min 1) | `1800` | Idle window before a session is TTL-evicted from `system.mcp_session` |
80
+ | `allowClientDelete` | `boolean` | `false` | When `true`, accepts client-issued `DELETE /mcp` to terminate a session |
81
+
82
+ 6. **Register a durable quota handler (optional, 5.2.0+).** For cost control that survives restarts and session cycling, register a handler with `server.setMcpQuotaHandler`. The handler runs **after** in-memory buckets admit the call. Keep any backing table unexported (no `@export`). A handler that throws **denies** the call (fail-closed). The handler receives `{ identity, tool, user, profile, sessionId }` and must return `true` to allow or `{ allowed: false, message?, retryAfterSeconds? }` to deny.
83
+
84
+ 7. **Configure Origin validation for browser clients.** MCP validates the `Origin` header to defend against DNS-rebinding attacks. Validation reuses each profile's existing CORS config:
85
+ - No `Origin` header (e.g. `curl`) → always accepted.
86
+ - CORS disabled or `*` allow-list → any `Origin` accepted.
87
+ - CORS enabled with an explicit allow-list → `Origin` not in the list returns `403 Forbidden`.
88
+
89
+ For browser-exposed deployments, enable CORS with an explicit (non-`*`) allow-list on the relevant profile.
43
90
 
44
91
  #### Examples
45
92
 
46
- Minimal application-profile setup for a project with `@export`ed tables:
93
+ **Minimal — both profiles on with all defaults:**
94
+
95
+ ```yaml
96
+ mcp:
97
+ operations: {}
98
+ application: {}
99
+ ```
100
+
101
+ **Production deployment — locked-down operations profile, raised application throughput, graceful logout:**
47
102
 
48
103
  ```yaml
49
- # harper-config.yaml
50
104
  mcp:
105
+ operations:
106
+ allow:
107
+ - describe_all
108
+ - describe_database
109
+ - system_information
110
+ - get_job
111
+ rateLimit:
112
+ perToolPerSecond: 5
113
+ perToolBurst: 10
51
114
  application:
52
- mountPath: /mcp
115
+ searchMaxResults: 500
116
+ rateLimit:
117
+ perToolPerSecond: 50
118
+ perToolBurst: 100
119
+ session:
120
+ idleTimeoutSeconds: 3600
121
+ allowClientDelete: true
53
122
  ```
54
123
 
55
- An MCP client pointed at `http://<host>:9926/mcp` then sees `get_*` / `search_*` / `create_*` / `update_*` / `delete_*` tools for every exported table, RBAC-filtered per authenticated user. See the [Connecting Clients](connecting-clients.md) skill for the handshake.
124
+ **Durable quota handler backed by an internal table:**
125
+
126
+ ```graphql
127
+ # schema.graphql — internal counter, no @export
128
+ type QuotaCounter @table {
129
+ id: ID @primaryKey
130
+ used: Int
131
+ }
132
+ ```
133
+
134
+ ```javascript
135
+ // resources.js
136
+ const DAILY_LIMIT = 100;
137
+
138
+ export class Answerer extends Resource {
139
+ static mcpTools = [{ name: 'answer', description: 'Answer a question', method: 'doAnswer' }];
140
+ async doAnswer(args) {
141
+ return { answered: args?.q ?? '' };
142
+ }
143
+ }
144
+
145
+ server.setMcpQuotaHandler(async ({ identity, tool, user, profile, sessionId }) => {
146
+ if (profile !== 'application') return true;
147
+ const id = identity ?? 'unknown';
148
+ const existing = await tables.QuotaCounter.get(id);
149
+ const used = (existing?.used ?? 0) + 1;
150
+ await tables.QuotaCounter.put({ id, used });
151
+ if (used > DAILY_LIMIT) {
152
+ return { allowed: false, message: 'daily quota reached', retryAfterSeconds: 3600 };
153
+ }
154
+ return true;
155
+ });
156
+ ```
157
+
158
+ #### Notes
159
+
160
+ - The `operations` profile mounts on port `9925`; the `application` profile mounts on port `9926`. These are the default ports — change `mountPath` only if `/mcp` collides with another route.
161
+ - Setting `mcp.operations.allow` **replaces** the default list entirely. Do not add `get_*` as a glob — it exposes `get_configuration` (which can return TLS/S3/auth secrets) and other sensitive operations.
162
+ - `perClientPerSecond` (5.2.0+) is disabled by default (`0`). Enable it to prevent session-cycling abuse.
163
+ - The durable quota handler (5.2.0+) is fail-closed: a thrown error denies the call. Race-safety under concurrent workers is the handler's responsibility — use atomic read-modify-write for production counters.
164
+ - Pass `undefined` to `server.setMcpQuotaHandler` to clear a registered handler. The latest registration wins on component reload.
165
+ - Version notes: the transport and tool surface shipped across 5.1.x (complete protocol surface — prompts, resources, subscriptions, completions, cancellation, progress — in 5.1.10+). Custom content resources (`mcpResources`) are 5.1.18+. Per-client rate limiting (`perClientPerSecond`, `perClientBurst`, `identityHeader`) and the durable quota handler (`server.setMcpQuotaHandler`) are 5.2.0+. **Verify the version before relying on gated features.** Unsupported config keys (such as the `rateLimit.perClient*` security controls) are **accepted and silently ignored** by older versions — nothing errors, the feature just doesn't run. After a session is evicted, the client must re-`initialize` to establish a new session.
166
+ - See [connecting-clients.md](connecting-clients.md) for how to connect MCP clients to these endpoints.
167
+ - See [rate-limiting.md](rate-limiting.md) for broader rate-limiting context.
56
168
 
57
169
  ### 1.2 Connecting Clients
58
170
 
@@ -8,56 +8,168 @@ metadata:
8
8
  sources:
9
9
  - reference/v5/mcp/overview.md
10
10
  - reference/v5/mcp/configuration.md
11
- sourceCommit: d7d2ddb120ce5f2ad39dc425f628f5a4f220c151
12
- inputHash: 236b335c4fc56602
11
+ sourceCommit: 2cfb81318f17e0aca2d600109e6ad813d2d0443e
12
+ inputHash: 53ecfa7dd46bed75
13
13
  ---
14
14
 
15
- # Enabling MCP
15
+ # Enabling and Configuring Harper MCP Server Profiles
16
16
 
17
- Instructions for exposing a Harper instance as a Model Context Protocol (MCP) server.
17
+ Instructions for the agent to enable and configure Harper's MCP server profiles (`operations` and `application`) using the `mcp:` block in `harper-config.yaml`.
18
18
 
19
19
  ## When to Use
20
20
 
21
- Use this skill when an AI client (Claude, an agent framework, or any MCP-capable tool) should talk to Harper directly — calling tools backed by your tables and Resources, reading data as MCP resources, or driving Harper operations.
21
+ Apply this rule when adding MCP support to a Harper deployment so that LLM hosts (Claude Desktop, Cursor, Zed, custom agents) can discover and invoke Harper operations and resources. Use it when configuring rate limits, session lifecycle, allow/deny lists, or durable quota policies for either MCP profile. See [connecting-clients.md](connecting-clients.md) for wiring an MCP client to a running Harper instance.
22
22
 
23
23
  ## How It Works
24
24
 
25
- Harper ships two independent MCP **profiles**, each a Streamable HTTP endpoint (MCP spec rev 2025-06-18):
25
+ 1. **Enable profiles via presence of their sub-block.** A profile is enabled by the **presence** of its sub-block under `mcp:` — there is no separate `enabled` flag. Add an empty map to enable a profile with all defaults:
26
26
 
27
- 1. **`application` profile** — the profile most apps want. Exposes your application's surface: auto-generated CRUD verb tools for `@export`ed tables, plus anything components opt in via `static mcpTools` / `static mcpPrompts` / `static mcpResources`. Mounts on the application HTTP port (default `9926`).
28
- 2. **`operations` profile** — exposes Harper's operations API as tools (user-filtered by operation permissions). Mounts on the operations port (default `9925`). Intended for administrative agents, not application clients.
27
+ ```yaml
28
+ mcp:
29
+ operations: {}
30
+ application: {}
31
+ ```
29
32
 
30
- Enable them in `harper-config.yaml` (or the equivalent `HARPER_SET_CONFIG` process env var):
33
+ - The `operations` profile mounts at `/mcp` on the **operations server** (default port `9925`).
34
+ - The `application` profile mounts at `/mcp` on the **application HTTP server** (default port `9926`).
31
35
 
32
- ```yaml
33
- mcp:
34
- application:
35
- mountPath: /mcp # path on the application HTTP port
36
- operations:
37
- mountPath: /mcp # path on the operations port
38
- ```
36
+ 2. **Configure the operations profile (`mcp.operations`).** This profile wraps Harper's operation catalog.
37
+
38
+ | Key | Type | Default | Purpose |
39
+ | ----------- | ----------------- | --------- | -------------------------------------------------------------------------------- |
40
+ | `mountPath` | `string` | `/mcp` | URL path the endpoint mounts on |
41
+ | `allow` | `array<string>` | See below | Operations exposed as MCP tools (glob or literal); **replaces** the default list |
42
+ | `deny` | `array<string>` | `[]` | Operations filtered out after `allow` is applied |
43
+ | `maxTools` | `integer` (min 1) | `200` | Max tools per `tools/list` response page |
44
+
45
+ Default `allow` list:
46
+
47
+ ```yaml
48
+ allow:
49
+ - describe_*
50
+ - list_*
51
+ - search_*
52
+ - get_job
53
+ - get_status
54
+ - get_analytics
55
+ - get_metrics
56
+ - system_information
57
+ - read_log
58
+ - read_audit_log
59
+ ```
60
+
61
+ Setting `allow` **replaces** the default — it does not merge. To expose destructive operations (e.g. `set_configuration`, `drop_table`), add them explicitly.
62
+
63
+ 3. **Configure the application profile (`mcp.application`).** This profile walks your exported `Resource` classes and generates one MCP tool per implemented REST verb. It accepts all `mcp.operations.*` keys plus:
64
+
65
+ | Key | Type | Default | Purpose |
66
+ | ------------------ | ----------------- | ------- | ------------------------------------------------------------------ |
67
+ | `searchMaxResults` | `integer` (min 1) | `1000` | Hard cap on records a `search_<resource>` tool can return per call |
68
+
69
+ 4. **Set rate limits for each profile (`mcp.<profile>.rateLimit.*`).** Token-bucket limits apply per `(session, tool)` pair and per session overall. Exhausted buckets return `result.isError = true` with `kind: "rate_limited"` — not a JSON-RPC error. See [rate-limiting.md](rate-limiting.md) for broader context.
70
+
71
+ | Key | Type | Default (operations) | Default (application) | Purpose |
72
+ | -------------------- | ----------------- | ----------------------------------- | --------------------- | ----------------------------------------------------------------- |
73
+ | `perToolPerSecond` | `number` (min 0) | `10` | `25` | Sustained per-tool refill rate; `0` disables |
74
+ | `perToolBurst` | `number` (min 0) | `20` | `50` | Burst capacity of the per-tool bucket |
75
+ | `sessionConcurrency` | `integer` (min 0) | `25` | `50` | Max concurrent `tools/call` per session |
76
+ | `sessionPerSecond` | `number` (min 0) | `100` | `200` | Sustained per-session rate across all tools |
77
+ | `perClientPerSecond` | `number` (min 0) | `0` (disabled) | `0` (disabled) | Sustained rate keyed on client identity (5.2.0+) |
78
+ | `perClientBurst` | `number` (min 0) | `perClientPerSecond` floored at `1` | same | Burst capacity of the per-client bucket |
79
+ | `identityHeader` | `string` | unset (socket IP) | unset | Trusted header supplying client identity (e.g. `x-forwarded-for`) |
80
+
81
+ `perClientPerSecond` (5.2.0+) survives session cycling. Only set `identityHeader` when a trusted proxy strips or replaces it on untrusted traffic — a client-controlled header bypasses per-client limits entirely.
39
82
 
40
- A profile is enabled by the **presence** of its config sub-block — there is no separate `enabled` flag, so `mcp: { application: {} }` alone turns the application profile on with defaults. Each profile is independent; enable only what you need. Key per-profile options:
83
+ 5. **Configure session lifecycle (`mcp.session.*`).** These settings apply to both profiles. A dropped session causes the next request bearing that session id to receive HTTP 404; the client is expected to re-`initialize`.
41
84
 
42
- - `mountPath` — path the endpoint mounts on (default `/mcp`).
43
- - `allow` / `deny` (operations profile only) — glob patterns or literal operation names selecting which operations become tools. The default is a deliberately read-only list; setting `allow` **replaces** it (no merge), so destructive operations like `set_configuration` must be opted in explicitly.
44
- - `maxTools` — page size for `tools/list` responses (default 200); overflow pages via the MCP cursor.
45
- - `rateLimit.*` — see the [Rate Limiting](rate-limiting.md) skill.
46
- - durable quotas — registered in code with `server.setMcpQuotaHandler`, not a config key; see the [Durable Quotas](durable-quotas.md) skill.
85
+ | Key | Type | Default | Purpose |
86
+ | -------------------- | ----------------- | ------- | ----------------------------------------------------------------------- |
87
+ | `idleTimeoutSeconds` | `integer` (min 1) | `1800` | Idle window before a session is TTL-evicted from `system.mcp_session` |
88
+ | `allowClientDelete` | `boolean` | `false` | When `true`, accepts client-issued `DELETE /mcp` to terminate a session |
47
89
 
48
- Version notes: the transport and tool surface shipped across 5.1.x (complete protocol surface — prompts, resources, subscriptions, completions, cancellation, progress — in 5.1.10+). Custom content resources (`mcpResources`) are 5.1.18+. Per-client rate limiting and durable quotas are 5.2.0+.
90
+ 6. **Register a durable quota handler (optional, 5.2.0+).** For cost control that survives restarts and session cycling, register a handler with `server.setMcpQuotaHandler`. The handler runs **after** in-memory buckets admit the call. Keep any backing table unexported (no `@export`). A handler that throws **denies** the call (fail-closed). The handler receives `{ identity, tool, user, profile, sessionId }` and must return `true` to allow or `{ allowed: false, message?, retryAfterSeconds? }` to deny.
49
91
 
50
- **Verify the version before relying on gated features.** Unsupported config keys (such as the `rateLimit.perClient*` security controls) are **accepted and silently ignored** by older versions — nothing errors, the feature just doesn't run. Check `serverInfo.version` in the `initialize` response (or read `harper://about`) first, and after configuring a limit, prove it denies at least once before trusting it.
92
+ 7. **Configure Origin validation for browser clients.** MCP validates the `Origin` header to defend against DNS-rebinding attacks. Validation reuses each profile's existing CORS config:
93
+ - No `Origin` header (e.g. `curl`) → always accepted.
94
+ - CORS disabled or `*` allow-list → any `Origin` accepted.
95
+ - CORS enabled with an explicit allow-list → `Origin` not in the list returns `403 Forbidden`.
96
+
97
+ For browser-exposed deployments, enable CORS with an explicit (non-`*`) allow-list on the relevant profile.
51
98
 
52
99
  ## Examples
53
100
 
54
- Minimal application-profile setup for a project with `@export`ed tables:
101
+ **Minimal — both profiles on with all defaults:**
55
102
 
56
103
  ```yaml
57
- # harper-config.yaml
58
104
  mcp:
105
+ operations: {}
106
+ application: {}
107
+ ```
108
+
109
+ **Production deployment — locked-down operations profile, raised application throughput, graceful logout:**
110
+
111
+ ```yaml
112
+ mcp:
113
+ operations:
114
+ allow:
115
+ - describe_all
116
+ - describe_database
117
+ - system_information
118
+ - get_job
119
+ rateLimit:
120
+ perToolPerSecond: 5
121
+ perToolBurst: 10
59
122
  application:
60
- mountPath: /mcp
123
+ searchMaxResults: 500
124
+ rateLimit:
125
+ perToolPerSecond: 50
126
+ perToolBurst: 100
127
+ session:
128
+ idleTimeoutSeconds: 3600
129
+ allowClientDelete: true
61
130
  ```
62
131
 
63
- An MCP client pointed at `http://<host>:9926/mcp` then sees `get_*` / `search_*` / `create_*` / `update_*` / `delete_*` tools for every exported table, RBAC-filtered per authenticated user. See the [Connecting Clients](connecting-clients.md) skill for the handshake.
132
+ **Durable quota handler backed by an internal table:**
133
+
134
+ ```graphql
135
+ # schema.graphql — internal counter, no @export
136
+ type QuotaCounter @table {
137
+ id: ID @primaryKey
138
+ used: Int
139
+ }
140
+ ```
141
+
142
+ ```javascript
143
+ // resources.js
144
+ const DAILY_LIMIT = 100;
145
+
146
+ export class Answerer extends Resource {
147
+ static mcpTools = [{ name: 'answer', description: 'Answer a question', method: 'doAnswer' }];
148
+ async doAnswer(args) {
149
+ return { answered: args?.q ?? '' };
150
+ }
151
+ }
152
+
153
+ server.setMcpQuotaHandler(async ({ identity, tool, user, profile, sessionId }) => {
154
+ if (profile !== 'application') return true;
155
+ const id = identity ?? 'unknown';
156
+ const existing = await tables.QuotaCounter.get(id);
157
+ const used = (existing?.used ?? 0) + 1;
158
+ await tables.QuotaCounter.put({ id, used });
159
+ if (used > DAILY_LIMIT) {
160
+ return { allowed: false, message: 'daily quota reached', retryAfterSeconds: 3600 };
161
+ }
162
+ return true;
163
+ });
164
+ ```
165
+
166
+ ## Notes
167
+
168
+ - The `operations` profile mounts on port `9925`; the `application` profile mounts on port `9926`. These are the default ports — change `mountPath` only if `/mcp` collides with another route.
169
+ - Setting `mcp.operations.allow` **replaces** the default list entirely. Do not add `get_*` as a glob — it exposes `get_configuration` (which can return TLS/S3/auth secrets) and other sensitive operations.
170
+ - `perClientPerSecond` (5.2.0+) is disabled by default (`0`). Enable it to prevent session-cycling abuse.
171
+ - The durable quota handler (5.2.0+) is fail-closed: a thrown error denies the call. Race-safety under concurrent workers is the handler's responsibility — use atomic read-modify-write for production counters.
172
+ - Pass `undefined` to `server.setMcpQuotaHandler` to clear a registered handler. The latest registration wins on component reload.
173
+ - Version notes: the transport and tool surface shipped across 5.1.x (complete protocol surface — prompts, resources, subscriptions, completions, cancellation, progress — in 5.1.10+). Custom content resources (`mcpResources`) are 5.1.18+. Per-client rate limiting (`perClientPerSecond`, `perClientBurst`, `identityHeader`) and the durable quota handler (`server.setMcpQuotaHandler`) are 5.2.0+. **Verify the version before relying on gated features.** Unsupported config keys (such as the `rateLimit.perClient*` security controls) are **accepted and silently ignored** by older versions — nothing errors, the feature just doesn't run. After a session is evicted, the client must re-`initialize` to establish a new session.
174
+ - See [connecting-clients.md](connecting-clients.md) for how to connect MCP clients to these endpoints.
175
+ - See [rate-limiting.md](rate-limiting.md) for broader rate-limiting context.