@harperfast/template-react-ts-studio 1.12.8 → 1.12.9
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.
- package/.agents/skills/harper-best-practices/AGENTS.md +514 -445
- package/.agents/skills/harper-best-practices/rules/automatic-apis.md +96 -93
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +84 -58
- package/.agents/skills/harper-best-practices/rules/extending-tables.md +27 -35
- package/.agents/skills/harper-best-practices/rules/handling-binary-data.md +40 -39
- package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +93 -37
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +68 -66
- package/.agents/skills/harper-best-practices/rules/using-blob-datatype.md +24 -28
- package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +98 -105
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +28 -3
- package/.agents/skills/harper-mcp/AGENTS.md +138 -26
- package/.agents/skills/harper-mcp/rules/enabling-mcp.md +140 -28
- package/agent/skills/harper-best-practices/AGENTS.md +514 -445
- package/agent/skills/harper-best-practices/rules/automatic-apis.md +96 -93
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +84 -58
- package/agent/skills/harper-best-practices/rules/extending-tables.md +27 -35
- package/agent/skills/harper-best-practices/rules/handling-binary-data.md +40 -39
- package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +93 -37
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +68 -66
- package/agent/skills/harper-best-practices/rules/using-blob-datatype.md +24 -28
- package/agent/skills/harper-best-practices/rules/v5-upgrade.md +98 -105
- package/agent/skills/harper-best-practices/rules.manifest.yaml +28 -3
- package/agent/skills/harper-mcp/AGENTS.md +138 -26
- package/agent/skills/harper-mcp/rules/enabling-mcp.md +140 -28
- package/package.json +1 -1
- 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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
19
|
+
```yaml
|
|
20
|
+
mcp:
|
|
21
|
+
operations: {}
|
|
22
|
+
application: {}
|
|
23
|
+
```
|
|
21
24
|
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
115
|
+
searchMaxResults: 500
|
|
116
|
+
rateLimit:
|
|
117
|
+
perToolPerSecond: 50
|
|
118
|
+
perToolBurst: 100
|
|
119
|
+
session:
|
|
120
|
+
idleTimeoutSeconds: 3600
|
|
121
|
+
allowClientDelete: true
|
|
53
122
|
```
|
|
54
123
|
|
|
55
|
-
|
|
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:
|
|
12
|
-
inputHash:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
27
|
+
```yaml
|
|
28
|
+
mcp:
|
|
29
|
+
operations: {}
|
|
30
|
+
application: {}
|
|
31
|
+
```
|
|
29
32
|
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
123
|
+
searchMaxResults: 500
|
|
124
|
+
rateLimit:
|
|
125
|
+
perToolPerSecond: 50
|
|
126
|
+
perToolBurst: 100
|
|
127
|
+
session:
|
|
128
|
+
idleTimeoutSeconds: 3600
|
|
129
|
+
allowClientDelete: true
|
|
61
130
|
```
|
|
62
131
|
|
|
63
|
-
|
|
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.
|
package/package.json
CHANGED
package/skills-lock.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"source": "harperfast/skills",
|
|
6
6
|
"sourceType": "github",
|
|
7
7
|
"skillPath": "harper-best-practices/SKILL.md",
|
|
8
|
-
"computedHash": "
|
|
8
|
+
"computedHash": "b157e80968413da12ccbbe21f4b64eb0d1bcfc7c0f166b03f51a0013fe8901a0"
|
|
9
9
|
},
|
|
10
10
|
"harper-local-dev": {
|
|
11
11
|
"source": "harperfast/skills",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"source": "harperfast/skills",
|
|
18
18
|
"sourceType": "github",
|
|
19
19
|
"skillPath": "harper-mcp/SKILL.md",
|
|
20
|
-
"computedHash": "
|
|
20
|
+
"computedHash": "397b88d772506560acf6315408656d15eb20bcc540bdaa286574c00d7b3e8268"
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
}
|