@frontmcp/skills 1.2.1 → 1.4.0
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/README.md +38 -29
- package/catalog/TEMPLATE.md +26 -0
- package/catalog/create-tool/SKILL.md +318 -0
- package/catalog/create-tool/examples/01-basic-class-tool.md +112 -0
- package/catalog/create-tool/examples/02-basic-function-tool.md +80 -0
- package/catalog/create-tool/examples/03-tool-with-zod-shape-output.md +78 -0
- package/catalog/create-tool/examples/04-tool-with-zod-schema-output.md +97 -0
- package/catalog/create-tool/examples/05-tool-with-primitive-output.md +93 -0
- package/catalog/create-tool/examples/06-tool-with-media-output.md +109 -0
- package/catalog/create-tool/examples/08-tool-with-provider-injection.md +110 -0
- package/catalog/create-tool/examples/09-tool-with-multiple-providers.md +107 -0
- package/catalog/create-tool/examples/11-tool-with-fetch.md +94 -0
- package/catalog/create-tool/examples/12-tool-with-fetch-and-retries.md +115 -0
- package/catalog/create-tool/examples/13-tool-with-single-auth-provider.md +85 -0
- package/catalog/create-tool/examples/14-tool-with-multiple-auth-providers.md +105 -0
- package/catalog/create-tool/examples/15-tool-with-credential-vault.md +115 -0
- package/catalog/create-tool/examples/16-tool-with-rate-limit.md +71 -0
- package/catalog/create-tool/examples/17-tool-with-concurrency-and-timeout.md +101 -0
- package/catalog/create-tool/examples/18-tool-with-progress-and-notify.md +96 -0
- package/catalog/create-tool/examples/19-tool-with-elicitation.md +102 -0
- package/catalog/create-tool/examples/20-tool-with-annotations.md +125 -0
- package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +107 -0
- package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +93 -0
- package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +112 -0
- package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +127 -0
- package/catalog/create-tool/examples/25-tool-handing-off-to-job.md +143 -0
- package/catalog/create-tool/examples/26-tool-with-resource-link-output.md +94 -0
- package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +90 -0
- package/catalog/create-tool/references/annotations.md +96 -0
- package/catalog/create-tool/references/auth-providers.md +167 -0
- package/catalog/create-tool/references/availability.md +106 -0
- package/catalog/create-tool/references/decorator-options.md +95 -0
- package/catalog/create-tool/references/derived-types.md +102 -0
- package/catalog/create-tool/references/elicitation.md +128 -0
- package/catalog/create-tool/references/error-handling.md +128 -0
- package/catalog/create-tool/references/execution-context.md +158 -0
- package/catalog/create-tool/references/file-layout.md +96 -0
- package/catalog/create-tool/references/function-style-builder.md +118 -0
- package/catalog/create-tool/references/input-schema.md +141 -0
- package/catalog/create-tool/references/output-schema.md +175 -0
- package/catalog/create-tool/references/quick-start.md +124 -0
- package/catalog/create-tool/references/registration.md +132 -0
- package/catalog/create-tool/references/remote-and-esm.md +68 -0
- package/catalog/create-tool/references/testing.md +59 -0
- package/catalog/create-tool/references/throttling.md +109 -0
- package/catalog/create-tool/references/ui-widgets.md +198 -0
- package/catalog/create-tool/rules/always-define-output-schema.md +77 -0
- package/catalog/create-tool/rules/derive-execute-types.md +57 -0
- package/catalog/create-tool/rules/input-schema-is-raw-shape.md +76 -0
- package/catalog/create-tool/rules/no-toolcontext-generics.md +50 -0
- package/catalog/create-tool/rules/no-try-catch-around-execute.md +79 -0
- package/catalog/create-tool/rules/register-in-app.md +76 -0
- package/catalog/create-tool/rules/snake-case-tool-names.md +45 -0
- package/catalog/create-tool/rules/use-this-fail-for-business-errors.md +75 -0
- package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +76 -0
- package/catalog/create-tool/rules/widget-resource-mode-host-detect.md +61 -0
- package/catalog/frontmcp-auth-ui/SKILL.md +146 -0
- package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/login-slot.md +97 -0
- package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/multi-step-auth-extra.md +133 -0
- package/catalog/frontmcp-auth-ui/references/custom-auth-ui.md +162 -0
- package/catalog/frontmcp-authorities/SKILL.md +55 -18
- package/catalog/frontmcp-authorities/references/authority-profiles.md +25 -1
- package/catalog/frontmcp-authorities/references/custom-evaluators.md +1 -1
- package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +9 -0
- package/catalog/frontmcp-channels/SKILL.md +7 -1
- package/catalog/frontmcp-config/SKILL.md +14 -7
- package/catalog/frontmcp-config/examples/configure-auth/local-credential-vault.md +94 -0
- package/catalog/frontmcp-config/examples/configure-auth/local-secure-store.md +138 -0
- package/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md +45 -23
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-behind-tunnel.md +73 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-consent-enforcement.md +87 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-dcr-control.md +67 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-minimal.md +62 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-multi-provider-orchestration.md +93 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +18 -20
- package/catalog/frontmcp-config/examples/configure-auth-modes/local-single-operator.md +66 -0
- package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +37 -23
- package/catalog/frontmcp-config/examples/configure-http/custom-http-routes.md +98 -0
- package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +17 -9
- package/catalog/frontmcp-config/references/configure-auth-modes.md +86 -23
- package/catalog/frontmcp-config/references/configure-auth.md +296 -50
- package/catalog/frontmcp-config/references/configure-deployment-targets.md +84 -1
- package/catalog/frontmcp-config/references/configure-http.md +203 -14
- package/catalog/frontmcp-config/references/configure-session.md +14 -7
- package/catalog/frontmcp-deployment/SKILL.md +17 -15
- package/catalog/frontmcp-deployment/references/build-for-mcpb.md +1 -1
- package/catalog/frontmcp-deployment/references/deploy-manifest-yaml.md +308 -0
- package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +174 -0
- package/catalog/frontmcp-deployment/references/mcp-client-integration.md +145 -2
- package/catalog/frontmcp-development/SKILL.md +36 -50
- package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +14 -0
- package/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md +85 -9
- package/catalog/frontmcp-development/references/create-job.md +45 -11
- package/catalog/frontmcp-development/references/create-provider.md +80 -8
- package/catalog/frontmcp-development/references/create-skill-with-tools.md +31 -0
- package/catalog/frontmcp-development/references/create-skill.md +45 -0
- package/catalog/frontmcp-development/references/decorators-guide.md +15 -15
- package/catalog/frontmcp-extensibility/SKILL.md +1 -1
- package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +8 -6
- package/catalog/frontmcp-extensibility/references/skill-audit-log.md +7 -2
- package/catalog/frontmcp-guides/SKILL.md +8 -8
- package/catalog/frontmcp-observability/SKILL.md +16 -8
- package/catalog/frontmcp-observability/examples/metrics-endpoint/enable-metrics-endpoint.md +77 -0
- package/catalog/frontmcp-observability/references/metrics-endpoint.md +161 -0
- package/catalog/frontmcp-production-readiness/SKILL.md +1 -1
- package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -2
- package/catalog/frontmcp-setup/SKILL.md +12 -12
- package/catalog/frontmcp-setup/examples/frontmcp-skills-usage/install-and-search-skills.md +19 -1
- package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
- package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +260 -19
- package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
- package/catalog/frontmcp-setup/references/setup-project.md +29 -0
- package/catalog/frontmcp-setup/references/setup-sqlite.md +68 -9
- package/catalog/frontmcp-testing/SKILL.md +26 -18
- package/catalog/frontmcp-testing/references/test-auth.md +24 -0
- package/catalog/skills-manifest.json +676 -146
- package/package.json +1 -1
- package/src/manifest.d.ts +72 -1
- package/src/manifest.js +4 -1
- package/src/manifest.js.map +1 -1
- package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +0 -61
- package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -84
- package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -92
- package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +0 -92
- package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +0 -59
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +0 -101
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +0 -62
- package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +0 -101
- package/catalog/frontmcp-development/references/create-tool-annotations.md +0 -48
- package/catalog/frontmcp-development/references/create-tool-output-schema-types.md +0 -71
- package/catalog/frontmcp-development/references/create-tool.md +0 -728
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 17-tool-with-concurrency-and-timeout
|
|
3
|
+
level: advanced
|
|
4
|
+
description: 'Tool with `concurrency` + `timeout` for a real bottleneck (PDF rendering) — caps simultaneous in-flight work AND hard-caps per-call duration.'
|
|
5
|
+
tags: [throttling, concurrency, timeout]
|
|
6
|
+
features:
|
|
7
|
+
- 'Capping simultaneous in-flight executions with `concurrency: { maxConcurrent }` (server-wide by default)'
|
|
8
|
+
- "Hard-bounding any single call with `timeout: { executeMs }` so a wedged invocation can't hold a concurrency slot indefinitely"
|
|
9
|
+
- 'Giving long child work its own deadline, since `timeout` bounds `execute()` but does not pass an abort signal into it'
|
|
10
|
+
- 'Combining `rateLimit` + `concurrency` + `timeout` as a production triple'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Concurrency And Timeout
|
|
14
|
+
|
|
15
|
+
Tool with `concurrency` + `timeout` for a real bottleneck (PDF rendering) — caps simultaneous in-flight work AND hard-caps per-call duration.
|
|
16
|
+
|
|
17
|
+
For tools that hold a real bottleneck (CPU / GPU / DB write connection), `concurrency` caps simultaneous in-flight executions. Pair with `timeout` so a stuck call can't permanently block a slot.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/render-pdf.tool.ts
|
|
23
|
+
import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
const inputSchema = {
|
|
26
|
+
template: z.enum(['invoice', 'report', 'contract']),
|
|
27
|
+
data: z.record(z.string(), z.unknown()),
|
|
28
|
+
};
|
|
29
|
+
const outputSchema = { data: z.string(), mimeType: z.literal('application/pdf'), byteCount: z.number().int() };
|
|
30
|
+
|
|
31
|
+
@Tool({
|
|
32
|
+
name: 'render_pdf',
|
|
33
|
+
description: 'Render a PDF from a template + data — capped at 5 concurrent renders',
|
|
34
|
+
inputSchema,
|
|
35
|
+
outputSchema,
|
|
36
|
+
rateLimit: { maxRequests: 100, windowMs: 60_000 }, // outer cap — burst protection
|
|
37
|
+
concurrency: { maxConcurrent: 5 }, // inner cap — at most 5 PDFs at once
|
|
38
|
+
timeout: { executeMs: 30_000 }, // hard per-call deadline
|
|
39
|
+
annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false },
|
|
40
|
+
})
|
|
41
|
+
export class RenderPdfTool extends ToolContext {
|
|
42
|
+
async execute(input: { template: 'invoice' | 'report' | 'contract'; data: Record<string, unknown> }) {
|
|
43
|
+
// Pass a per-render deadline straight into the renderer. `timeout` caps the
|
|
44
|
+
// overall execute() duration at the framework level, but it does NOT hand
|
|
45
|
+
// execute() an abort signal — so bound long child work explicitly here.
|
|
46
|
+
const pdfBuffer = await this.renderPdf(input.template, input.data, { deadlineMs: 25_000 });
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
data: pdfBuffer.toString('base64'),
|
|
50
|
+
mimeType: 'application/pdf' as const,
|
|
51
|
+
byteCount: pdfBuffer.byteLength,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private async renderPdf(
|
|
56
|
+
_template: string,
|
|
57
|
+
_data: Record<string, unknown>,
|
|
58
|
+
options: { deadlineMs: number },
|
|
59
|
+
): Promise<Buffer> {
|
|
60
|
+
// pretend this is puppeteer / wkhtmltopdf / a Rust binary that honors its own deadline
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
// clear the deadline only once the render actually resolves
|
|
63
|
+
const done = setTimeout(() => {
|
|
64
|
+
clearTimeout(deadline);
|
|
65
|
+
resolve(Buffer.from('%PDF-1.4 …'));
|
|
66
|
+
}, 1_000);
|
|
67
|
+
const deadline = setTimeout(() => {
|
|
68
|
+
clearTimeout(done);
|
|
69
|
+
reject(new Error('render exceeded deadline'));
|
|
70
|
+
}, options.deadlineMs);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## What This Demonstrates
|
|
77
|
+
|
|
78
|
+
- Capping simultaneous in-flight executions with `concurrency: { maxConcurrent }` (server-wide by default)
|
|
79
|
+
- Hard-bounding any single call with `timeout: { executeMs }` so a wedged invocation can't hold a concurrency slot indefinitely
|
|
80
|
+
- Giving long child work its own deadline, since `timeout` bounds `execute()` but does not pass an abort signal into it
|
|
81
|
+
- Combining `rateLimit` + `concurrency` + `timeout` as a production triple
|
|
82
|
+
|
|
83
|
+
## How they interact
|
|
84
|
+
|
|
85
|
+
Per call, in order:
|
|
86
|
+
|
|
87
|
+
1. `rateLimit` → reject early if over limit (no concurrency slot consumed)
|
|
88
|
+
2. `concurrency` → queue until a slot opens (queue depth is unbounded by default)
|
|
89
|
+
3. `timeout` → wraps `execute()` once it actually runs; on expiry, throws `ExecutionTimeoutError` (from `@frontmcp/guard`) and aborts the wrapped execution internally
|
|
90
|
+
|
|
91
|
+
The three controls are **orthogonal** — each addresses a different failure mode:
|
|
92
|
+
|
|
93
|
+
| Control | Protects against |
|
|
94
|
+
| ------------- | ---------------------------------------------- |
|
|
95
|
+
| `rateLimit` | Quota / billing overrun |
|
|
96
|
+
| `concurrency` | Resource exhaustion (CPU, GPU, DB connections) |
|
|
97
|
+
| `timeout` | Stuck calls holding slots forever |
|
|
98
|
+
|
|
99
|
+
## Why a child-level deadline matters
|
|
100
|
+
|
|
101
|
+
`timeout` aborts the wrapped `execute()` at the framework level and throws `ExecutionTimeoutError`, but it does **not** pass an abort signal into your code — there is no `this.context.abortSignal`. So if the underlying PDF renderer has no deadline of its own, it keeps going after the timeout fires, burning CPU even though the call already errored out to the client. Give long child work its own bound (as `deadlineMs` does above) so the renderer can clean up and free the concurrency slot for the next queued call.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 18-tool-with-progress-and-notify
|
|
3
|
+
level: intermediate
|
|
4
|
+
description: "Long-running tool emitting progress updates (`this.progress`), log notifications (`this.notify`), and stage markers (`this.mark`) — the standard pattern for jobs you don't want to feel hung."
|
|
5
|
+
tags: [progress, notifications, mark, long-running]
|
|
6
|
+
features:
|
|
7
|
+
- 'Emitting per-item progress with `await this.progress(current, total, message)`'
|
|
8
|
+
- 'Sending free-form log notifications at `info` / `warning` / `error` levels with `await this.notify(message, level)`'
|
|
9
|
+
- 'Marking execution stages with `this.mark(stage)` so observability tools have breadcrumbs'
|
|
10
|
+
- "Letting `this.progress(...)` return `false` cheaply when no progress token was provided (zero-cost when nobody's listening)"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Progress And Notify
|
|
14
|
+
|
|
15
|
+
Long-running tool emitting progress updates (`this.progress`), log notifications (`this.notify`), and stage markers (`this.mark`) — the standard pattern for jobs you don't want to feel hung.
|
|
16
|
+
|
|
17
|
+
Anything that takes more than a couple of seconds should emit progress. The framework's notifications API is a single line per emission; clients render progress bars / activity logs / breadcrumb timelines from it.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/process-items.tool.ts
|
|
23
|
+
import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
const inputSchema = {
|
|
26
|
+
items: z.array(z.string()).min(1).max(1_000),
|
|
27
|
+
};
|
|
28
|
+
const outputSchema = {
|
|
29
|
+
processed: z.number().int(),
|
|
30
|
+
failed: z.number().int(),
|
|
31
|
+
results: z.array(z.object({ item: z.string(), status: z.enum(['ok', 'failed']) })),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
@Tool({
|
|
35
|
+
name: 'process_items',
|
|
36
|
+
description: 'Process a batch of items, emitting progress per item',
|
|
37
|
+
inputSchema,
|
|
38
|
+
outputSchema,
|
|
39
|
+
timeout: { executeMs: 5 * 60_000 }, // 5min ceiling
|
|
40
|
+
annotations: { idempotentHint: true, openWorldHint: false },
|
|
41
|
+
})
|
|
42
|
+
export class ProcessItemsTool extends ToolContext {
|
|
43
|
+
async execute(input: { items: string[] }) {
|
|
44
|
+
this.mark('validation');
|
|
45
|
+
// (Zod already validated; this.mark adds a server-side breadcrumb)
|
|
46
|
+
|
|
47
|
+
this.mark('processing');
|
|
48
|
+
const results: { item: string; status: 'ok' | 'failed' }[] = [];
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < input.items.length; i++) {
|
|
51
|
+
const item = input.items[i];
|
|
52
|
+
const ok = await this.processOne(item);
|
|
53
|
+
results.push({ item, status: ok ? 'ok' : 'failed' });
|
|
54
|
+
|
|
55
|
+
// Emit progress; cheap no-op if the request didn't send a progress token
|
|
56
|
+
await this.progress(i + 1, input.items.length, `Processed ${item}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const failed = results.filter((r) => r.status === 'failed').length;
|
|
60
|
+
if (failed > 0) {
|
|
61
|
+
await this.notify(`${failed}/${input.items.length} items failed`, 'warning');
|
|
62
|
+
} else {
|
|
63
|
+
await this.notify(`All ${input.items.length} items processed`, 'info');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.mark('complete');
|
|
67
|
+
return { processed: results.length, failed, results };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async processOne(_item: string): Promise<boolean> {
|
|
71
|
+
return Math.random() > 0.05;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
> **Testing.** Tests that intercept `this.progress` / `this.notify` events live in the dedicated `testing` skill — `@frontmcp/testing` exposes the notification stream via the `TestServer` + Playwright `test`/`expect` fixture surface.
|
|
77
|
+
|
|
78
|
+
## What This Demonstrates
|
|
79
|
+
|
|
80
|
+
- Emitting per-item progress with `await this.progress(current, total, message)`
|
|
81
|
+
- Sending free-form log notifications at `info` / `warning` / `error` levels with `await this.notify(message, level)`
|
|
82
|
+
- Marking execution stages with `this.mark(stage)` so observability tools have breadcrumbs
|
|
83
|
+
- Letting `this.progress(...)` return `false` cheaply when no progress token was provided (zero-cost when nobody's listening)
|
|
84
|
+
|
|
85
|
+
## When to use which
|
|
86
|
+
|
|
87
|
+
| Use | For |
|
|
88
|
+
| ------------------------------ | -------------------------------------------------------------------------- |
|
|
89
|
+
| `this.progress(n, total, msg)` | Quantitative progress — clients render a progress bar |
|
|
90
|
+
| `this.notify(msg, level?)` | Qualitative updates — log lines, status, warnings |
|
|
91
|
+
| `this.mark(stage)` | Server-side only — surfaced in logs/metrics/traces, not sent to the client |
|
|
92
|
+
|
|
93
|
+
## Don't
|
|
94
|
+
|
|
95
|
+
- Don't call `this.progress` in a tight loop with no `total`. Without `total`, clients can't render a meaningful bar.
|
|
96
|
+
- Don't push a `this.notify` per loop iteration. That's progress. Use `this.progress` for granular updates and reserve `this.notify` for events the user genuinely needs to see (failures, warnings, milestones).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 19-tool-with-elicitation
|
|
3
|
+
level: advanced
|
|
4
|
+
description: 'Tool that pauses mid-execution to ask the user for confirmation + extra input via `this.elicit(...)` — the safe pattern for destructive or expensive actions.'
|
|
5
|
+
tags: [elicitation, this.elicit, destructive-action, confirmation]
|
|
6
|
+
features:
|
|
7
|
+
- 'Calling `this.elicit(message, z.object({ ... }))` to request interactive input mid-`execute()`'
|
|
8
|
+
- 'Branching on `result.status` — `accept` / `decline` / `cancel` — and matching the early returns against `outputSchema`'
|
|
9
|
+
- 'Pairing elicitation with `annotations.destructiveHint: true` so clients know to render the confirmation prominently'
|
|
10
|
+
- "Requiring `elicitation: { enabled: true }` at the `@FrontMcp({...})` server level — and what fails when it isn't"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Elicitation
|
|
14
|
+
|
|
15
|
+
Tool that pauses mid-execution to ask the user for confirmation + extra input via `this.elicit(...)` — the safe pattern for destructive or expensive actions.
|
|
16
|
+
|
|
17
|
+
For destructive or expensive actions, elicitation is the safe pattern. The tool starts, pauses, asks the user "are you sure? what reason?", and finishes (or cancels) based on the answer.
|
|
18
|
+
|
|
19
|
+
> Requires `elicitation: { enabled: true }` on `@FrontMcp({...})`. Without it, every `this.elicit(...)` throws `ElicitationDisabledError` at runtime.
|
|
20
|
+
|
|
21
|
+
## Code
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// src/main.ts
|
|
25
|
+
import { FrontMcp } from '@frontmcp/sdk';
|
|
26
|
+
|
|
27
|
+
@FrontMcp({
|
|
28
|
+
info: { name: 'demo', version: '1.0.0' },
|
|
29
|
+
apps: [MainApp],
|
|
30
|
+
elicitation: { enabled: true }, // ← must be enabled for this.elicit to work
|
|
31
|
+
})
|
|
32
|
+
export default class DemoServer {}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// src/apps/main/tools/delete-user.tool.ts
|
|
37
|
+
import { PublicMcpError, Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
38
|
+
|
|
39
|
+
import { USER_SERVICE } from '../tokens';
|
|
40
|
+
|
|
41
|
+
const inputSchema = { userId: z.string().describe('User ID to delete') };
|
|
42
|
+
const outputSchema = z.discriminatedUnion('outcome', [
|
|
43
|
+
z.object({ outcome: z.literal('deleted'), userId: z.string(), reason: z.string().optional() }),
|
|
44
|
+
z.object({ outcome: z.literal('cancelled'), userId: z.string(), reason: z.string() }),
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
@Tool({
|
|
48
|
+
name: 'delete_user',
|
|
49
|
+
description: 'Delete a user account — requires explicit confirmation',
|
|
50
|
+
inputSchema,
|
|
51
|
+
outputSchema,
|
|
52
|
+
annotations: { destructiveHint: true, idempotentHint: true, openWorldHint: false },
|
|
53
|
+
})
|
|
54
|
+
export class DeleteUserTool extends ToolContext {
|
|
55
|
+
async execute(input: { userId: string }) {
|
|
56
|
+
const users = this.get(USER_SERVICE);
|
|
57
|
+
const user = await users.findById(input.userId);
|
|
58
|
+
if (!user) this.fail(new PublicMcpError(`No such user: ${input.userId}`));
|
|
59
|
+
|
|
60
|
+
const elicited = await this.elicit(
|
|
61
|
+
`Permanently delete ${user.email}? This cannot be undone.`,
|
|
62
|
+
z.object({
|
|
63
|
+
confirm: z.boolean().describe('Set to true to confirm'),
|
|
64
|
+
reason: z.string().optional().describe('Reason for the audit log'),
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (elicited.status === 'cancel') {
|
|
69
|
+
return { outcome: 'cancelled' as const, userId: input.userId, reason: 'User closed prompt' };
|
|
70
|
+
}
|
|
71
|
+
if (elicited.status === 'decline' || !elicited.content?.confirm) {
|
|
72
|
+
return { outcome: 'cancelled' as const, userId: input.userId, reason: 'User declined' };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await users.delete(input.userId, { reason: elicited.content.reason });
|
|
76
|
+
return { outcome: 'deleted' as const, userId: input.userId, reason: elicited.content.reason };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## What This Demonstrates
|
|
82
|
+
|
|
83
|
+
- Calling `this.elicit(message, z.object({ ... }))` to request interactive input mid-`execute()`
|
|
84
|
+
- Branching on `result.status` — `accept` / `decline` / `cancel` — and matching the early returns against `outputSchema`
|
|
85
|
+
- Pairing elicitation with `annotations.destructiveHint: true` so clients know to render the confirmation prominently
|
|
86
|
+
- Requiring `elicitation: { enabled: true }` at the `@FrontMcp({...})` server level — and what fails when it isn't
|
|
87
|
+
|
|
88
|
+
## `result.status` matrix
|
|
89
|
+
|
|
90
|
+
| Status | When | Always check `result.content`? |
|
|
91
|
+
| ----------- | ----------------------------------------- | ----------------------------------------------- |
|
|
92
|
+
| `'accept'` | User filled the form and submitted | Yes — `result.content` is typed from the schema |
|
|
93
|
+
| `'decline'` | User clicked decline / no | No — `content` is absent |
|
|
94
|
+
| `'cancel'` | User closed the prompt without responding | No — `content` is absent |
|
|
95
|
+
|
|
96
|
+
The Zod schema you pass to `this.elicit` defines the `result.content` type when status is `'accept'`.
|
|
97
|
+
|
|
98
|
+
## Early returns must match `outputSchema`
|
|
99
|
+
|
|
100
|
+
The `cancelled` branch returns `{ outcome: 'cancelled', userId, reason }` which matches the `z.discriminatedUnion` outputSchema. If `outputSchema` were `z.object({ deleted: z.boolean() })`, you'd return `{ deleted: false }` instead.
|
|
101
|
+
|
|
102
|
+
The framework validates the return regardless of how you got there — early elicitation returns are no exception.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 20-tool-with-annotations
|
|
3
|
+
level: basic
|
|
4
|
+
description: 'Four tools showing the standard annotation combinations — read-only query, destructive delete, send-email side-effecting, external-API search — and the client behavior each combination opts into.'
|
|
5
|
+
tags: [annotations, readOnlyHint, destructiveHint, idempotentHint, openWorldHint]
|
|
6
|
+
features:
|
|
7
|
+
- 'Setting `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` to opt into specific client behaviors (auto-retry, confirmation gating, parallelization)'
|
|
8
|
+
- 'Providing a human-readable `title` that overrides the snake_case `name` in client UIs'
|
|
9
|
+
- "Picking the conservative defaults when the annotations aren't obvious (omitting fields is safer than guessing)"
|
|
10
|
+
- 'Why `send_email` sets `idempotentHint: false` (each call sends a new email) while `delete_user` sets it to `true` (deleting twice still leaves the user deleted)'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Annotations
|
|
14
|
+
|
|
15
|
+
Four tools showing the standard annotation combinations — read-only query, destructive delete, send-email side-effecting, external-API search — and the client behavior each combination opts into.
|
|
16
|
+
|
|
17
|
+
`annotations` are advisory but the client uses them to decide whether to gate, parallelize, or retry. Four canonical combinations cover most tools.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/annotations.tool.ts
|
|
23
|
+
import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
// 1. Read-only query — safe to call freely, parallelizable, auto-retryable
|
|
26
|
+
@Tool({
|
|
27
|
+
name: 'search_users',
|
|
28
|
+
description: 'Search users by name or email',
|
|
29
|
+
inputSchema: { query: z.string(), limit: z.number().int().min(1).max(100).default(10) },
|
|
30
|
+
outputSchema: { users: z.array(z.object({ id: z.string(), email: z.string().email() })) },
|
|
31
|
+
annotations: {
|
|
32
|
+
title: 'Search users',
|
|
33
|
+
readOnlyHint: true,
|
|
34
|
+
destructiveHint: false,
|
|
35
|
+
idempotentHint: true,
|
|
36
|
+
openWorldHint: false, // local DB only
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
export class SearchUsersTool extends ToolContext {
|
|
40
|
+
async execute(_input: { query: string; limit: number }) {
|
|
41
|
+
return { users: [] };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. Destructive admin action — confirmation gated, retryable
|
|
46
|
+
@Tool({
|
|
47
|
+
name: 'delete_user',
|
|
48
|
+
description: 'Permanently delete a user account',
|
|
49
|
+
inputSchema: { userId: z.string() },
|
|
50
|
+
outputSchema: { deleted: z.boolean() },
|
|
51
|
+
annotations: {
|
|
52
|
+
title: 'Delete user',
|
|
53
|
+
readOnlyHint: false,
|
|
54
|
+
destructiveHint: true,
|
|
55
|
+
idempotentHint: true, // deleting twice leaves the user deleted — safe to retry
|
|
56
|
+
openWorldHint: false,
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
export class DeleteUserTool extends ToolContext {
|
|
60
|
+
async execute(_input: { userId: string }) {
|
|
61
|
+
return { deleted: true };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 3. Send-email — side effect, NOT idempotent, external service
|
|
66
|
+
@Tool({
|
|
67
|
+
name: 'send_email',
|
|
68
|
+
description: 'Send an email via SMTP',
|
|
69
|
+
inputSchema: { to: z.string().email(), subject: z.string(), body: z.string() },
|
|
70
|
+
outputSchema: { messageId: z.string() },
|
|
71
|
+
annotations: {
|
|
72
|
+
title: 'Send email',
|
|
73
|
+
readOnlyHint: false,
|
|
74
|
+
destructiveHint: false,
|
|
75
|
+
idempotentHint: false, // each call sends a NEW email — don't auto-retry blindly
|
|
76
|
+
openWorldHint: true,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
export class SendEmailTool extends ToolContext {
|
|
80
|
+
async execute(_input: { to: string; subject: string; body: string }) {
|
|
81
|
+
return { messageId: `<${crypto.randomUUID()}@example.com>` };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. External-API search — read-only but talks to the open world
|
|
86
|
+
@Tool({
|
|
87
|
+
name: 'web_search',
|
|
88
|
+
description: 'Search the web via an external search API',
|
|
89
|
+
inputSchema: { query: z.string() },
|
|
90
|
+
outputSchema: { results: z.array(z.object({ title: z.string(), url: z.string().url() })) },
|
|
91
|
+
annotations: {
|
|
92
|
+
title: 'Web search',
|
|
93
|
+
readOnlyHint: true,
|
|
94
|
+
destructiveHint: false,
|
|
95
|
+
idempotentHint: true,
|
|
96
|
+
openWorldHint: true, // calls external service
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
export class WebSearchTool extends ToolContext {
|
|
100
|
+
async execute(_input: { query: string }) {
|
|
101
|
+
return { results: [] };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## What This Demonstrates
|
|
107
|
+
|
|
108
|
+
- Setting `readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint` to opt into specific client behaviors (auto-retry, confirmation gating, parallelization)
|
|
109
|
+
- Providing a human-readable `title` that overrides the snake_case `name` in client UIs
|
|
110
|
+
- Picking the conservative defaults when the annotations aren't obvious (omitting fields is safer than guessing)
|
|
111
|
+
- Why `send_email` sets `idempotentHint: false` (each call sends a new email) while `delete_user` sets it to `true` (deleting twice still leaves the user deleted)
|
|
112
|
+
|
|
113
|
+
## How clients use each annotation
|
|
114
|
+
|
|
115
|
+
| Annotation | Common client behavior |
|
|
116
|
+
| ----------------------- | ------------------------------------------------------- |
|
|
117
|
+
| `readOnlyHint: true` | May parallelize calls; doesn't gate behind confirmation |
|
|
118
|
+
| `destructiveHint: true` | Shows "are you sure?" before execution |
|
|
119
|
+
| `idempotentHint: true` | Auto-retries on transient failures |
|
|
120
|
+
| `openWorldHint: false` | Hints that the tool is offline-safe |
|
|
121
|
+
| `title` | Replaces the snake_case `name` in the UI |
|
|
122
|
+
|
|
123
|
+
## When to omit
|
|
124
|
+
|
|
125
|
+
If the right value isn't obvious, omit. The defaults are conservative — `readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`. Clients will gate and not parallelize, which is the safe wrong choice.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 21-tool-with-availability-constraints
|
|
3
|
+
level: advanced
|
|
4
|
+
description: 'Three tools showing the `availableWhen` axes — macOS-only OS gate, production+Node runtime gate, and a `surface` gate that allows agent + job invocation but blocks direct MCP-client calls.'
|
|
5
|
+
tags: [availableWhen, os, runtime, surface, EntryUnavailableError]
|
|
6
|
+
features:
|
|
7
|
+
- "Restricting a tool to macOS with `availableWhen: { os: ['darwin'] }`"
|
|
8
|
+
- "Composing constraints — `runtime: ['node']` AND `env: ['production']` — both must match for the tool to be available"
|
|
9
|
+
- 'Using the `surface` axis to expose an internal tool to agents and jobs while hiding it from direct user invocation'
|
|
10
|
+
- 'Knowing what happens on mismatch — `EntryUnavailableError` (`-32003` FORBIDDEN) with `data.missingAxes` so clients show the right "not available here" reason'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Availability Constraints
|
|
14
|
+
|
|
15
|
+
Three tools showing the `availableWhen` axes — macOS-only OS gate, production+Node runtime gate, and a `surface` gate that allows agent + job invocation but blocks direct MCP-client calls.
|
|
16
|
+
|
|
17
|
+
`availableWhen` is a hard registry-level constraint. Tools that don't match are filtered out of `tools/list` AND blocked from execution. Three real-world axes:
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/availability.tool.ts
|
|
23
|
+
import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
// 1. macOS-only — uses Apple-specific APIs
|
|
26
|
+
@Tool({
|
|
27
|
+
name: 'apple_notes_search',
|
|
28
|
+
description: 'Search Apple Notes via the native bridge',
|
|
29
|
+
inputSchema: { query: z.string() },
|
|
30
|
+
outputSchema: { notes: z.array(z.object({ id: z.string(), title: z.string() })) },
|
|
31
|
+
availableWhen: { os: ['darwin'] },
|
|
32
|
+
})
|
|
33
|
+
export class AppleNotesSearchTool extends ToolContext {
|
|
34
|
+
async execute(_input: { query: string }) {
|
|
35
|
+
return { notes: [] };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 2. Production-only AND Node runtime — uses production-specific deploy infra
|
|
40
|
+
@Tool({
|
|
41
|
+
name: 'deploy_to_production',
|
|
42
|
+
description: 'Deploy a service to production',
|
|
43
|
+
inputSchema: { service: z.string(), version: z.string() },
|
|
44
|
+
outputSchema: { deploymentId: z.string() },
|
|
45
|
+
availableWhen: { runtime: ['node'], env: ['production'] },
|
|
46
|
+
annotations: { destructiveHint: true, idempotentHint: false },
|
|
47
|
+
})
|
|
48
|
+
export class DeployToProductionTool extends ToolContext {
|
|
49
|
+
async execute(_input: { service: string; version: string }) {
|
|
50
|
+
return { deploymentId: 'd_42' };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 3. Agent / job only — internal tool, blocked from direct MCP client invocation
|
|
55
|
+
@Tool({
|
|
56
|
+
name: 'rotate_secrets',
|
|
57
|
+
description: 'Rotate the application signing keys',
|
|
58
|
+
inputSchema: {},
|
|
59
|
+
outputSchema: { rotated: z.boolean() },
|
|
60
|
+
availableWhen: { surface: ['agent', 'job'] }, // not 'mcp' — chat UIs can't call this directly
|
|
61
|
+
annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
62
|
+
})
|
|
63
|
+
export class RotateSecretsTool extends ToolContext {
|
|
64
|
+
async execute() {
|
|
65
|
+
return { rotated: true };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## What This Demonstrates
|
|
71
|
+
|
|
72
|
+
- Restricting a tool to macOS with `availableWhen: { os: ['darwin'] }`
|
|
73
|
+
- Composing constraints — `runtime: ['node']` AND `env: ['production']` — both must match for the tool to be available
|
|
74
|
+
- Using the `surface` axis to expose an internal tool to agents and jobs while hiding it from direct user invocation
|
|
75
|
+
- Knowing what happens on mismatch — `EntryUnavailableError` (`-32003` FORBIDDEN) with `data.missingAxes` so clients show the right "not available here" reason
|
|
76
|
+
|
|
77
|
+
## Axes (recap)
|
|
78
|
+
|
|
79
|
+
| Axis | Values |
|
|
80
|
+
| ------------ | ----------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `os` | `'darwin'`, `'linux'`, `'win32'` |
|
|
82
|
+
| `runtime` | `'node'`, `'browser'`, `'edge'`, `'bun'`, `'deno'` |
|
|
83
|
+
| `deployment` | `'serverless'`, `'standalone'`, `'distributed'`, `'browser'` |
|
|
84
|
+
| `provider` | `'bare'`, `'docker'`, `'vercel'`, `'lambda'`, `'cloudflare'`, … |
|
|
85
|
+
| `target` | `'cli'`, `'node'`, `'vercel'`, `'lambda'`, `'cloudflare'`, … (set by `frontmcp build --target`) |
|
|
86
|
+
| `surface` | `'mcp'`, `'cli'`, `'agent'`, `'job'`, `'http-trigger'` — per-call axis |
|
|
87
|
+
| `env` | `'production'`, `'development'`, `'test'` |
|
|
88
|
+
|
|
89
|
+
Multiple axes are AND-ed. Multiple values within an axis are OR-ed.
|
|
90
|
+
|
|
91
|
+
## Why declarative beats imperative checks
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
// ❌ imperative — tool still appears in tools/list everywhere; users see "this won't work here" only at call time
|
|
95
|
+
async execute(input) {
|
|
96
|
+
if (!this.isPlatform('darwin')) this.fail(new PublicMcpError('macOS only'));
|
|
97
|
+
// …
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ✅ declarative — tool is filtered out of tools/list on non-macOS servers; users never see it
|
|
101
|
+
@Tool({
|
|
102
|
+
availableWhen: { os: ['darwin'] },
|
|
103
|
+
// …
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
The declarative form removes the tool from `tools/list` on non-matching servers — AI clients won't propose using it. Imperative checks leak the tool's existence to users who can't use it.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 22-tool-with-ui-html-template
|
|
3
|
+
level: intermediate
|
|
4
|
+
description: "Tool with an inline HTML function template — `ui: { template: (ctx) => '<div>…</div>' }` — for a quick widget that doesn't need a separate `.tsx` file."
|
|
5
|
+
tags: [ui, ui-widgets, html-template, escapeHtml, TemplateContext]
|
|
6
|
+
features:
|
|
7
|
+
- 'Adding a `ui:` block with a function template `(ctx: TemplateContext<In, Out>) => string`'
|
|
8
|
+
- 'Annotating `ctx` explicitly to dodge the TS7006 inference gap on the union `ui.template` type'
|
|
9
|
+
- "Always escaping user-controlled output with `ctx.helpers.escapeHtml(...)` so the widget can't XSS itself"
|
|
10
|
+
- 'Reading from `ctx.output` and `ctx.helpers` — the typed runtime context the template renderer hands you'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Ui Html Template
|
|
14
|
+
|
|
15
|
+
Tool with an inline HTML function template — `ui: { template: (ctx) => '<div>…</div>' }` — for a quick widget that doesn't need a separate `.tsx` file.
|
|
16
|
+
|
|
17
|
+
For widgets that don't need React / state / interactivity, an inline function template is the simplest form. Read `ctx.output`, escape user-controlled fields, return an HTML string.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/show-weather-card.tool.ts
|
|
23
|
+
import { Tool, ToolContext, z, type TemplateContext } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
const inputSchema = { city: z.string() };
|
|
26
|
+
const outputSchema = {
|
|
27
|
+
city: z.string(),
|
|
28
|
+
temperatureF: z.number(),
|
|
29
|
+
conditions: z.string(),
|
|
30
|
+
};
|
|
31
|
+
type In = { city: string };
|
|
32
|
+
type Out = { city: string; temperatureF: number; conditions: string };
|
|
33
|
+
|
|
34
|
+
@Tool({
|
|
35
|
+
name: 'show_weather_card',
|
|
36
|
+
description: 'Show current weather as a card',
|
|
37
|
+
inputSchema,
|
|
38
|
+
outputSchema,
|
|
39
|
+
ui: {
|
|
40
|
+
widgetDescription: 'Current weather card',
|
|
41
|
+
template: (ctx: TemplateContext<In, Out>) => `
|
|
42
|
+
<div style="padding:16px;font-family:system-ui;border-radius:12px;background:#f5f7fa">
|
|
43
|
+
<h2 style="margin:0 0 8px">${ctx.helpers.escapeHtml(ctx.output.city)}</h2>
|
|
44
|
+
<p style="font-size:48px;margin:0">${ctx.output.temperatureF}°F</p>
|
|
45
|
+
<p style="margin:8px 0 0">${ctx.helpers.escapeHtml(ctx.output.conditions)}</p>
|
|
46
|
+
</div>
|
|
47
|
+
`,
|
|
48
|
+
},
|
|
49
|
+
})
|
|
50
|
+
export class ShowWeatherCardTool extends ToolContext {
|
|
51
|
+
async execute(input: In): Promise<Out> {
|
|
52
|
+
return { city: input.city, temperatureF: 72, conditions: 'Sunny' };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What This Demonstrates
|
|
58
|
+
|
|
59
|
+
- Adding a `ui:` block with a function template `(ctx: TemplateContext<In, Out>) => string`
|
|
60
|
+
- Annotating `ctx` explicitly to dodge the TS7006 inference gap on the union `ui.template` type
|
|
61
|
+
- Always escaping user-controlled output with `ctx.helpers.escapeHtml(...)` so the widget can't XSS itself
|
|
62
|
+
- Reading from `ctx.output` and `ctx.helpers` — the typed runtime context the template renderer hands you
|
|
63
|
+
|
|
64
|
+
## Why annotate `ctx` explicitly
|
|
65
|
+
|
|
66
|
+
`ui.template` is a union of multiple callable shapes (`TemplateBuilderFn | string | ((props: any) => any) | FileSource`). TypeScript can't pick a single contextual type for the arrow's parameter, so `template: (ctx) => …` fails under `strict` / `noImplicitAny` with TS7006:
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
Parameter 'ctx' implicitly has an 'any' type.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Two ways out:
|
|
73
|
+
|
|
74
|
+
- Annotate `ctx: TemplateContext<In, Out>` (this example) — fastest fix for a small inline widget.
|
|
75
|
+
- Move the widget to a `.tsx` file and use the FileSource form (`{ file: widgetPath }`) — recommended for anything non-trivial. See [`23-tool-with-ui-filesource-tsx`](./23-tool-with-ui-filesource-tsx.md).
|
|
76
|
+
|
|
77
|
+
## When function templates are the right choice
|
|
78
|
+
|
|
79
|
+
- Tiny widget — a card, a table row, a status badge
|
|
80
|
+
- No state / interactivity
|
|
81
|
+
- No external CSS / fonts / scripts beyond what `escapeHtml` can produce
|
|
82
|
+
|
|
83
|
+
Move to a `.tsx` FileSource widget the moment you reach for React, useState, event handlers, or anything beyond static markup.
|
|
84
|
+
|
|
85
|
+
## What `ctx.helpers` includes
|
|
86
|
+
|
|
87
|
+
| Helper | Purpose |
|
|
88
|
+
| ------------------------------ | ----------------------------------------------------------- |
|
|
89
|
+
| `escapeHtml(str)` | Escape HTML entities; returns `''` for null/undefined |
|
|
90
|
+
| `formatDate(date, format?)` | Locale-formatted date |
|
|
91
|
+
| `formatCurrency(amount, ccy?)` | ISO-4217 currency formatting |
|
|
92
|
+
| `uniqueId(prefix?)` | Deterministic unique ID for DOM elements |
|
|
93
|
+
| `jsonEmbed(data)` | Safely embed JSON in a `<script>` tag (escapes `</script>`) |
|