@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,175 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: output-schema
|
|
3
|
+
description: Define the tool's output contract — Zod shape, primitives, media, multi-content arrays.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `outputSchema` reference
|
|
7
|
+
|
|
8
|
+
`outputSchema` is **always required** ([rule](../rules/always-define-output-schema.md)). It declares what `execute()` returns and gives the framework permission to strip any fields you didn't declare — the safety net against accidental PII / token / debug-trace leaks.
|
|
9
|
+
|
|
10
|
+
## Supported shapes
|
|
11
|
+
|
|
12
|
+
| Shape | Use for | Returns |
|
|
13
|
+
| --------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------ |
|
|
14
|
+
| **Zod raw shape** | Structured JSON | `{ field: z.string(), count: z.number() }` |
|
|
15
|
+
| **Zod schema** | Complex types (unions, discriminated unions, arrays of objects, transforms) | `z.object({…})`, `z.array(…)`, `z.discriminatedUnion('kind', […])` |
|
|
16
|
+
| **Primitive literal** | Single value | `'string'`, `'number'`, `'boolean'`, `'date'` |
|
|
17
|
+
| **Media literal** | Binary / link content | `'image'`, `'audio'`, `'resource'`, `'resource_link'` |
|
|
18
|
+
| **Array of literals** | Multi-content response | `['string', 'image']` — text + image in one response |
|
|
19
|
+
|
|
20
|
+
## Zod raw shape (most common)
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
const outputSchema = {
|
|
24
|
+
temperatureF: z.number(),
|
|
25
|
+
conditions: z.string(),
|
|
26
|
+
humidityPct: z.number().int().min(0).max(100),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
@Tool({ name: 'get_weather', inputSchema, outputSchema })
|
|
30
|
+
class GetWeatherTool extends ToolContext {
|
|
31
|
+
async execute(input: GetWeatherInput): Promise<GetWeatherOutput> {
|
|
32
|
+
const data = await this.fetch(`https://api.weather.example/${input.city}`).then((r) => r.json());
|
|
33
|
+
// Even if `data` contains { temperatureF, conditions, internalApiKey, debugTrace, … }
|
|
34
|
+
// only temperatureF / conditions / humidityPct flow through.
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Zod schema (full Zod)
|
|
41
|
+
|
|
42
|
+
When the output is a union, discriminated union, or array of objects:
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
const outputSchema = z.discriminatedUnion('kind', [
|
|
46
|
+
z.object({ kind: z.literal('user'), id: z.string(), name: z.string() }),
|
|
47
|
+
z.object({ kind: z.literal('group'), id: z.string(), members: z.array(z.string()) }),
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
@Tool({ name: 'resolve_principal', inputSchema, outputSchema })
|
|
51
|
+
class ResolvePrincipalTool extends ToolContext {
|
|
52
|
+
async execute(input: { handle: string }) {
|
|
53
|
+
return { kind: 'user' as const, id: 'u_1', name: 'Ada' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`z.object()` is fine here — it's only the **top-level `inputSchema`** that must be a raw shape, not `outputSchema`.
|
|
59
|
+
|
|
60
|
+
## Primitive literals
|
|
61
|
+
|
|
62
|
+
For single-value outputs:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
@Tool({ name: 'add', inputSchema: { a: z.number(), b: z.number() }, outputSchema: 'number' })
|
|
66
|
+
class AddTool extends ToolContext {
|
|
67
|
+
execute(input: { a: number; b: number }): number {
|
|
68
|
+
return input.a + input.b;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Tool({ name: 'now', inputSchema: {}, outputSchema: 'date' })
|
|
73
|
+
class NowTool extends ToolContext {
|
|
74
|
+
execute(): Date {
|
|
75
|
+
return new Date();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Media literals
|
|
81
|
+
|
|
82
|
+
Binary content or links to MCP resources:
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
@Tool({ name: 'render_chart', inputSchema, outputSchema: 'image' })
|
|
86
|
+
class RenderChartTool extends ToolContext {
|
|
87
|
+
async execute(input: ChartInput): Promise<{ type: 'image'; data: string; mimeType: string }> {
|
|
88
|
+
return {
|
|
89
|
+
type: 'image' as const, // required — without it the content block is dropped
|
|
90
|
+
data: 'iVBORw0KGgoAAAANSU…', // base64
|
|
91
|
+
mimeType: 'image/png',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each return object must carry the matching `type` discriminator (`'image'`, `'audio'`, `'resource'`, `'resource_link'`) — without it the content block is silently dropped:
|
|
98
|
+
|
|
99
|
+
| Literal | Return shape |
|
|
100
|
+
| ----------------- | ---------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `'image'` | `{ type: 'image', data: base64String, mimeType: 'image/png' \| 'image/jpeg' \| … }` |
|
|
102
|
+
| `'audio'` | `{ type: 'audio', data: base64String, mimeType: 'audio/wav' \| 'audio/mpeg' \| … }` |
|
|
103
|
+
| `'resource'` | `{ type: 'resource', resource: { uri: 'custom://…', mimeType?, text? \| blob? } }` |
|
|
104
|
+
| `'resource_link'` | `{ type: 'resource_link', uri: 'custom://…' }` (link only — host fetches via `resources/read`) |
|
|
105
|
+
|
|
106
|
+
See [`26-tool-with-resource-link-output`](../examples/26-tool-with-resource-link-output.md) for the resource-link pattern.
|
|
107
|
+
|
|
108
|
+
## Multi-content arrays
|
|
109
|
+
|
|
110
|
+
Some tools return more than one block — e.g. a text summary plus an image:
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
@Tool({ name: 'analyze_image', inputSchema, outputSchema: ['string', 'image'] })
|
|
114
|
+
class AnalyzeImageTool extends ToolContext {
|
|
115
|
+
async execute(input: { imageUrl: string }): Promise<[string, { type: 'image'; data: string; mimeType: string }]> {
|
|
116
|
+
const summary = 'Detected: 2 people, 1 cat.';
|
|
117
|
+
const annotated = await this.annotate(input.imageUrl);
|
|
118
|
+
return [summary, { type: 'image' as const, data: annotated, mimeType: 'image/png' }];
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Advertisement in `tools/list`
|
|
124
|
+
|
|
125
|
+
Structured object outputs are converted to JSON Schema and advertised on the tool's `outputSchema` in `tools/list` — symmetric with how `inputSchema` is advertised:
|
|
126
|
+
|
|
127
|
+
- **Advertised:** a Zod raw shape (`{ field: z.string() }`) or a top-level `z.object({...})` — these serialize to a `type: 'object'` JSON Schema.
|
|
128
|
+
- **Not advertised:** primitives (`'string'` / `'number'` / `'boolean'` / `'date'`), media (`'image'` / `'audio'` / `'resource'` / `'resource_link'`), multi-content arrays (`['string', 'image']`), and unions (`z.discriminatedUnion(...)`). These flow through `content` rather than `structuredContent`, and the MCP spec requires `outputSchema` to be a top-level `type: 'object'` — so there is nothing object-shaped to advertise. Runtime output validation still applies to every form.
|
|
129
|
+
|
|
130
|
+
If a client reports _"Output schema recommended,"_ declare a structured object `outputSchema` (a raw shape or `z.object`) — that's the form that gets advertised.
|
|
131
|
+
|
|
132
|
+
### Exposure mode
|
|
133
|
+
|
|
134
|
+
A cascading `output` policy controls _how_ the (object-shaped) schema reaches clients in `tools/list`. Declare it on `@FrontMcp`, `@App`, or `@Tool`:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
output?: {
|
|
138
|
+
schemaMode?: 'definition' | 'description' | 'both' | 'none'; // default 'definition'
|
|
139
|
+
schemaDescriptionFormat?: 'summary' | 'jsonSchema'; // default 'summary'
|
|
140
|
+
};
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
| `schemaMode` | Effect |
|
|
144
|
+
| --------------- | --------------------------------------------------------------------------------------- |
|
|
145
|
+
| `'definition'` | **Default.** Advertise the schema as the tool's `outputSchema` (JSON Schema). |
|
|
146
|
+
| `'description'` | Fold a readable rendering of the schema into the tool description; omit `outputSchema`. |
|
|
147
|
+
| `'both'` | Advertise as `outputSchema` **and** fold it into the description. |
|
|
148
|
+
| `'none'` | Expose nothing — no `outputSchema`, no description suffix. |
|
|
149
|
+
|
|
150
|
+
`schemaDescriptionFormat` controls the rendering for `'description'` / `'both'`: `'summary'` (default) is a compact property list; `'jsonSchema'` is a fenced JSON Schema block.
|
|
151
|
+
|
|
152
|
+
The effective value resolves with a **Tool > App > server > default** cascade — set a server-wide default on `@FrontMcp` and override per tool. This mirrors the OpenAPI adapter's `outputSchema.mode`.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// This tool folds its schema into the description instead of advertising `outputSchema`
|
|
156
|
+
@Tool({ name: 'get_status', inputSchema, outputSchema, output: { schemaMode: 'description' } })
|
|
157
|
+
class GetStatusTool extends ToolContext {
|
|
158
|
+
/* … */
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The default (`'definition'`) preserves the current behavior described above — omit `output` entirely and nothing changes.
|
|
163
|
+
|
|
164
|
+
## Why this matters
|
|
165
|
+
|
|
166
|
+
- **Data leak prevention** — without `outputSchema`, accidentally returning `{ result, internalApiKey }` leaks the key. With it, only `result` flows.
|
|
167
|
+
- **CodeCall compatibility** — the CodeCall plugin uses `outputSchema` to chain tool calls in its VM. Tools without it degrade chain-ability.
|
|
168
|
+
- **Compile-time type safety** — `ToolContext` infers `execute()`'s return type from `outputSchema` (when `ToolOutputOf<>` is used). The compiler catches divergence at build time.
|
|
169
|
+
- **Self-documenting** — a structured object `outputSchema` is converted to JSON Schema and exposed in `tools/list` (see [Advertisement in `tools/list`](#advertisement-in-toolslist)); AI clients pick tools partly based on what they return.
|
|
170
|
+
|
|
171
|
+
## See also
|
|
172
|
+
|
|
173
|
+
- [`input-schema.md`](./input-schema.md)
|
|
174
|
+
- [`derived-types.md`](./derived-types.md)
|
|
175
|
+
- [`rules/always-define-output-schema.md`](../rules/always-define-output-schema.md)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quick-start
|
|
3
|
+
description: 60-second tour — minimal tool, schemas, registration, calling it.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Quick start
|
|
7
|
+
|
|
8
|
+
Goal: a working tool in five files (schema, tool, app, server, spec) in 60 seconds.
|
|
9
|
+
|
|
10
|
+
## 1. The schemas (single source of truth)
|
|
11
|
+
|
|
12
|
+
```typescript
|
|
13
|
+
// src/apps/main/tools/greet-user.schema.ts
|
|
14
|
+
import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';
|
|
15
|
+
|
|
16
|
+
export const inputSchema = {
|
|
17
|
+
name: z.string().describe('The name of the user to greet'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const outputSchema = {
|
|
21
|
+
greeting: z.string(),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type GreetUserInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
|
|
25
|
+
export type GreetUserOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## 2. The tool
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
// src/apps/main/tools/greet-user.tool.ts
|
|
32
|
+
import { Tool, ToolContext } from '@frontmcp/sdk';
|
|
33
|
+
|
|
34
|
+
import { inputSchema, outputSchema, type GreetUserInput, type GreetUserOutput } from './greet-user.schema';
|
|
35
|
+
|
|
36
|
+
@Tool({
|
|
37
|
+
name: 'greet_user',
|
|
38
|
+
description: 'Greet a user by name',
|
|
39
|
+
inputSchema,
|
|
40
|
+
outputSchema,
|
|
41
|
+
})
|
|
42
|
+
export class GreetUserTool extends ToolContext {
|
|
43
|
+
async execute(input: GreetUserInput): Promise<GreetUserOutput> {
|
|
44
|
+
return { greeting: `Hello, ${input.name}!` };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## 3. The app
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
// src/apps/main/index.ts
|
|
53
|
+
import { App } from '@frontmcp/sdk';
|
|
54
|
+
|
|
55
|
+
import { GreetUserTool } from './tools/greet-user.tool';
|
|
56
|
+
|
|
57
|
+
@App({
|
|
58
|
+
name: 'main',
|
|
59
|
+
tools: [GreetUserTool],
|
|
60
|
+
})
|
|
61
|
+
export class MainApp {}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## 4. The server
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
// src/main.ts
|
|
68
|
+
import { FrontMcp } from '@frontmcp/sdk';
|
|
69
|
+
|
|
70
|
+
import { MainApp } from './apps/main';
|
|
71
|
+
|
|
72
|
+
@FrontMcp({
|
|
73
|
+
info: { name: 'demo', version: '1.0.0' },
|
|
74
|
+
apps: [MainApp],
|
|
75
|
+
})
|
|
76
|
+
export default class DemoServer {}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## 5. The test
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// src/apps/main/tools/greet-user.e2e.spec.ts
|
|
83
|
+
import { expect, test } from '@frontmcp/testing';
|
|
84
|
+
|
|
85
|
+
test.use({
|
|
86
|
+
server: './src/main.ts',
|
|
87
|
+
port: 3003,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('greets the user', async ({ mcp }) => {
|
|
91
|
+
// mcp.tools.call returns a ToolResultWrapper around the MCP CallToolResult —
|
|
92
|
+
// not the bare return value. Assert on the MCP result, not on `{ greeting }` directly.
|
|
93
|
+
const result = await mcp.tools.call('greet_user', { name: 'Ada' });
|
|
94
|
+
|
|
95
|
+
expect(result).toBeSuccessful();
|
|
96
|
+
// structuredContent mirrors the typed output schema.
|
|
97
|
+
expect(result.json()).toEqual({ greeting: 'Hello, Ada!' });
|
|
98
|
+
// …or assert on the rendered text content.
|
|
99
|
+
expect(result).toHaveTextContent('Hello, Ada!');
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Run it
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
yarn dev # starts the server
|
|
107
|
+
yarn test # runs the spec
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## What you just did
|
|
111
|
+
|
|
112
|
+
- Hoisted the **schemas** to their own file (so specs / generated clients can reuse them).
|
|
113
|
+
- Derived `execute()`'s **input/output types** from the schemas via `ToolInputOf<>` / `ToolOutputOf<>`. The schema is the single source of truth — change a Zod field and the type follows.
|
|
114
|
+
- Used a **raw Zod shape** for `inputSchema` (not `z.object({...})`) — the framework wraps it internally.
|
|
115
|
+
- Always defined an **`outputSchema`**. Without it, any field your code accidentally returns leaks to the client.
|
|
116
|
+
- Registered the tool in an **`@App({ tools })`**, not directly on `@FrontMcp` — apps own modularity and per-app lifecycle / auth.
|
|
117
|
+
- Wrote a test using the **`@frontmcp/testing`** fixture API (`test.use` + `{ mcp }`) — booting the real server and asserting on the MCP `CallToolResult` (via `result.json()` / `toHaveTextContent`), not on a bare return value.
|
|
118
|
+
|
|
119
|
+
## What's next
|
|
120
|
+
|
|
121
|
+
- Add a real implementation → read [`execution-context.md`](./execution-context.md) for `this.get`, `this.fetch`, `this.notify`.
|
|
122
|
+
- Return structured / media / multi-content output → [`output-schema.md`](./output-schema.md).
|
|
123
|
+
- Make it interactive with a UI widget → [`ui-widgets.md`](./ui-widgets.md).
|
|
124
|
+
- Pick an example matching your scenario from [`SKILL.md` § Scenario routing table](../SKILL.md#scenario-routing-table).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: registration
|
|
3
|
+
description: @App({ tools }) vs @FrontMcp({ tools }), multi-app composition.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Registering tools
|
|
7
|
+
|
|
8
|
+
## Best practice — register in `@App`
|
|
9
|
+
|
|
10
|
+
```typescript
|
|
11
|
+
// src/apps/main/index.ts
|
|
12
|
+
import { App } from '@frontmcp/sdk';
|
|
13
|
+
|
|
14
|
+
import { AddNumbersTool, GreetUserTool, SearchDocumentsTool } from './tools';
|
|
15
|
+
|
|
16
|
+
@App({
|
|
17
|
+
name: 'main',
|
|
18
|
+
tools: [GreetUserTool, SearchDocumentsTool, AddNumbersTool],
|
|
19
|
+
})
|
|
20
|
+
export class MainApp {}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// src/main.ts
|
|
25
|
+
import { FrontMcp } from '@frontmcp/sdk';
|
|
26
|
+
|
|
27
|
+
import { MainApp } from './apps/main';
|
|
28
|
+
|
|
29
|
+
@FrontMcp({
|
|
30
|
+
info: { name: 'demo', version: '1.0.0' },
|
|
31
|
+
apps: [MainApp],
|
|
32
|
+
})
|
|
33
|
+
export default class DemoServer {}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Apps provide:
|
|
37
|
+
|
|
38
|
+
- **Modularity** — each app is a self-contained surface; you can install / uninstall / disable apps without touching the others.
|
|
39
|
+
- **Per-app auth** — different apps can use different `auth: { mode: 'public' | 'transparent' | 'local' | 'remote' }`.
|
|
40
|
+
- **Per-app providers** — DI tokens registered in `@App({ providers })` are visible only to tools in that app.
|
|
41
|
+
- **Per-app lifecycle hooks** — `onAppStart`, `onAppStop`, etc.
|
|
42
|
+
|
|
43
|
+
## Escape hatch — top-level `@FrontMcp({ tools })`
|
|
44
|
+
|
|
45
|
+
For single-app servers, you can register tools directly on `@FrontMcp` instead of declaring an `@App`:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
@FrontMcp({
|
|
49
|
+
info: { name: 'demo', version: '1.0.0' },
|
|
50
|
+
tools: [GreetUserTool, SearchDocumentsTool],
|
|
51
|
+
})
|
|
52
|
+
export default class DemoServer {}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`@FrontMcp` accepts the same arrays as `@App`: `tools`, `resources`, `prompts`, `providers`, `plugins`, `jobs`, `channels`, `authorities`, `skills`.
|
|
56
|
+
|
|
57
|
+
Use this for prototypes / very small servers. Promote to an `@App` as soon as you want any of the per-app benefits above.
|
|
58
|
+
|
|
59
|
+
See [`rules/register-in-app.md`](../rules/register-in-app.md).
|
|
60
|
+
|
|
61
|
+
## Multi-app composition
|
|
62
|
+
|
|
63
|
+
Real-world servers have multiple apps. Each app owns its own tools, providers, and (optionally) auth mode:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
@FrontMcp({
|
|
67
|
+
info: { name: 'company-mcp', version: '1.0.0' },
|
|
68
|
+
apps: [PublicApp, AuthenticatedApp, AdminApp],
|
|
69
|
+
})
|
|
70
|
+
export default class CompanyServer {}
|
|
71
|
+
|
|
72
|
+
@App({
|
|
73
|
+
name: 'public',
|
|
74
|
+
auth: { mode: 'public', anonymousScopes: ['read:public'] },
|
|
75
|
+
tools: [SearchPublicDocsTool],
|
|
76
|
+
})
|
|
77
|
+
class PublicApp {}
|
|
78
|
+
|
|
79
|
+
@App({
|
|
80
|
+
name: 'authenticated',
|
|
81
|
+
auth: { mode: 'remote', clientId: process.env.OAUTH_CLIENT_ID },
|
|
82
|
+
tools: [GetMyProfileTool, UpdateMyProfileTool],
|
|
83
|
+
})
|
|
84
|
+
class AuthenticatedApp {}
|
|
85
|
+
|
|
86
|
+
@App({
|
|
87
|
+
name: 'admin',
|
|
88
|
+
auth: { mode: 'remote', requiredScopes: ['admin'] },
|
|
89
|
+
tools: [DeleteUserTool, GrantRoleTool],
|
|
90
|
+
})
|
|
91
|
+
class AdminApp {}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Tool names must be unique **across the whole server** — even though they live in different apps. The tool name is the lookup key in `tools/call`.
|
|
95
|
+
|
|
96
|
+
## Tool sharing across apps
|
|
97
|
+
|
|
98
|
+
Same tool registered in two apps:
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
@App({ name: 'public', tools: [SearchTool] })
|
|
102
|
+
@App({ name: 'authenticated', tools: [SearchTool] })
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
This is fine — the tool instance is constructed per-scope. Each app sees its own. But you'll want different names if the auth posture differs:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
@App({ name: 'public', tools: [SearchPublicTool] })
|
|
109
|
+
@App({ name: 'authenticated', tools: [SearchPrivateTool] })
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Conditional registration
|
|
113
|
+
|
|
114
|
+
For tools that should only register in certain envs:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
@App({
|
|
118
|
+
name: 'main',
|
|
119
|
+
tools: [
|
|
120
|
+
GreetUserTool,
|
|
121
|
+
...(process.env.NODE_ENV !== 'production' ? [DebugTool] : []),
|
|
122
|
+
],
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Better — use `availableWhen: { env: ['development', 'test'] }` on the tool itself. Same effect, plus the constraint is self-documenting on the tool.
|
|
127
|
+
|
|
128
|
+
## See also
|
|
129
|
+
|
|
130
|
+
- [`rules/register-in-app.md`](../rules/register-in-app.md)
|
|
131
|
+
- [`availability.md`](./availability.md)
|
|
132
|
+
- `architecture` skill — multi-app patterns, module boundaries, scope / DI tokens
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: remote-and-esm
|
|
3
|
+
description: Tool.esm / Tool.remote — load tools from ESM URLs or remote MCP servers.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Remote and ESM tools
|
|
7
|
+
|
|
8
|
+
Two ways to register tools you don't ship directly in your codebase:
|
|
9
|
+
|
|
10
|
+
## `Tool.esm(...)` — ESM URL
|
|
11
|
+
|
|
12
|
+
Loads a tool implementation from an ES module published to npm or hosted on a CDN.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
const RemoteTool = Tool.esm('@my-org/tools@^1.0.0', 'MyTool', {
|
|
16
|
+
description: 'A tool loaded from an ES module',
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
@App({ name: 'main', tools: [RemoteTool] })
|
|
20
|
+
class MainApp {}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
| Arg | Purpose |
|
|
24
|
+
| ------------ | -------------------------------------------------------------------------------------------------------------------- |
|
|
25
|
+
| `specifier` | npm package + optional semver range (`'@my-org/tools@^1.0.0'`) OR a full URL (`'https://esm.sh/@acme/widget@2.1.0'`) |
|
|
26
|
+
| `exportName` | Named export to load from the module |
|
|
27
|
+
| `options` | Optional override for description / annotations / throttling — the module's defaults are used otherwise |
|
|
28
|
+
|
|
29
|
+
The framework loads the module at server startup. Compatibility tip: the loaded module should export a `@Tool`-decorated class or a `tool({...})(handler)` value.
|
|
30
|
+
|
|
31
|
+
## `Tool.remote(...)` — remote MCP server
|
|
32
|
+
|
|
33
|
+
Proxies a tool from another MCP server. Tool calls hop through your server to the remote.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const CloudTool = Tool.remote('https://example.com/tools/cloud-tool', 'CloudTool', {
|
|
37
|
+
description: 'A tool loaded from a remote MCP server',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
@App({ name: 'main', tools: [CloudTool] })
|
|
41
|
+
class MainApp {}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
| Arg | Purpose |
|
|
45
|
+
| ----------- | ------------------------------------------ |
|
|
46
|
+
| `serverUrl` | Remote MCP server URL |
|
|
47
|
+
| `toolName` | The remote tool's `name` |
|
|
48
|
+
| `options` | Local overrides (description, annotations) |
|
|
49
|
+
|
|
50
|
+
The framework establishes a long-lived connection to the remote server at startup and re-uses it for every call. Auth headers from your server's session can be forwarded — configure via the remote-server registration in `@FrontMcp({ remoteServers: [...] })`.
|
|
51
|
+
|
|
52
|
+
## When to use
|
|
53
|
+
|
|
54
|
+
| Pattern | When |
|
|
55
|
+
| ---------------------------- | ------------------------------------------------------------------------------------------ |
|
|
56
|
+
| `@Tool` (class in your code) | Your tool, your code. The default. |
|
|
57
|
+
| `Tool.esm(...)` | Third-party tool packages, internal monorepo tools served via a CDN, shared tool libraries |
|
|
58
|
+
| `Tool.remote(...)` | Federation — your server exposes a tool that physically lives on another MCP server |
|
|
59
|
+
|
|
60
|
+
## Limitations
|
|
61
|
+
|
|
62
|
+
- **`Tool.esm`**: the loaded module runs in the same Node process. You inherit its dependencies. Pin versions; don't `^` against untrusted modules.
|
|
63
|
+
- **`Tool.remote`**: a remote outage means the proxied tool fails. Pair with `timeout` and consider a fallback. Auth headers may or may not be forwarded depending on your federation config.
|
|
64
|
+
|
|
65
|
+
## See also
|
|
66
|
+
|
|
67
|
+
- [`registration.md`](./registration.md)
|
|
68
|
+
- [`decorator-options.md`](./decorator-options.md)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: testing
|
|
3
|
+
description: Per-tool unit tests — pointer to the dedicated `testing` skill for the canonical patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Per-tool unit testing
|
|
7
|
+
|
|
8
|
+
Every tool ships with `<name>.tool.spec.ts`. The canonical test surface lives in the dedicated **`testing` skill** — this reference is a short pointer plus the per-tool checklist.
|
|
9
|
+
|
|
10
|
+
## What `@frontmcp/testing` actually exposes
|
|
11
|
+
|
|
12
|
+
| Surface | Purpose |
|
|
13
|
+
| ------------------------------------------------------------ | ---------------------------------------------------------- |
|
|
14
|
+
| `TestServer` (+ `TestServerOptions`) | Boot a real `@FrontMcp({...})` server in-process for tests |
|
|
15
|
+
| `McpTestClient` (+ `McpTestClientBuilder`) | Client to talk to the test server |
|
|
16
|
+
| `test` (Playwright-style fixture) + `expect` + `mcpMatchers` | Test runner and matchers |
|
|
17
|
+
| `mockResponse`, `httpMock`, `httpResponse`, `interceptors` | HTTP / outbound mocking |
|
|
18
|
+
| `TestUsers`, `TestTokenFactory`, `AuthHeaders` | Auth fixtures for authenticated tools |
|
|
19
|
+
| `MockOAuthServer`, `MockCimdServer`, `MockAPIServer` | Mocks for end-to-end auth flows |
|
|
20
|
+
|
|
21
|
+
The `testing` skill is where you'll find:
|
|
22
|
+
|
|
23
|
+
- The canonical `test({ mcp }) => …` fixture pattern.
|
|
24
|
+
- How to register a single tool / app for a focused test scope.
|
|
25
|
+
- How to assert MCP responses via `mcpMatchers` (`toHaveRenderedHtml`, `toBeXssSafe`, `toContainBoundValue`, etc.).
|
|
26
|
+
- How to mock outbound HTTP with `httpMock` (so `this.fetch` returns deterministic responses).
|
|
27
|
+
- How to inject mock providers for DI tokens.
|
|
28
|
+
- How to drive interactive `this.elicit` flows from a test.
|
|
29
|
+
- How to assert `this.progress` / `this.notify` event streams.
|
|
30
|
+
- E2E patterns (subprocess CLI exec, real-port transports — those live in `apps/e2e/demo-e2e-*/`, not in per-library specs).
|
|
31
|
+
|
|
32
|
+
## Per-tool unit test — what to cover
|
|
33
|
+
|
|
34
|
+
For every tool, the spec covers (at minimum):
|
|
35
|
+
|
|
36
|
+
- [ ] **Happy path** — typical valid input → expected output, with `mcpMatchers` asserting the shape of the response.
|
|
37
|
+
- [ ] **Input-validation rejection** — Zod constraints fire (`min`, `max`, `regex`, `enum`).
|
|
38
|
+
- [ ] **At least one business-logic failure path** — `this.fail(new SomeMcpError(...))` triggers, the client sees the right MCP error code.
|
|
39
|
+
- [ ] **Mocked DI** — providers swapped via the testing harness; verify the tool calls the right service methods.
|
|
40
|
+
- [ ] **(If `this.fetch`)** — outbound HTTP mocked via `httpMock`, asserting URL / headers / body.
|
|
41
|
+
- [ ] **(If `this.elicit`)** — pre-arrange the elicitation response in the test harness and assert the `accept` / `decline` / `cancel` branches.
|
|
42
|
+
- [ ] **(If `this.progress`)** — assert the progress events fire with the expected `current` / `total` values.
|
|
43
|
+
|
|
44
|
+
## File naming
|
|
45
|
+
|
|
46
|
+
- `<name>.tool.spec.ts` — never `.test.ts` (the repo enforces `.spec.ts`).
|
|
47
|
+
- Co-located with the tool source — `src/apps/<app>/tools/<name>/<name>.tool.spec.ts` (folder-per-tool layout) OR `src/apps/<app>/tools/<name>.tool.spec.ts` (flat sibling).
|
|
48
|
+
|
|
49
|
+
## Don't
|
|
50
|
+
|
|
51
|
+
- Don't boot the full real server (production transport, real auth, real DBs) for a per-tool unit test. Use the testing skill's focused fixtures.
|
|
52
|
+
- Don't assert the precise text of `PublicMcpError` messages. Assert the error class / `errorCode` (e.g. `'RATE_LIMIT_EXCEEDED'`, `'RESOURCE_NOT_FOUND'`) so the test isn't brittle to wording.
|
|
53
|
+
- Don't put end-to-end tests in per-library `__tests__/`. They belong in `apps/e2e/demo-e2e-*/` per the e2e-tests-location rule.
|
|
54
|
+
|
|
55
|
+
## See also
|
|
56
|
+
|
|
57
|
+
- `testing` skill — the canonical fixtures, mock patterns, and matchers
|
|
58
|
+
- [`error-handling.md`](./error-handling.md) — error codes to assert against
|
|
59
|
+
- [`execution-context.md`](./execution-context.md) — what `this.*` does, so you know what to mock
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: throttling
|
|
3
|
+
description: rateLimit, concurrency, timeout — semantics, interaction, defaults.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Throttling — `rateLimit`, `concurrency`, `timeout`
|
|
7
|
+
|
|
8
|
+
Three independent controls on `@Tool({...})`. Apply them when the tool calls expensive services, holds a scarce resource, or could legitimately hang.
|
|
9
|
+
|
|
10
|
+
## `rateLimit`
|
|
11
|
+
|
|
12
|
+
Cap invocations over a time window.
|
|
13
|
+
|
|
14
|
+
```typescript
|
|
15
|
+
@Tool({
|
|
16
|
+
name: 'send_notification',
|
|
17
|
+
// 100 calls / minute, shared across all callers (partitionBy defaults to 'global')
|
|
18
|
+
rateLimit: { maxRequests: 100, windowMs: 60_000 },
|
|
19
|
+
// For a per-session limit instead:
|
|
20
|
+
// rateLimit: { maxRequests: 100, windowMs: 60_000, partitionBy: 'session' },
|
|
21
|
+
// …
|
|
22
|
+
})
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **Partition**: `partitionBy: 'global'` by default — one shared limit across all callers. To scope the limit per session, set `partitionBy: 'session'` (the session ID becomes the rate-limit key). Other values: `'userId'`, `'ip'`, or a custom `(ctx) => string` function.
|
|
26
|
+
- **Behavior on overflow**: the tool call returns a `RateLimitError` (HTTP status 429, MCP error code `'RATE_LIMIT_EXCEEDED'`). The retry-after hint is carried in the error message string (`Rate limit exceeded. Retry after N seconds`) so clients can back off intelligently.
|
|
27
|
+
- **No half-allowed**: a call either counts fully toward the limit or doesn't run at all. Long-running calls don't block the window — only the start counts.
|
|
28
|
+
|
|
29
|
+
## `concurrency`
|
|
30
|
+
|
|
31
|
+
Cap simultaneous in-flight executions.
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
@Tool({
|
|
35
|
+
name: 'render_pdf',
|
|
36
|
+
concurrency: { maxConcurrent: 5 }, // at most 5 PDFs rendering at once
|
|
37
|
+
// …
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- **Scope**: server-wide by default. Concurrency caps the resource — there's no point in per-session concurrency for shared resources like CPU / GPU / DB connection pools.
|
|
42
|
+
- **Behavior on overflow**: the call **queues** until a slot opens. Queue depth is unbounded by default; pair with a `timeout` to avoid pathological backups.
|
|
43
|
+
- **Use for**: tools that hold a real bottleneck — image / PDF rendering, ML inference, DB write transactions.
|
|
44
|
+
|
|
45
|
+
## `timeout`
|
|
46
|
+
|
|
47
|
+
Hard deadline on a single execution.
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
@Tool({
|
|
51
|
+
name: 'long_query',
|
|
52
|
+
timeout: { executeMs: 30_000 }, // 30s
|
|
53
|
+
// …
|
|
54
|
+
})
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- **Scope**: per call. Wraps the entire `execute()` invocation.
|
|
58
|
+
- **Behavior on timeout**: the framework throws an `ExecutionTimeoutError` (from `@frontmcp/guard`, code `'EXECUTION_TIMEOUT'`, HTTP status 408) and aborts the wrapped execution. The abort is internal to the timeout guard — it is **not** surfaced into `execute()` as a readable signal, so don't expect to observe it from inside the tool body.
|
|
59
|
+
- **Default**: no timeout. Tools can hang forever unless `timeout` is set.
|
|
60
|
+
|
|
61
|
+
## Interaction
|
|
62
|
+
|
|
63
|
+
The three controls are **orthogonal** — they apply independently:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
@Tool({
|
|
67
|
+
name: 'expensive_operation',
|
|
68
|
+
rateLimit: { maxRequests: 10, windowMs: 60_000 }, // ≤10 starts / min
|
|
69
|
+
concurrency: { maxConcurrent: 2 }, // ≤2 simultaneous
|
|
70
|
+
timeout: { executeMs: 30_000 }, // ≤30s each
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Order of effects per call:
|
|
75
|
+
|
|
76
|
+
1. `rateLimit` checked → reject early if over limit (no concurrency slot consumed)
|
|
77
|
+
2. `concurrency` checked → queue if all slots taken
|
|
78
|
+
3. `timeout` armed → wraps `execute()` once it runs
|
|
79
|
+
|
|
80
|
+
## Common combinations
|
|
81
|
+
|
|
82
|
+
| Scenario | Recipe |
|
|
83
|
+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
84
|
+
| Spammy external API | `rateLimit: { maxRequests: 60, windowMs: 60_000 }` (60/min) |
|
|
85
|
+
| Shared DB / GPU | `concurrency: { maxConcurrent: 5 }` |
|
|
86
|
+
| Anything calling LLMs / 3rd-party HTTP | `timeout: { executeMs: 30_000 }` |
|
|
87
|
+
| All three | `rateLimit: { maxRequests: 10, windowMs: 60_000 }, concurrency: { maxConcurrent: 2 }, timeout: { executeMs: 30_000 }` |
|
|
88
|
+
|
|
89
|
+
## Timeout and abort signals
|
|
90
|
+
|
|
91
|
+
`timeout` does **not** hand your `execute()` an abort signal — the abort lives inside the timeout guard and is not exposed on the context. `FrontMcpContext` has no `abortSignal` property, so don't reach for `this.context.abortSignal`.
|
|
92
|
+
|
|
93
|
+
The only tool-level abort signal is `this.signal`, and it is populated **only** for task-augmented `tools/call` invocations (cancelled via `tasks/cancel`) — not by `timeout`. It is `undefined` for ordinary calls, so guard for that:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
async execute(input: { url: string }) {
|
|
97
|
+
// this.signal is defined only for task-augmented calls; undefined otherwise
|
|
98
|
+
const response = await this.fetch(input.url, { signal: this.signal });
|
|
99
|
+
return response.json();
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For ordinary calls, rely on `this.fetch`'s own per-request timeout (default 30s) to bound in-flight HTTP work; `timeout` then caps the overall `execute()` duration.
|
|
104
|
+
|
|
105
|
+
## See also
|
|
106
|
+
|
|
107
|
+
- [`decorator-options.md`](./decorator-options.md)
|
|
108
|
+
- [`error-handling.md`](./error-handling.md)
|
|
109
|
+
- [`execution-context.md`](./execution-context.md)
|