@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,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
|
+
```
|