@frontmcp/skills 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/README.md +38 -29
  2. package/catalog/TEMPLATE.md +26 -0
  3. package/catalog/create-tool/SKILL.md +318 -0
  4. package/catalog/create-tool/examples/01-basic-class-tool.md +112 -0
  5. package/catalog/create-tool/examples/02-basic-function-tool.md +80 -0
  6. package/catalog/create-tool/examples/03-tool-with-zod-shape-output.md +78 -0
  7. package/catalog/create-tool/examples/04-tool-with-zod-schema-output.md +97 -0
  8. package/catalog/create-tool/examples/05-tool-with-primitive-output.md +93 -0
  9. package/catalog/create-tool/examples/06-tool-with-media-output.md +109 -0
  10. package/catalog/create-tool/examples/08-tool-with-provider-injection.md +110 -0
  11. package/catalog/create-tool/examples/09-tool-with-multiple-providers.md +107 -0
  12. package/catalog/create-tool/examples/11-tool-with-fetch.md +94 -0
  13. package/catalog/create-tool/examples/12-tool-with-fetch-and-retries.md +115 -0
  14. package/catalog/create-tool/examples/13-tool-with-single-auth-provider.md +85 -0
  15. package/catalog/create-tool/examples/14-tool-with-multiple-auth-providers.md +105 -0
  16. package/catalog/create-tool/examples/15-tool-with-credential-vault.md +115 -0
  17. package/catalog/create-tool/examples/16-tool-with-rate-limit.md +71 -0
  18. package/catalog/create-tool/examples/17-tool-with-concurrency-and-timeout.md +101 -0
  19. package/catalog/create-tool/examples/18-tool-with-progress-and-notify.md +96 -0
  20. package/catalog/create-tool/examples/19-tool-with-elicitation.md +102 -0
  21. package/catalog/create-tool/examples/20-tool-with-annotations.md +125 -0
  22. package/catalog/create-tool/examples/21-tool-with-availability-constraints.md +107 -0
  23. package/catalog/create-tool/examples/22-tool-with-ui-html-template.md +93 -0
  24. package/catalog/create-tool/examples/23-tool-with-ui-filesource-tsx.md +112 -0
  25. package/catalog/create-tool/examples/24-tool-with-ui-csp-and-bridge.md +127 -0
  26. package/catalog/create-tool/examples/25-tool-handing-off-to-job.md +143 -0
  27. package/catalog/create-tool/examples/26-tool-with-resource-link-output.md +94 -0
  28. package/catalog/create-tool/examples/27-tool-with-examples-metadata.md +90 -0
  29. package/catalog/create-tool/references/annotations.md +96 -0
  30. package/catalog/create-tool/references/auth-providers.md +167 -0
  31. package/catalog/create-tool/references/availability.md +106 -0
  32. package/catalog/create-tool/references/decorator-options.md +95 -0
  33. package/catalog/create-tool/references/derived-types.md +102 -0
  34. package/catalog/create-tool/references/elicitation.md +128 -0
  35. package/catalog/create-tool/references/error-handling.md +128 -0
  36. package/catalog/create-tool/references/execution-context.md +158 -0
  37. package/catalog/create-tool/references/file-layout.md +96 -0
  38. package/catalog/create-tool/references/function-style-builder.md +118 -0
  39. package/catalog/create-tool/references/input-schema.md +141 -0
  40. package/catalog/create-tool/references/output-schema.md +175 -0
  41. package/catalog/create-tool/references/quick-start.md +124 -0
  42. package/catalog/create-tool/references/registration.md +132 -0
  43. package/catalog/create-tool/references/remote-and-esm.md +68 -0
  44. package/catalog/create-tool/references/testing.md +59 -0
  45. package/catalog/create-tool/references/throttling.md +109 -0
  46. package/catalog/create-tool/references/ui-widgets.md +198 -0
  47. package/catalog/create-tool/rules/always-define-output-schema.md +77 -0
  48. package/catalog/create-tool/rules/derive-execute-types.md +57 -0
  49. package/catalog/create-tool/rules/input-schema-is-raw-shape.md +76 -0
  50. package/catalog/create-tool/rules/no-toolcontext-generics.md +50 -0
  51. package/catalog/create-tool/rules/no-try-catch-around-execute.md +79 -0
  52. package/catalog/create-tool/rules/register-in-app.md +76 -0
  53. package/catalog/create-tool/rules/snake-case-tool-names.md +45 -0
  54. package/catalog/create-tool/rules/use-this-fail-for-business-errors.md +75 -0
  55. package/catalog/create-tool/rules/widget-paths-anchor-with-import-meta-url.md +76 -0
  56. package/catalog/create-tool/rules/widget-resource-mode-host-detect.md +61 -0
  57. package/catalog/frontmcp-auth-ui/SKILL.md +146 -0
  58. package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/login-slot.md +97 -0
  59. package/catalog/frontmcp-auth-ui/examples/custom-auth-ui/multi-step-auth-extra.md +133 -0
  60. package/catalog/frontmcp-auth-ui/references/custom-auth-ui.md +162 -0
  61. package/catalog/frontmcp-authorities/SKILL.md +55 -18
  62. package/catalog/frontmcp-authorities/references/authority-profiles.md +25 -1
  63. package/catalog/frontmcp-authorities/references/custom-evaluators.md +1 -1
  64. package/catalog/frontmcp-authorities/references/rbac-abac-rebac.md +9 -0
  65. package/catalog/frontmcp-channels/SKILL.md +7 -1
  66. package/catalog/frontmcp-config/SKILL.md +14 -7
  67. package/catalog/frontmcp-config/examples/configure-auth/local-credential-vault.md +94 -0
  68. package/catalog/frontmcp-config/examples/configure-auth/local-secure-store.md +138 -0
  69. package/catalog/frontmcp-config/examples/configure-auth/remote-oauth-with-vault.md +45 -23
  70. package/catalog/frontmcp-config/examples/configure-auth-modes/local-behind-tunnel.md +73 -0
  71. package/catalog/frontmcp-config/examples/configure-auth-modes/local-consent-enforcement.md +87 -0
  72. package/catalog/frontmcp-config/examples/configure-auth-modes/local-dcr-control.md +67 -0
  73. package/catalog/frontmcp-config/examples/configure-auth-modes/local-minimal.md +62 -0
  74. package/catalog/frontmcp-config/examples/configure-auth-modes/local-multi-provider-orchestration.md +93 -0
  75. package/catalog/frontmcp-config/examples/configure-auth-modes/local-self-signed-tokens.md +18 -20
  76. package/catalog/frontmcp-config/examples/configure-auth-modes/local-single-operator.md +66 -0
  77. package/catalog/frontmcp-config/examples/configure-auth-modes/remote-enterprise-oauth.md +37 -23
  78. package/catalog/frontmcp-config/examples/configure-http/custom-http-routes.md +98 -0
  79. package/catalog/frontmcp-config/examples/configure-skills-http/audit-log-redis.md +17 -9
  80. package/catalog/frontmcp-config/references/configure-auth-modes.md +86 -23
  81. package/catalog/frontmcp-config/references/configure-auth.md +296 -50
  82. package/catalog/frontmcp-config/references/configure-deployment-targets.md +84 -1
  83. package/catalog/frontmcp-config/references/configure-http.md +203 -14
  84. package/catalog/frontmcp-config/references/configure-session.md +14 -7
  85. package/catalog/frontmcp-deployment/SKILL.md +17 -15
  86. package/catalog/frontmcp-deployment/references/build-for-mcpb.md +1 -1
  87. package/catalog/frontmcp-deployment/references/deploy-manifest-yaml.md +308 -0
  88. package/catalog/frontmcp-deployment/references/deploy-to-cloudflare-skills-only.md +174 -0
  89. package/catalog/frontmcp-deployment/references/mcp-client-integration.md +145 -2
  90. package/catalog/frontmcp-development/SKILL.md +36 -50
  91. package/catalog/frontmcp-development/examples/create-provider/basic-database-provider.md +14 -0
  92. package/catalog/frontmcp-development/examples/create-provider/config-and-api-providers.md +85 -9
  93. package/catalog/frontmcp-development/references/create-job.md +45 -11
  94. package/catalog/frontmcp-development/references/create-provider.md +80 -8
  95. package/catalog/frontmcp-development/references/create-skill-with-tools.md +31 -0
  96. package/catalog/frontmcp-development/references/create-skill.md +45 -0
  97. package/catalog/frontmcp-development/references/decorators-guide.md +15 -15
  98. package/catalog/frontmcp-extensibility/SKILL.md +1 -1
  99. package/catalog/frontmcp-extensibility/examples/skill-audit-log/verify-chain.md +8 -6
  100. package/catalog/frontmcp-extensibility/references/skill-audit-log.md +7 -2
  101. package/catalog/frontmcp-guides/SKILL.md +8 -8
  102. package/catalog/frontmcp-observability/SKILL.md +16 -8
  103. package/catalog/frontmcp-observability/examples/metrics-endpoint/enable-metrics-endpoint.md +77 -0
  104. package/catalog/frontmcp-observability/references/metrics-endpoint.md +161 -0
  105. package/catalog/frontmcp-production-readiness/SKILL.md +1 -1
  106. package/catalog/frontmcp-production-readiness/examples/common-checklist/security-hardening.md +3 -2
  107. package/catalog/frontmcp-setup/SKILL.md +12 -12
  108. package/catalog/frontmcp-setup/examples/frontmcp-skills-usage/install-and-search-skills.md +19 -1
  109. package/catalog/frontmcp-setup/examples/multi-app-composition/per-app-auth-and-isolation.md +7 -4
  110. package/catalog/frontmcp-setup/references/frontmcp-skills-usage.md +260 -19
  111. package/catalog/frontmcp-setup/references/multi-app-composition.md +6 -5
  112. package/catalog/frontmcp-setup/references/setup-project.md +29 -0
  113. package/catalog/frontmcp-setup/references/setup-sqlite.md +68 -9
  114. package/catalog/frontmcp-testing/SKILL.md +26 -18
  115. package/catalog/frontmcp-testing/references/test-auth.md +24 -0
  116. package/catalog/skills-manifest.json +676 -146
  117. package/package.json +1 -1
  118. package/src/manifest.d.ts +72 -1
  119. package/src/manifest.js +4 -1
  120. package/src/manifest.js.map +1 -1
  121. package/catalog/frontmcp-development/examples/create-tool/basic-class-tool.md +0 -61
  122. package/catalog/frontmcp-development/examples/create-tool/tool-with-di-and-errors.md +0 -84
  123. package/catalog/frontmcp-development/examples/create-tool/tool-with-rate-limiting-and-progress.md +0 -92
  124. package/catalog/frontmcp-development/examples/create-tool-annotations/destructive-delete-tool.md +0 -92
  125. package/catalog/frontmcp-development/examples/create-tool-annotations/readonly-query-tool.md +0 -59
  126. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/primitive-and-media-outputs.md +0 -101
  127. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-raw-shape-output.md +0 -62
  128. package/catalog/frontmcp-development/examples/create-tool-output-schema-types/zod-schema-advanced-output.md +0 -101
  129. package/catalog/frontmcp-development/references/create-tool-annotations.md +0 -48
  130. package/catalog/frontmcp-development/references/create-tool-output-schema-types.md +0 -71
  131. package/catalog/frontmcp-development/references/create-tool.md +0 -728
@@ -0,0 +1,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.