@frontmcp/skills 1.3.0 → 1.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +38 -29
  2. package/catalog/TEMPLATE.md +26 -0
  3. package/catalog/create-tool/SKILL.md +318 -0
  4. package/catalog/create-tool/examples/01-basic-class-tool.md +112 -0
  5. package/catalog/create-tool/examples/02-basic-function-tool.md +80 -0
  6. package/catalog/create-tool/examples/03-tool-with-zod-shape-output.md +78 -0
  7. package/catalog/create-tool/examples/04-tool-with-zod-schema-output.md +97 -0
  8. package/catalog/create-tool/examples/05-tool-with-primitive-output.md +93 -0
  9. package/catalog/create-tool/examples/06-tool-with-media-output.md +109 -0
  10. package/catalog/create-tool/examples/08-tool-with-provider-injection.md +110 -0
  11. package/catalog/create-tool/examples/09-tool-with-multiple-providers.md +107 -0
  12. package/catalog/create-tool/examples/11-tool-with-fetch.md +94 -0
  13. package/catalog/create-tool/examples/12-tool-with-fetch-and-retries.md +115 -0
  14. package/catalog/create-tool/examples/13-tool-with-single-auth-provider.md +85 -0
  15. package/catalog/create-tool/examples/14-tool-with-multiple-auth-providers.md +105 -0
  16. package/catalog/create-tool/examples/15-tool-with-credential-vault.md +115 -0
  17. package/catalog/create-tool/examples/16-tool-with-rate-limit.md +71 -0
  18. package/catalog/create-tool/examples/17-tool-with-concurrency-and-timeout.md +101 -0
  19. package/catalog/create-tool/examples/18-tool-with-progress-and-notify.md +96 -0
  20. package/catalog/create-tool/examples/19-tool-with-elicitation.md +102 -0
  21. package/catalog/create-tool/examples/20-tool-with-annotations.md +125 -0
  22. package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +107 -0
  23. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +93 -0
  24. package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +112 -0
  25. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +127 -0
  26. package/catalog/create-tool/examples/25-tool-handing-off-to-job.md +143 -0
  27. package/catalog/create-tool/examples/26-tool-with-resource-link-output.md +94 -0
  28. package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +90 -0
  29. package/catalog/create-tool/references/annotations.md +96 -0
  30. package/catalog/create-tool/references/auth-providers.md +167 -0
  31. package/catalog/create-tool/references/availability.md +106 -0
  32. package/catalog/create-tool/references/decorator-options.md +95 -0
  33. package/catalog/create-tool/references/derived-types.md +102 -0
  34. package/catalog/create-tool/references/elicitation.md +128 -0
  35. package/catalog/create-tool/references/error-handling.md +128 -0
  36. package/catalog/create-tool/references/execution-context.md +158 -0
  37. package/catalog/create-tool/references/file-layout.md +96 -0
  38. package/catalog/create-tool/references/function-style-builder.md +118 -0
  39. package/catalog/create-tool/references/input-schema.md +141 -0
  40. package/catalog/create-tool/references/output-schema.md +175 -0
  41. package/catalog/create-tool/references/quick-start.md +124 -0
  42. package/catalog/create-tool/references/registration.md +132 -0
  43. package/catalog/create-tool/references/remote-and-esm.md +68 -0
  44. package/catalog/create-tool/references/testing.md +59 -0
  45. package/catalog/create-tool/references/throttling.md +109 -0
  46. package/catalog/create-tool/references/ui-widgets.md +198 -0
  47. package/catalog/create-tool/rules/always-define-output-schema.md +77 -0
  48. package/catalog/create-tool/rules/derive-execute-types.md +57 -0
  49. package/catalog/create-tool/rules/input-schema-is-raw-shape.md +76 -0
  50. package/catalog/create-tool/rules/no-toolcontext-generics.md +50 -0
  51. package/catalog/create-tool/rules/no-try-catch-around-execute.md +79 -0
  52. package/catalog/create-tool/rules/register-in-app.md +76 -0
  53. package/catalog/create-tool/rules/snake-case-tool-names.md +45 -0
  54. package/catalog/create-tool/rules/use-this-fail-for-business-errors.md +75 -0
  55. package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +76 -0
  56. package/catalog/create-tool/rules/widget-resource-mode-host-detect.md +61 -0
  57. package/catalog/frontmcp-auth-ui/SKILL.md +146 -0
  58. package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/login-slot.md +97 -0
  59. package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/multi-step-auth-extra.md +133 -0
  60. package/catalog/frontmcp-auth-ui/references/custom-auth-ui.md +162 -0
  61. package/catalog/frontmcp-authorities/SKILL.md +55 -18
  62. package/catalog/frontmcp-authorities/references/authority-profiles.md +25 -1
  63. package/catalog/frontmcp-authorities/references/custom-evaluators.md +1 -1
  64. package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +9 -0
  65. package/catalog/frontmcp-channels/SKILL.md +7 -1
  66. package/catalog/frontmcp-config/SKILL.md +9 -2
  67. package/catalog/frontmcp-config/examples/configure-auth/local-credential-vault.md +94 -0
  68. package/catalog/frontmcp-config/examples/configure-auth/local-secure-store.md +138 -0
  69. package/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md +45 -23
  70. package/catalog/frontmcp-config/examples/configure-auth-modes/local-behind-tunnel.md +73 -0
  71. package/catalog/frontmcp-config/examples/configure-auth-modes/local-consent-enforcement.md +87 -0
  72. package/catalog/frontmcp-config/examples/configure-auth-modes/local-dcr-control.md +67 -0
  73. package/catalog/frontmcp-config/examples/configure-auth-modes/local-minimal.md +62 -0
  74. package/catalog/frontmcp-config/examples/configure-auth-modes/local-multi-provider-orchestration.md +93 -0
  75. package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +18 -20
  76. package/catalog/frontmcp-config/examples/configure-auth-modes/local-single-operator.md +66 -0
  77. package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +37 -23
  78. package/catalog/frontmcp-config/examples/configure-http/custom-http-routes.md +98 -0
  79. package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +17 -9
  80. package/catalog/frontmcp-config/references/configure-auth-modes.md +86 -23
  81. package/catalog/frontmcp-config/references/configure-auth.md +296 -50
  82. package/catalog/frontmcp-config/references/configure-http.md +149 -15
  83. package/catalog/frontmcp-deployment/SKILL.md +15 -13
  84. package/catalog/frontmcp-deployment/references/deploy-manifest-yaml.md +308 -0
  85. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +174 -0
  86. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +38 -2
  87. package/catalog/frontmcp-development/SKILL.md +30 -44
  88. package/catalog/frontmcp-development/references/decorators-guide.md +15 -15
  89. package/catalog/frontmcp-extensibility/SKILL.md +1 -1
  90. package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +8 -6
  91. package/catalog/frontmcp-extensibility/references/skill-audit-log.md +7 -2
  92. package/catalog/frontmcp-guides/SKILL.md +1 -1
  93. package/catalog/frontmcp-observability/SKILL.md +1 -1
  94. package/catalog/frontmcp-production-readiness/SKILL.md +1 -1
  95. package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -2
  96. package/catalog/frontmcp-setup/SKILL.md +1 -1
  97. package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
  98. package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
  99. package/catalog/frontmcp-testing/SKILL.md +9 -1
  100. package/catalog/frontmcp-testing/references/test-auth.md +24 -0
  101. package/catalog/skills-manifest.json +653 -149
  102. package/package.json +1 -1
  103. package/src/manifest.d.ts +72 -1
  104. package/src/manifest.js +4 -1
  105. package/src/manifest.js.map +1 -1
  106. package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +0 -80
  107. package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -132
  108. package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -110
  109. package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +0 -92
  110. package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +0 -59
  111. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +0 -101
  112. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +0 -62
  113. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +0 -101
  114. package/catalog/frontmcp-development/references/create-tool-annotations.md +0 -48
  115. package/catalog/frontmcp-development/references/create-tool-output-schema-types.md +0 -71
  116. package/catalog/frontmcp-development/references/create-tool.md +0 -806
@@ -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>`) |