@harperfast/template-react-ts-studio 1.11.2 → 1.11.4

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 (54) hide show
  1. package/.agents/skills/harper-best-practices/AGENTS.md +702 -490
  2. package/.agents/skills/harper-best-practices/SKILL.md +1 -0
  3. package/.agents/skills/harper-best-practices/rules/automatic-apis.md +46 -78
  4. package/.agents/skills/harper-best-practices/rules/defining-relationships.md +41 -63
  5. package/.agents/skills/harper-best-practices/rules/delegating-to-the-built-in-agent.md +141 -0
  6. package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +59 -45
  7. package/.agents/skills/harper-best-practices/rules/load-env.md +24 -23
  8. package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +62 -78
  9. package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +92 -65
  10. package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +53 -55
  11. package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +98 -62
  12. package/.agents/skills/harper-best-practices/rules/vector-indexing.md +113 -42
  13. package/.agents/skills/harper-best-practices/rules.manifest.yaml +9 -0
  14. package/.agents/skills/harper-mcp/AGENTS.md +501 -0
  15. package/.agents/skills/harper-mcp/SKILL.md +91 -0
  16. package/.agents/skills/harper-mcp/rules/automatic-verb-tools.md +50 -0
  17. package/.agents/skills/harper-mcp/rules/connecting-clients.md +59 -0
  18. package/.agents/skills/harper-mcp/rules/custom-mcp-prompts.md +52 -0
  19. package/.agents/skills/harper-mcp/rules/custom-mcp-resources.md +64 -0
  20. package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +72 -0
  21. package/.agents/skills/harper-mcp/rules/durable-quotas.md +73 -0
  22. package/.agents/skills/harper-mcp/rules/enabling-mcp.md +63 -0
  23. package/.agents/skills/harper-mcp/rules/rate-limiting.md +62 -0
  24. package/.agents/skills/harper-mcp/rules/resources-surface.md +44 -0
  25. package/.agents/skills/harper-mcp/rules/security-posture.md +36 -0
  26. package/.agents/skills/harper-mcp/rules.manifest.yaml +203 -0
  27. package/agent/skills/harper-best-practices/AGENTS.md +702 -490
  28. package/agent/skills/harper-best-practices/SKILL.md +1 -0
  29. package/agent/skills/harper-best-practices/rules/automatic-apis.md +46 -78
  30. package/agent/skills/harper-best-practices/rules/defining-relationships.md +41 -63
  31. package/agent/skills/harper-best-practices/rules/delegating-to-the-built-in-agent.md +141 -0
  32. package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +59 -45
  33. package/agent/skills/harper-best-practices/rules/load-env.md +24 -23
  34. package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +62 -78
  35. package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +92 -65
  36. package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +53 -55
  37. package/agent/skills/harper-best-practices/rules/v5-upgrade.md +98 -62
  38. package/agent/skills/harper-best-practices/rules/vector-indexing.md +113 -42
  39. package/agent/skills/harper-best-practices/rules.manifest.yaml +9 -0
  40. package/agent/skills/harper-mcp/AGENTS.md +501 -0
  41. package/agent/skills/harper-mcp/SKILL.md +83 -0
  42. package/agent/skills/harper-mcp/rules/automatic-verb-tools.md +50 -0
  43. package/agent/skills/harper-mcp/rules/connecting-clients.md +59 -0
  44. package/agent/skills/harper-mcp/rules/custom-mcp-prompts.md +52 -0
  45. package/agent/skills/harper-mcp/rules/custom-mcp-resources.md +64 -0
  46. package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +72 -0
  47. package/agent/skills/harper-mcp/rules/durable-quotas.md +73 -0
  48. package/agent/skills/harper-mcp/rules/enabling-mcp.md +63 -0
  49. package/agent/skills/harper-mcp/rules/rate-limiting.md +62 -0
  50. package/agent/skills/harper-mcp/rules/resources-surface.md +44 -0
  51. package/agent/skills/harper-mcp/rules/security-posture.md +36 -0
  52. package/agent/skills/harper-mcp/rules.manifest.yaml +203 -0
  53. package/package.json +1 -1
  54. package/skills-lock.json +7 -1
