@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.
Files changed (131) 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 +14 -7
  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-deployment-targets.md +84 -1
  83. package/catalog/frontmcp-config/references/configure-http.md +203 -14
  84. package/catalog/frontmcp-config/references/configure-session.md +14 -7
  85. package/catalog/frontmcp-deployment/SKILL.md +17 -15
  86. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +1 -1
  87. package/catalog/frontmcp-deployment/references/deploy-manifest-yaml.md +308 -0
  88. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +174 -0
  89. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +145 -2
  90. package/catalog/frontmcp-development/SKILL.md +36 -50
  91. package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +14 -0
  92. package/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md +85 -9
  93. package/catalog/frontmcp-development/references/create-job.md +45 -11
  94. package/catalog/frontmcp-development/references/create-provider.md +80 -8
  95. package/catalog/frontmcp-development/references/create-skill-with-tools.md +31 -0
  96. package/catalog/frontmcp-development/references/create-skill.md +45 -0
  97. package/catalog/frontmcp-development/references/decorators-guide.md +15 -15
  98. package/catalog/frontmcp-extensibility/SKILL.md +1 -1
  99. package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +8 -6
  100. package/catalog/frontmcp-extensibility/references/skill-audit-log.md +7 -2
  101. package/catalog/frontmcp-guides/SKILL.md +8 -8
  102. package/catalog/frontmcp-observability/SKILL.md +16 -8
  103. package/catalog/frontmcp-observability/examples/metrics-endpoint/enable-metrics-endpoint.md +77 -0
  104. package/catalog/frontmcp-observability/references/metrics-endpoint.md +161 -0
  105. package/catalog/frontmcp-production-readiness/SKILL.md +1 -1
  106. package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -2
  107. package/catalog/frontmcp-setup/SKILL.md +12 -12
  108. package/catalog/frontmcp-setup/examples/frontmcp-skills-usage/install-and-search-skills.md +19 -1
  109. package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
  110. package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +260 -19
  111. package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
  112. package/catalog/frontmcp-setup/references/setup-project.md +29 -0
  113. package/catalog/frontmcp-setup/references/setup-sqlite.md +68 -9
  114. package/catalog/frontmcp-testing/SKILL.md +26 -18
  115. package/catalog/frontmcp-testing/references/test-auth.md +24 -0
  116. package/catalog/skills-manifest.json +676 -146
  117. package/package.json +1 -1
  118. package/src/manifest.d.ts +72 -1
  119. package/src/manifest.js +4 -1
  120. package/src/manifest.js.map +1 -1
  121. package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +0 -61
  122. package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -84
  123. package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -92
  124. package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +0 -92
  125. package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +0 -59
  126. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +0 -101
  127. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +0 -62
  128. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +0 -101
  129. package/catalog/frontmcp-development/references/create-tool-annotations.md +0 -48
  130. package/catalog/frontmcp-development/references/create-tool-output-schema-types.md +0 -71
  131. package/catalog/frontmcp-development/references/create-tool.md +0 -728
