@harperfast/template-react-ts-studio 1.12.3 → 1.12.5
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 +362 -322
- package/.agents/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/.agents/skills/harper-best-practices/rules/checking-authentication.md +253 -209
- package/.agents/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/.agents/skills/harper-best-practices/rules/programmatic-table-requests.md +103 -89
- package/.agents/skills/harper-best-practices/rules.manifest.yaml +62 -3
- package/.agents/skills/harper-mcp/AGENTS.md +77 -31
- package/.agents/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/.agents/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/agent/skills/harper-best-practices/AGENTS.md +362 -322
- package/agent/skills/harper-best-practices/SKILL.md +9 -3
- package/agent/skills/harper-best-practices/rules/adding-tables-with-schemas.md +7 -7
- package/agent/skills/harper-best-practices/rules/checking-authentication.md +253 -209
- package/agent/skills/harper-best-practices/rules/custom-resources.md +28 -45
- package/agent/skills/harper-best-practices/rules/programmatic-table-requests.md +103 -89
- package/agent/skills/harper-best-practices/rules.manifest.yaml +62 -3
- package/agent/skills/harper-mcp/AGENTS.md +77 -31
- package/agent/skills/harper-mcp/SKILL.md +10 -3
- package/agent/skills/harper-mcp/rules/custom-mcp-tools.md +79 -33
- package/agent/skills/harper-mcp/rules.manifest.yaml +1 -1
- package/package.json +1 -1
- package/skills-lock.json +2 -2
|
@@ -155,23 +155,77 @@ To expose read-only _data_, rely on RBAC: a role without write permissions never
|
|
|
155
155
|
|
|
156
156
|
### 2.2 Custom MCP Tools
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
Instructions for the agent to follow when exposing custom instance methods as MCP tools via a static `mcpTools` array on a Resource class.
|
|
159
159
|
|
|
160
160
|
#### When to Use
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
Apply this rule when a component needs to expose non-verb instance methods as callable MCP tools. Use it whenever you need custom business logic (e.g., reconciliation, aggregation, batch operations) accessible via the MCP transport beyond the auto-generated verb tools.
|
|
163
163
|
|
|
164
164
|
#### How It Works
|
|
165
165
|
|
|
166
|
-
1. **Declare
|
|
166
|
+
1. **Declare a static `mcpTools` array** on the Resource class. Each entry names the tool, maps it to an instance method, provides a description, and defines an `inputSchema`.
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
class Orders extends Tables.orders {
|
|
170
|
+
static mcpTools = [
|
|
171
|
+
{
|
|
172
|
+
name: 'reconcile_unsettled',
|
|
173
|
+
method: 'reconcileUnsettled',
|
|
174
|
+
description: 'Reconcile all orders flagged as unsettled and emit a summary',
|
|
175
|
+
inputSchema: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } },
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
async reconcileUnsettled({ since }) {
|
|
183
|
+
/* ... */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
2. **Understand the security model.** Custom tools are exposed to **every** MCP session — including `anonymous`, unauthenticated ones. The MCP layer does **not**:
|
|
189
|
+
- Run an `allow*` gate automatically.
|
|
190
|
+
- Open a Resource transaction automatically.
|
|
191
|
+
- Filter the tool from `tools/list` based on user role.
|
|
192
|
+
|
|
193
|
+
Unlike auto-generated verb tools (which are RBAC-filtered per user at `tools/list` time and enforce table permissions on call), a custom tool is listed to every session and its method executes even when no user is logged in. `context.user` may be empty.
|
|
194
|
+
|
|
195
|
+
3. **Enforce access control inside the method.** Because the MCP layer performs no authentication or ACL check, the method itself is fully responsible. Check `context.user` at the top of the method and throw when the caller doesn't qualify:
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
async reconcileUnsettled({ since }) {
|
|
199
|
+
if (!context.user || !context.user.roles.includes('admin')) {
|
|
200
|
+
throw new Error('Unauthorized');
|
|
201
|
+
}
|
|
202
|
+
/* ... */
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
4. **Delegate to static Resource operations correctly.** The MCP-created instance context carries the authenticated user and a one-shot `authorize` flag:
|
|
207
|
+
- For the **first** delegated operation, pass `this.getContext()` directly — the `authorize` flag is consumed by that call:
|
|
208
|
+
```javascript
|
|
209
|
+
const result = await Orders.get(target, this.getContext());
|
|
210
|
+
```
|
|
211
|
+
- For **every subsequent** delegated operation, create a fresh `RequestTarget`, set `target.checkPermission = true` so authorization derives from `context.user`, and pass `this.getContext()`:
|
|
212
|
+
```javascript
|
|
213
|
+
target.checkPermission = true;
|
|
214
|
+
const next = await Orders.get(target, this.getContext());
|
|
215
|
+
```
|
|
216
|
+
- **Never** accept `checkPermission` from tool arguments or other client input.
|
|
217
|
+
|
|
218
|
+
#### Examples
|
|
219
|
+
|
|
220
|
+
Full class with a custom MCP tool that guards access via `context.user`:
|
|
167
221
|
|
|
168
222
|
```javascript
|
|
169
|
-
|
|
223
|
+
class Orders extends Tables.orders {
|
|
170
224
|
static mcpTools = [
|
|
171
225
|
{
|
|
172
226
|
name: 'reconcile_unsettled',
|
|
173
|
-
description: 'Reconcile all unsettled orders and return a summary',
|
|
174
227
|
method: 'reconcileUnsettled',
|
|
228
|
+
description: 'Reconcile all orders flagged as unsettled and emit a summary',
|
|
175
229
|
inputSchema: {
|
|
176
230
|
type: 'object',
|
|
177
231
|
properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } },
|
|
@@ -179,39 +233,31 @@ export class Orders extends tables.Orders {
|
|
|
179
233
|
},
|
|
180
234
|
];
|
|
181
235
|
|
|
182
|
-
async reconcileUnsettled(
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
187
|
-
```
|
|
236
|
+
async reconcileUnsettled({ since }) {
|
|
237
|
+
// Reject anonymous or unauthorized callers — the MCP layer does not do this.
|
|
238
|
+
if (!context.user) {
|
|
239
|
+
throw new Error('Authentication required');
|
|
240
|
+
}
|
|
188
241
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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:
|
|
242
|
+
// First delegated operation: use the one-shot authorize flag via this.getContext().
|
|
243
|
+
const target = { since };
|
|
244
|
+
const unsettled = await Orders.get(target, this.getContext());
|
|
193
245
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
246
|
+
// Subsequent delegated operations: fresh RequestTarget with checkPermission = true.
|
|
247
|
+
const updateTarget = { id: unsettled.id };
|
|
248
|
+
updateTarget.checkPermission = true;
|
|
249
|
+
await Orders.update(updateTarget, this.getContext());
|
|
198
250
|
}
|
|
199
|
-
// ...
|
|
200
251
|
}
|
|
201
252
|
```
|
|
202
253
|
|
|
203
|
-
|
|
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
|
-
```
|
|
254
|
+
#### Notes
|
|
213
255
|
|
|
214
|
-
|
|
256
|
+
- `mcpTools` is a **static** array — declare it on the class, not on instances.
|
|
257
|
+
- The `allow*` gates that protect auto-generated verb tools do **not** run for custom tools. All authorization logic must live inside the method.
|
|
258
|
+
- `context.user` is the canonical way to identify the caller; it may be empty for `anonymous` sessions, so always check before trusting it.
|
|
259
|
+
- Never derive `checkPermission` from client-supplied tool arguments — always set it explicitly in your method code.
|
|
260
|
+
- If your custom tool performs expensive or repeated operations, consider applying [rate limiting](rate-limiting.md) or [durable quotas](durable-quotas.md) inside the method to prevent abuse by unauthenticated callers.
|
|
215
261
|
|
|
216
262
|
### 2.3 Custom MCP Prompts
|
|
217
263
|
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
name: harper-mcp
|
|
3
|
+
description: Comprehensive guide to Harper's Model Context Protocol (MCP)
|
|
4
|
+
interface, covering server setup, client connection, automatic and custom
|
|
5
|
+
tools, prompts, resources, rate limiting, durable quotas, and the security
|
|
6
|
+
model. Triggers on tasks involving MCP servers on Harper, AI-client
|
|
7
|
+
integration, and exposing Harper data or behavior to LLM agents.
|
|
8
|
+
license: Apache-2.0
|
|
9
|
+
metadata:
|
|
10
|
+
author: harper
|
|
11
|
+
version: 1.0.0
|
|
5
12
|
---
|
|
6
13
|
# Harper MCP
|
|
7
14
|
|
|
@@ -7,29 +7,83 @@ metadata:
|
|
|
7
7
|
mode: generate
|
|
8
8
|
sources:
|
|
9
9
|
- reference/v5/mcp/tools-and-resources.md#Custom `mcpTools` opt-in
|
|
10
|
-
sourceCommit:
|
|
11
|
-
inputHash:
|
|
10
|
+
sourceCommit: 9e6ecf87dd25bb488ad44dc2876ca6439ccd682e
|
|
11
|
+
inputHash: 692801362e747c73
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# Custom MCP Tools
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
Instructions for the agent to follow when exposing custom instance methods as MCP tools via a static `mcpTools` array on a Resource class.
|
|
17
17
|
|
|
18
18
|
## When to Use
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Apply this rule when a component needs to expose non-verb instance methods as callable MCP tools. Use it whenever you need custom business logic (e.g., reconciliation, aggregation, batch operations) accessible via the MCP transport beyond the auto-generated verb tools.
|
|
21
21
|
|
|
22
22
|
## How It Works
|
|
23
23
|
|
|
24
|
-
1. **Declare
|
|
24
|
+
1. **Declare a static `mcpTools` array** on the Resource class. Each entry names the tool, maps it to an instance method, provides a description, and defines an `inputSchema`.
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
class Orders extends Tables.orders {
|
|
28
|
+
static mcpTools = [
|
|
29
|
+
{
|
|
30
|
+
name: 'reconcile_unsettled',
|
|
31
|
+
method: 'reconcileUnsettled',
|
|
32
|
+
description: 'Reconcile all orders flagged as unsettled and emit a summary',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
async reconcileUnsettled({ since }) {
|
|
41
|
+
/* ... */
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. **Understand the security model.** Custom tools are exposed to **every** MCP session — including `anonymous`, unauthenticated ones. The MCP layer does **not**:
|
|
47
|
+
- Run an `allow*` gate automatically.
|
|
48
|
+
- Open a Resource transaction automatically.
|
|
49
|
+
- Filter the tool from `tools/list` based on user role.
|
|
50
|
+
|
|
51
|
+
Unlike auto-generated verb tools (which are RBAC-filtered per user at `tools/list` time and enforce table permissions on call), a custom tool is listed to every session and its method executes even when no user is logged in. `context.user` may be empty.
|
|
52
|
+
|
|
53
|
+
3. **Enforce access control inside the method.** Because the MCP layer performs no authentication or ACL check, the method itself is fully responsible. Check `context.user` at the top of the method and throw when the caller doesn't qualify:
|
|
54
|
+
|
|
55
|
+
```javascript
|
|
56
|
+
async reconcileUnsettled({ since }) {
|
|
57
|
+
if (!context.user || !context.user.roles.includes('admin')) {
|
|
58
|
+
throw new Error('Unauthorized');
|
|
59
|
+
}
|
|
60
|
+
/* ... */
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
4. **Delegate to static Resource operations correctly.** The MCP-created instance context carries the authenticated user and a one-shot `authorize` flag:
|
|
65
|
+
- For the **first** delegated operation, pass `this.getContext()` directly — the `authorize` flag is consumed by that call:
|
|
66
|
+
```javascript
|
|
67
|
+
const result = await Orders.get(target, this.getContext());
|
|
68
|
+
```
|
|
69
|
+
- For **every subsequent** delegated operation, create a fresh `RequestTarget`, set `target.checkPermission = true` so authorization derives from `context.user`, and pass `this.getContext()`:
|
|
70
|
+
```javascript
|
|
71
|
+
target.checkPermission = true;
|
|
72
|
+
const next = await Orders.get(target, this.getContext());
|
|
73
|
+
```
|
|
74
|
+
- **Never** accept `checkPermission` from tool arguments or other client input.
|
|
75
|
+
|
|
76
|
+
## Examples
|
|
77
|
+
|
|
78
|
+
Full class with a custom MCP tool that guards access via `context.user`:
|
|
25
79
|
|
|
26
80
|
```javascript
|
|
27
|
-
|
|
81
|
+
class Orders extends Tables.orders {
|
|
28
82
|
static mcpTools = [
|
|
29
83
|
{
|
|
30
84
|
name: 'reconcile_unsettled',
|
|
31
|
-
description: 'Reconcile all unsettled orders and return a summary',
|
|
32
85
|
method: 'reconcileUnsettled',
|
|
86
|
+
description: 'Reconcile all orders flagged as unsettled and emit a summary',
|
|
33
87
|
inputSchema: {
|
|
34
88
|
type: 'object',
|
|
35
89
|
properties: { since: { type: 'string', description: 'ISO 8601 timestamp' } },
|
|
@@ -37,36 +91,28 @@ export class Orders extends tables.Orders {
|
|
|
37
91
|
},
|
|
38
92
|
];
|
|
39
93
|
|
|
40
|
-
async reconcileUnsettled(
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
45
|
-
```
|
|
94
|
+
async reconcileUnsettled({ since }) {
|
|
95
|
+
// Reject anonymous or unauthorized callers — the MCP layer does not do this.
|
|
96
|
+
if (!context.user) {
|
|
97
|
+
throw new Error('Authentication required');
|
|
98
|
+
}
|
|
46
99
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
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:
|
|
100
|
+
// First delegated operation: use the one-shot authorize flag via this.getContext().
|
|
101
|
+
const target = { since };
|
|
102
|
+
const unsettled = await Orders.get(target, this.getContext());
|
|
51
103
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
104
|
+
// Subsequent delegated operations: fresh RequestTarget with checkPermission = true.
|
|
105
|
+
const updateTarget = { id: unsettled.id };
|
|
106
|
+
updateTarget.checkPermission = true;
|
|
107
|
+
await Orders.update(updateTarget, this.getContext());
|
|
56
108
|
}
|
|
57
|
-
// ...
|
|
58
109
|
}
|
|
59
110
|
```
|
|
60
111
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## Examples
|
|
64
|
-
|
|
65
|
-
Warn-worthy anti-pattern — a public instance with an expensive tool and no gating:
|
|
66
|
-
|
|
67
|
-
```javascript
|
|
68
|
-
static mcpTools = [{ name: 'answer', method: 'llmAnswer', ... }];
|
|
69
|
-
async llmAnswer(args) { return await callExpensiveModel(args.q); } // anonymous callers burn your budget
|
|
70
|
-
```
|
|
112
|
+
## Notes
|
|
71
113
|
|
|
72
|
-
|
|
114
|
+
- `mcpTools` is a **static** array — declare it on the class, not on instances.
|
|
115
|
+
- The `allow*` gates that protect auto-generated verb tools do **not** run for custom tools. All authorization logic must live inside the method.
|
|
116
|
+
- `context.user` is the canonical way to identify the caller; it may be empty for `anonymous` sessions, so always check before trusting it.
|
|
117
|
+
- Never derive `checkPermission` from client-supplied tool arguments — always set it explicitly in your method code.
|
|
118
|
+
- If your custom tool performs expensive or repeated operations, consider applying [rate limiting](rate-limiting.md) or [durable quotas](durable-quotas.md) inside the method to prevent abuse by unauthenticated callers.
|
package/package.json
CHANGED
package/skills-lock.json
CHANGED
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
"source": "harperfast/skills",
|
|
6
6
|
"sourceType": "github",
|
|
7
7
|
"skillPath": "harper-best-practices/SKILL.md",
|
|
8
|
-
"computedHash": "
|
|
8
|
+
"computedHash": "6656243a1b2fc48e9872f62e11fc0aaf6948d7893bdaae6aac437267e6843163"
|
|
9
9
|
},
|
|
10
10
|
"harper-mcp": {
|
|
11
11
|
"source": "harperfast/skills",
|
|
12
12
|
"sourceType": "github",
|
|
13
13
|
"skillPath": "harper-mcp/SKILL.md",
|
|
14
|
-
"computedHash": "
|
|
14
|
+
"computedHash": "a0f94e58af6b4128853dc4033e246d7fde2e07416d2aaa705bfd8d984e21ea34"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
}
|