@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,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 02-basic-function-tool
|
|
3
|
+
level: basic
|
|
4
|
+
description: 'Function-style `tool({...})(handler)` for a tiny pure-input tool — pick this over a class only when the tool needs no DI / lifecycle / UI.'
|
|
5
|
+
tags: [foundation, function-tool, tool-builder]
|
|
6
|
+
features:
|
|
7
|
+
- 'Using the `tool({...})(handler)` builder for a one-liner'
|
|
8
|
+
- "Returning a primitive via `outputSchema: 'number'`"
|
|
9
|
+
- 'Registering the function-style tool in `@App({ tools })` exactly like a class tool'
|
|
10
|
+
- "When function-style is the right choice (and when it isn't)"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Basic Function Tool
|
|
14
|
+
|
|
15
|
+
Function-style `tool({...})(handler)` for a tiny pure-input tool — pick this over a class only when the tool needs no DI / lifecycle / UI.
|
|
16
|
+
|
|
17
|
+
For trivial pure-input tools, the `tool()` function builder is a one-liner alternative to `@Tool` + class.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/add-numbers.tool.ts
|
|
23
|
+
import { tool, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
export const AddNumbers = tool({
|
|
26
|
+
name: 'add_numbers',
|
|
27
|
+
description: 'Add two numbers',
|
|
28
|
+
inputSchema: {
|
|
29
|
+
a: z.number().describe('First number'),
|
|
30
|
+
b: z.number().describe('Second number'),
|
|
31
|
+
},
|
|
32
|
+
outputSchema: 'number',
|
|
33
|
+
})((input) => input.a + input.b);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// src/apps/main/tools/add-numbers.e2e.spec.ts
|
|
38
|
+
import { expect, test } from '@frontmcp/testing';
|
|
39
|
+
|
|
40
|
+
test.use({
|
|
41
|
+
server: './src/main.ts',
|
|
42
|
+
port: 3004,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('adds two numbers', async ({ mcp }) => {
|
|
46
|
+
// mcp.tools.call returns a ToolResultWrapper around the MCP CallToolResult.
|
|
47
|
+
// A primitive `outputSchema: 'number'` surfaces as text content "5" and
|
|
48
|
+
// structuredContent `{ content: 5 }` — assert on the MCP result, not on `5` directly.
|
|
49
|
+
const result = await mcp.tools.call('add_numbers', { a: 2, b: 3 });
|
|
50
|
+
|
|
51
|
+
expect(result).toBeSuccessful();
|
|
52
|
+
expect(result).toHaveTextContent('5');
|
|
53
|
+
expect(result.json()).toEqual({ content: 5 });
|
|
54
|
+
});
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// src/apps/main/index.ts
|
|
59
|
+
import { App } from '@frontmcp/sdk';
|
|
60
|
+
|
|
61
|
+
import { AddNumbers } from './tools/add-numbers.tool';
|
|
62
|
+
|
|
63
|
+
@App({ name: 'main', tools: [AddNumbers] })
|
|
64
|
+
export class MainApp {}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What This Demonstrates
|
|
68
|
+
|
|
69
|
+
- Using the `tool({...})(handler)` builder for a one-liner
|
|
70
|
+
- Returning a primitive via `outputSchema: 'number'`
|
|
71
|
+
- Registering the function-style tool in `@App({ tools })` exactly like a class tool
|
|
72
|
+
- When function-style is the right choice (and when it isn't)
|
|
73
|
+
|
|
74
|
+
## When to pick function-style over class
|
|
75
|
+
|
|
76
|
+
- ✅ Pure math / formatting / parsing — no DI, no lifecycle, no UI widget
|
|
77
|
+
- ❌ Anything that needs `this.get(TOKEN)` — promote to class
|
|
78
|
+
- ❌ Anything with a `ui:` widget — class + folder-per-tool layout is cleaner
|
|
79
|
+
|
|
80
|
+
See [`references/function-style-builder.md`](../references/function-style-builder.md) for the full `tool()` surface, including `(input, ctx)` handler form with `ctx.get` / `ctx.fetch` (to fail, `throw new PublicMcpError(...)` — `ctx.fail` is class-tool only).
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 03-tool-with-zod-shape-output
|
|
3
|
+
level: basic
|
|
4
|
+
description: Tool returning structured JSON declared via a Zod raw shape outputSchema — the recommended pattern for any complex output.
|
|
5
|
+
tags: [output-schema, zod-shape, structured-output]
|
|
6
|
+
features:
|
|
7
|
+
- 'Declaring `outputSchema` as a Zod raw shape `{ field: z.string(), … }`'
|
|
8
|
+
- 'Constraining values with `.int().min(0)` so invalid output is rejected at the boundary'
|
|
9
|
+
- "Letting unrelated fields returned by the implementation (e.g. an upstream API's extras) be stripped silently"
|
|
10
|
+
- "Deriving `OrderSummaryOutput` once so the type and runtime contract can't drift"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Zod Shape Output
|
|
14
|
+
|
|
15
|
+
Tool returning structured JSON declared via a Zod raw shape outputSchema — the recommended pattern for any complex output.
|
|
16
|
+
|
|
17
|
+
For structured JSON, declare `outputSchema` as a Zod raw shape — the same form as `inputSchema`. The shape is the runtime contract AND the source of the TypeScript output type.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/order-summary.schema.ts
|
|
23
|
+
import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
export const inputSchema = {
|
|
26
|
+
orderId: z.string().uuid().describe('Order UUID'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const outputSchema = {
|
|
30
|
+
orderId: z.string(),
|
|
31
|
+
customer: z.string(),
|
|
32
|
+
totalUsd: z.number(),
|
|
33
|
+
itemCount: z.number().int().min(0),
|
|
34
|
+
pendingCount: z.number().int().min(0),
|
|
35
|
+
status: z.enum(['pending', 'paid', 'shipped', 'delivered', 'cancelled']),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type OrderSummaryInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
|
|
39
|
+
export type OrderSummaryOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/apps/main/tools/order-summary.tool.ts
|
|
44
|
+
import { Tool, ToolContext } from '@frontmcp/sdk';
|
|
45
|
+
|
|
46
|
+
import { ORDERS_REPO } from '../tokens';
|
|
47
|
+
import { inputSchema, outputSchema, type OrderSummaryInput, type OrderSummaryOutput } from './order-summary.schema';
|
|
48
|
+
|
|
49
|
+
@Tool({
|
|
50
|
+
name: 'order_summary',
|
|
51
|
+
description: 'Summary for an order — totals, item counts, and status.',
|
|
52
|
+
inputSchema,
|
|
53
|
+
outputSchema,
|
|
54
|
+
})
|
|
55
|
+
export class OrderSummaryTool extends ToolContext {
|
|
56
|
+
async execute(input: OrderSummaryInput): Promise<OrderSummaryOutput> {
|
|
57
|
+
const repo = this.get(ORDERS_REPO);
|
|
58
|
+
const order = await repo.findById(input.orderId);
|
|
59
|
+
// `order` may carry { …, internalNotes, paymentProviderRef, debug }
|
|
60
|
+
// — none of those are in outputSchema, so they're stripped before returning.
|
|
61
|
+
return {
|
|
62
|
+
orderId: order.id,
|
|
63
|
+
customer: order.customerName,
|
|
64
|
+
totalUsd: order.totalCents / 100,
|
|
65
|
+
itemCount: order.items.length,
|
|
66
|
+
pendingCount: order.items.filter((i) => i.status === 'pending').length,
|
|
67
|
+
status: order.status,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## What This Demonstrates
|
|
74
|
+
|
|
75
|
+
- Declaring `outputSchema` as a Zod raw shape `{ field: z.string(), … }`
|
|
76
|
+
- Constraining values with `.int().min(0)` so invalid output is rejected at the boundary
|
|
77
|
+
- Letting unrelated fields returned by the implementation (e.g. an upstream API's extras) be stripped silently
|
|
78
|
+
- Deriving `OrderSummaryOutput` once so the type and runtime contract can't drift
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 04-tool-with-zod-schema-output
|
|
3
|
+
level: advanced
|
|
4
|
+
description: 'Tool returning a discriminated union via a full `z.discriminatedUnion(...)` outputSchema — for outputs that branch on a kind field.'
|
|
5
|
+
tags: [output-schema, zod-schema, discriminated-union]
|
|
6
|
+
features:
|
|
7
|
+
- 'Using a full Zod schema (`z.discriminatedUnion(...)`) as `outputSchema` instead of a raw shape'
|
|
8
|
+
- 'Branching the runtime output on a discriminant `kind` literal'
|
|
9
|
+
- 'Letting TypeScript narrow the return type per branch (via `as const` on the discriminant)'
|
|
10
|
+
- 'Knowing when full Zod schemas are the right pick (unions, transforms) and when a raw shape is enough'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Zod Schema Output
|
|
14
|
+
|
|
15
|
+
Tool returning a discriminated union via a full `z.discriminatedUnion(...)` outputSchema — for outputs that branch on a kind field.
|
|
16
|
+
|
|
17
|
+
When the output has more than one shape (e.g. `user` vs `group`), use a full Zod schema instead of a raw shape. `z.discriminatedUnion` is the cleanest pattern.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/resolve-principal.schema.ts
|
|
23
|
+
import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
export const inputSchema = {
|
|
26
|
+
handle: z.string().describe('A user or group handle, e.g. `@ada` or `#engineering`'),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// Full Zod schema — discriminated union on `kind`.
|
|
30
|
+
export const outputSchema = z.discriminatedUnion('kind', [
|
|
31
|
+
z.object({
|
|
32
|
+
kind: z.literal('user'),
|
|
33
|
+
id: z.string(),
|
|
34
|
+
name: z.string(),
|
|
35
|
+
email: z.string().email(),
|
|
36
|
+
}),
|
|
37
|
+
z.object({
|
|
38
|
+
kind: z.literal('group'),
|
|
39
|
+
id: z.string(),
|
|
40
|
+
name: z.string(),
|
|
41
|
+
memberCount: z.number().int().min(0),
|
|
42
|
+
}),
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
export type ResolvePrincipalInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
|
|
46
|
+
export type ResolvePrincipalOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// src/apps/main/tools/resolve-principal.tool.ts
|
|
51
|
+
import { PublicMcpError, Tool, ToolContext } from '@frontmcp/sdk';
|
|
52
|
+
|
|
53
|
+
import { PRINCIPALS } from '../tokens';
|
|
54
|
+
import {
|
|
55
|
+
inputSchema,
|
|
56
|
+
outputSchema,
|
|
57
|
+
type ResolvePrincipalInput,
|
|
58
|
+
type ResolvePrincipalOutput,
|
|
59
|
+
} from './resolve-principal.schema';
|
|
60
|
+
|
|
61
|
+
@Tool({
|
|
62
|
+
name: 'resolve_principal',
|
|
63
|
+
description: 'Resolve a handle to a user or group',
|
|
64
|
+
inputSchema,
|
|
65
|
+
outputSchema,
|
|
66
|
+
})
|
|
67
|
+
export class ResolvePrincipalTool extends ToolContext {
|
|
68
|
+
async execute(input: ResolvePrincipalInput): Promise<ResolvePrincipalOutput> {
|
|
69
|
+
const svc = this.get(PRINCIPALS);
|
|
70
|
+
if (input.handle.startsWith('@')) {
|
|
71
|
+
const user = await svc.findUserByHandle(input.handle.slice(1));
|
|
72
|
+
return { kind: 'user' as const, id: user.id, name: user.name, email: user.email };
|
|
73
|
+
}
|
|
74
|
+
if (input.handle.startsWith('#')) {
|
|
75
|
+
const group = await svc.findGroupBySlug(input.handle.slice(1));
|
|
76
|
+
return { kind: 'group' as const, id: group.id, name: group.name, memberCount: group.members.length };
|
|
77
|
+
}
|
|
78
|
+
this.fail(new PublicMcpError(`Unknown handle prefix: ${input.handle[0]}`));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## What This Demonstrates
|
|
84
|
+
|
|
85
|
+
- Using a full Zod schema (`z.discriminatedUnion(...)`) as `outputSchema` instead of a raw shape
|
|
86
|
+
- Branching the runtime output on a discriminant `kind` literal
|
|
87
|
+
- Letting TypeScript narrow the return type per branch (via `as const` on the discriminant)
|
|
88
|
+
- Knowing when full Zod schemas are the right pick (unions, transforms) and when a raw shape is enough
|
|
89
|
+
|
|
90
|
+
## When to use a full Zod schema for output
|
|
91
|
+
|
|
92
|
+
| Use raw shape | Use full Zod schema |
|
|
93
|
+
| ---------------------------------------- | -------------------------------------------------------- |
|
|
94
|
+
| Single object with a fixed set of fields | Union of multiple shapes (`z.discriminatedUnion`) |
|
|
95
|
+
| All fields known statically | Arrays of complex objects (`z.array(z.object(...))`) |
|
|
96
|
+
| No transforms / refinements needed | Need `z.transform(...)`, `z.refine(...)`, `z.brand(...)` |
|
|
97
|
+
| Default and recommended | When the raw shape can't express the contract |
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 05-tool-with-primitive-output
|
|
3
|
+
level: basic
|
|
4
|
+
description: "Tool returning a single primitive — `outputSchema: 'string' | 'number' | 'boolean' | 'date'` for single-value outputs."
|
|
5
|
+
tags: [output-schema, primitive-output]
|
|
6
|
+
features:
|
|
7
|
+
- "Using a primitive literal (`'string'`, `'number'`, `'boolean'`, `'date'`) for `outputSchema`"
|
|
8
|
+
- 'Returning the bare value directly from `execute()` instead of wrapping it'
|
|
9
|
+
- Picking primitive literals over a one-field Zod shape for ergonomic clarity
|
|
10
|
+
- 'Four concrete tools in one file (`fmt_currency`, `add`, `is_palindrome`, `now`) demonstrating each primitive form'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Primitive Output
|
|
14
|
+
|
|
15
|
+
Tool returning a single primitive — `outputSchema: 'string' | 'number' | 'boolean' | 'date'` for single-value outputs.
|
|
16
|
+
|
|
17
|
+
For tools that return a single value, declare `outputSchema` as a primitive literal. The framework wraps the bare return in the right MCP content block.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/primitives.tool.ts
|
|
23
|
+
import { Tool, tool, ToolContext, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
// 1. string output
|
|
26
|
+
@Tool({
|
|
27
|
+
name: 'fmt_currency',
|
|
28
|
+
description: 'Format a number as USD',
|
|
29
|
+
inputSchema: { amount: z.number() },
|
|
30
|
+
outputSchema: 'string',
|
|
31
|
+
})
|
|
32
|
+
export class FmtCurrencyTool extends ToolContext {
|
|
33
|
+
execute(input: { amount: number }): string {
|
|
34
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(input.amount);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. number output (function-style)
|
|
39
|
+
export const Add = tool({
|
|
40
|
+
name: 'add',
|
|
41
|
+
description: 'Add two numbers',
|
|
42
|
+
inputSchema: { a: z.number(), b: z.number() },
|
|
43
|
+
outputSchema: 'number',
|
|
44
|
+
})((input) => input.a + input.b);
|
|
45
|
+
|
|
46
|
+
// 3. boolean output
|
|
47
|
+
@Tool({
|
|
48
|
+
name: 'is_palindrome',
|
|
49
|
+
description: 'Check whether a string reads the same forward and backward',
|
|
50
|
+
inputSchema: { text: z.string() },
|
|
51
|
+
outputSchema: 'boolean',
|
|
52
|
+
})
|
|
53
|
+
export class IsPalindromeTool extends ToolContext {
|
|
54
|
+
execute(input: { text: string }): boolean {
|
|
55
|
+
const t = input.text.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
56
|
+
return t === t.split('').reverse().join('');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 4. date output
|
|
61
|
+
@Tool({
|
|
62
|
+
name: 'now',
|
|
63
|
+
description: 'Current server time',
|
|
64
|
+
inputSchema: {},
|
|
65
|
+
outputSchema: 'date',
|
|
66
|
+
})
|
|
67
|
+
export class NowTool extends ToolContext {
|
|
68
|
+
execute(): Date {
|
|
69
|
+
return new Date();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## What This Demonstrates
|
|
75
|
+
|
|
76
|
+
- Using a primitive literal (`'string'`, `'number'`, `'boolean'`, `'date'`) for `outputSchema`
|
|
77
|
+
- Returning the bare value directly from `execute()` instead of wrapping it
|
|
78
|
+
- Picking primitive literals over a one-field Zod shape for ergonomic clarity
|
|
79
|
+
- Four concrete tools in one file (`fmt_currency`, `add`, `is_palindrome`, `now`) demonstrating each primitive form
|
|
80
|
+
|
|
81
|
+
## When to pick primitive literals over a Zod shape
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// ✅ primitive literal — clean
|
|
85
|
+
outputSchema: 'number',
|
|
86
|
+
execute() { return 42; }
|
|
87
|
+
|
|
88
|
+
// ❌ one-field Zod shape — unnecessarily nested
|
|
89
|
+
outputSchema: { value: z.number() },
|
|
90
|
+
execute() { return { value: 42 }; }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The primitive form returns the bare value; the shape form wraps it in `{ value }`. Pick primitive when you literally want one value back.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 06-tool-with-media-output
|
|
3
|
+
level: intermediate
|
|
4
|
+
description: "Tool returning binary content (image / audio) or a multi-content array of `[text, image]` — for outputs that aren't plain JSON."
|
|
5
|
+
tags: [output-schema, media-output, image, multi-content]
|
|
6
|
+
features:
|
|
7
|
+
- "Returning a base64-encoded image with `outputSchema: 'image'` and `{ type: 'image', data, mimeType }`"
|
|
8
|
+
- "Returning audio with `outputSchema: 'audio'` (same `{ type: 'audio', data, mimeType }` shape, audio MIME types)"
|
|
9
|
+
- "Returning multi-content via `outputSchema: ['string', 'image']` — text summary + annotated image in one response"
|
|
10
|
+
- "When to pick a media literal vs `'resource_link'` (host-fetched URI)"
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Media Output
|
|
14
|
+
|
|
15
|
+
Tool returning binary content (image / audio) or a multi-content array of `[text, image]` — for outputs that aren't plain JSON.
|
|
16
|
+
|
|
17
|
+
Media outputs use the literal forms: `'image'`, `'audio'`, `'resource'`, `'resource_link'`, or a mixed array like `['string', 'image']`.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tools/render-chart.tool.ts
|
|
23
|
+
import { Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
24
|
+
|
|
25
|
+
// 1. Image output — base64-encoded
|
|
26
|
+
@Tool({
|
|
27
|
+
name: 'render_chart',
|
|
28
|
+
description: 'Render a bar chart as PNG',
|
|
29
|
+
inputSchema: {
|
|
30
|
+
labels: z.array(z.string()),
|
|
31
|
+
values: z.array(z.number()),
|
|
32
|
+
},
|
|
33
|
+
outputSchema: 'image',
|
|
34
|
+
})
|
|
35
|
+
export class RenderChartTool extends ToolContext {
|
|
36
|
+
async execute(input: { labels: string[]; values: number[] }) {
|
|
37
|
+
const pngBuffer = await this.renderPng(input);
|
|
38
|
+
return {
|
|
39
|
+
type: 'image' as const, // required — without it the content block is dropped
|
|
40
|
+
data: pngBuffer.toString('base64'),
|
|
41
|
+
mimeType: 'image/png' as const,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async renderPng(_input: { labels: string[]; values: number[] }): Promise<Buffer> {
|
|
46
|
+
return Buffer.from('iVBORw0KGgo…', 'base64'); // tiny placeholder
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Audio output
|
|
51
|
+
@Tool({
|
|
52
|
+
name: 'tts',
|
|
53
|
+
description: 'Synthesize speech from text',
|
|
54
|
+
inputSchema: { text: z.string() },
|
|
55
|
+
outputSchema: 'audio',
|
|
56
|
+
})
|
|
57
|
+
export class TtsTool extends ToolContext {
|
|
58
|
+
async execute(input: { text: string }) {
|
|
59
|
+
const wavBuffer = await this.synthesize(input.text);
|
|
60
|
+
return {
|
|
61
|
+
type: 'audio' as const, // required — without it the content block is dropped
|
|
62
|
+
data: wavBuffer.toString('base64'),
|
|
63
|
+
mimeType: 'audio/wav' as const,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private async synthesize(_text: string): Promise<Buffer> {
|
|
68
|
+
return Buffer.alloc(0);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Multi-content — text summary + annotated image
|
|
73
|
+
@Tool({
|
|
74
|
+
name: 'analyze_image',
|
|
75
|
+
description: 'Detect objects and return a summary + annotated image',
|
|
76
|
+
inputSchema: { imageUrl: z.string().url() },
|
|
77
|
+
outputSchema: ['string', 'image'],
|
|
78
|
+
})
|
|
79
|
+
export class AnalyzeImageTool extends ToolContext {
|
|
80
|
+
async execute(input: { imageUrl: string }) {
|
|
81
|
+
const detection = await this.detect(input.imageUrl);
|
|
82
|
+
const summary = `Detected: ${detection.objects.join(', ')}.`;
|
|
83
|
+
const annotated = await this.annotate(input.imageUrl, detection);
|
|
84
|
+
return [
|
|
85
|
+
summary,
|
|
86
|
+
{ type: 'image' as const, data: annotated.toString('base64'), mimeType: 'image/png' as const },
|
|
87
|
+
] as const;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private async detect(_url: string) {
|
|
91
|
+
return { objects: ['cat', 'laptop'] };
|
|
92
|
+
}
|
|
93
|
+
private async annotate(_url: string, _d: { objects: string[] }): Promise<Buffer> {
|
|
94
|
+
return Buffer.alloc(0);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## What This Demonstrates
|
|
100
|
+
|
|
101
|
+
- Returning a base64-encoded image with `outputSchema: 'image'` and `{ type: 'image', data, mimeType }`
|
|
102
|
+
- Returning audio with `outputSchema: 'audio'` (same `{ type: 'audio', data, mimeType }` shape, audio MIME types)
|
|
103
|
+
- Returning multi-content via `outputSchema: ['string', 'image']` — text summary + annotated image in one response
|
|
104
|
+
- When to pick a media literal vs `'resource_link'` (host-fetched URI)
|
|
105
|
+
|
|
106
|
+
## Media literal vs `'resource_link'`
|
|
107
|
+
|
|
108
|
+
- **`'image'` / `'audio'`** — inlines the bytes (base64) in the response. Best for small payloads (< ~1 MB). Simple — the client gets the data immediately.
|
|
109
|
+
- **`'resource_link'`** — returns `{ uri: 'custom://…' }`; the client calls `resources/read` to fetch. Best for large payloads or when caching matters — see [`26-tool-with-resource-link-output`](./26-tool-with-resource-link-output.md).
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 08-tool-with-provider-injection
|
|
3
|
+
level: intermediate
|
|
4
|
+
description: 'Tool that resolves a DI-registered service via `this.get(TOKEN)` and uses it to power `execute()` — the standard pattern for tools that talk to a database or external API.'
|
|
5
|
+
tags: [di, provider, this.get, error-handling]
|
|
6
|
+
features:
|
|
7
|
+
- "Defining a typed DI token with `Symbol('UserService')` and `Token<UserService>`"
|
|
8
|
+
- 'Implementing a `@Provider` and registering it in the same `@App` as the tool'
|
|
9
|
+
- 'Resolving the service inside `execute()` via `this.get(USER_SERVICE)` (throws when missing)'
|
|
10
|
+
- 'Translating "not found" into `ResourceNotFoundError` via `this.fail(...)` so the client gets a proper MCP error code (-32002)'
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Provider Injection
|
|
14
|
+
|
|
15
|
+
Tool that resolves a DI-registered service via `this.get(TOKEN)` and uses it to power `execute()` — the standard pattern for tools that talk to a database or external API.
|
|
16
|
+
|
|
17
|
+
The canonical pattern. A service lives behind a typed token, gets registered as a `@Provider` in the same `@App`, and the tool resolves it via `this.get(TOKEN)`.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tokens.ts
|
|
23
|
+
import type { Token } from '@frontmcp/di';
|
|
24
|
+
|
|
25
|
+
export interface UserService {
|
|
26
|
+
findById(id: string): Promise<{ id: string; name: string; email: string } | null>;
|
|
27
|
+
}
|
|
28
|
+
export const USER_SERVICE: Token<UserService> = Symbol('UserService');
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
// src/apps/main/providers/user-service.provider.ts
|
|
33
|
+
import { Provider } from '@frontmcp/sdk';
|
|
34
|
+
|
|
35
|
+
import { USER_SERVICE, type UserService } from '../tokens';
|
|
36
|
+
|
|
37
|
+
@Provider({ provide: USER_SERVICE })
|
|
38
|
+
export class UserServiceProvider implements UserService {
|
|
39
|
+
async findById(id: string) {
|
|
40
|
+
// pretend this hits a database
|
|
41
|
+
if (id === 'u_1') return { id: 'u_1', name: 'Ada', email: 'ada@example.com' };
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// src/apps/main/tools/get-user.schema.ts
|
|
49
|
+
import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';
|
|
50
|
+
|
|
51
|
+
export const inputSchema = { id: z.string().describe('User ID') };
|
|
52
|
+
export const outputSchema = { id: z.string(), name: z.string(), email: z.string().email() };
|
|
53
|
+
|
|
54
|
+
export type GetUserInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
|
|
55
|
+
export type GetUserOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
// src/apps/main/tools/get-user.tool.ts
|
|
60
|
+
import { ResourceNotFoundError, Tool, ToolContext } from '@frontmcp/sdk';
|
|
61
|
+
|
|
62
|
+
import { USER_SERVICE } from '../tokens';
|
|
63
|
+
import { inputSchema, outputSchema, type GetUserInput, type GetUserOutput } from './get-user.schema';
|
|
64
|
+
|
|
65
|
+
@Tool({
|
|
66
|
+
name: 'get_user',
|
|
67
|
+
description: 'Get a user by ID',
|
|
68
|
+
inputSchema,
|
|
69
|
+
outputSchema,
|
|
70
|
+
})
|
|
71
|
+
export class GetUserTool extends ToolContext {
|
|
72
|
+
async execute(input: GetUserInput): Promise<GetUserOutput> {
|
|
73
|
+
const users = this.get(USER_SERVICE); // throws DependencyNotFoundError if not registered
|
|
74
|
+
const user = await users.findById(input.id);
|
|
75
|
+
if (!user) {
|
|
76
|
+
this.fail(new ResourceNotFoundError(`user:${input.id}`)); // never returns
|
|
77
|
+
}
|
|
78
|
+
return user; // typed as non-null because this.fail is `never`
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// src/apps/main/index.ts
|
|
85
|
+
import { App } from '@frontmcp/sdk';
|
|
86
|
+
|
|
87
|
+
import { UserServiceProvider } from './providers/user-service.provider';
|
|
88
|
+
import { GetUserTool } from './tools/get-user.tool';
|
|
89
|
+
|
|
90
|
+
@App({
|
|
91
|
+
name: 'main',
|
|
92
|
+
providers: [UserServiceProvider],
|
|
93
|
+
tools: [GetUserTool],
|
|
94
|
+
})
|
|
95
|
+
export class MainApp {}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> **Testing.** Tests for tools with DI use `@frontmcp/testing`'s `TestServer` with the provider replaced for the test scope. The full pattern (including the canonical `test({ mcp })` fixture and `mcpMatchers`) lives in the dedicated `testing` skill.
|
|
99
|
+
|
|
100
|
+
## What This Demonstrates
|
|
101
|
+
|
|
102
|
+
- Defining a typed DI token with `Symbol('UserService')` and `Token<UserService>`
|
|
103
|
+
- Implementing a `@Provider` and registering it in the same `@App` as the tool
|
|
104
|
+
- Resolving the service inside `execute()` via `this.get(USER_SERVICE)` (throws when missing)
|
|
105
|
+
- Translating "not found" into `ResourceNotFoundError` via `this.fail(...)` so the client gets a proper MCP error code (-32002)
|
|
106
|
+
|
|
107
|
+
## `this.get` vs `this.tryGet`
|
|
108
|
+
|
|
109
|
+
- `this.get(TOKEN)` — throws `DependencyNotFoundError` if not registered. Use when the tool genuinely requires the dep.
|
|
110
|
+
- `this.tryGet(TOKEN)` — returns `undefined` if not registered. Use when the tool degrades gracefully (e.g. optional cache).
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: 09-tool-with-multiple-providers
|
|
3
|
+
level: intermediate
|
|
4
|
+
description: 'Tool composing three DI services — config (env-only), cache (optional, `tryGet`), and database (required) — the realistic shape for a production tool.'
|
|
5
|
+
tags: [di, multiple-providers, cache-aside, tryGet]
|
|
6
|
+
features:
|
|
7
|
+
- 'Resolving multiple providers via `this.get(TOKEN)` and `this.tryGet(TOKEN)`'
|
|
8
|
+
- 'Cache-aside pattern — check `tryGet(CACHE)` first, fall back to the database'
|
|
9
|
+
- 'Reading typed config from a `CONFIG` token vs `process.env` directly'
|
|
10
|
+
- Letting the tool work in production (with cache) AND in test (without it)
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# Tool With Multiple Providers
|
|
14
|
+
|
|
15
|
+
Tool composing three DI services — config (env-only), cache (optional, `tryGet`), and database (required) — the realistic shape for a production tool.
|
|
16
|
+
|
|
17
|
+
Production tools usually compose several services: config + cache + database is the standard trio. This example wires all three.
|
|
18
|
+
|
|
19
|
+
## Code
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/apps/main/tokens.ts
|
|
23
|
+
import type { Token } from '@frontmcp/di';
|
|
24
|
+
|
|
25
|
+
export interface AppConfig {
|
|
26
|
+
weatherApiKey: string;
|
|
27
|
+
cacheTtlSeconds: number;
|
|
28
|
+
}
|
|
29
|
+
export interface CacheService {
|
|
30
|
+
get<T>(key: string): Promise<T | null>;
|
|
31
|
+
set<T>(key: string, value: T, ttlSeconds: number): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export interface WeatherRepo {
|
|
34
|
+
loadFromDb(city: string): Promise<{ temperatureF: number; conditions: string } | null>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const CONFIG: Token<AppConfig> = Symbol('AppConfig');
|
|
38
|
+
export const CACHE: Token<CacheService> = Symbol('CacheService');
|
|
39
|
+
export const WEATHER_REPO: Token<WeatherRepo> = Symbol('WeatherRepo');
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
// src/apps/main/tools/get-weather.tool.ts
|
|
44
|
+
import { ResourceNotFoundError, Tool, ToolContext, z } from '@frontmcp/sdk';
|
|
45
|
+
|
|
46
|
+
import { CACHE, CONFIG, WEATHER_REPO } from '../tokens';
|
|
47
|
+
|
|
48
|
+
const inputSchema = { city: z.string().describe('City name') };
|
|
49
|
+
const outputSchema = {
|
|
50
|
+
city: z.string(),
|
|
51
|
+
temperatureF: z.number(),
|
|
52
|
+
conditions: z.string(),
|
|
53
|
+
cached: z.boolean(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
@Tool({
|
|
57
|
+
name: 'get_weather',
|
|
58
|
+
description: 'Current weather — cache-aside, falls back to the DB',
|
|
59
|
+
inputSchema,
|
|
60
|
+
outputSchema,
|
|
61
|
+
})
|
|
62
|
+
export class GetWeatherTool extends ToolContext {
|
|
63
|
+
async execute(input: { city: string }) {
|
|
64
|
+
const config = this.get(CONFIG); // required — throws if missing
|
|
65
|
+
const cache = this.tryGet(CACHE); // optional — production has it, tests skip it
|
|
66
|
+
const repo = this.get(WEATHER_REPO); // required
|
|
67
|
+
|
|
68
|
+
const cacheKey = `weather:${input.city.toLowerCase()}`;
|
|
69
|
+
|
|
70
|
+
if (cache) {
|
|
71
|
+
const cached = await cache.get<{ temperatureF: number; conditions: string }>(cacheKey);
|
|
72
|
+
if (cached) {
|
|
73
|
+
return { city: input.city, ...cached, cached: true };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const fresh = await repo.loadFromDb(input.city);
|
|
78
|
+
if (!fresh) {
|
|
79
|
+
this.fail(new ResourceNotFoundError(`weather:${input.city}`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (cache) {
|
|
83
|
+
await cache.set(cacheKey, fresh, config.cacheTtlSeconds);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return { city: input.city, ...fresh, cached: false };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## What This Demonstrates
|
|
92
|
+
|
|
93
|
+
- Resolving multiple providers via `this.get(TOKEN)` and `this.tryGet(TOKEN)`
|
|
94
|
+
- Cache-aside pattern — check `tryGet(CACHE)` first, fall back to the database
|
|
95
|
+
- Reading typed config from a `CONFIG` token vs `process.env` directly
|
|
96
|
+
- Letting the tool work in production (with cache) AND in test (without it)
|
|
97
|
+
|
|
98
|
+
## Why `tryGet` for cache
|
|
99
|
+
|
|
100
|
+
- In production: the app registers a Redis-backed cache provider. `this.tryGet(CACHE)` returns it.
|
|
101
|
+
- In tests: tests skip registering the cache. `tryGet(CACHE)` returns `undefined`. The tool falls through to the DB. No special test setup required.
|
|
102
|
+
|
|
103
|
+
## Why `this.get(CONFIG)` instead of `process.env.WEATHER_API_KEY`
|
|
104
|
+
|
|
105
|
+
- Config goes through a typed provider — tests inject a mock `{ weatherApiKey: 'test', cacheTtlSeconds: 0 }` without touching `process.env`.
|
|
106
|
+
- The shape is enforced by TypeScript; renaming a config field is a compile-time error across all callers.
|
|
107
|
+
- Multiple apps on the same server can have different config provider implementations.
|