@@ -0,0 +1,501 @@
1
+ # Harper MCP
2
+
3
+ Guidelines for exposing a Harper instance as a Model Context Protocol (MCP) server and for building the tools, prompts, and resources AI clients consume. Harper implements MCP Streamable HTTP (spec rev 2025-06-18) with two independent profiles: `application` (your app's surface) and `operations` (Harper administration).
4
+
5
+ ## 1. Setup & Connection
6
+
7
+ ### 1.1 Enabling MCP
8
+
9
+ Instructions for exposing a Harper instance as a Model Context Protocol (MCP) server.
10
+
11
+ #### When to Use
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.
14
+
15
+ #### How It Works
16
+
17
+ Harper ships two independent MCP **profiles**, each a Streamable HTTP endpoint (MCP spec rev 2025-06-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.
21
+
22
+ Enable them in `harper-config.yaml` (or the equivalent `HARPER_SET_CONFIG` process env var):
23
+
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
+ ```
31
+
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:
33
+
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.
39
+
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+.
41
+
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.
43
+
44
+ #### Examples
45
+
46
+ Minimal application-profile setup for a project with `@export`ed tables:
47
+
48
+ ```yaml
49
+ # harper-config.yaml
50
+ mcp:
51
+ application:
52
+ mountPath: /mcp
53
+ ```
54
+
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.
56
+
57
+ ### 1.2 Connecting Clients
58
+
59
+ The wire-level contract an MCP client (or your own HTTP code) must follow against a Harper MCP endpoint.
60
+
61
+ #### When to Use
62
+
63
+ Use this skill when configuring an MCP client against Harper, writing integration tests that drive `/mcp` directly, or debugging 400/404 responses from the endpoint.
64
+
65
+ #### How It Works
66
+
67
+ Harper implements MCP **Streamable HTTP** (spec rev 2025-06-18; rev 2025-03-26 also accepted):
68
+
69
+ 1. **Initialize.** POST a JSON-RPC `initialize` to the mount path with `Accept: application/json, text/event-stream`. The response carries the negotiated `protocolVersion`, the server's capabilities, and — critically — an `Mcp-Session-Id` response header.
70
+ 2. **Session header.** Every subsequent request MUST send that `Mcp-Session-Id` back. Missing/unknown ids get 400/404 (a 404 means re-initialize — sessions idle out after `mcp.session.idleTimeoutSeconds`, default 30 minutes).
71
+ 3. **Protocol-version header.** Requests after initialize should send `MCP-Protocol-Version: <negotiated version>`. A header naming a _different_ supported version than the session negotiated is rejected (400). A **missing** header is accepted as the session's own negotiated version (5.2.0+, patched into 5.1.x; older 5.1 releases treated a missing header as `2025-03-26` and rejected it as a mismatch on `2025-06-18` sessions).
72
+ 4. **Server-push SSE.** A GET on the mount path (with `Accept: text/event-stream`) opens the server→client stream used for `notifications/*/list_changed`, resource-update notifications, progress, and server-initiated requests. Some flows require it: `resources/subscribe` is rejected until the session has an open SSE stream.
73
+ 5. **Authentication.** Standard Harper authentication applies (Basic auth, tokens, mTLS — whatever the instance is configured with). Anonymous sessions are accepted when the deployment allows them; see the [Security Posture](security-posture.md) skill for what anonymous callers can reach.
74
+
75
+ #### Examples
76
+
77
+ Full curl handshake:
78
+
79
+ ```bash
80
+ # 1. initialize — capture the session id from the response headers
81
+ curl -si http://localhost:9926/mcp \
82
+ -H 'content-type: application/json' \
83
+ -H 'accept: application/json, text/event-stream' \
84
+ -u admin:password \
85
+ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
86
+ "protocolVersion":"2025-06-18","capabilities":{},
87
+ "clientInfo":{"name":"my-client","version":"1.0"}}}'
88
+ # → 200, header: Mcp-Session-Id: <uuid>
89
+
90
+ # 2. subsequent calls carry the session + protocol headers
91
+ curl -s http://localhost:9926/mcp \
92
+ -H 'content-type: application/json' \
93
+ -H 'accept: application/json, text/event-stream' \
94
+ -H 'mcp-session-id: <uuid>' \
95
+ -H 'mcp-protocol-version: 2025-06-18' \
96
+ -u admin:password \
97
+ -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
98
+ ```
99
+
100
+ Claude Desktop / MCP-client configuration is just the URL (plus auth): point a Streamable HTTP transport at `http://<host>:9926/mcp`.
101
+
102
+ Debugging cheat sheet:
103
+
104
+ - `400 missing Mcp-Session-Id header` — you skipped step 2.
105
+ - `404` on a previously working session — idle timeout; re-initialize.
106
+ - `400 MCP-Protocol-Version mismatch` — you sent a header naming a different version than the session negotiated.
107
+ - `406` — missing the required `Accept` media type (JSON for POST, `text/event-stream` for GET).
108
+ - `403 origin_not_allowed` — browser-origin request rejected by CORS-tied origin validation; see [Security Posture](security-posture.md).
109
+
110
+ ## 2. Tools & Prompts
111
+
112
+ ### 2.1 Automatic Verb Tools
113
+
114
+ The zero-code tool surface: every `@export`ed table becomes a family of CRUD tools on the application profile.
115
+
116
+ #### When to Use
117
+
118
+ Use this skill when deciding what an MCP client will see for a given schema, when tools are unexpectedly missing from `tools/list`, or when trimming a large generated surface.
119
+
120
+ #### How It Works
121
+
122
+ 1. **Generation.** For each exported table `Widget`, the application profile registers `get_Widget`, `search_Widget`, `create_Widget`, `update_Widget`, and `delete_Widget` tools. Names preserve the Resource path's case (`/` and `.` become `_`, other non-identifier characters are dropped); path collisions get a deterministic database-name prefix. Input/output schemas are derived from the table's typed attributes, so clients get real parameter validation and result shapes.
123
+ 2. **RBAC is enforced, twice.** `tools/list` is filtered per authenticated user (a user with no read permission on a table does not see its `get_`/`search_` tools), and calls run through the same permission enforcement as REST — including per-record `allow*` predicates on Resource subclasses. This is the key contrast with [custom tools](custom-mcp-tools.md), which are visible to everyone.
124
+ 3. **`exportTypes` gating.** A Resource registered with `exportTypes: { mcp: false }` is excluded from MCP enumeration entirely, independent of its REST exposure.
125
+ 4. **Surface controls.** On the application profile, trim per Resource with `exportTypes: { mcp: false }`; `maxTools` sets the `tools/list` page size (default 200, cursor pages overflow). The `allow`/`deny` glob filters belong to the **operations** profile's tool generation, not this one. Prefer trimming to what the AI actually needs — every tool costs client context.
126
+ 5. **Live registration (5.1.18+).** The tool registry rebuilds lazily when the underlying Resource registry changes (schema changes, deploys, components that finish loading after boot), so tools stay in sync without restarts; connected sessions receive `notifications/tools/list_changed` when their visible set actually changes. On earlier 5.1.x, registration depends on schema-creation events — a restart on an existing data root can come up with an **empty custom-tool registry** (the tables already exist, so no event fires); upgrading is the fix.
127
+ 6. **Plain `Resource` classes get partial tool families.** An exported non-table `Resource` subclass surfaces verb tools only for the REST verbs it actually has (typically a lone `create_*` from the base `post`). To host `mcpTools`/`mcpResources` without any verb surface, register the class via `server.resources.set(name, Class, { mcp: false })`-style exportTypes at registration — note a `static exportTypes` field on the class is NOT read.
128
+
129
+ #### Examples
130
+
131
+ ```graphql
132
+ # schema.graphql
133
+ type Widget @table @export {
134
+ id: ID @primaryKey
135
+ name: String @indexed
136
+ price: Float
137
+ }
138
+ ```
139
+
140
+ With `mcp.application.mountPath` set, `tools/list` (as a user with read/write on Widget) includes:
141
+
142
+ ```json
143
+ { "name": "search_Widget", "inputSchema": { "properties": { "conditions": { "...": "..." } } } }
144
+ { "name": "get_Widget", "inputSchema": { "properties": { "id": { "type": "string" } } } }
145
+ { "name": "create_Widget", "...": "..." }
146
+ ```
147
+
148
+ Excluding an exported Resource from MCP while keeping its REST surface — set the `mcp` exportType at registration (a `static exportTypes` field on the class is NOT read):
149
+
150
+ ```javascript
151
+ server.resources.set('internal-thing', InternalThing, { mcp: false });
152
+ ```
153
+
154
+ To expose read-only _data_, rely on RBAC: a role without write permissions never sees `create_`/`update_`/`delete_` tools for the table.
155
+
156
+ ### 2.2 Custom MCP Tools
157
+
158
+ Expose non-CRUD operations — an LLM-backed `answer`, a domain action, a report generator — as first-class MCP tools.
159
+
160
+ #### When to Use
161
+
162
+ Use this skill when the auto-generated verb tools aren't enough: the AI should invoke _behavior_, not just CRUD. Also read it before shipping any custom tool on a publicly reachable instance — the security model differs from verb tools.
163
+
164
+ #### How It Works
165
+
166
+ 1. **Declare `static mcpTools`** on a Resource class (typically in `resources.js`/`resources.ts`):
167
+
168
+ ```javascript
169
+ export class Orders extends tables.Orders {
170
+ static mcpTools = [
171
+ {
172
+ name: 'reconcile_unsettled',
173
+ description: 'Reconcile all unsettled orders and return a summary',
174
+ method: 'reconcileUnsettled',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } },
178
+ },
179
+ },
180
+ ];
181
+
182
+ async reconcileUnsettled(args, context) {
183
+ // context: { user, profile, sessionId, signal, progress?, serverRequest? }
184
+ return { reconciled: 12 };
185
+ }
186
+ }
187
+ ```
188
+
189
+ 2. **Dispatch is live-class.** Calls construct an instance of the class currently in the Resource registry, so an exported subclass (and its access-control overrides) always wins after a reload/deploy.
190
+ 3. **Per-call context.** The second argument carries `user`, `profile`, `sessionId`, an `AbortSignal` (`signal`) wired to MCP cancellation, and — on streaming calls — `progress()` and `serverRequest()`. Guard optional members (`context.progress?.(…)`).
191
+ 4. **Results and errors.** Return values are wrapped into MCP tool results (objects become structured content). Thrown errors surface as `isError: true` tool results with the message only — stack traces stay in the server log.
192
+ 5. **Security: custom tools are exposed to ANY session, including anonymous ones.** Unlike verb tools (RBAC-filtered per user), a custom tool is listed to every session and its method executes even with no logged-in user (`context.user` may be empty). The method runs inside the normal `transactional()` envelope, so data access it performs still hits per-record `allow*` predicates — but the _tool itself_ has no gate. To restrict one:
193
+
194
+ ```javascript
195
+ async reconcileUnsettled(args, context) {
196
+ if (!context.user?.username) {
197
+ throw new Error('authentication required');
198
+ }
199
+ // ...
200
+ }
201
+ ```
202
+
203
+ 6. **Cost control for public tools.** A cost-bearing anonymous tool needs more than auth checks — see [Rate Limiting](rate-limiting.md) (per-client buckets survive session cycling) and [Durable Quotas](durable-quotas.md) (persisted per-identity limits).
204
+
205
+ #### Examples
206
+
207
+ Warn-worthy anti-pattern — a public instance with an expensive tool and no gating:
208
+
209
+ ```javascript
210
+ static mcpTools = [{ name: 'answer', method: 'llmAnswer', ... }];
211
+ async llmAnswer(args) { return await callExpensiveModel(args.q); } // anonymous callers burn your budget
212
+ ```
213
+
214
+ Fixed: check `context.user` (or accept anonymity deliberately) _and_ configure `rateLimit.perClientPerSecond` + a `quota` hook.
215
+
216
+ ### 2.3 Custom MCP Prompts
217
+
218
+ Publish parameterized prompt templates that MCP clients surface to their users (slash-command style).
219
+
220
+ #### When to Use
221
+
222
+ Use this skill when your application wants to hand well-crafted, data-aware prompts to any connected AI client — "summarize this account", "draft a reply about order X" — instead of hoping each client writes a good one.
223
+
224
+ #### How It Works
225
+
226
+ 1. **Declare `static mcpPrompts`** on a Resource class. Each entry carries a `render` **function** (not a method name):
227
+
228
+ ```javascript
229
+ export class Support extends tables.Ticket {
230
+ static mcpPrompts = [
231
+ {
232
+ name: 'draft_reply',
233
+ title: 'Draft support reply',
234
+ description: 'Draft a support reply for a ticket',
235
+ arguments: [{ name: 'ticketId', description: 'Ticket to reply to', required: true }],
236
+ async render(args) {
237
+ const ticket = await Support.get(args.ticketId);
238
+ if (!ticket) throw new Error(`ticket not found: ${args.ticketId}`);
239
+ return {
240
+ messages: [
241
+ {
242
+ role: 'user',
243
+ content: { type: 'text', text: `Draft a courteous reply to: ${ticket.body}` },
244
+ },
245
+ ],
246
+ };
247
+ },
248
+ },
249
+ ];
250
+ }
251
+ ```
252
+
253
+ 2. **Entry shape.** `name` and `render` are required (invalid entries are skipped with a warning); `title`, `description`, and `arguments` (`{ name, description?, required? }`) are optional metadata surfaced to clients.
254
+ 3. **Render contract.** `render(args)` receives the client-supplied argument values (strings) and returns the MCP prompt result shape — a `messages` array of `{ role, content }` entries (`content.type`: `text`, `image`, `audio`, or `resource`). It runs server-side and can read tables, so prompts can embed live data.
255
+ 4. **Surface.** Prompts appear in `prompts/list` and render via `prompts/get`; declared arguments are served through `completion/complete`. Connected sessions get `notifications/prompts/list_changed` when the set changes (reload/deploy).
256
+ 5. **Exposure.** Like custom tools, prompts are listed to every session on the profile, including anonymous ones — keep secrets out of prompt text and gate inside `render` if needed.
257
+
258
+ #### Examples
259
+
260
+ A data-aware prompt beats a static template: fetch current state (ticket, order, account) inside the render method so the client's LLM starts from live facts rather than asking for them turn by turn.
261
+
262
+ ## 3. Resources
263
+
264
+ ### 3.1 Resources Surface
265
+
266
+ What `resources/list`, `resources/read`, `resources/templates/list`, and `resources/subscribe` expose from a Harper instance.
267
+
268
+ #### When to Use
269
+
270
+ Use this skill when an MCP client should _read_ from Harper (schemas, API descriptions, data descriptors) rather than call tools, or when wiring change notifications.
271
+
272
+ #### How It Works
273
+
274
+ Three URI families appear on the application profile:
275
+
276
+ 1. **`harper://` metadata resources** (no HTTP equivalent):
277
+ - `harper://about` — server version, profile, protocol versions, capabilities (both profiles).
278
+ - `harper://schema/{database}/{table}` — per-table attribute definitions, RBAC-filtered at read time (also offered as a URI template).
279
+ - `harper://openapi` — the OpenAPI 3.0.3 document for the REST surface.
280
+ - `harper://operations` — operations profile only; the caller's allowed operation names.
281
+ 2. **`harper+rest://<host>:<port>/<path>` descriptors** — one per exported Resource passing the `exportTypes.mcp` gate. Reading one returns a small JSON descriptor (path, database, table, and a hint to use the corresponding verb tools for records) — it is a pointer, not a data dump. The scheme is `harper+rest` (5.1.18+) because the MCP spec reserves `https://` for resources a client can fetch from the web; legacy `http(s)://` URIs from older listings still read and subscribe.
282
+ 3. **Author-defined custom resources** — arbitrary URIs/templates served by component code; see the [Custom MCP Resources](custom-mcp-resources.md) skill. Custom URIs take precedence over the discovered surfaces on `resources/read`.
283
+
284
+ Other behaviors:
285
+
286
+ - **Subscriptions.** Row-backed application resources support `resources/subscribe` (change notifications delivered on the SSE stream). The session must have its GET SSE stream open first — subscribe calls before that are rejected with an instructive error. Subscriptions are restored on session resume.
287
+ - **`notifications/resources/list_changed`.** Sessions are notified when their _visible_ resource set (including templates) actually changes — per-session diffing keeps no-op rebuilds silent.
288
+ - **Pagination.** All list endpoints use opaque cursors per the MCP spec; treat `nextCursor` as a black box.
289
+
290
+ #### Examples
291
+
292
+ Reading a table's schema as an AI-consumable resource:
293
+
294
+ ```json
295
+ → {"method":"resources/read","params":{"uri":"harper://schema/data/widget"}}
296
+ ← {"result":{"contents":[{"uri":"harper://schema/data/widget","mimeType":"application/json",
297
+ "text":"{\"attributes\":[{\"name\":\"id\",\"type\":\"ID\"},...]}"}]}}
298
+ ```
299
+
300
+ Point a client at `harper://openapi` when it needs the whole REST contract in one read.
301
+
302
+ ### 3.2 Custom MCP Resources
303
+
304
+ Serve author-defined content — documentation pages, rendered reports, binary assets — under your own URIs (5.1.18+).
305
+
306
+ #### When to Use
307
+
308
+ Use this skill to build a content surface for AI clients: a public docs server over MCP, per-entity report resources, or any read-oriented content the auto-generated descriptors can't express.
309
+
310
+ #### How It Works
311
+
312
+ 1. **Declare `static mcpResources`** on a Resource class. Each entry has exactly one of `uri` (fixed) or `uriTemplate`, plus a `method` naming the instance method that serves reads:
313
+
314
+ ```javascript
315
+ export class DocsPages extends Resource {
316
+ static mcpResources = [
317
+ {
318
+ uri: 'docs:///index',
319
+ name: 'docs index',
320
+ description: 'List of all documentation pages',
321
+ mimeType: 'text/markdown',
322
+ method: 'readIndex',
323
+ },
324
+ {
325
+ uriTemplate: 'docs:///{+path}',
326
+ name: 'docs page',
327
+ mimeType: 'text/markdown',
328
+ method: 'readPage',
329
+ completions: { path: ['guides/install.md', 'guides/deploy.md'] },
330
+ },
331
+ ];
332
+
333
+ async readIndex() {
334
+ return { text: '- docs:///guides/install.md\n…', mimeType: 'text/markdown' };
335
+ }
336
+
337
+ async readPage(params /* { path } */, context /* { user, profile, sessionId } */) {
338
+ // {+path} spans segments BY DESIGN (it can contain `/` and `..`), so never
339
+ // hand it to filesystem path construction unvalidated. A keyed lookup like
340
+ // this is inherently safe; if you must touch the filesystem, resolve and
341
+ // verify containment first.
342
+ const page = PAGES.get(params.path);
343
+ if (!page) throw new Error(`no such page: ${params.path}`);
344
+ return { text: page, mimeType: 'text/markdown' };
345
+ }
346
+ }
347
+ ```
348
+
349
+ 2. **Templates.** `{name}` matches exactly one path segment; `{+name}` matches across segments (RFC-6570-style reserved expansion — how MCP clients expand templates). The single-segment contract is enforced through percent-decoding: a URI smuggling an encoded separator (`%2F`/`%5C`) into a `{name}` slot simply fails to match, so `{name}` is safe to use in path construction. Duplicate parameter names are rejected at registration.
350
+ 3. **Schemes.** Pick a custom scheme (`docs:///…` above). The reserved schemes — `harper:`, `harper+rest:`, `http:`, `https:` — are rejected at registration (and a template cannot parameterize the scheme position), so custom entries can never shadow the built-in surfaces.
351
+ 4. **Content shapes.** The read method returns a string (text), `{ text, mimeType? }`, `{ blob, mimeType? }` (base64 binary), or any other object (serialized as JSON). Read errors surface to the client as a sanitized JSON-RPC error; the raw error goes to the server log only.
352
+ 5. **Listing and completion.** Fixed URIs appear in `resources/list`, templates in `resources/templates/list`; declared `completions` values serve `completion/complete` per template parameter. Custom URIs win over the discovered surfaces on `resources/read`.
353
+ 6. **Anonymous by design.** Custom resources are served to any session — including unauthenticated ones (the public-docs case this feature targets). The MCP layer performs no auth check for them; gate inside the read method (`context.user`) if content is restricted.
354
+ 7. **No tables required.** A tableless component's resources register reliably — the registry rebuilds lazily per request when component loading completes after boot.
355
+
356
+ #### Examples
357
+
358
+ Public docs server: the two-entry declaration above plus `mcp.application.mountPath` is the whole server — clients browse `resources/list`, complete paths, and read pages, all anonymous. Pair with [Rate Limiting](rate-limiting.md) and [Durable Quotas](durable-quotas.md) if any companion tools bear cost.
359
+
360
+ ## 4. Operations & Security
361
+
362
+ ### 4.1 Rate Limiting
363
+
364
+ In-memory token buckets that bound `tools/call` throughput per profile.
365
+
366
+ #### When to Use
367
+
368
+ Use this skill when tuning MCP throughput, and **always** when a tool is exposed on a public or anonymous-accessible instance — the default limits alone do not stop session-cycling abuse.
369
+
370
+ #### How It Works
371
+
372
+ Four session-scoped limits ship with sensible defaults (per profile, configurable under `mcp.<profile>.rateLimit.*`):
373
+
374
+ - `perToolPerSecond` / `perToolBurst` — sustained rate and burst per (session, tool).
375
+ - `sessionConcurrency` — max in-flight calls per session.
376
+ - `sessionPerSecond` — sustained rate per session across all tools.
377
+
378
+ Limit hits return an `isError` tool result with `kind: 'rate_limited'` and a `scope` field (not a JSON-RPC error), so the calling LLM can see and back off.
379
+
380
+ **Session buckets are evadable by anonymous clients**: `initialize → call → drop session → repeat` gets fresh buckets every loop. Two additions close that (5.2.0+):
381
+
382
+ - `perClientPerSecond` / `perClientBurst` (default off) — a bucket keyed on **client identity** rather than session, surviving the cycling loop. Burst defaults to the sustained rate, floored at one whole token (a fractional rate like `0.1` = "6/minute" still admits its first call). Denials report `scope: 'per_client'`.
383
+ - `identityHeader` — identity defaults to the client socket IP; deployments behind a reverse proxy can name a trusted header (typically `x-forwarded-for`, first value wins). **Only set this when the proxy strips or replaces the header on untrusted traffic** — a client-controlled identity header lets callers mint fresh identities per request and bypass the limit; Harper logs a startup warning when it is configured.
384
+
385
+ All bucket state is in-memory per worker: it resets on restart and is not shared across workers. For durable, restart-surviving limits, see the [Durable Quotas](durable-quotas.md) skill.
386
+
387
+ On Harper versions before 5.2.0 the `perClient*`/`identityHeader` keys are **accepted and silently ignored** — verify `serverInfo.version` and prove a denial once before trusting the limit (see [Enabling MCP](enabling-mcp.md)).
388
+
389
+ #### Examples
390
+
391
+ Public docs server with a cost-bearing `answer` tool — bound instantaneous abuse:
392
+
393
+ ```yaml
394
+ mcp:
395
+ application:
396
+ mountPath: /mcp
397
+ rateLimit:
398
+ perClientPerSecond: 0.5 # 30/minute sustained per client
399
+ perClientBurst: 5
400
+ ```
401
+
402
+ Behind a proxy that manages `X-Forwarded-For` correctly:
403
+
404
+ ```yaml
405
+ mcp:
406
+ application:
407
+ rateLimit:
408
+ identityHeader: x-forwarded-for
409
+ perClientPerSecond: 0.5
410
+ ```
411
+
412
+ ### 4.2 Durable Quotas
413
+
414
+ Persisted, restart-surviving cost controls for `tools/call`, registered as a plain function in your component code (5.2.0+).
415
+
416
+ #### When to Use
417
+
418
+ Use this skill when in-memory [rate limiting](rate-limiting.md) is not enough as a cost control — typically a public, unauthenticated, cost-bearing tool (an LLM-backed `answer`) where you need "N calls per identity per day" semantics that survive restarts and span workers.
419
+
420
+ #### How It Works
421
+
422
+ 1. **Register a handler function** with `server.setMcpQuotaHandler` at component load — the policy is a **function**, never an exposed Resource. It is opt-in: no handler registered means calls are allowed. The latest registration wins, so a reloaded component replaces the previous handler; pass `undefined` to clear it.
423
+ 2. **The handler is called before each admitted `tools/call`** with `{ identity, tool, user, profile, sessionId }` (`identity` is the client identity from the rate-limit layer — socket IP or trusted-header value — and may be `undefined`). Return `true` (or any truthy non-object) to allow, or `{ allowed: false, message?, retryAfterSeconds? }` to deny; denials surface as `isError` tool results with `kind: 'quota_exceeded'` plus your `message`/`retryAfterSeconds`.
424
+ 3. **Gate per profile in code.** The single handler receives `profile`, so branch on it (e.g. `if (profile !== 'application') return true`) rather than configuring per-profile hooks.
425
+ 4. **Ordering.** The handler runs _after_ the in-memory buckets admit the call, so rate-limited clients cannot spam a table-backed handler.
426
+ 5. **Fail-closed.** A handler that throws **denies** the call — the raw error goes to the server log only; the client sees a sanitized message. Cost protection that silently disables itself on a bug is worse than a hard failure. Harper calls the handler once per attempted tool call; whether you count on check or on success is your business.
427
+ 6. **Race-safety is your handler's business.** It can run concurrently for the same identity — within a worker (interleavings across your own `await`s) and across workers. A naive `get` → `put used+1` counter undercounts under concurrency and admits calls past the limit; make the read-modify-write atomic (a transaction that serializes conflicting writers, a compare-and-set retry loop, or a store with native atomic increments).
428
+
429
+ #### Examples
430
+
431
+ Persisted per-identity counter backed by an **internal** table (schema + registration):
432
+
433
+ ```graphql
434
+ # schema.graphql — INTERNAL counter. No @export, so no client can read or reset it.
435
+ type QuotaCounter @table {
436
+ id: ID @primaryKey
437
+ used: Int
438
+ }
439
+ ```
440
+
441
+ ```javascript
442
+ // resources.js
443
+ const DAILY_LIMIT = 100;
444
+
445
+ // The cost-bearing tool clients call (exported — this is the public surface).
446
+ export class Answerer extends Resource {
447
+ static mcpTools = [{ name: 'answer', description: 'Answer a question', method: 'doAnswer' }];
448
+ async doAnswer(args) {
449
+ return { answered: args?.q ?? '' };
450
+ }
451
+ }
452
+
453
+ // Register the durable quota policy as a function, backed by the internal counter table.
454
+ server.setMcpQuotaHandler(async ({ identity, tool, user, profile, sessionId }) => {
455
+ if (profile !== 'application') return true; // gate per profile in code
456
+ const id = identity ?? 'unknown';
457
+ // NOTE: naive get-then-put shown for shape; production code must make this
458
+ // read-modify-write atomic (see Race-safety above).
459
+ const existing = await tables.QuotaCounter.get(id);
460
+ const used = (existing?.used ?? 0) + 1;
461
+ await tables.QuotaCounter.put({ id, used });
462
+ if (used > DAILY_LIMIT) {
463
+ return { allowed: false, message: 'daily quota reached', retryAfterSeconds: 3600 };
464
+ }
465
+ return true;
466
+ });
467
+ ```
468
+
469
+ Because the policy is a plain function and not a Resource, it exposes no `update_/delete_` MCP tools or REST surface — unlike an exported class, whose inherited CRUD would let a permitted client reset its own counter. Keep the storage table **unexported** (no `@export`) so it stays off every transport.
470
+
471
+ Also verify the handler actually runs (call the tool past the limit once): on Harper versions before 5.2.0 `server.setMcpQuotaHandler` is unavailable — see [Enabling MCP](enabling-mcp.md).
472
+
473
+ ### 4.3 Security Posture
474
+
475
+ What is and is not protected on a Harper MCP endpoint, and the checklist for exposing one publicly.
476
+
477
+ #### When to Use
478
+
479
+ Read this skill before exposing any MCP profile beyond localhost, and when reasoning about what an anonymous or low-privilege caller can reach.
480
+
481
+ #### How It Works
482
+
483
+ 1. **Authentication is Harper's, not MCP's.** The MCP layer adds no login gate. Sessions bind to whatever user Harper's auth pipeline resolves — which can be nobody: anonymous sessions initialize, list, and call successfully where the deployment allows unauthenticated requests.
484
+ 2. **Two different tool trust models.** Auto-generated verb tools are RBAC-filtered per user and enforce table permissions (including per-record `allow*` predicates) on every call. Custom `mcpTools` / `mcpPrompts` / `mcpResources` are exposed to **every** session — access control is entirely the author's method's responsibility. Never assume a custom tool is login-gated.
485
+ 3. **Origin validation (DNS-rebinding defense).** Browser-origin requests are validated against Harper's CORS configuration; a disallowed `Origin` gets 403. The secure default for anything browser-reachable beyond loopback: enable CORS with an explicit allow-list (`http.cors` + `http.corsAccessList` for the application profile; `operationsApi.network.*` for operations). CORS off means origin checks are off — appropriate only for localhost/non-browser clients.
486
+ 4. **Error hygiene.** Tool and resource errors cross the wire as sanitized messages; stack traces and raw author errors stay in the server log.
487
+ 5. **Audit.** Every `tools/call` (including rate-limited, quota-denied, and protocol-error outcomes) emits an audit entry — profile, session, tool, user, duration, status, with sensitive-looking argument keys redacted.
488
+ 6. **Template safety.** Custom resource URI templates enforce their single-segment contract against encoded-separator smuggling, and custom URIs cannot claim reserved schemes — see [Custom MCP Resources](custom-mcp-resources.md).
489
+
490
+ #### Examples
491
+
492
+ Hardening checklist for a public application-profile endpoint:
493
+
494
+ - [ ] Every custom tool/prompt/resource either tolerates anonymous callers or checks `context.user` and throws.
495
+ - [ ] `rateLimit.perClientPerSecond` set (session limits alone are cycle-evadable); `identityHeader` only if the proxy strips it.
496
+ - [ ] A durable `quota` hook backs any cost-bearing tool ([Durable Quotas](durable-quotas.md)), with an atomic counter.
497
+ - [ ] CORS allow-list configured if browsers will reach the endpoint.
498
+ - [ ] The tool surface is trimmed to what the AI needs: `exportTypes: { mcp: false }` on internal Resources (application), a deliberate `allow` list (operations — remember it replaces the read-only default).
499
+ - [ ] Audit log shipping somewhere you actually read.
500
+ - [ ] Version verified (`serverInfo.version` ≥ the feature gates you rely on) and each protection **proven to deny once** — older versions accept and silently ignore `rateLimit.perClient*` / `quota.*` keys.
501
+ - [ ] The quota hook's table is not itself exposed — register it via `server.resources.set('McpQuota', McpQuota, { mcp: false, rest: false })` instead of module-exporting it (a `static exportTypes` field is not read); otherwise clients can reset their own counters.
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: harper-mcp
3
+ description: Comprehensive guide to Harper's Model Context Protocol (MCP) interface,
4
+ covering server setup, client connection, automatic and custom tools, prompts,
5
+ resources, rate limiting, durable quotas, and the security model.
6
+ Triggers on tasks involving MCP servers on Harper, AI-client integration,
7
+ and exposing Harper data or behavior to LLM agents.
8
+ license: Apache-2.0
9
+ metadata:
10
+ author: harper
11
+ version: '1.0.0'
12
+ ---
13
+
14
+ # Harper MCP
15
+
16
+ Guidelines for exposing a Harper instance as a Model Context Protocol (MCP) server and for building the tools, prompts, and resources AI clients consume. Harper implements MCP Streamable HTTP (spec rev 2025-06-18) with two independent profiles: `application` (your app's surface) and `operations` (Harper administration).
17
+
18
+ ## When to Use
19
+
20
+ Reference these guidelines when:
21
+
22
+ - Enabling or configuring the MCP endpoint on a Harper instance
23
+ - Connecting an MCP client (Claude, agent frameworks, custom HTTP code) to Harper
24
+ - Deciding what tools an AI should see for a schema, or trimming that surface
25
+ - Exposing custom behavior (`mcpTools`), prompt templates (`mcpPrompts`), or content (`mcpResources`) to AI clients
26
+ - Protecting a public or anonymous-accessible MCP endpoint (rate limits, durable quotas, hardening)
27
+ - Debugging MCP wire errors (session/protocol headers, 400s, SSE)
28
+
29
+ ## How It Works
30
+
31
+ 1. Start with `enabling-mcp` to mount a profile, then `connecting-clients` for the handshake contract.
32
+ 2. For the tool surface, consult `automatic-verb-tools` first — most CRUD needs are covered with zero code — and reach for `custom-mcp-tools` only for real behavior.
33
+ 3. For content and templates, use `custom-mcp-resources` and `custom-mcp-prompts`; `resources-surface` explains what exists without any code.
34
+ 4. Before any public exposure, work through `security-posture`'s checklist and configure `rate-limiting` (+ `durable-quotas` for cost-bearing tools).
35
+
36
+ ## Examples
37
+
38
+ See the concrete examples embedded in each rule (curl handshakes, `static mcpTools`/`mcpResources` declarations, quota-hook implementations, and hardening configs).
39
+
40
+ <!-- BEGIN GENERATED INDEX -->
41
+
42
+ ## Rule Categories by Priority
43
+
44
+ | Priority | Category | Impact | Prefix |
45
+ | -------- | --------------------- | ------ | ------------ |
46
+ | 1 | Setup & Connection | HIGH | `setup-` |
47
+ | 2 | Tools & Prompts | HIGH | `tools-` |
48
+ | 3 | Resources | MEDIUM | `resources-` |
49
+ | 4 | Operations & Security | HIGH | `ops-` |
50
+
51
+ ## Quick Reference
52
+
53
+ ### 1. Setup & Connection (HIGH)
54
+
55
+ - `enabling-mcp` — How to enable and configure Harper's MCP server profiles (application and operations).
56
+ - `connecting-clients` — How MCP clients connect to Harper - the initialize handshake, session and protocol-version headers, and authentication.
57
+
58
+ ### 2. Tools & Prompts (HIGH)
59
+
60
+ - `automatic-verb-tools` — How Harper auto-generates CRUD MCP tools from exported tables, with RBAC filtering and allow/deny/maxTools controls.
61
+ - `custom-mcp-tools` — How to expose custom instance methods as MCP tools via static mcpTools, including the anonymous-exposure security model.
62
+ - `custom-mcp-prompts` — How to publish reusable prompt templates to MCP clients via static mcpPrompts.
63
+
64
+ ### 3. Resources (MEDIUM)
65
+
66
+ - `resources-surface` — The MCP resources surface - harper:// metadata URIs, harper+rest:// table descriptors, templates, subscriptions, and list_changed notifications.
67
+ - `custom-mcp-resources` — How to serve custom content (docs pages, reports, binaries) as MCP resources via static mcpResources with URI templates and completions.
68
+
69
+ ### 4. Operations & Security (HIGH)
70
+
71
+ - `rate-limiting` — MCP tools/call rate limiting - per-tool, per-session, and per-client-identity token buckets, and the identityHeader trust model.
72
+ - `durable-quotas` — Operator-pluggable durable quotas for MCP tools/call via the server.setMcpQuotaHandler registration hook, with a race-safe counter pattern.
73
+ - `security-posture` — The MCP security model - anonymous access, RBAC boundaries, origin validation, audit logging, and the hardening checklist for public instances.
74
+
75
+ <!-- END GENERATED INDEX -->
76
+
77
+ ## How to Use
78
+
79
+ Read individual rule files for detailed explanations and code examples:
80
+
81
+ ```
82
+ rules/enabling-mcp.md
83
+ rules/connecting-clients.md
84
+ rules/custom-mcp-tools.md
85
+ rules/custom-mcp-resources.md
86
+ rules/security-posture.md
87
+ ```
88
+
89
+ ## Full Compiled Document
90
+
91
+ For the complete guide with all rules expanded: `AGENTS.md`
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: automatic-verb-tools
3
+ description: How Harper auto-generates CRUD MCP tools from exported tables, with RBAC filtering and allow/deny/maxTools controls.
4
+ metadata:
5
+ mode: synthesized
6
+ ---
7
+
8
+ # Automatic Verb Tools
9
+
10
+ The zero-code tool surface: every `@export`ed table becomes a family of CRUD tools on the application profile.
11
+
12
+ ## When to Use
13
+
14
+ Use this skill when deciding what an MCP client will see for a given schema, when tools are unexpectedly missing from `tools/list`, or when trimming a large generated surface.
15
+
16
+ ## How It Works
17
+
18
+ 1. **Generation.** For each exported table `Widget`, the application profile registers `get_Widget`, `search_Widget`, `create_Widget`, `update_Widget`, and `delete_Widget` tools. Names preserve the Resource path's case (`/` and `.` become `_`, other non-identifier characters are dropped); path collisions get a deterministic database-name prefix. Input/output schemas are derived from the table's typed attributes, so clients get real parameter validation and result shapes.
19
+ 2. **RBAC is enforced, twice.** `tools/list` is filtered per authenticated user (a user with no read permission on a table does not see its `get_`/`search_` tools), and calls run through the same permission enforcement as REST — including per-record `allow*` predicates on Resource subclasses. This is the key contrast with [custom tools](custom-mcp-tools.md), which are visible to everyone.
20
+ 3. **`exportTypes` gating.** A Resource registered with `exportTypes: { mcp: false }` is excluded from MCP enumeration entirely, independent of its REST exposure.
21
+ 4. **Surface controls.** On the application profile, trim per Resource with `exportTypes: { mcp: false }`; `maxTools` sets the `tools/list` page size (default 200, cursor pages overflow). The `allow`/`deny` glob filters belong to the **operations** profile's tool generation, not this one. Prefer trimming to what the AI actually needs — every tool costs client context.
22
+ 5. **Live registration (5.1.18+).** The tool registry rebuilds lazily when the underlying Resource registry changes (schema changes, deploys, components that finish loading after boot), so tools stay in sync without restarts; connected sessions receive `notifications/tools/list_changed` when their visible set actually changes. On earlier 5.1.x, registration depends on schema-creation events — a restart on an existing data root can come up with an **empty custom-tool registry** (the tables already exist, so no event fires); upgrading is the fix.
23
+ 6. **Plain `Resource` classes get partial tool families.** An exported non-table `Resource` subclass surfaces verb tools only for the REST verbs it actually has (typically a lone `create_*` from the base `post`). To host `mcpTools`/`mcpResources` without any verb surface, register the class via `server.resources.set(name, Class, { mcp: false })`-style exportTypes at registration — note a `static exportTypes` field on the class is NOT read.
24
+
25
+ ## Examples
26
+
27
+ ```graphql
28
+ # schema.graphql
29
+ type Widget @table @export {
30
+ id: ID @primaryKey
31
+ name: String @indexed
32
+ price: Float
33
+ }
34
+ ```
35
+
36
+ With `mcp.application.mountPath` set, `tools/list` (as a user with read/write on Widget) includes:
37
+
38
+ ```json
39
+ { "name": "search_Widget", "inputSchema": { "properties": { "conditions": { "...": "..." } } } }
40
+ { "name": "get_Widget", "inputSchema": { "properties": { "id": { "type": "string" } } } }
41
+ { "name": "create_Widget", "...": "..." }
42
+ ```
43
+
44
+ Excluding an exported Resource from MCP while keeping its REST surface — set the `mcp` exportType at registration (a `static exportTypes` field on the class is NOT read):
45
+
46
+ ```javascript
47
+ server.resources.set('internal-thing', InternalThing, { mcp: false });
48
+ ```
49
+
50
+ To expose read-only _data_, rely on RBAC: a role without write permissions never sees `create_`/`update_`/`delete_` tools for the table.