@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.
- 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 +9 -2
- 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-http.md +149 -15
- package/catalog/frontmcp-deployment/SKILL.md +15 -13
- 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 +38 -2
- package/catalog/frontmcp-development/SKILL.md +30 -44
- 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 +1 -1
- package/catalog/frontmcp-observability/SKILL.md +1 -1
- 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 +1 -1
- package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
- package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
- package/catalog/frontmcp-testing/SKILL.md +9 -1
- package/catalog/frontmcp-testing/references/test-auth.md +24 -0
- package/catalog/skills-manifest.json +653 -149
- 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 -80
- package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -132
- package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -110
- 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 -806
|
@@ -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)
|