@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.
@@ -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
- Expose non-CRUD operations — an LLM-backed `answer`, a domain action, a report generator — as first-class MCP tools.
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
- 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.
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 `static mcpTools`** on a Resource class (typically in `resources.js`/`resources.ts`):
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
- export class Orders extends tables.Orders {
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(args, context) {
183
- // context: { user, profile, sessionId, signal, progress?, serverRequest? }
184
- return { reconciled: 12 };
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
- 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:
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
- ```javascript
195
- async reconcileUnsettled(args, context) {
196
- if (!context.user?.username) {
197
- throw new Error('authentication required');
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
- 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
- ```
254
+ #### Notes
213
255
 
214
- Fixed: check `context.user` (or accept anonymity deliberately) _and_ configure `rateLimit.perClientPerSecond` + a `quota` hook.
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
 
@@ -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: d7d2ddb120ce5f2ad39dc425f628f5a4f220c151
11
- inputHash: 84a2ffac9bb7b66a
10
+ sourceCommit: 9e6ecf87dd25bb488ad44dc2876ca6439ccd682e
11
+ inputHash: 692801362e747c73
12
12
  ---
13
13
 
14
14
  # Custom MCP Tools
15
15
 
16
- Expose non-CRUD operations — an LLM-backed `answer`, a domain action, a report generator — as first-class MCP tools.
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
- 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.
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 `static mcpTools`** on a Resource class (typically in `resources.js`/`resources.ts`):
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
- export class Orders extends tables.Orders {
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(args, context) {
41
- // context: { user, profile, sessionId, signal, progress?, serverRequest? }
42
- return { reconciled: 12 };
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
- 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.
48
- 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?.(…)`).
49
- 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.
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
- ```javascript
53
- async reconcileUnsettled(args, context) {
54
- if (!context.user?.username) {
55
- throw new Error('authentication required');
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
- 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).
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
- Fixed: check `context.user` (or accept anonymity deliberately) _and_ configure `rateLimit.perClientPerSecond` + a `quota` hook.
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.
@@ -90,7 +90,7 @@ rules:
90
90
  must_cover:
91
91
  - mcpTools
92
92
  - anonymous
93
- - live-class
93
+ - 'allow*'
94
94
  - context.user
95
95
  cross_links:
96
96
  - rate-limiting