@frontmcp/skills 1.3.0 → 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.
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,198 @@
1
+ ---
2
+ name: ui-widgets
3
+ description: @Tool({ ui }) — template formats, servingMode, host-detect resourceMode, CSP, widgetAccessible, MCP Apps spec.
4
+ ---
5
+
6
+ # Tool UI widgets
7
+
8
+ The `ui:` field on `@Tool({...})` attaches an HTML widget to the tool's response. Supported hosts (OpenAI Apps SDK, Claude Artifacts, MCP Inspector) render the widget in a sandboxed iframe alongside the JSON output, using the MCP Apps extension ([SEP-1865](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/1865)) and the `ui://widget/{toolName}.html` resource URI scheme.
9
+
10
+ ## Quick recipe
11
+
12
+ ```typescript
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const widgetPath = fileURLToPath(new URL('./weather.widget.tsx', import.meta.url));
16
+
17
+ @Tool({
18
+ name: 'get_weather',
19
+ description: 'Current weather for a city',
20
+ inputSchema,
21
+ outputSchema,
22
+ ui: {
23
+ template: { file: widgetPath },
24
+ widgetDescription: 'Current weather card',
25
+ },
26
+ })
27
+ class GetWeatherTool extends ToolContext {
28
+ async execute(input: GetWeatherInput): Promise<GetWeatherOutput> {
29
+ /* … */
30
+ }
31
+ }
32
+ ```
33
+
34
+ That's it. The framework:
35
+
36
+ - Pre-compiles the widget at startup, registers it at `ui://widget/get_weather.html`.
37
+ - Auto-detects the connecting client — `resourceMode: 'inline'` for Claude (React bundled in), `'cdn'` for OpenAI / ChatGPT / Cursor (esm.sh import map, smaller payload).
38
+ - Emits `ui.csp` (if set) on the resource's `_meta.ui.csp` — Claude actually honors it (it ignores CSP declared on the tool).
39
+
40
+ ## Template formats
41
+
42
+ | Format | Shape | When |
43
+ | ---------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------- |
44
+ | **FileSource (recommended)** | `{ file: widgetPath }` | `.tsx` / `.jsx` / `.html` source files. Anchor with `import.meta.url`. |
45
+ | **Function** | `(ctx) => string` | Quick demo / one-liner HTML. Annotate `ctx: TemplateContext<In, Out>` ([why](#typescript-gotcha-ts7006)). |
46
+ | **HTML / MDX string** | `'<div>…</div>'` or `'# Title\n<Card />'` | Static markup; pair with `mdxComponents` for MDX. |
47
+ | **React component** | `MyWidget` | SSR React. Set `hydrate: false` (default) for Claude/ChatGPT. |
48
+
49
+ The renderer auto-detects which one you passed.
50
+
51
+ ## TypeScript gotcha (TS7006)
52
+
53
+ Inline `template: (ctx) => …` under `strict` fails with `Parameter 'ctx' implicitly has an 'any' type` — `ui.template` is a union of multiple callable shapes so TypeScript can't pick a contextual type. Annotate explicitly:
54
+
55
+ ```typescript
56
+ import { type TemplateContext } from '@frontmcp/sdk';
57
+
58
+ ui: {
59
+ template: (ctx: TemplateContext<MyInput, MyOutput>) =>
60
+ `<div>${ctx.helpers.escapeHtml(ctx.output.label)}</div>`,
61
+ }
62
+ ```
63
+
64
+ Or use the FileSource form — it sidesteps the issue.
65
+
66
+ ## `ToolUIConfig` fields
67
+
68
+ | Field | Default | Purpose |
69
+ | --------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
70
+ | `template` | — | Required. Function / HTML-string / React component / `{ file }` FileSource. |
71
+ | `widgetDescription` | — | Human-readable description surfaced to the host UI. |
72
+ | `servingMode` | `'auto'` | `'inline'` / `'static'` / `'hybrid'` / `'direct-url'` / `'custom-url'`. `'auto'` picks the best per-host. |
73
+ | `displayMode` | `'inline'` | `'inline'` / `'fullscreen'` / `'pip'` — host display hint. |
74
+ | `preferredHeight` | — | `number` (px) or CSS string (`'50vh'`). Initial widget height; auto-resize grows/shrinks from this baseline. |
75
+ | `minHeight` / `maxHeight` | — | `number` (px) or CSS string. Clamp the widget height; auto-resize never reports outside this range. |
76
+ | `aspectRatio` | — | CSS `aspect-ratio` (`'16 / 9'` or `1.5`). Hosts that honor it size by ratio instead of measured height. |
77
+ | `autoResize` | `true` | Auto-report content height to the host via a debounced `ResizeObserver` on `#root`. Set `false` to opt out (CSS still applies). |
78
+ | `csp` | — | `{ connectDomains?, resourceDomains? }` — emitted on the resource content's `_meta.ui.csp` (#455). Claude honors CSP only here. |
79
+ | `contentSecurity` | strict | `{ allowUnsafeLinks?, allowInlineScripts?, bypassSanitization? }` — keep defaults. |
80
+ | `widgetAccessible` | `false` | `true` exposes `window.FrontMcpBridge.callTool` in the widget. |
81
+ | `resourceUri` | auto | Override the `ui://widget/{toolName}.html` URI. |
82
+ | `uiType` | `'auto'` | Force `'html'` / `'react'` / `'mdx'` / `'markdown'`. |
83
+ | `resourceMode` | host-detect | `'cdn'` / `'inline'`. Leave unset — the framework host-detects (Claude → `'inline'`, #456). |
84
+ | `hydrate` | `false` | Enable React hydration after SSR. Off by default — avoids React error #418 in Claude. |
85
+ | `externals`, `dependencies` | — | CDN externals for FileSource widgets. |
86
+ | `customShell`, `invocationStatus`, `widgetCapabilities`, `prefersBorder`, `sandboxDomain`, `htmlResponsePrefix` | — | Platform-specific knobs. |
87
+
88
+ ## Path resolution gotcha (#444)
89
+
90
+ Bare `template: { file: './widget.tsx' }` resolves against `process.cwd()`, **not** the tool file. Always anchor:
91
+
92
+ ```typescript
93
+ import { fileURLToPath } from 'node:url';
94
+
95
+ const widgetPath = fileURLToPath(new URL('./weather.widget.tsx', import.meta.url));
96
+ ui: {
97
+ template: {
98
+ file: widgetPath,
99
+ },
100
+ }
101
+ ```
102
+
103
+ See [`rules/widget-paths-anchor-with-import-meta-url.md`](../rules/widget-paths-anchor-with-import-meta-url.md).
104
+
105
+ ## `@frontmcp/ui` prerequisite (#443)
106
+
107
+ `.tsx` / `.jsx` FileSource widgets require `@frontmcp/ui` in the consuming project — the bundler injects an auto-generated React mount that imports `McpBridgeProvider` from `@frontmcp/ui/react`:
108
+
109
+ ```bash
110
+ npm install @frontmcp/ui
111
+ # or: yarn add @frontmcp/ui / pnpm add @frontmcp/ui
112
+ ```
113
+
114
+ Match the version to `@frontmcp/sdk`. Without it, server-side bundling fails with a friendly error pointing at this requirement.
115
+
116
+ ## Widget bridge — `window.FrontMcpBridge`
117
+
118
+ When the widget needs to read tool data or invoke other tools, the bridge IIFE is injected automatically. Set `widgetAccessible: true` to enable `callTool`:
119
+
120
+ ```typescript
121
+ ui: {
122
+ template: (ctx) => `
123
+ <button id="refresh">Refresh</button>
124
+ <script>
125
+ document.getElementById('refresh').onclick = async () => {
126
+ const result = await window.FrontMcpBridge.callTool('get_weather', { city: 'NYC' });
127
+ console.log(result);
128
+ };
129
+ </script>
130
+ `,
131
+ widgetAccessible: true,
132
+ }
133
+ ```
134
+
135
+ | Bridge method | Purpose |
136
+ | --------------------------------------------------------------- | ------------------------------------------------------- |
137
+ | `callTool(name, args)` | Invoke another tool (requires `widgetAccessible: true`) |
138
+ | `getToolInput()` / `getToolOutput()` / `getStructuredContent()` | Read the tool data |
139
+ | `getWidgetState()` / `setWidgetState(state)` | Persisted per-widget state |
140
+ | `getHostContext()` / `getTheme()` / `getDisplayMode()` | Host context |
141
+ | `hasCapability(cap)` | Probe adapter capabilities |
142
+ | `onToolResponseMetadata(cb)` | Subscribe to `ui/html` arrival (inline mode) |
143
+
144
+ The bridge routes to the right host adapter (OpenAI SDK / Claude postMessage / FrontMCP direct) automatically. **Never call `window.openai.*` directly** — it works on OpenAI but breaks everywhere else.
145
+
146
+ ## Host considerations
147
+
148
+ | Host | Notes |
149
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
150
+ | **OpenAI Apps SDK** | Any CDN works. The widget is advertised the same way as for every host — `tools/list` emits `_meta.ui.resourceUri` pointing at `ui://widget/{toolName}.html` (the MCP Apps `ui/*` namespace); there is no `openai/outputTemplate` key. |
151
+ | **Claude (MCP-UI)** | Widget iframe blocks all external script execution. Use `resourceMode: 'inline'` (auto-detected when you leave it unset) so React bundles in. CSP must be on the resource — framework handles it via `ui.csp` (#455 fix). |
152
+ | **MCP Inspector** | Useful for local development. Static mode works fine. |
153
+ | **Gemini / unknown** | `ui` is ignored — JSON output is returned. |
154
+
155
+ ## Widget sizing
156
+
157
+ Set sizing in the `ui` config — no hand-rolled `ui/notifications/size-changed` + `ResizeObserver` needed:
158
+
159
+ ```typescript
160
+ ui: {
161
+ template: MediaPlayerWidget,
162
+ preferredHeight: 480, // number → px; or a CSS string like '50vh'
163
+ minHeight: 200,
164
+ maxHeight: '80vh',
165
+ aspectRatio: '16 / 9', // optional; '16 / 9' or a number like 1.78
166
+ autoResize: true, // default; reports content height as it changes
167
+ }
168
+ ```
169
+
170
+ What FrontMCP does with it:
171
+
172
+ - **Static sizing CSS** — `preferredHeight` (initial `height`), `minHeight`, `maxHeight`, and `aspectRatio` are injected as a `<style>` block on `html` / `body` / `#root`, so the widget opens at the right size before any JS runs.
173
+ - **`_meta` hints** — the same values ride along on the response/discovery `_meta` as `ui/preferredHeight`, `ui/minHeight`, `ui/maxHeight`, `ui/aspectRatio` (and nested under `_meta.ui` in `tools/list`), so hosts that read sizing from metadata pick it up.
174
+ - **Runtime auto-resize** — when `autoResize !== false` and `ResizeObserver` is available, the bridge observes `#root` and reports the measured height to the host (debounced via `requestAnimationFrame`), also firing a `widget:resize` event you can listen for. Call `window.FrontMcpBridge.setSize({ height, width, aspectRatio })` to report manually.
175
+
176
+ Per-host behavior:
177
+
178
+ - **Claude / static widgets** — the host measures the iframe DOM height itself, so auto-resize is effectively CSS-only (the `setSize` report is a no-op). The injected CSS is what makes a fixed-tall widget (media players, canvases) open without clipping.
179
+ - **OpenAI ChatGPT** — auto-resize forwards to the Apps SDK sizing API when one is exposed; otherwise the SDK's own DOM measurement applies.
180
+ - **ext-apps hosts** — the measured size is reported via a `ui/setSize` request (parallels `ui/setDisplayMode`).
181
+ - **Gemini / generic / unknown** — `setSize` is a no-op; only the static CSS applies.
182
+
183
+ `displayMode: 'fullscreen'` remains a separate, best-effort hint a host may ignore.
184
+
185
+ ## Current limitations
186
+
187
+ - **Don't push large payloads through the widget.** Claude's sandbox CSP blocks external `connect-src`, so an inline widget can't reliably lazy-load multi-MB data, and a single MCP message is a poor carrier for it either. For large or streamed data, return a `resource_link` (see [`output-schema.md`](./output-schema.md)) and let the host fetch the resource — don't embed it in the widget or the tool result.
188
+
189
+ ## Examples
190
+
191
+ - [`22-tool-with-ui-html-template`](../examples/22-tool-with-ui-html-template.md) — inline function template
192
+ - [`23-tool-with-ui-filesource-tsx`](../examples/23-tool-with-ui-filesource-tsx.md) — `.tsx` widget, host-detect
193
+ - [`24-tool-with-ui-csp-and-bridge`](../examples/24-tool-with-ui-csp-and-bridge.md) — CSP + `widgetAccessible` + bridge
194
+
195
+ ## Related rules
196
+
197
+ - [`rules/widget-paths-anchor-with-import-meta-url.md`](../rules/widget-paths-anchor-with-import-meta-url.md)
198
+ - [`rules/widget-resource-mode-host-detect.md`](../rules/widget-resource-mode-host-detect.md)
@@ -0,0 +1,77 @@
1
+ ---
2
+ name: always-define-output-schema
3
+ constraint: Every `@Tool` defines `outputSchema`.
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: every tool defines `outputSchema`
8
+
9
+ ## The rule
10
+
11
+ Every `@Tool({...})` block declares `outputSchema`. There is no acceptable case for omitting it on a production tool.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ @Tool({
17
+ name: 'get_weather',
18
+ description: 'Current weather for a city',
19
+ inputSchema: { city: z.string() },
20
+ outputSchema: {
21
+ temperatureF: z.number(),
22
+ conditions: z.string(),
23
+ },
24
+ })
25
+ class GetWeatherTool extends ToolContext {
26
+ async execute(input: { city: string }) {
27
+ const apiResponse = await this.fetch(`https://api.example.com/weather?city=${input.city}`);
28
+ const data = await apiResponse.json();
29
+ // Even though `data` contains { temperatureF, conditions, internalApiKey, debugTrace, … }
30
+ // — the outputSchema strips everything except `temperatureF` and `conditions`.
31
+ return data;
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Bad
37
+
38
+ ```typescript
39
+ // ❌ no outputSchema — every field in `data` flows through to the client
40
+ @Tool({
41
+ name: 'get_weather',
42
+ description: 'Current weather for a city',
43
+ inputSchema: { city: z.string() },
44
+ })
45
+ class GetWeatherTool extends ToolContext {
46
+ async execute(input: { city: string }) {
47
+ const apiResponse = await this.fetch(`https://api.example.com/weather?city=${input.city}`);
48
+ return apiResponse.json(); // ← internalApiKey, debugTrace, PII … all leak
49
+ }
50
+ }
51
+ ```
52
+
53
+ ## Why
54
+
55
+ 1. **Output validation prevents data leaks.** Without `outputSchema`, every field your code accidentally includes (or that an upstream API drops in unsolicited — auth tokens, internal IDs, debug traces, PII) reaches the MCP client. With it, only declared fields pass through; everything else is stripped.
56
+ 2. **CodeCall plugin compatibility.** The CodeCall plugin uses `outputSchema` to understand what a tool returns, enabling correct VM-based orchestration and pass-by-reference. Tools without `outputSchema` degrade CodeCall's ability to chain calls.
57
+ 3. **Type safety on `execute()`'s return type.** With `outputSchema` declared, `ToolContext` infers the expected return type from it. The compiler tells you when your return value diverges from the declared shape.
58
+ 4. **Self-documenting tools.** A structured object `outputSchema` (a Zod raw shape or `z.object`) is converted to JSON Schema and advertised in `tools/list`, so AI clients can choose the right tool by what it returns. Primitive / media / multi-content-array / union outputs flow through `content` instead and aren't advertised as `outputSchema` — that's expected (the MCP `outputSchema` must be a top-level object).
59
+
60
+ ## How to apply
61
+
62
+ - For structured data: **Zod raw shape** is the recommended form — `{ field: z.string(), count: z.number() }`. Strict, validated, JSON-serializable.
63
+ - For a single primitive: use the literal — `outputSchema: 'string' | 'number' | 'boolean' | 'date'`.
64
+ - For media: `'image'`, `'audio'`, `'resource'`, `'resource_link'`.
65
+ - For multi-content: an array — `outputSchema: ['string', 'image']`.
66
+ - For complex types not expressible as a raw shape: full Zod schemas — `z.object(...)`, `z.discriminatedUnion([...])`, etc. (Note: this is the one place `z.object()` is allowed — it's _not_ allowed for `inputSchema`.)
67
+
68
+ See [`output-schema.md`](../references/output-schema.md) for the full taxonomy.
69
+
70
+ ## Verification
71
+
72
+ ```bash
73
+ # Grep for tools without outputSchema — should return 0 hits
74
+ grep -L 'outputSchema:' $(grep -rl '@Tool' --include='*.tool.ts' src/)
75
+ ```
76
+
77
+ A failing CI step that runs this grep is the cheapest way to enforce the rule across a codebase.
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: derive-execute-types
3
+ constraint: '`execute()` parameter and return types come from `ToolInputOf<>` / `ToolOutputOf<>` — never duplicated inline.'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: derive `execute()` types from the schemas
8
+
9
+ ## The rule
10
+
11
+ `execute()`'s parameter and return types are derived from the hoisted schemas via `ToolInputOf<>` / `ToolOutputOf<>`. Hand-typing the shape inline next to the schema is a second declaration of the same contract.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ // <name>.schema.ts
17
+ export const inputSchema = { city: z.string() };
18
+ export const outputSchema = { temperatureF: z.number() };
19
+ export type GetWeatherInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
20
+ export type GetWeatherOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
21
+
22
+ // <name>.tool.ts
23
+ async execute(input: GetWeatherInput): Promise<GetWeatherOutput> {
24
+ return { temperatureF: 72 };
25
+ }
26
+ ```
27
+
28
+ ## Bad
29
+
30
+ ```typescript
31
+ // ❌ inline annotation duplicates the schema's shape
32
+ async execute(input: { city: string }): Promise<{ temperatureF: number }> {
33
+ return { temperatureF: 72 };
34
+ }
35
+ ```
36
+
37
+ Why it's bad: change the schema (`city` → `location`, add `units`) without touching the annotation and TypeScript happily compiles. Runtime Zod validation rejects the request that the compiler accepted.
38
+
39
+ ## Why
40
+
41
+ - **Single source of truth** — the schema defines the contract. Types derived from it can't drift. Hand-typed shapes silently rot when the schema changes.
42
+ - **Re-importable** — specs, sibling tools, and generated clients all `import { GetWeatherInput }` from the same `.schema.ts` file. They get one canonical type.
43
+ - **Compiler catches drift** — change a schema field, and the compiler flags every place that reads the old shape. Inline annotations defeat this.
44
+
45
+ ## How to apply
46
+
47
+ - Always hoist `inputSchema` / `outputSchema` to `<name>.schema.ts`.
48
+ - Always export `type <X>Input = ToolInputOf<{ inputSchema: typeof inputSchema }>;` and `type <X>Output = ToolOutputOf<{ outputSchema: typeof outputSchema }>;` next to them.
49
+ - Import the derived types into the tool file and use them on `execute()`.
50
+ - Form 2 (`z.infer<z.ZodObject<typeof inputSchema>>`) produces an identical type — pick whichever fits the surrounding code.
51
+
52
+ ## Verification
53
+
54
+ ```bash
55
+ # Find tool files where execute() uses an inline object literal type — likely a violation
56
+ grep -rE 'execute\(input:\s*\{' --include='*.tool.ts' src/
57
+ ```
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: input-schema-is-raw-shape
3
+ constraint: '`inputSchema` is a raw Zod shape, never `z.object(...)`.'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: `inputSchema` is a raw Zod shape
8
+
9
+ ## The rule
10
+
11
+ The top-level value of `inputSchema` is a plain object mapping field names to Zod types. The framework wraps it in `z.object(...)` internally and validates every call.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ @Tool({
17
+ name: 'search',
18
+ inputSchema: {
19
+ query: z.string().min(1),
20
+ limit: z.number().int().min(1).max(100).default(10),
21
+ },
22
+ })
23
+ ```
24
+
25
+ Nested `z.object({...})` INSIDE a field is fine — only the top-level value must be a raw shape:
26
+
27
+ ```typescript
28
+ inputSchema: {
29
+ user: z.object({ id: z.string(), name: z.string() }), // OK — nested
30
+ filter: z.array(z.string()),
31
+ }
32
+ ```
33
+
34
+ ## Bad
35
+
36
+ ```typescript
37
+ // ❌ wrapped at the top level
38
+ @Tool({
39
+ name: 'search',
40
+ inputSchema: z.object({
41
+ query: z.string(),
42
+ }),
43
+ })
44
+
45
+ // ❌ wrapped via z.union / z.intersection at the top
46
+ inputSchema: z.union([z.object({...}), z.object({...})]),
47
+ ```
48
+
49
+ ## Why
50
+
51
+ - **Type inference** — `ToolInputOf<{ inputSchema: typeof inputSchema }>` and the SDK's decorator inference both assume the raw-shape form. Wrapping breaks both.
52
+ - **`@Tool` metadata** — the decorator reads `inputSchema` as a `Record<string, ZodType>` to compute the JSON schema published in `tools/list`. A wrapped `z.object` defeats this.
53
+ - **Consistency** — every tool in the codebase uses the same form. A wrapped `inputSchema` is an outlier that confuses readers and breaks grep-based code search.
54
+
55
+ If you legitimately need a union or transform for input, do it inside `execute()`:
56
+
57
+ ```typescript
58
+ inputSchema: {
59
+ start: z.string().datetime(),
60
+ end: z.string().datetime(),
61
+ }
62
+
63
+ async execute(input) {
64
+ if (input.start >= input.end) {
65
+ this.fail(new InvalidInputError('start must be before end'));
66
+ }
67
+ // …
68
+ }
69
+ ```
70
+
71
+ ## Verification
72
+
73
+ ```bash
74
+ # Any `inputSchema: z.` is a violation
75
+ grep -rE 'inputSchema:\s*z\.' src/**/*.tool.ts
76
+ ```
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: no-toolcontext-generics
3
+ constraint: '`class MyTool extends ToolContext` — never `extends ToolContext<typeof inputSchema>`.'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: don't parameterize `ToolContext` with explicit generics
8
+
9
+ ## The rule
10
+
11
+ `ToolContext` infers input / output types from the `@Tool({...})` decorator at the **class** level automatically. Adding explicit generics is redundant — and prevents the inference from flowing correctly when the decorator's shape changes.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ @Tool({ name: 'greet', inputSchema, outputSchema })
17
+ class GreetTool extends ToolContext {
18
+ async execute(input: GreetInput): Promise<GreetOutput> {
19
+ return { greeting: `Hello, ${input.name}!` };
20
+ }
21
+ }
22
+ ```
23
+
24
+ ## Bad
25
+
26
+ ```typescript
27
+ // ❌ explicit generics — redundant and brittle
28
+ @Tool({ name: 'greet', inputSchema, outputSchema })
29
+ class GreetTool extends ToolContext<typeof inputSchema, typeof outputSchema> {
30
+ // …
31
+ }
32
+
33
+ // ❌ partial generics — even worse, hides which way drift goes
34
+ class GreetTool extends ToolContext<typeof inputSchema> {
35
+ // …
36
+ }
37
+ ```
38
+
39
+ ## Why
40
+
41
+ - **The decorator already infers.** `@Tool` carries the input/output schemas in its options; the SDK's decorator hooks the inferred types into the class. Explicit generics either match (redundant) or mismatch (silently break the inferred type).
42
+ - **Schema changes flow automatically.** With plain `extends ToolContext`, changing a Zod field in `<name>.schema.ts` updates everything that uses `ToolInputOf` / `ToolOutputOf` — including the `execute()` annotation. With explicit generics, you have to remember to update them.
43
+ - **Consistency** — every tool in the codebase uses plain `extends ToolContext`. Explicit generics are an outlier that flag a misunderstanding of how the decorator works.
44
+
45
+ ## Verification
46
+
47
+ ```bash
48
+ # Any `extends ToolContext<` is a violation
49
+ grep -rn 'extends ToolContext<' src/**/*.tool.ts
50
+ ```
@@ -0,0 +1,79 @@
1
+ ---
2
+ name: no-try-catch-around-execute
3
+ constraint: 'Do not wrap the body of `execute()` in `try/catch`. The framework owns the error flow.'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: don't `try/catch` around `execute()`
8
+
9
+ ## The rule
10
+
11
+ The framework's tool-execution flow catches exceptions, formats them into proper JSON-RPC errors, runs error hooks, emits notifications. Wrapping the `execute()` body in a `try/catch` defeats all of that.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ async execute(input: Input) {
17
+ const record = await this.findRecord(input.id);
18
+ if (!record) {
19
+ this.fail(new ResourceNotFoundError(`record:${input.id}`)); // controlled error
20
+ }
21
+ return doWork(record); // any other throw propagates to the framework
22
+ }
23
+ ```
24
+
25
+ ## Bad
26
+
27
+ ```typescript
28
+ // ❌ swallows everything, hides infrastructure errors, breaks the framework's flow
29
+ async execute(input: Input) {
30
+ try {
31
+ const record = await this.findRecord(input.id);
32
+ return doWork(record);
33
+ } catch (err) {
34
+ this.fail(err instanceof Error ? err : new Error(String(err)));
35
+ }
36
+ }
37
+
38
+ // ❌ even worse — silently returns a default and the client never sees the failure
39
+ async execute(input: Input) {
40
+ try {
41
+ return await doWork(input);
42
+ } catch {
43
+ return { ok: false }; // 💣
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## Why
49
+
50
+ - **The framework's flow already catches.** It logs the error with full context, emits structured notifications, formats the JSON-RPC error with the right code, and runs error hooks (audit, metrics, telemetry). Wrapping defeats all of that.
51
+ - **Raw `Error` messages get redacted.** Without `PublicMcpError`, the framework treats the message as potentially-sensitive and replaces it with "Internal error". Manual try/catch + `this.fail(err)` strips the public-message guarantee.
52
+ - **Observability breaks.** Distributed-tracing spans, metrics counters, and audit logs all key off the framework-caught error. Manual try/catch hides the error from them.
53
+
54
+ ## The narrow exception
55
+
56
+ If you have a specific failure mode the framework can't classify (a particular HTTP status, an upstream error code that means "not found" instead of "server error"), catch JUST THAT case and convert to `this.fail`:
57
+
58
+ ```typescript
59
+ async execute(input: Input) {
60
+ const response = await this.fetch(url, init);
61
+ if (response.status === 404) {
62
+ this.fail(new ResourceNotFoundError(`upstream:${input.id}`));
63
+ }
64
+ if (!response.ok) {
65
+ this.fail(new PublicMcpError(`Upstream returned ${response.status}`));
66
+ }
67
+ // 5xx, network errors, timeouts — let them propagate to the framework
68
+ return response.json();
69
+ }
70
+ ```
71
+
72
+ That's not wrapping the whole `execute()` — that's targeted conversion of one specific signal. Different shape, different purpose.
73
+
74
+ ## Verification
75
+
76
+ ```bash
77
+ # Find try/catch directly inside execute() — should return 0 hits
78
+ grep -rPzo '(?s)async execute\([^)]*\) \{.*?try \{' src/**/*.tool.ts
79
+ ```
@@ -0,0 +1,76 @@
1
+ ---
2
+ name: register-in-app
3
+ constraint: 'Register tools in `@App({ tools })`, not directly on `@FrontMcp({ tools })` (the latter is the simple-server escape hatch).'
4
+ severity: recommended
5
+ ---
6
+
7
+ # Rule: register tools in `@App`
8
+
9
+ ## The rule
10
+
11
+ Register tools in an `@App({ tools })`. `@FrontMcp({ tools })` is the escape hatch for single-app prototypes — promote to an `@App` as soon as you want any of the per-app benefits.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ @App({
17
+ name: 'main',
18
+ providers: [UserServiceProvider],
19
+ tools: [GreetUserTool, GetUserTool],
20
+ })
21
+ class MainApp {}
22
+
23
+ @FrontMcp({
24
+ info: { name: 'demo', version: '1.0.0' },
25
+ apps: [MainApp],
26
+ })
27
+ export default class DemoServer {}
28
+ ```
29
+
30
+ ## Acceptable (single-app prototypes)
31
+
32
+ ```typescript
33
+ @FrontMcp({
34
+ info: { name: 'demo', version: '1.0.0' },
35
+ tools: [GreetUserTool], // top-level — fine for very small servers
36
+ })
37
+ export default class DemoServer {}
38
+ ```
39
+
40
+ ## Why prefer `@App`
41
+
42
+ | Top-level `@FrontMcp({ tools })` | `@App({ tools })` |
43
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
44
+ | One auth posture for the whole server | Per-app `auth: { mode: 'public' \| 'transparent' \| 'local' \| 'remote' }` |
45
+ | Providers visible to every tool | DI scope per app — tokens registered in `@App({ providers })` are visible only to its own tools |
46
+ | No per-app lifecycle | `onAppStart`, `onAppStop`, app-level hooks |
47
+ | Hard to refactor when you need to split | Already split |
48
+
49
+ ## When to promote to `@App`
50
+
51
+ Whenever any of the following:
52
+
53
+ - You want different auth modes for different parts of the surface (public vs authenticated vs admin)
54
+ - Tools share local services that other apps shouldn't see
55
+ - You want per-app lifecycle hooks
56
+ - You're past ~5 tools in one server
57
+
58
+ Promoting from top-level to an `@App` is a one-line refactor:
59
+
60
+ ```typescript
61
+ // before
62
+ @FrontMcp({ tools: [A, B, C] })
63
+
64
+ // after
65
+ @App({ name: 'main', tools: [A, B, C] }) class MainApp {}
66
+ @FrontMcp({ apps: [MainApp] })
67
+ ```
68
+
69
+ ## Severity
70
+
71
+ This rule is `recommended`, not `required`. Single-app prototypes can stay on top-level `@FrontMcp({ tools })` indefinitely. The push to `@App` is about future-proofing for the moment you need any per-app concern — which usually arrives.
72
+
73
+ ## See also
74
+
75
+ - `architecture` skill — multi-app patterns, module boundaries, DI scope
76
+ - [`references/registration.md`](../references/registration.md)
@@ -0,0 +1,45 @@
1
+ ---
2
+ name: snake-case-tool-names
3
+ constraint: 'Tool `name:` field is always `snake_case` (e.g. `get_weather`, not `getWeather`).'
4
+ severity: required
5
+ ---
6
+
7
+ # Rule: tool names are `snake_case`
8
+
9
+ ## The rule
10
+
11
+ The `name:` field on `@Tool({...})` is `snake_case`. Lowercase letters, digits, underscores. No camelCase, no kebab-case, no PascalCase.
12
+
13
+ ## Good
14
+
15
+ ```typescript
16
+ @Tool({ name: 'get_weather' })
17
+ @Tool({ name: 'create_issue' })
18
+ @Tool({ name: 'list_repos' })
19
+ @Tool({ name: 'send_email' })
20
+ @Tool({ name: 'rotate_secrets' })
21
+ ```
22
+
23
+ ## Bad
24
+
25
+ ```typescript
26
+ @Tool({ name: 'getWeather' }) // ❌ camelCase
27
+ @Tool({ name: 'get-weather' }) // ❌ kebab-case
28
+ @Tool({ name: 'GetWeather' }) // ❌ PascalCase
29
+ @Tool({ name: 'GET_WEATHER' }) // ❌ uppercase
30
+ ```
31
+
32
+ ## Why
33
+
34
+ - **MCP protocol convention.** Tool names are the lookup key for `tools/call` across the entire MCP ecosystem. Servers and clients consistently expect `snake_case`.
35
+ - **Cross-platform consistency.** Some clients (and LLM prompt templates) normalize tool names for display; `snake_case` survives roundtrips. Mixed-case can be folded inconsistently.
36
+ - **Searchable.** `git grep create_issue` finds every reference; `git grep create.issue` (regex required for cross-case) is more work.
37
+
38
+ > The CLASS name stays `PascalCase` (`GetWeatherTool`). Only the `name:` _field_ is `snake_case`. The mismatch is intentional — class names follow TypeScript conventions, MCP tool names follow MCP conventions.
39
+
40
+ ## Verification
41
+
42
+ ```bash
43
+ # Find non-snake_case tool names — should return 0 hits
44
+ grep -rE "name:\s*'[^']*[A-Z-][^']*'" $(grep -rl '@Tool' src/**/*.tool.ts)
45
+ ```