@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.
- package/.agents/skills/harper-best-practices/AGENTS.md +702 -490
- package/.agents/skills/harper-best-practices/SKILL.md +1 -0
- package/.agents/skills/harper-best-practices/rules/automatic-apis.md +46 -78
- package/.agents/skills/harper-best-practices/rules/defining-relationships.md +41 -63
- package/.agents/skills/harper-best-practices/rules/delegating-to-the-built-in-agent.md +141 -0
- package/.agents/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +59 -45
- package/.agents/skills/harper-best-practices/rules/load-env.md +24 -23
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +62 -78
- package/.agents/skills/harper-best-practices/rules/querying-rest-apis.md +92 -65
- package/.agents/skills/harper-best-practices/rules/schema-design-tooling.md +53 -55
- package/.agents/skills/harper-best-practices/rules/v5-upgrade.md +98 -62
- package/.agents/skills/harper-best-practices/rules/vector-indexing.md +113 -42
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +9 -0
- package/.agents/skills/harper-mcp/AGENTS.md +501 -0
- package/.agents/skills/harper-mcp/SKILL.md +91 -0
- package/.agents/skills/harper-mcp/rules/automatic-verb-tools.md +50 -0
- package/.agents/skills/harper-mcp/rules/connecting-clients.md +59 -0
- package/.agents/skills/harper-mcp/rules/custom-mcp-prompts.md +52 -0
- package/.agents/skills/harper-mcp/rules/custom-mcp-resources.md +64 -0
- package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +72 -0
- package/.agents/skills/harper-mcp/rules/durable-quotas.md +73 -0
- package/.agents/skills/harper-mcp/rules/enabling-mcp.md +63 -0
- package/.agents/skills/harper-mcp/rules/rate-limiting.md +62 -0
- package/.agents/skills/harper-mcp/rules/resources-surface.md +44 -0
- package/.agents/skills/harper-mcp/rules/security-posture.md +36 -0
- package/.agents/skills/harper-mcp/rules.manifest.yaml +203 -0
- package/agent/skills/harper-best-practices/AGENTS.md +702 -490
- package/agent/skills/harper-best-practices/SKILL.md +1 -0
- package/agent/skills/harper-best-practices/rules/automatic-apis.md +46 -78
- package/agent/skills/harper-best-practices/rules/defining-relationships.md +41 -63
- package/agent/skills/harper-best-practices/rules/delegating-to-the-built-in-agent.md +141 -0
- package/agent/skills/harper-best-practices/rules/deploying-to-harper-fabric.md +59 -45
- package/agent/skills/harper-best-practices/rules/load-env.md +24 -23
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +62 -78
- package/agent/skills/harper-best-practices/rules/querying-rest-apis.md +92 -65
- package/agent/skills/harper-best-practices/rules/schema-design-tooling.md +53 -55
- package/agent/skills/harper-best-practices/rules/v5-upgrade.md +98 -62
- package/agent/skills/harper-best-practices/rules/vector-indexing.md +113 -42
- package/agent/skills/harper-best-practices/rules.manifest.yaml +9 -0
- package/agent/skills/harper-mcp/AGENTS.md +501 -0
- package/agent/skills/harper-mcp/SKILL.md +83 -0
- package/agent/skills/harper-mcp/rules/automatic-verb-tools.md +50 -0
- package/agent/skills/harper-mcp/rules/connecting-clients.md +59 -0
- package/agent/skills/harper-mcp/rules/custom-mcp-prompts.md +52 -0
- package/agent/skills/harper-mcp/rules/custom-mcp-resources.md +64 -0
- package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +72 -0
- package/agent/skills/harper-mcp/rules/durable-quotas.md +73 -0
- package/agent/skills/harper-mcp/rules/enabling-mcp.md +63 -0
- package/agent/skills/harper-mcp/rules/rate-limiting.md +62 -0
- package/agent/skills/harper-mcp/rules/resources-surface.md +44 -0
- package/agent/skills/harper-mcp/rules/security-posture.md +36 -0
- package/agent/skills/harper-mcp/rules.manifest.yaml +203 -0
- package/package.json +1 -1
- 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.
|