@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,112 @@
1
+ ---
2
+ name: 23-tool-with-ui-filesource-tsx
3
+ level: advanced
4
+ description: 'Tool with a `.tsx` widget in a separate file via the `FileSource` form — the recommended pattern for any React widget. Path anchored with `import.meta.url` so it survives any cwd.'
5
+ tags: [ui, ui-widgets, FileSource, tsx, import.meta.url, host-detect]
6
+ features:
7
+ - 'Pointing `template` at a sibling `.tsx` file via the `FileSource` form `{ file: ... }`'
8
+ - "Anchoring the path to the tool source with `fileURLToPath(new URL('./...widget.tsx', import.meta.url))` so `process.cwd()` doesn't matter"
9
+ - "Leaving `resourceMode` unset — the framework host-detects (`'inline'` for Claude, `'cdn'` for others)"
10
+ - "Naming the widget `*.widget.tsx` so the scaffolded `tsconfig.json`'s `exclude` keeps it out of the server typecheck"
11
+ ---
12
+
13
+ # Tool With Ui Filesource Tsx
14
+
15
+ Tool with a `.tsx` widget in a separate file via the `FileSource` form — the recommended pattern for any React widget. Path anchored with `import.meta.url` so it survives any cwd.
16
+
17
+ For any React widget, FileSource is the right pattern. The widget lives in its own `.widget.tsx` file with its own React imports; the tool decorator just points at it.
18
+
19
+ > **Prerequisite:** `@frontmcp/ui` installed at the same version as `@frontmcp/sdk`. Without it, server-side bundling fails — the framework injects an auto-generated React mount that imports `McpBridgeProvider` from `@frontmcp/ui/react`.
20
+
21
+ ## Code
22
+
23
+ ```typescript
24
+ // src/apps/main/tools/sales-chart/sales-chart.schema.ts
25
+ import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';
26
+
27
+ export const inputSchema = { year: z.number().int().min(2000).max(2100) };
28
+ export const outputSchema = {
29
+ year: z.number(),
30
+ monthly: z.array(z.object({ month: z.string(), revenueUsd: z.number() })),
31
+ };
32
+ export type SalesChartInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
33
+ export type SalesChartOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
34
+ ```
35
+
36
+ ```typescript
37
+ // src/apps/main/tools/sales-chart/sales-chart.tool.ts
38
+ import { fileURLToPath } from 'node:url';
39
+
40
+ import { Tool, ToolContext } from '@frontmcp/sdk';
41
+
42
+ import { inputSchema, outputSchema, type SalesChartInput, type SalesChartOutput } from './sales-chart.schema';
43
+
44
+ // Anchor the widget path to THIS source file — bare relative paths resolve
45
+ // against process.cwd() (issue #444), which fails in any non-trivial layout.
46
+ const widgetPath = fileURLToPath(new URL('./sales-chart.widget.tsx', import.meta.url));
47
+
48
+ @Tool({
49
+ name: 'sales_chart',
50
+ description: 'Render a yearly sales bar chart',
51
+ inputSchema,
52
+ outputSchema,
53
+ ui: {
54
+ widgetDescription: 'Monthly revenue chart',
55
+ template: { file: widgetPath },
56
+ // resourceMode is intentionally UNSET — framework host-detects: 'inline' for Claude
57
+ // (React bundled in, widget renders under Claude's CSP), 'cdn' for OpenAI / ChatGPT /
58
+ // Cursor / MCP Inspector (smaller payload from esm.sh). Issue #456.
59
+ hydrate: false, // SSR-only — dodges React error #418 in iframe sandboxes
60
+ },
61
+ })
62
+ export class SalesChartTool extends ToolContext {
63
+ async execute(input: SalesChartInput): Promise<SalesChartOutput> {
64
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
65
+ return { year: input.year, monthly: months.map((month, i) => ({ month, revenueUsd: 10_000 + i * 1_250 })) };
66
+ }
67
+ }
68
+ ```
69
+
70
+ ```tsx
71
+ // src/apps/main/tools/sales-chart/sales-chart.widget.tsx
72
+ import { useEffect, useRef } from 'react';
73
+
74
+ type Props = { output: { year: number; monthly: Array<{ month: string; revenueUsd: number }> } };
75
+
76
+ export default function SalesChartWidget({ output }: Props) {
77
+ const canvasRef = useRef<HTMLCanvasElement>(null);
78
+
79
+ useEffect(() => {
80
+ if (!canvasRef.current) return;
81
+ const ctx = canvasRef.current.getContext('2d');
82
+ if (!ctx) return;
83
+ // …simple bar chart, no external deps so it runs everywhere
84
+ ctx.fillStyle = '#3b82f6';
85
+ output.monthly.forEach((m, i) => {
86
+ const barHeight = (m.revenueUsd / 25_000) * 280;
87
+ ctx.fillRect(i * 28 + 8, 300 - barHeight, 20, barHeight);
88
+ });
89
+ }, [output]);
90
+
91
+ return (
92
+ <div style={{ padding: 16, fontFamily: 'system-ui' }}>
93
+ <h2 style={{ margin: 0 }}>Sales — {output.year}</h2>
94
+ <canvas ref={canvasRef} width={360} height={320} style={{ marginTop: 12 }} />
95
+ </div>
96
+ );
97
+ }
98
+ ```
99
+
100
+ ## What This Demonstrates
101
+
102
+ - Pointing `template` at a sibling `.tsx` file via the `FileSource` form `{ file: ... }`
103
+ - Anchoring the path to the tool source with `fileURLToPath(new URL('./...widget.tsx', import.meta.url))` so `process.cwd()` doesn't matter
104
+ - Leaving `resourceMode` unset — the framework host-detects (`'inline'` for Claude, `'cdn'` for others)
105
+ - Naming the widget `*.widget.tsx` so the scaffolded `tsconfig.json`'s `exclude` keeps it out of the server typecheck
106
+
107
+ ## Why these defaults matter
108
+
109
+ - **`import.meta.url` anchoring** — relative paths in `FileSource` resolve against `process.cwd()`, not the tool file (#444). Running the server from a different directory breaks the widget at tool-call time. Anchoring fixes it once.
110
+ - **`resourceMode` unset** — leave it. The framework picks `'inline'` for Claude (React bundled into the widget — actually renders) and `'cdn'` for everyone else (smaller payload via esm.sh). Setting it explicitly only locks in one behavior across all clients.
111
+ - **`hydrate: false`** — default. React SSR output is static HTML; the bridge IIFE handles any interactivity. Enabling hydration creates React error #418 in Claude's iframe sandbox where the client-side render diverges from the SSR render.
112
+ - **`*.widget.tsx` naming** — the scaffolded `tsconfig.json` excludes `**/*.widget.tsx` from the server typecheck (#445). The widget compiles via uipack/esbuild at render time with its own React-aware config.
@@ -0,0 +1,127 @@
1
+ ---
2
+ name: 24-tool-with-ui-csp-and-bridge
3
+ level: advanced
4
+ description: 'Interactive tool widget that fetches from an allow-listed CSP origin and invokes another tool via `window.FrontMcpBridge.callTool` — the full pattern for live-data widgets that need cross-tool composition.'
5
+ tags: [ui, csp, widgetAccessible, FrontMcpBridge, interactive-widget]
6
+ features:
7
+ - "Restricting the widget's outbound `fetch` via `ui.csp.connectDomains` (emitted on the resource per #455)"
8
+ - 'Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs'
9
+ - "Embedding initial data into the widget's inline `<script>` safely via `ctx.helpers.jsonEmbed(...)` (escapes `</script>`)"
10
+ - 'Surfacing in-flight status via `invocationStatus.invoking` / `invoked` so the host UI shows feedback'
11
+ ---
12
+
13
+ # Tool With Ui Csp And Bridge
14
+
15
+ Interactive tool widget that fetches from an allow-listed CSP origin and invokes another tool via `window.FrontMcpBridge.callTool` — the full pattern for live-data widgets that need cross-tool composition.
16
+
17
+ The advanced widget pattern. The tool's response renders a stock-quote card with a "Refresh" button that calls another tool through the bridge. CSP locks down outbound fetches; the bridge routes cross-tool calls to the right host adapter (OpenAI / Claude / direct) automatically.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/get-quote/get-quote.tool.ts
23
+ import { PublicMcpError, Tool, ToolContext, z } from '@frontmcp/sdk';
24
+
25
+ const inputSchema = { symbol: z.string().regex(/^[A-Z]{1,5}$/) };
26
+ const outputSchema = { symbol: z.string(), priceUsd: z.number(), asOf: z.string() };
27
+
28
+ @Tool({
29
+ name: 'get_quote',
30
+ description: 'Get the latest stock price for a ticker symbol',
31
+ inputSchema,
32
+ outputSchema,
33
+ })
34
+ export class GetQuoteTool extends ToolContext {
35
+ async execute(input: { symbol: string }) {
36
+ const res = await this.fetch(`https://api.market.example/quote/${input.symbol}`);
37
+ if (!res.ok) {
38
+ this.fail(new PublicMcpError(`Quote upstream returned ${res.status} ${res.statusText}`));
39
+ }
40
+ const body = (await res.json()) as { price: number; ts: string };
41
+ return { symbol: input.symbol, priceUsd: body.price, asOf: body.ts };
42
+ }
43
+ }
44
+ ```
45
+
46
+ ```typescript
47
+ // src/apps/main/tools/show-quote/show-quote.tool.ts
48
+ import { PublicMcpError, Tool, ToolContext, z, type TemplateContext } from '@frontmcp/sdk';
49
+
50
+ const inputSchema = { symbol: z.string().regex(/^[A-Z]{1,5}$/) };
51
+ const outputSchema = { symbol: z.string(), priceUsd: z.number(), asOf: z.string() };
52
+ type In = { symbol: string };
53
+ type Out = { symbol: string; priceUsd: number; asOf: string };
54
+
55
+ @Tool({
56
+ name: 'show_quote',
57
+ description: 'Render a live quote widget for a ticker symbol',
58
+ inputSchema,
59
+ outputSchema,
60
+ ui: {
61
+ widgetDescription: 'Live stock quote with refresh',
62
+ widgetAccessible: true, // required for window.FrontMcpBridge.callTool
63
+ invocationStatus: { invoking: 'Fetching quote…', invoked: 'Quote loaded' },
64
+ csp: {
65
+ // CSP applies to the widget iframe — only allow fetches to our own market-data API.
66
+ // Framework emits this on the resource's _meta.ui.csp so Claude honors it (#455).
67
+ connectDomains: ['https://api.market.example'],
68
+ },
69
+ template: (ctx: TemplateContext<In, Out>) => {
70
+ const initial = ctx.helpers.jsonEmbed(ctx.output);
71
+ return `
72
+ <div style="padding:16px;font-family:system-ui">
73
+ <h2 style="margin:0">${ctx.helpers.escapeHtml(ctx.output.symbol)}</h2>
74
+ <p id="price" style="font-size:36px;margin:8px 0">$${ctx.output.priceUsd.toFixed(2)}</p>
75
+ <p id="asof" style="color:#666;margin:0">As of ${ctx.helpers.escapeHtml(ctx.output.asOf)}</p>
76
+ <button id="refresh" style="margin-top:12px;padding:8px 16px">Refresh</button>
77
+ <script>
78
+ (function () {
79
+ var current = ${initial};
80
+ var btn = document.getElementById('refresh');
81
+ btn.addEventListener('click', async function () {
82
+ btn.disabled = true;
83
+ btn.textContent = 'Refreshing…';
84
+ try {
85
+ // FrontMcpBridge routes to OpenAI SDK / Claude postMessage / FrontMCP direct
86
+ // automatically. NEVER call window.openai.* directly — it breaks cross-host.
87
+ var next = await window.FrontMcpBridge.callTool('get_quote', { symbol: current.symbol });
88
+ current = next;
89
+ document.getElementById('price').textContent = '$' + next.priceUsd.toFixed(2);
90
+ document.getElementById('asof').textContent = 'As of ' + next.asOf;
91
+ } finally {
92
+ btn.disabled = false;
93
+ btn.textContent = 'Refresh';
94
+ }
95
+ });
96
+ })();
97
+ </script>
98
+ </div>
99
+ `;
100
+ },
101
+ },
102
+ })
103
+ export class ShowQuoteTool extends ToolContext {
104
+ async execute(input: In): Promise<Out> {
105
+ const res = await this.fetch(`https://api.market.example/quote/${input.symbol}`);
106
+ if (!res.ok) {
107
+ this.fail(new PublicMcpError(`Quote upstream returned ${res.status} ${res.statusText}`));
108
+ }
109
+ const body = (await res.json()) as { price: number; ts: string };
110
+ return { symbol: input.symbol, priceUsd: body.price, asOf: body.ts };
111
+ }
112
+ }
113
+ ```
114
+
115
+ ## What This Demonstrates
116
+
117
+ - Restricting the widget's outbound `fetch` via `ui.csp.connectDomains` (emitted on the resource per #455)
118
+ - Opting the widget into cross-tool calls with `widgetAccessible: true` and using `window.FrontMcpBridge.callTool(name, args)` instead of host-specific APIs
119
+ - Embedding initial data into the widget's inline `<script>` safely via `ctx.helpers.jsonEmbed(...)` (escapes `</script>`)
120
+ - Surfacing in-flight status via `invocationStatus.invoking` / `invoked` so the host UI shows feedback
121
+
122
+ ## Why these choices
123
+
124
+ - **`widgetAccessible: true`** — required for `window.FrontMcpBridge.callTool`. Without it, the bridge is read-only (the widget can read `getToolInput` / `getToolOutput` but can't invoke tools).
125
+ - **`csp.connectDomains`** — limits what the widget can `fetch` to. Without a CSP, the host's default applies (which may block everything in Claude). With `connectDomains: ['https://api.market.example']`, only that origin is reachable.
126
+ - **`window.FrontMcpBridge.callTool` not `window.openai.callTool`** — the bridge handles host detection. `window.openai.*` works on OpenAI Apps SDK but breaks everywhere else.
127
+ - **`jsonEmbed` not `JSON.stringify`** — `JSON.stringify` doesn't escape `</script>` and can break out of the inline script tag. `jsonEmbed` does.
@@ -0,0 +1,143 @@
1
+ ---
2
+ name: 25-tool-handing-off-to-job
3
+ level: advanced
4
+ description: 'Thin tool that validates input and enqueues a `@Job` to do the heavy lifting — the right pattern for any operation that takes more than a few seconds.'
5
+ tags: [composition, jobs, job-handoff, hideFromDiscovery]
6
+ features:
7
+ - 'Splitting a long-running operation into a thin tool (validates, enqueues, returns a tracking handle) plus a `@Job` (does the work)'
8
+ - Returning the job ID + status URL from the tool so the client can poll or stream updates
9
+ - "Using `availableWhen: { surface: ['mcp', 'agent'] }` on the tool while leaving the heavy `@Job` invisible to direct invocation"
10
+ - 'Why this beats running the heavy work inside `execute()` (avoids tool-call timeout limits, lets the job retry independently)'
11
+ ---
12
+
13
+ # Tool Handing Off To Job
14
+
15
+ Thin tool that validates input and enqueues a `@Job` to do the heavy lifting — the right pattern for any operation that takes more than a few seconds.
16
+
17
+ Tools are for synchronous-ish interactions (≤30s end-to-end). Anything longer should run in a `@Job`, with a thin tool to kick it off.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/jobs/export-data.job.ts
23
+ import { Job, JobContext } from '@frontmcp/sdk';
24
+
25
+ @Job({
26
+ name: 'export_data',
27
+ description: 'Export a dataset to CSV',
28
+ retry: { maxAttempts: 3, backoff: 'exponential' },
29
+ })
30
+ export class ExportDataJob extends JobContext {
31
+ async run(args: { datasetId: string; format: 'csv' | 'json' }) {
32
+ await this.progress(0, 100, 'Loading dataset…');
33
+ const rows = await this.loadDataset(args.datasetId);
34
+
35
+ await this.progress(50, 100, 'Serializing…');
36
+ const blob = await this.serialize(rows, args.format);
37
+
38
+ await this.progress(95, 100, 'Uploading…');
39
+ const downloadUrl = await this.upload(blob);
40
+
41
+ await this.progress(100, 100, 'Done');
42
+ return { downloadUrl, rowCount: rows.length };
43
+ }
44
+
45
+ private async loadDataset(_id: string) {
46
+ return [{ a: 1 }, { a: 2 }];
47
+ }
48
+ private async serialize(_rows: unknown[], _fmt: string) {
49
+ return Buffer.alloc(0);
50
+ }
51
+ private async upload(_blob: Buffer) {
52
+ return 'https://exports.example/x.csv';
53
+ }
54
+ }
55
+ ```
56
+
57
+ ```typescript
58
+ // src/apps/main/tools/export-data.tool.ts
59
+ import { ResourceNotFoundError, Tool, ToolContext, z } from '@frontmcp/sdk';
60
+
61
+ import { DATASETS, JOBS } from '../tokens';
62
+
63
+ const inputSchema = {
64
+ datasetId: z.string().uuid(),
65
+ format: z.enum(['csv', 'json']).default('csv'),
66
+ };
67
+ const outputSchema = {
68
+ jobId: z.string(),
69
+ statusUrl: z.string().url(),
70
+ estimatedSeconds: z.number().int(),
71
+ };
72
+
73
+ @Tool({
74
+ name: 'export_data',
75
+ description: 'Start a dataset export — returns a job handle the client can poll',
76
+ inputSchema,
77
+ outputSchema,
78
+ availableWhen: { surface: ['mcp', 'agent'] },
79
+ annotations: { idempotentHint: false, openWorldHint: false },
80
+ })
81
+ export class ExportDataTool extends ToolContext {
82
+ async execute(input: { datasetId: string; format: 'csv' | 'json' }) {
83
+ // 1. AUTHORIZE BEFORE ENQUEUEING. The job inherits this caller's auth scope at
84
+ // enqueue time, so an unchecked tool here would let any caller export any
85
+ // dataset they happen to know the ID of. Concretely: look up the dataset
86
+ // scoped to the caller's tenant / user identity, fail fast if missing.
87
+ const datasets = this.get(DATASETS);
88
+ const userId = this.auth.user.sub;
89
+ const tenantId = this.auth.claims['tenantId'] as string | undefined;
90
+ const dataset = await datasets.findForUser(input.datasetId, { userId, tenantId });
91
+ if (!dataset) {
92
+ this.fail(new ResourceNotFoundError(`dataset:${input.datasetId}`));
93
+ }
94
+
95
+ // 2. Enqueue the job — runs in the caller's auth scope (the job's JobContext
96
+ // re-checks tenant / user access inside `run()` as a defense-in-depth).
97
+ const jobs = this.get(JOBS);
98
+ const job = await jobs.enqueue('export_data', input, { userId, tenantId });
99
+
100
+ // 3. Return a handle the client can poll / stream.
101
+ return {
102
+ jobId: job.id,
103
+ statusUrl: `/jobs/${job.id}`,
104
+ estimatedSeconds: 30,
105
+ };
106
+ }
107
+ }
108
+ ```
109
+
110
+ ```typescript
111
+ // src/apps/main/index.ts
112
+ import { App } from '@frontmcp/sdk';
113
+
114
+ import { ExportDataJob } from './jobs/export-data.job';
115
+ import { ExportDataTool } from './tools/export-data.tool';
116
+
117
+ @App({
118
+ name: 'main',
119
+ tools: [ExportDataTool],
120
+ jobs: [ExportDataJob],
121
+ })
122
+ export class MainApp {}
123
+ ```
124
+
125
+ ## What This Demonstrates
126
+
127
+ - Splitting a long-running operation into a thin tool (validates, enqueues, returns a tracking handle) plus a `@Job` (does the work)
128
+ - Returning the job ID + status URL from the tool so the client can poll or stream updates
129
+ - Using `availableWhen: { surface: ['mcp', 'agent'] }` on the tool while leaving the heavy `@Job` invisible to direct invocation
130
+ - Why this beats running the heavy work inside `execute()` (avoids tool-call timeout limits, lets the job retry independently)
131
+
132
+ ## Why hand off
133
+
134
+ | Inside `execute()` | Inside a `@Job` |
135
+ | ----------------------------------------------- | --------------------------------------------------------------------------------------------- |
136
+ | Capped by the tool's `timeout` (typically ≤30s) | Designed for minutes / hours |
137
+ | One attempt — fails on transient errors | Retry config — `maxAttempts`, `backoff` |
138
+ | Progress emits to the current MCP session only | Job runs independently of the session — can survive disconnects (with a persistent job store) |
139
+ | Synchronous from the client's POV | Asynchronous — client polls `statusUrl` or subscribes to a channel |
140
+
141
+ If the work can take >10 seconds OR needs to survive a session drop OR needs retry-on-failure, it belongs in a job.
142
+
143
+ See `create-job` for the full job surface — retry, progress, batching, permission scopes.
@@ -0,0 +1,94 @@
1
+ ---
2
+ name: 26-tool-with-resource-link-output
3
+ level: advanced
4
+ description: "Tool returning `outputSchema: 'resource_link'` — the URI is sent to the client; the client fetches the body via `resources/read`. The right pattern for large or cacheable payloads."
5
+ tags: [output-schema, resource_link, large-payload, caching]
6
+ features:
7
+ - "Returning `outputSchema: 'resource_link'` from a tool — `{ type: 'resource_link', uri }`, body fetched separately"
8
+ - "Pairing the tool with a matching `@Resource({ uri: 'export://{exportId}.csv' })` URI template that resolves to the actual body"
9
+ - "When `'resource_link'` beats `'image'` / `'audio'` / a raw byte response (large payloads, cacheable URIs, deferred fetch)"
10
+ - 'Cross-linking to the `create-resource` skill for the URI-template resource on the other end'
11
+ ---
12
+
13
+ # Tool With Resource Link Output
14
+
15
+ Tool returning `outputSchema: 'resource_link'` — the URI is sent to the client; the client fetches the body via `resources/read`. The right pattern for large or cacheable payloads.
16
+
17
+ When the tool's output is large (>1MB) or cacheable, return a `'resource_link'` instead of inlining the bytes. The tool returns just `{ uri }`; the client decides when to fetch the body via `resources/read`.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/resources/export.resource.ts
23
+ // (Lives in this app — see the create-resource skill for the full URI-template surface)
24
+ import { Resource, ResourceContext } from '@frontmcp/sdk';
25
+
26
+ import { EXPORTS } from '../tokens';
27
+
28
+ @Resource({
29
+ uri: 'export://{exportId}.csv',
30
+ description: 'Generated export CSV',
31
+ })
32
+ export class ExportCsvResource extends ResourceContext<{ exportId: string }> {
33
+ async read(params: { exportId: string }) {
34
+ const exports = this.get(EXPORTS);
35
+ const csv = await exports.loadCsv(params.exportId); // returns Buffer or stream
36
+ return {
37
+ contents: [{ uri: `export://${params.exportId}.csv`, mimeType: 'text/csv', blob: csv.toString('base64') }],
38
+ };
39
+ }
40
+ }
41
+ ```
42
+
43
+ ```typescript
44
+ // src/apps/main/tools/start-export.tool.ts
45
+ import { Tool, ToolContext, z } from '@frontmcp/sdk';
46
+
47
+ import { EXPORTS } from '../tokens';
48
+
49
+ const inputSchema = {
50
+ datasetId: z.string().uuid(),
51
+ };
52
+ const outputSchema = 'resource_link';
53
+
54
+ @Tool({
55
+ name: 'start_export',
56
+ description: 'Generate an export and return a resource link the client can read',
57
+ inputSchema,
58
+ outputSchema,
59
+ annotations: { idempotentHint: false },
60
+ })
61
+ export class StartExportTool extends ToolContext {
62
+ async execute(input: { datasetId: string }) {
63
+ const exports = this.get(EXPORTS);
64
+ const exportId = await exports.create(input.datasetId);
65
+
66
+ // Return the resource link — include the `type: 'resource_link'` discriminator
67
+ // (without it the content block is dropped). The client fetches the body via resources/read.
68
+ return { type: 'resource_link' as const, uri: `export://${exportId}.csv` };
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## What This Demonstrates
74
+
75
+ - Returning `outputSchema: 'resource_link'` from a tool — `{ type: 'resource_link', uri }`, body fetched separately
76
+ - Pairing the tool with a matching `@Resource({ uri: 'export://{exportId}.csv' })` URI template that resolves to the actual body
77
+ - When `'resource_link'` beats `'image'` / `'audio'` / a raw byte response (large payloads, cacheable URIs, deferred fetch)
78
+ - Cross-linking to the `create-resource` skill for the URI-template resource on the other end
79
+
80
+ ## `'resource_link'` vs `'resource'` vs `'image'` / `'audio'`
81
+
82
+ | Output | When |
83
+ | --------------------- | --------------------------------------------------------------------------------------------------------------------------- |
84
+ | `'resource_link'` | URI only. Best for large payloads (>1MB), cacheable content, deferred-fetch UX. Tool stays cheap; client fetches on demand. |
85
+ | `'resource'` | URI + inline content in one response. Best when the client always needs the body immediately (small-to-medium size). |
86
+ | `'image'` / `'audio'` | Inlined base64. Simplest for small media (≤1MB). No URI involved. |
87
+
88
+ ## Why split tool + resource
89
+
90
+ - **Tool runs fast** — just generates the URI; doesn't materialize the whole payload in the response.
91
+ - **Client caches by URI** — repeated requests for `export://abc.csv` hit the client's cache; the tool only runs again if the URI changes.
92
+ - **Resource lifecycle is independent** — you can expire resources, regenerate them on demand, version them by URI suffix.
93
+
94
+ See the `create-resource` skill for URI templates, parameter validation, multi-content reads, and binary vs text resources.
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: 27-tool-with-examples-metadata
3
+ level: basic
4
+ description: 'Tool with the `examples: [...]` field on `@Tool({...})` — concrete input (and optional expected output) examples consumed by the CodeCall `codecall:describe` tool to give agents accurate usage examples.'
5
+ tags: [examples-metadata, codecall, describe]
6
+ features:
7
+ - 'Adding `examples: [{ description, input, output? }]` to `@Tool({...})` so `codecall:describe` surfaces canned invocations'
8
+ - 'Writing realistic example inputs so the generated describe output is concrete, not abstract'
9
+ - 'Including `output?` for examples where showing the expected result helps an agent understand the tool'
10
+ - 'Why `examples` are advisory metadata — not emitted in `tools/list`, only consumed by `codecall:describe`'
11
+ ---
12
+
13
+ # Tool With Examples Metadata
14
+
15
+ Tool with the `examples: [...]` field on `@Tool({...})` — concrete input (and optional expected output) examples consumed by the CodeCall `codecall:describe` tool to give agents accurate usage examples.
16
+
17
+ The `examples` field is purely advisory — the CodeCall `describe` tool uses it as the highest-priority source of usage examples (user-provided examples take precedence over auto-generated ones, up to 5). It is **not** emitted in the `tools/list` MCP response. Use it for any tool that benefits from concrete usage hints when CodeCall is enabled.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/convert-currency.tool.ts
23
+ import { Tool, ToolContext, ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';
24
+
25
+ const inputSchema = {
26
+ amount: z.number().positive(),
27
+ from: z.string().regex(/^[A-Z]{3}$/, 'ISO 4217 code, e.g. USD'),
28
+ to: z.string().regex(/^[A-Z]{3}$/),
29
+ };
30
+ const outputSchema = { converted: z.number(), rate: z.number(), asOf: z.string() };
31
+
32
+ @Tool({
33
+ name: 'convert_currency',
34
+ description: 'Convert an amount from one currency to another',
35
+ inputSchema,
36
+ outputSchema,
37
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
38
+ examples: [
39
+ {
40
+ description: 'Convert 100 USD to EUR',
41
+ input: { amount: 100, from: 'USD', to: 'EUR' },
42
+ output: { converted: 91.4, rate: 0.914, asOf: '2026-05-29T12:00:00Z' },
43
+ },
44
+ {
45
+ description: 'Convert 1,000,000 GBP to JPY',
46
+ input: { amount: 1_000_000, from: 'GBP', to: 'JPY' },
47
+ },
48
+ {
49
+ description: 'Convert 50 EUR to USD',
50
+ input: { amount: 50, from: 'EUR', to: 'USD' },
51
+ },
52
+ ],
53
+ })
54
+ export class ConvertCurrencyTool extends ToolContext {
55
+ async execute(
56
+ input: ToolInputOf<{ inputSchema: typeof inputSchema }>,
57
+ ): Promise<ToolOutputOf<{ outputSchema: typeof outputSchema }>> {
58
+ const rate = await this.fetchRate(input.from, input.to);
59
+ return { converted: +(input.amount * rate).toFixed(2), rate, asOf: new Date().toISOString() };
60
+ }
61
+
62
+ private async fetchRate(_from: string, _to: string): Promise<number> {
63
+ return 0.914;
64
+ }
65
+ }
66
+ ```
67
+
68
+ ## What This Demonstrates
69
+
70
+ - Adding `examples: [{ description, input, output? }]` to `@Tool({...})` so `codecall:describe` surfaces canned invocations
71
+ - Writing realistic example inputs so the generated describe output is concrete, not abstract
72
+ - Including `output?` for examples where showing the expected result helps an agent understand the tool
73
+ - Why `examples` are advisory metadata — not emitted in `tools/list`, only consumed by `codecall:describe`
74
+
75
+ ## Where examples show up
76
+
77
+ - **`codecall:describe`** — the CodeCall plugin's `describe` tool uses these as its top-priority source of usage examples (user-provided examples win over auto-generated ones, capped at 5). This is the one place `examples` is actually read.
78
+ - **Not in `tools/list`** — the `tools/list` MCP response does not include `examples`; clients never see them there.
79
+
80
+ ## When to include `output?`
81
+
82
+ - ✅ When showing the expected output makes the tool's purpose clearer at a glance.
83
+ - ✅ When an agent benefits from seeing a representative result shape in the `codecall:describe` output before composing a call.
84
+ - ❌ For tools where the output is highly variable / live data (e.g. `web_search` results). Just show the input.
85
+
86
+ ## Don't
87
+
88
+ - Don't put sensitive example data — `examples` are surfaced verbatim to agents via `codecall:describe`. Use synthetic IDs (`u_1`), test addresses (`@example.com`), demo amounts.
89
+ - Don't pad with low-value examples just to hit a count. 2–4 well-chosen examples beats 20 trivial ones.
90
+ - Don't rely on `examples` for documentation — they're advisory hints, not formal docs. Put substantive guidance in `description`.
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: annotations
3
+ description: readOnlyHint, destructiveHint, idempotentHint, openWorldHint, title — behavioral hints for clients.
4
+ ---
5
+
6
+ # `annotations`
7
+
8
+ Optional behavioral hints on `@Tool({...})`. AI clients use them to decide whether to gate the call behind a confirmation dialog, parallelize calls, retry on failure, etc.
9
+
10
+ ```typescript
11
+ @Tool({
12
+ name: 'delete_user',
13
+ description: 'Delete a user account',
14
+ inputSchema,
15
+ outputSchema,
16
+ annotations: {
17
+ title: 'Delete user',
18
+ readOnlyHint: false,
19
+ destructiveHint: true,
20
+ idempotentHint: true,
21
+ openWorldHint: false,
22
+ },
23
+ })
24
+ ```
25
+
26
+ ## Fields
27
+
28
+ | Field | Type | Default | Meaning |
29
+ | ----------------- | --------- | ----------------------------------- | ------------------------------------------------------------------------------ |
30
+ | `title` | `string` | — | Human-readable display name for client UIs (overrides `name` for presentation) |
31
+ | `readOnlyHint` | `boolean` | `false` | Tool only reads — no side effects. Safe to call freely. |
32
+ | `destructiveHint` | `boolean` | `true` (when `readOnlyHint: false`) | Tool may delete or overwrite. Clients usually trigger a confirmation. |
33
+ | `idempotentHint` | `boolean` | `false` | Repeated calls with same input produce the same result. Safe to retry. |
34
+ | `openWorldHint` | `boolean` | `true` | Tool interacts with external services / network. `false` = local-only. |
35
+
36
+ ## How clients use them
37
+
38
+ | Annotation | Common client behavior |
39
+ | ----------------------- | ------------------------------------------------------------------ |
40
+ | `readOnlyHint: true` | Tool may run in parallel with other reads; no confirmation needed |
41
+ | `destructiveHint: true` | Confirmation dialog before invocation; "are you sure?" |
42
+ | `idempotentHint: true` | Auto-retry on transient failures |
43
+ | `openWorldHint: false` | Hint that the tool is offline-safe |
44
+ | `title` | Shown in tool pickers and history instead of the snake_case `name` |
45
+
46
+ ## Common combinations
47
+
48
+ ```typescript
49
+ // Read-only query tool — safe, retryable
50
+ annotations: {
51
+ readOnlyHint: true,
52
+ destructiveHint: false,
53
+ idempotentHint: true,
54
+ openWorldHint: false,
55
+ }
56
+
57
+ // Destructive admin action
58
+ annotations: {
59
+ destructiveHint: true,
60
+ idempotentHint: true, // deleting twice still leaves the thing deleted
61
+ openWorldHint: false,
62
+ }
63
+
64
+ // Send an email
65
+ annotations: {
66
+ readOnlyHint: false,
67
+ destructiveHint: false, // not destroying, just sending
68
+ idempotentHint: false, // each call sends a new email
69
+ openWorldHint: true,
70
+ }
71
+
72
+ // External API search
73
+ annotations: {
74
+ readOnlyHint: true,
75
+ destructiveHint: false,
76
+ idempotentHint: true,
77
+ openWorldHint: true,
78
+ }
79
+ ```
80
+
81
+ ## Don't lie to the client
82
+
83
+ Annotations are advisory but the client trusts them:
84
+
85
+ - Don't set `readOnlyHint: true` on a tool that writes — clients may parallelize it.
86
+ - Don't set `idempotentHint: true` on a tool that has incremental side effects — clients may retry and double up.
87
+ - Don't omit `destructiveHint: true` on a delete — users may not get the confirmation they need.
88
+
89
+ ## When to omit annotations entirely
90
+
91
+ For ambiguous or non-obvious cases, omit. The defaults (`readOnlyHint: false`, `destructiveHint: true`, `idempotentHint: false`, `openWorldHint: true`) are the **conservative** assumption — clients will gate and not parallelize, which is the safe wrong choice if you're unsure.
92
+
93
+ ## See also
94
+
95
+ - [`20-tool-with-annotations`](../examples/20-tool-with-annotations.md)
96
+ - [`decorator-options.md`](./decorator-options.md)