@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,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)
|