@@ -0,0 +1,128 @@
1
+ ---
2
+ name: error-handling
3
+ description: this.fail, MCP error classes, error flow — when to throw vs fail.
4
+ ---
5
+
6
+ # Error handling
7
+
8
+ ## The rule
9
+
10
+ **Don't `try/catch` around `execute()`** ([rule](../rules/no-try-catch-around-execute.md)). The framework's flow catches exceptions, formats them into proper JSON-RPC errors, runs error hooks, and emits notifications. Wrapping the body defeats all of that.
11
+
12
+ ```typescript
13
+ // ❌ swallows the error, breaks the framework's flow
14
+ async execute(input: Input) {
15
+ try {
16
+ const result = await someOperation();
17
+ return result;
18
+ } catch (err) {
19
+ this.fail(err instanceof Error ? err : new Error(String(err)));
20
+ }
21
+ }
22
+
23
+ // ✅ let it propagate
24
+ async execute(input: Input) {
25
+ return await someOperation();
26
+ }
27
+ ```
28
+
29
+ ## Business-logic errors → `this.fail`
30
+
31
+ For errors the user / agent should see (not-found, permission-denied, invalid-input, conflict, etc.), use `this.fail(new SomeMcpError(…))`:
32
+
33
+ ```typescript
34
+ async execute(input: { id: string }) {
35
+ const record = await this.findRecord(input.id);
36
+ if (!record) {
37
+ this.fail(new ResourceNotFoundError(`record:${input.id}`));
38
+ // ↑ never returns; flow aborts here
39
+ }
40
+ // … keep going with `record` (typed as non-null because fail() doesn't return)
41
+ }
42
+ ```
43
+
44
+ `this.fail` throws internally and never returns — TypeScript knows it's a `never`-returning method.
45
+
46
+ ## Infrastructure errors → propagate
47
+
48
+ For errors the framework should handle uniformly (network failure, DB unavailable, timeout), just let them throw. The framework wraps them in an `InternalMcpError` with the message redacted before reaching the client, and logs the original for ops.
49
+
50
+ ## MCP error classes
51
+
52
+ All from `@frontmcp/sdk`. The two roots: `PublicMcpError` (message reaches the client verbatim) and `InternalMcpError` (message is redacted; full details go to logs).
53
+
54
+ | Class | Error code | HTTP | When |
55
+ | ----------------------- | ---------------------------------------- | ---- | ----------------------------------------------------------------- |
56
+ | `PublicMcpError` | — | — | Base for public errors. Subclass for domain-specific cases. |
57
+ | `InternalMcpError` | — | — | Base for redacted infra errors |
58
+ | `ResourceNotFoundError` | `'RESOURCE_NOT_FOUND'` (-32002 JSON-RPC) | 404 | A specific resource doesn't exist |
59
+ | `ToolNotFoundError` | `'TOOL_NOT_FOUND'` | 404 | Tool name not registered |
60
+ | `InvalidInputError` | `'INVALID_INPUT'` | 400 | Cross-field / business-rule input invalid (Zod handles per-field) |
61
+ | `InvalidMethodError` | `'INVALID_METHOD'` | 400 | Wrong protocol method called |
62
+ | `UnauthorizedError` | `'UNAUTHORIZED'` (-32001 JSON-RPC) | 401 | Missing credentials |
63
+ | `EntryUnavailableError` | `'FORBIDDEN'` (-32003 JSON-RPC) | 403 | `availableWhen` mismatch at call time |
64
+ | `RateLimitError` | `'RATE_LIMIT_EXCEEDED'` | 429 | Rate-limit fired |
65
+ | `QuotaExceededError` | `'QUOTA_EXCEEDED'` | 429 | Quota-style limit fired |
66
+ | `PayloadTooLargeError` | `'PAYLOAD_TOO_LARGE'` | 413 | Body limit exceeded |
67
+
68
+ `MCP_ERROR_CODES` is the JSON-RPC numeric-code constant map (`UNAUTHORIZED: -32001`, `RESOURCE_NOT_FOUND: -32002`, `FORBIDDEN: -32003`, `INVALID_PARAMS: -32602`, `INTERNAL_ERROR: -32603`, etc.). The classes use string error codes; the numeric codes appear on the JSON-RPC wire response.
69
+
70
+ ```typescript
71
+ import { InvalidInputError, MCP_ERROR_CODES, PublicMcpError, ResourceNotFoundError } from '@frontmcp/sdk';
72
+
73
+ this.fail(new ResourceNotFoundError(`record:${input.id}`));
74
+ this.fail(new InvalidInputError('start must be before end'));
75
+ ```
76
+
77
+ ## Custom error classes
78
+
79
+ Subclass `PublicMcpError` for domain-specific errors:
80
+
81
+ ```typescript
82
+ class QuotaExceededError extends PublicMcpError {
83
+ readonly mcpErrorCode = -32100; // any custom code outside the reserved JSON-RPC ranges
84
+
85
+ constructor(public readonly remaining: number) {
86
+ super(`Quota exceeded — ${remaining} requests left in window`);
87
+ }
88
+
89
+ toJsonRpcError() {
90
+ return {
91
+ code: this.mcpErrorCode,
92
+ message: this.getPublicMessage(),
93
+ data: { remaining: this.remaining },
94
+ };
95
+ }
96
+ }
97
+
98
+ // usage
99
+ this.fail(new QuotaExceededError(0));
100
+ ```
101
+
102
+ The `data` payload lets you surface structured info to the client (rate-limit remaining, validation field errors, etc.) without leaking internals.
103
+
104
+ ## `PublicMcpError` vs raw `Error`
105
+
106
+ | Throw | Client sees |
107
+ | -------------------------------------- | ----------------------------------------------------------------------- |
108
+ | `new PublicMcpError('Quota exceeded')` | `{ code: -32603, message: 'Quota exceeded' }` |
109
+ | `new Error('Quota exceeded')` | `{ code: -32603, message: 'Internal error' }` (the message is REDACTED) |
110
+
111
+ Raw `Error`s have their messages **redacted** before reaching the client — the framework treats them as potentially-sensitive infrastructure errors. For anything the client should read, use `PublicMcpError` or a subclass.
112
+
113
+ ## Non-null assertions are forbidden
114
+
115
+ ```typescript
116
+ // ❌ masks failures
117
+ const rec = this.defs.get(token)!;
118
+
119
+ // ✅ proper handling
120
+ const rec = this.defs.get(token);
121
+ if (!rec) this.fail(new ResourceNotFoundError(`def:${token}`));
122
+ ```
123
+
124
+ ## See also
125
+
126
+ - [`rules/no-try-catch-around-execute.md`](../rules/no-try-catch-around-execute.md)
127
+ - [`rules/use-this-fail-for-business-errors.md`](../rules/use-this-fail-for-business-errors.md)
128
+ - [`execution-context.md`](./execution-context.md)
@@ -0,0 +1,158 @@
1
+ ---
2
+ name: execution-context
3
+ description: What ToolContext provides at runtime — this.get, this.fetch, this.notify, this.context.
4
+ ---
5
+
6
+ # `ToolContext` runtime API
7
+
8
+ `ToolContext` extends `ExecutionContextBase`. Inside `execute()` you have access to:
9
+
10
+ ## Methods
11
+
12
+ | Method | Purpose |
13
+ | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
14
+ | `execute(input: In): Promise<Out>` | The method you implement |
15
+ | `this.get(token)` | Resolve a DI dependency. Throws `DependencyNotFoundError` if not registered. |
16
+ | `this.tryGet(token)` | Resolve a DI dependency. Returns `undefined` if not registered. |
17
+ | `this.fail(err)` | Abort execution, trigger the error flow. **Never returns.** Use for business-logic errors. |
18
+ | `this.respond(value)` | Early-return with a value. Validates against `outputSchema`. **Never returns** (throws `FlowControl.respond`). |
19
+ | `this.mark(stage)` | Set the active execution stage for debugging / tracing |
20
+ | `this.fetch(input, init?)` | HTTP fetch with context propagation (trace headers, etc.) |
21
+ | `this.notify(message, level?)` | Send a log-level notification to the client |
22
+ | `this.progress(progress, total?, message?)` | Send a progress notification. Returns `Promise<boolean>` (false when no progress token in request) |
23
+ | `this.notifyResourceUpdated(uri)` | Tell subscribed clients a resource's contents changed (`notifications/resources/updated`) |
24
+ | `this.notifyResourceListChanged()` | Tell clients the resource list changed (`notifications/resources/list_changed`) |
25
+ | `this.elicit(message, schema)` | Request interactive input from the user mid-execution. See [`elicitation.md`](./elicitation.md) |
26
+ | `this.isPlatform(os)` / `this.isRuntime(rt)` / `this.isEnv(env)` | Imperative platform checks (declarative form is `availableWhen` — see [`availability.md`](./availability.md)) |
27
+
28
+ ## Properties
29
+
30
+ | Property | Type | Description |
31
+ | --------------- | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
32
+ | `this.input` | `In` | The validated input object (same value as the `input` parameter) |
33
+ | `this.output` | `Out \| undefined` | The output value (available after `execute()` returns) |
34
+ | `this.metadata` | tool metadata | Frozen view of the `@Tool({...})` config |
35
+ | `this.scope` | scope instance | The current scope — DI lookups, child scopes |
36
+ | `this.context` | `FrontMcpContext` | Per-request context (see below) |
37
+ | `this.auth` | `FrontMcpAuthContext` | User identity & claims — `this.auth.user.sub`, `this.auth.claims['…']`, `hasRole()`, `hasPermission()`, `hasScope()`. Use this (not `this.context.authInfo`) for identity. |
38
+
39
+ ## `this.context` (FrontMcpContext)
40
+
41
+ | Property | Type | Description |
42
+ | -------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
43
+ | `requestId` | `string` | Unique ID for this request |
44
+ | `sessionId` | `string` | Session identifier (for stateful transports) |
45
+ | `scopeId` | `string` | Scope identifier (for multi-app servers) |
46
+ | `authInfo` | `AuthInfo` | Raw validated access token — `token`, `clientId`, `scopes`, `expiresAt?`, `resource?`, `extra?`. For user identity / JWT claims use `this.auth` (`this.auth.user.sub`, `this.auth.claims['…']`) instead. |
47
+ | `traceContext` | `TraceContext` | Distributed-tracing context (propagated to `this.fetch` automatically) |
48
+ | `timestamp` | `number` | Request start timestamp |
49
+ | `metadata` | `RequestMetadata` | Request headers, client IP, MCP client name/version |
50
+
51
+ ## DI: `this.get` vs `this.tryGet`
52
+
53
+ ```typescript
54
+ import { Token } from '@frontmcp/di';
55
+
56
+ interface UserService { findById(id: string): Promise<User | null>; }
57
+ const USER_SERVICE: Token<UserService> = Symbol('UserService');
58
+
59
+ async execute(input: { userId: string }) {
60
+ // Throws DependencyNotFoundError if USER_SERVICE isn't registered in scope
61
+ const users = this.get(USER_SERVICE);
62
+
63
+ // Returns undefined if not registered — for optional deps
64
+ const cache = this.tryGet(CACHE);
65
+ if (cache) {
66
+ const cached = await cache.get(input.userId);
67
+ if (cached) return cached;
68
+ }
69
+
70
+ const user = await users.findById(input.userId);
71
+ if (!user) this.fail(new ResourceNotFoundError(`user:${input.userId}`));
72
+ return user;
73
+ }
74
+ ```
75
+
76
+ Use `this.get` (throws) when the tool genuinely requires the dependency. Use `this.tryGet` (returns undefined) when the tool degrades gracefully without it (e.g., optional cache, optional metrics emitter).
77
+
78
+ ## HTTP: `this.fetch`
79
+
80
+ `this.fetch` is a thin wrapper around the standard `fetch` that propagates the request's `traceContext` so downstream services can stitch the call into the same trace.
81
+
82
+ ```typescript
83
+ async execute(input: { url: string }) {
84
+ const response = await this.fetch(input.url);
85
+ if (!response.ok) {
86
+ this.fail(new InternalMcpError(`upstream returned ${response.status}`));
87
+ }
88
+ return response.json();
89
+ }
90
+ ```
91
+
92
+ It accepts the same arguments as standard `fetch`:
93
+
94
+ ```typescript
95
+ this.fetch(url, {
96
+ method: 'POST',
97
+ headers: { 'content-type': 'application/json' },
98
+ body: JSON.stringify(payload),
99
+ signal: AbortSignal.timeout(5_000),
100
+ });
101
+ ```
102
+
103
+ > Don't `try/catch` around the fetch and swallow errors — let infrastructure errors propagate to the framework. Only use `this.fail` for **business-logic** errors. See [`error-handling.md`](./error-handling.md).
104
+
105
+ ## Notifications: `this.notify` + `this.progress`
106
+
107
+ ```typescript
108
+ async execute(input: { items: string[] }) {
109
+ this.mark('validation');
110
+ // …
111
+ this.mark('processing');
112
+ for (let i = 0; i < input.items.length; i++) {
113
+ await this.progress(i + 1, input.items.length, `Processing ${input.items[i]}`);
114
+ await this.processItem(input.items[i]);
115
+ }
116
+ await this.notify(`Processed ${input.items.length} items`, 'info');
117
+ this.mark('complete');
118
+ return { processed: input.items.length };
119
+ }
120
+ ```
121
+
122
+ - `this.notify(msg, level?)` — sends `notifications/message` to the client (`debug` / `info` / `warning` / `error`). Always-best-effort.
123
+ - `this.progress(n, total?, msg?)` — sends `notifications/progress` IF the request had a progress token. Returns `false` when no token was provided (so the call costs almost nothing if nobody's listening).
124
+ - `this.mark(stage)` — server-side breadcrumb, surfaced in logs / metrics / traces. No client notification.
125
+ - `this.notifyResourceUpdated(uri)` — sends `notifications/resources/updated` to every session subscribed to `uri` (via `resources/subscribe`); no-op for non-subscribers. Call it when a tool mutates state that backs a `@Resource` so subscribers re-fetch.
126
+ - `this.notifyResourceListChanged()` — broadcasts `notifications/resources/list_changed` so clients re-run `resources/list`. Call it after a tool adds or removes resources at runtime.
127
+
128
+ ```typescript
129
+ async execute(input: { id: string; title: string }) {
130
+ await this.get(NOTES).save(input.id, input.title);
131
+ this.notifyResourceUpdated(`notes://${input.id}`); // subscribers re-fetch this resource
132
+ return { ok: true };
133
+ }
134
+ ```
135
+
136
+ See [`18-tool-with-progress-and-notify`](../examples/18-tool-with-progress-and-notify.md).
137
+
138
+ ## Early return: `this.respond`
139
+
140
+ Both `return value` and `this.respond(value)` validate against `outputSchema`. `this.respond` throws an internal `FlowControl.respond` and never returns — useful for early-exit branches:
141
+
142
+ ```typescript
143
+ async execute(input: Input) {
144
+ const cached = await this.tryGet(CACHE)?.get(input.key);
145
+ if (cached) this.respond(cached); // never returns; just for early exit
146
+
147
+ const result = await this.compute(input);
148
+ return result;
149
+ }
150
+ ```
151
+
152
+ > `this.respond` doesn't bypass `outputSchema` — its argument is validated like a normal return value.
153
+
154
+ ## See also
155
+
156
+ - [`error-handling.md`](./error-handling.md)
157
+ - [`auth-providers.md`](./auth-providers.md) — `this.authProviders` (and `this.auth` for user identity / claims)
158
+ - [`elicitation.md`](./elicitation.md) — `this.elicit`
@@ -0,0 +1,96 @@
1
+ ---
2
+ name: file-layout
3
+ description: Flat sibling vs folder-per-tool layouts. <name>.schema.ts / <name>.tool.ts / <name>.tool.spec.ts.
4
+ ---
5
+
6
+ # File layout for tools
7
+
8
+ Two endorsed layouts. Pick based on tool count per app and whether each tool has local helpers / fixtures / error types.
9
+
10
+ ## Flat siblings (≤3 tools per app, or each tool fits in one screen)
11
+
12
+ ```text
13
+ src/apps/main/tools/
14
+ ├── get-weather.tool.ts # @Tool class, execute()
15
+ ├── get-weather.schema.ts # input/output schemas + derived types
16
+ ├── get-weather.tool.spec.ts # unit tests
17
+ ├── greet-user.tool.ts
18
+ ├── greet-user.schema.ts
19
+ └── greet-user.tool.spec.ts
20
+ ```
21
+
22
+ ## Folder-per-tool (>3 tools per app, or tool has helpers / fixtures / errors)
23
+
24
+ ```text
25
+ src/apps/main/tools/
26
+ ├── get-weather/
27
+ │ ├── get-weather.tool.ts # @Tool class, execute()
28
+ │ ├── get-weather.schema.ts # input/output schemas + derived types
29
+ │ ├── get-weather.tool.spec.ts # unit tests
30
+ │ ├── get-weather.errors.ts # GetWeatherUnavailableError etc.
31
+ │ ├── get-weather.fixtures.ts # test fixtures, shared with the spec
32
+ │ ├── helpers.ts # tool-local utility functions
33
+ │ ├── index.ts # barrel re-export
34
+ │ └── get-weather.widget.tsx # optional UI widget (if ui: { file: … })
35
+ └── greet-user/
36
+ └── …
37
+ ```
38
+
39
+ `index.ts` for the folder layout:
40
+
41
+ ```typescript
42
+ // src/apps/main/tools/get-weather/index.ts
43
+ export { GetWeatherTool } from './get-weather.tool';
44
+ export {
45
+ inputSchema as getWeatherInputSchema,
46
+ outputSchema as getWeatherOutputSchema,
47
+ type GetWeatherInput,
48
+ type GetWeatherOutput,
49
+ } from './get-weather.schema';
50
+ ```
51
+
52
+ ## File-name conventions
53
+
54
+ | File | Purpose |
55
+ | --------------------- | -------------------------------------------------------------------------------------------- |
56
+ | `<name>.tool.ts` | The `@Tool`-decorated class (or `tool({...})(handler)` value) |
57
+ | `<name>.schema.ts` | `inputSchema`, `outputSchema`, and the derived `Input` / `Output` types |
58
+ | `<name>.tool.spec.ts` | Unit test (jest). NOT `.test.ts` — see [CLAUDE.md test-file-naming rule](../../../CLAUDE.md) |
59
+ | `<name>.widget.tsx` | Optional UI widget — `.widget.tsx` is excluded from server typecheck (#445 fix) |
60
+ | `<name>.errors.ts` | Optional — custom `PublicMcpError` subclasses for this tool |
61
+ | `<name>.fixtures.ts` | Optional — test fixtures the spec imports |
62
+
63
+ ## Why split schema and tool?
64
+
65
+ ```typescript
66
+ // ✅ Schema in its own file — re-importable by:
67
+ // - the tool itself
68
+ // - the .tool.spec.ts file
69
+ // - sibling tools (e.g. the create-X tool may share a schema field with the update-X tool)
70
+ // - generated clients
71
+ // - elicitation flows that reuse the same Zod field
72
+
73
+ // src/apps/main/tools/get-weather.schema.ts
74
+ export const inputSchema = { city: z.string() };
75
+ export const outputSchema = { temperatureF: z.number() };
76
+ export type GetWeatherInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
77
+ export type GetWeatherOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
78
+ ```
79
+
80
+ If you hoisted the entire `@Tool({...})` config, consumers would drag the `@Tool` decorator transitively. Schemas alone are inert.
81
+
82
+ ## Naming
83
+
84
+ - File names: `kebab-case` — `get-weather.tool.ts`.
85
+ - Class names: `PascalCase` — `GetWeatherTool`. The `Tool` suffix is conventional, not required.
86
+ - Tool `name:` field: `snake_case` — `get_weather`. MCP protocol convention. ([rule](../rules/snake-case-tool-names.md))
87
+
88
+ ## Widget files
89
+
90
+ `.tsx` / `.jsx` widget files use the `*.widget.tsx` naming convention. The scaffolded `tsconfig.json` excludes `**/*.widget.tsx` from the server typecheck (#445 fix) — widgets are bundled separately by `@frontmcp/uipack` (esbuild) at render time, with React loaded externally. If you want IDE typecheck for widget sources, add a sibling `tsconfig.widget.json` with `jsx: 'react-jsx'` and `include: ['src/**/*.widget.tsx']`.
91
+
92
+ ## See also
93
+
94
+ - [`derived-types.md`](./derived-types.md) — why schemas hoist
95
+ - [`testing.md`](./testing.md) — `.tool.spec.ts` patterns
96
+ - [`ui-widgets.md`](./ui-widgets.md) — widget file conventions
@@ -0,0 +1,118 @@
1
+ ---
2
+ name: function-style-builder
3
+ description: tool({...})(handler) — when to pick over a class, register, ctx parameter.
4
+ ---
5
+
6
+ # Function-style tools — `tool({...})(handler)`
7
+
8
+ For simple tools that don't need DI, lifecycle hooks, or UI widgets, the `tool()` function builder is a one-liner alternative to a class.
9
+
10
+ ## Shape
11
+
12
+ ```typescript
13
+ import { tool, z } from '@frontmcp/sdk';
14
+
15
+ const AddNumbers = tool({
16
+ name: 'add_numbers',
17
+ description: 'Add two numbers',
18
+ inputSchema: {
19
+ a: z.number().describe('First number'),
20
+ b: z.number().describe('Second number'),
21
+ },
22
+ outputSchema: 'number',
23
+ })((input) => input.a + input.b);
24
+ ```
25
+
26
+ Register it the same way as a class tool:
27
+
28
+ ```typescript
29
+ @App({ name: 'main', tools: [AddNumbers] })
30
+ class MainApp {}
31
+ ```
32
+
33
+ ## With `ctx`
34
+
35
+ The handler receives `(input, ctx)`. `ctx` is the tool's `ToolContext`, but from an external `(input, ctx) => …` handler you can only reach its **public** surface — the same members a class tool reaches via `this.*` that aren't `protected`:
36
+
37
+ ```typescript
38
+ import { PublicMcpError } from '@frontmcp/sdk';
39
+
40
+ const GetCurrentUser = tool({
41
+ name: 'get_current_user',
42
+ description: 'Return the authenticated user',
43
+ inputSchema: {},
44
+ outputSchema: { id: z.string(), email: z.string().email() },
45
+ })(async (_input, ctx) => {
46
+ // Identity comes from `ctx.auth` (not `ctx.context.authInfo`, which is the raw access token).
47
+ const userId = ctx.auth.user?.sub;
48
+ // `ctx.fail(...)` is protected → not callable here. Throw a PublicMcpError instead.
49
+ if (!userId) throw new PublicMcpError('No authenticated user');
50
+ const users = ctx.get(USER_SERVICE);
51
+ return users.findById(userId);
52
+ });
53
+ ```
54
+
55
+ `ctx` provides (public): `get`, `tryGet`, `mark`, `fetch`, `respond`, `context`, `scope`, `input`, `metadata`, `isPlatform`, `isRuntime`, `isEnv`, `callTool`, `auth`, `config`, `clientInfo`, `platform`.
56
+
57
+ > `fail`, `notify`, `progress`, and `elicit` are **`protected`** on `ToolContext` — they're reachable only from inside a class tool's `execute()` (via `this.*`), **not** from an external function-builder handler (calling them on `ctx` is a compile error). To signal a failure from a function tool, `throw new PublicMcpError(...)`. If you need `notify` / `progress` / `elicit`, use a class tool.
58
+
59
+ ## When to pick which
60
+
61
+ | Class (`@Tool` + `extends ToolContext`) | Function (`tool({...})(handler)`) |
62
+ | ------------------------------------------------- | -------------------------------------- |
63
+ | Needs DI (`this.get`) — most production tools | Pure-input math / formatting / parsing |
64
+ | Needs lifecycle / hooks | One-off conversions |
65
+ | Needs a `ui:` widget | No bridge / no widget |
66
+ | Wants a `.tool.spec.ts` with module-level helpers | Spec testing via simple closure |
67
+ | Needs to be extended / decorated | Standalone |
68
+
69
+ **Default to class.** Pick function only for tools that are trivially short AND don't need DI.
70
+
71
+ ## Async vs sync
72
+
73
+ The handler can be sync or async — both are fine:
74
+
75
+ ```typescript
76
+ tool({ … })((input) => input.a + input.b); // sync
77
+ tool({ … })(async (input, ctx) => { /* … */ }); // async
78
+ ```
79
+
80
+ ## All the decorator options work
81
+
82
+ `rateLimit`, `concurrency`, `timeout`, `annotations`, `authProviders`, `availableWhen`, `examples`, `visibility` (and the deprecated `hideFromDiscovery` alias) are all valid on the function builder:
83
+
84
+ ```typescript
85
+ const SendEmail = tool({
86
+ name: 'send_email',
87
+ description: 'Send an email via SendGrid',
88
+ inputSchema: { to: z.string().email(), subject: z.string(), body: z.string() },
89
+ outputSchema: { messageId: z.string() },
90
+ rateLimit: { maxRequests: 100, windowMs: 60_000 },
91
+ authProviders: ['sendgrid'],
92
+ annotations: { openWorldHint: true },
93
+ })(async (input, ctx) => {
94
+ const headers = await ctx.authProviders.headers('sendgrid');
95
+ // …
96
+ });
97
+ ```
98
+
99
+ The `ui:` block also works:
100
+
101
+ ```typescript
102
+ const ShowCard = tool({
103
+ name: 'show_card',
104
+ inputSchema: { text: z.string() },
105
+ outputSchema: { text: z.string() },
106
+ ui: {
107
+ template: (ctx) => `<div>${ctx.helpers.escapeHtml(ctx.output.text)}</div>`,
108
+ },
109
+ })((input) => ({ text: input.text }));
110
+ ```
111
+
112
+ …but at that point, you usually want a class for the file layout and `.tool.spec.ts` ergonomics.
113
+
114
+ ## See also
115
+
116
+ - [`02-basic-function-tool`](../examples/02-basic-function-tool.md)
117
+ - [`execution-context.md`](./execution-context.md) — same surface available on `ctx`
118
+ - [`registration.md`](./registration.md)
@@ -0,0 +1,141 @@
1
+ ---
2
+ name: input-schema
3
+ description: Define the tool's input contract — raw Zod shapes, refinements, defaults, optional fields.
4
+ ---
5
+
6
+ # `inputSchema` reference
7
+
8
+ The `inputSchema` field on `@Tool({...})` accepts a **Zod raw shape** — a plain object mapping field names to Zod types. The framework wraps it in `z.object(...)` internally and validates every call.
9
+
10
+ ## The raw-shape rule
11
+
12
+ ```typescript
13
+ // ✅ Raw shape
14
+ @Tool({
15
+ name: 'search',
16
+ inputSchema: {
17
+ query: z.string().min(1),
18
+ limit: z.number().int().min(1).max(100).default(10),
19
+ },
20
+ })
21
+
22
+ // ❌ z.object() at the top level
23
+ @Tool({
24
+ name: 'search',
25
+ inputSchema: z.object({
26
+ query: z.string(),
27
+ }),
28
+ })
29
+ ```
30
+
31
+ See [`rules/input-schema-is-raw-shape.md`](../rules/input-schema-is-raw-shape.md). The wrapper is the framework's job — wrapping it yourself confuses the type inference and breaks `ToolInputOf<>`.
32
+
33
+ ## Field types
34
+
35
+ | Want | Zod |
36
+ | ------------------- | ------------------------------------------------------------------------ |
37
+ | Required string | `z.string()` |
38
+ | Optional string | `z.string().optional()` |
39
+ | String with default | `z.string().default('hello')` |
40
+ | Bounded number | `z.number().int().min(1).max(100)` |
41
+ | Number with default | `z.number().default(10)` |
42
+ | Enum | `z.enum(['a', 'b', 'c'])` |
43
+ | Array | `z.array(z.string())` |
44
+ | Nested object | `z.object({ city: z.string() })` (Zod object **inside** a field is fine) |
45
+ | Discriminated union | `z.discriminatedUnion('kind', […])` |
46
+ | Date | `z.string().datetime()` (ISO 8601) or `z.date()` |
47
+ | URL | `z.string().url()` |
48
+ | Email | `z.string().email()` |
49
+ | Refined | `z.string().refine((v) => v.length % 2 === 0, 'must be even length')` |
50
+ | Branded | `z.string().brand<'UserId'>()` |
51
+
52
+ > Only the **top-level** `inputSchema` must be a raw shape. Nested objects use `z.object({...})` normally.
53
+
54
+ ## Descriptions
55
+
56
+ Every field should carry `.describe('…')` — it's shown to AI clients in `tools/list`, helping them choose argument values:
57
+
58
+ ```typescript
59
+ inputSchema: {
60
+ city: z.string().describe('City name, e.g. "Seattle" or "Tel Aviv"'),
61
+ units: z.enum(['celsius', 'fahrenheit']).default('celsius').describe('Temperature units'),
62
+ }
63
+ ```
64
+
65
+ ## Optional vs default
66
+
67
+ ```typescript
68
+ inputSchema: {
69
+ // Required — caller must provide
70
+ query: z.string(),
71
+
72
+ // Optional — execute() sees `string | undefined`
73
+ category: z.string().optional(),
74
+
75
+ // Optional with default — execute() sees `string` (the default fills in)
76
+ limit: z.number().default(10),
77
+ }
78
+ ```
79
+
80
+ ## Refinements
81
+
82
+ For cross-field validation, wrap individual fields:
83
+
84
+ ```typescript
85
+ inputSchema: {
86
+ start: z.string().datetime(),
87
+ end: z.string().datetime(),
88
+ // single-field refinements:
89
+ email: z.string().email().refine((v) => v.endsWith('@example.com'), 'must be a corporate email'),
90
+ }
91
+ ```
92
+
93
+ For cross-field refinements (`start < end`), keep `inputSchema` simple and validate in `execute()`:
94
+
95
+ ```typescript
96
+ async execute(input: { start: string; end: string }) {
97
+ if (input.start >= input.end) {
98
+ this.fail(new InvalidInputError('start must be before end'));
99
+ }
100
+ // …
101
+ }
102
+ ```
103
+
104
+ (Or use a custom `.transform` / `.refine` on a wrapped `z.object({...})` _outside_ `inputSchema` and pass `.shape` — but that's usually overkill.)
105
+
106
+ ## Empty input
107
+
108
+ A no-input tool declares an empty shape:
109
+
110
+ ```typescript
111
+ @Tool({
112
+ name: 'ping',
113
+ inputSchema: {},
114
+ outputSchema: 'string',
115
+ })
116
+ class PingTool extends ToolContext {
117
+ execute(): string {
118
+ return 'pong';
119
+ }
120
+ }
121
+ ```
122
+
123
+ ## Where the validated input lives
124
+
125
+ After validation, `execute(input)` receives the typed/defaulted input. It's also available via `this.input` if you'd rather:
126
+
127
+ ```typescript
128
+ async execute(_input: SearchInput) {
129
+ // these are equivalent:
130
+ const fromArg = _input.query;
131
+ const fromCtx = this.input.query;
132
+ }
133
+ ```
134
+
135
+ Prefer the parameter — it's typed without needing the `ToolInputOf<>` annotation on `this.input`.
136
+
137
+ ## See also
138
+
139
+ - [`output-schema.md`](./output-schema.md)
140
+ - [`derived-types.md`](./derived-types.md)
141
+ - [`rules/input-schema-is-raw-shape.md`](../rules/input-schema-is-raw-shape.md)