@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,94 @@
1
+ ---
2
+ name: 11-tool-with-fetch
3
+ level: intermediate
4
+ description: 'Tool calling an external HTTP API with `this.fetch` — context propagation, status-code handling, and bounding the call with a tool `timeout`.'
5
+ tags: [fetch, http, external-api, error-handling]
6
+ features:
7
+ - 'Using `this.fetch(url, init?)` so trace context propagates to the upstream service'
8
+ - 'Translating non-2xx HTTP responses into `PublicMcpError` so the MCP client gets a clean error'
9
+ - "Bounding the call with a tool `timeout` (and `this.fetch`'s built-in per-request timeout) — without relying on a non-existent context abort signal"
10
+ - "Letting genuine network errors (DNS failure, ECONNREFUSED) propagate to the framework's error flow"
11
+ ---
12
+
13
+ # Tool With Fetch
14
+
15
+ Tool calling an external HTTP API with `this.fetch` — context propagation, status-code handling, and bounding the call with a tool `timeout`.
16
+
17
+ `this.fetch` is the standard `fetch` plus trace-context propagation. Always use it instead of bare `fetch` so distributed tracing stitches the upstream call into the same trace as the MCP request.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/get-weather.tool.ts
23
+ import { PublicMcpError, ResourceNotFoundError, Tool, ToolContext, z } from '@frontmcp/sdk';
24
+
25
+ const inputSchema = {
26
+ city: z.string().describe('City name, e.g. "Seattle"'),
27
+ units: z.enum(['celsius', 'fahrenheit']).default('fahrenheit'),
28
+ };
29
+ const outputSchema = {
30
+ city: z.string(),
31
+ temperature: z.number(),
32
+ conditions: z.string(),
33
+ units: z.enum(['celsius', 'fahrenheit']),
34
+ };
35
+
36
+ @Tool({
37
+ name: 'get_weather',
38
+ description: 'Current weather from api.weather.example',
39
+ inputSchema,
40
+ outputSchema,
41
+ timeout: { executeMs: 10_000 }, // cap the whole call if the upstream hangs
42
+ })
43
+ export class GetWeatherTool extends ToolContext {
44
+ async execute(input: { city: string; units: 'celsius' | 'fahrenheit' }) {
45
+ const url = new URL('https://api.weather.example/v1/current');
46
+ url.searchParams.set('city', input.city);
47
+ url.searchParams.set('units', input.units);
48
+
49
+ const response = await this.fetch(url, {
50
+ headers: { accept: 'application/json' },
51
+ // `this.fetch` applies its own request timeout (default 30s). The tool-level
52
+ // `timeout` below caps the whole execute() at the framework level.
53
+ });
54
+
55
+ if (response.status === 404) {
56
+ this.fail(new ResourceNotFoundError(`weather:${input.city}`));
57
+ }
58
+ if (!response.ok) {
59
+ this.fail(new PublicMcpError(`Weather API returned ${response.status} ${response.statusText}`));
60
+ }
61
+
62
+ const body = (await response.json()) as { temp: number; summary: string };
63
+ return {
64
+ city: input.city,
65
+ temperature: body.temp,
66
+ conditions: body.summary,
67
+ units: input.units,
68
+ };
69
+ }
70
+ }
71
+ ```
72
+
73
+ ## What This Demonstrates
74
+
75
+ - Using `this.fetch(url, init?)` so trace context propagates to the upstream service
76
+ - Translating non-2xx HTTP responses into `PublicMcpError` so the MCP client gets a clean error
77
+ - Bounding the call with a tool `timeout` (and `this.fetch`'s built-in per-request timeout) — without relying on a non-existent context abort signal
78
+ - Letting genuine network errors (DNS failure, ECONNREFUSED) propagate to the framework's error flow
79
+
80
+ ## Don't do this
81
+
82
+ ```typescript
83
+ // ❌ swallows network errors, hides infrastructure problems from observability
84
+ async execute(input) {
85
+ try {
86
+ const response = await this.fetch(url);
87
+ return await response.json();
88
+ } catch (err) {
89
+ return { error: String(err) };
90
+ }
91
+ }
92
+ ```
93
+
94
+ Let infrastructure errors propagate. The framework wraps them in `InternalError` (`-32603`) for the client and logs them properly for ops. Only convert specific business-level conditions (status codes you know about) to `this.fail`.
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: 12-tool-with-fetch-and-retries
3
+ level: advanced
4
+ description: 'Tool calling a flaky external API with exponential backoff retries, an Idempotency-Key for safety, and respect for `Retry-After` on 429s.'
5
+ tags: [fetch, retries, exponential-backoff, idempotency-key, 429-rate-limit]
6
+ features:
7
+ - Retrying on transient errors (5xx + 429) with exponential backoff plus jitter
8
+ - "Generating an `Idempotency-Key` so retried POSTs don't duplicate side effects on the upstream"
9
+ - 'Respecting an upstream `Retry-After` header on 429 responses instead of guessing'
10
+ - "Capping the total retry budget with `timeout: { executeMs }` so a wedged upstream can't hang the call indefinitely"
11
+ ---
12
+
13
+ # Tool With Fetch And Retries
14
+
15
+ Tool calling a flaky external API with exponential backoff retries, an Idempotency-Key for safety, and respect for `Retry-After` on 429s.
16
+
17
+ Real upstreams flake. This shows the pattern that survives them: retry the right errors, back off correctly, generate an idempotency key so retried POSTs don't double-fire, and cap the whole thing with `timeout`.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/create-issue.tool.ts
23
+ import { PublicMcpError, Tool, ToolContext, z } from '@frontmcp/sdk';
24
+ import { randomUUID } from '@frontmcp/utils';
25
+
26
+ const inputSchema = {
27
+ repo: z.string().regex(/^[^\/]+\/[^\/]+$/, 'expected owner/repo'),
28
+ title: z.string().min(1).max(256),
29
+ body: z.string().max(65_536).optional(),
30
+ };
31
+ const outputSchema = { issueNumber: z.number().int().min(1), url: z.string().url() };
32
+
33
+ const MAX_ATTEMPTS = 4;
34
+ const BASE_DELAY_MS = 200;
35
+
36
+ @Tool({
37
+ name: 'create_issue',
38
+ description: 'Create a GitHub issue (retries on 5xx / 429)',
39
+ inputSchema,
40
+ outputSchema,
41
+ rateLimit: { maxRequests: 30, windowMs: 60_000 },
42
+ timeout: { executeMs: 30_000 }, // hard cap across all retries
43
+ annotations: { destructiveHint: false, idempotentHint: true, openWorldHint: true },
44
+ authProviders: ['github'],
45
+ })
46
+ export class CreateIssueTool extends ToolContext {
47
+ async execute(input: { repo: string; title: string; body?: string }) {
48
+ const headers = await this.authProviders.headers('github');
49
+ const idempotencyKey = randomUUID();
50
+
51
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
52
+ const response = await this.fetch(`https://api.github.com/repos/${input.repo}/issues`, {
53
+ method: 'POST',
54
+ headers: {
55
+ ...headers,
56
+ 'content-type': 'application/json',
57
+ 'idempotency-key': idempotencyKey,
58
+ },
59
+ body: JSON.stringify({ title: input.title, body: input.body }),
60
+ // `this.fetch` applies its own per-request timeout; the tool-level
61
+ // `timeout` below caps the whole retry loop at the framework level.
62
+ });
63
+
64
+ if (response.ok) {
65
+ const data = (await response.json()) as { number: number; html_url: string };
66
+ return { issueNumber: data.number, url: data.html_url };
67
+ }
68
+
69
+ // Non-retryable client errors → fail immediately
70
+ if (response.status >= 400 && response.status < 500 && response.status !== 429) {
71
+ const detail = await response.text();
72
+ this.fail(new PublicMcpError(`GitHub returned ${response.status}: ${detail.slice(0, 200)}`));
73
+ }
74
+
75
+ // Retryable (5xx or 429) — back off and try again
76
+ if (attempt === MAX_ATTEMPTS) {
77
+ this.fail(new PublicMcpError(`GitHub upstream failed after ${MAX_ATTEMPTS} attempts`));
78
+ }
79
+
80
+ // Retry-After is either a number of seconds ("120") or an HTTP-date — handle both.
81
+ const retryAfter = response.headers.get('retry-after');
82
+ const retryAfterMs = (() => {
83
+ if (!retryAfter) return undefined;
84
+ const seconds = Number(retryAfter);
85
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
86
+ const at = Date.parse(retryAfter);
87
+ return Number.isFinite(at) ? Math.max(0, at - Date.now()) : undefined;
88
+ })();
89
+ const baseDelay = retryAfterMs ?? BASE_DELAY_MS * 2 ** (attempt - 1);
90
+ const jitter = Math.floor(Math.random() * baseDelay * 0.2);
91
+ await this.notify(
92
+ `Attempt ${attempt} returned ${response.status}; retrying in ${baseDelay + jitter}ms`,
93
+ 'warning',
94
+ );
95
+ await new Promise((r) => setTimeout(r, baseDelay + jitter));
96
+ }
97
+ /* unreachable — this.fail above never returns */
98
+ this.fail(new PublicMcpError('unreachable'));
99
+ }
100
+ }
101
+ ```
102
+
103
+ ## What This Demonstrates
104
+
105
+ - Retrying on transient errors (5xx + 429) with exponential backoff plus jitter
106
+ - Generating an `Idempotency-Key` so retried POSTs don't duplicate side effects on the upstream
107
+ - Respecting an upstream `Retry-After` header on 429 responses instead of guessing
108
+ - Capping the total retry budget with `timeout: { executeMs }` so a wedged upstream can't hang the call indefinitely
109
+
110
+ ## Why these choices
111
+
112
+ - **`Idempotency-Key` is critical for POSTs.** Without it, a retry after a network glitch can create two issues. GitHub honors `Idempotency-Key` server-side; many other APIs (Stripe, etc.) do too.
113
+ - **Jitter prevents thundering herds.** All clients retrying at exactly 200ms / 400ms / 800ms create synchronized spikes. ±20% jitter spreads them.
114
+ - **`timeout: { executeMs: 30_000 }` is the safety net.** The retry loop alone can take 200 + 400 + 800 = 1.4s just in backoff. With a slow upstream, total time spirals — the tool-level timeout caps it.
115
+ - **Don't retry 4xx (except 429).** 4xx means "your request is wrong" — retrying won't help and may double up the upstream's accounting.
@@ -0,0 +1,85 @@
1
+ ---
2
+ name: 13-tool-with-single-auth-provider
3
+ level: intermediate
4
+ description: "Tool requiring a single OAuth provider via the `authProviders: ['github']` string shorthand — credentials loaded before `execute()` runs."
5
+ tags: [auth-providers, oauth, github, this.authProviders]
6
+ features:
7
+ - "Declaring a single required OAuth provider with the `authProviders: ['github']` shorthand"
8
+ - "Reading pre-formatted credentials via `await this.authProviders.headers('github')`"
9
+ - "Letting the framework reject calls whose required credential is missing **before** `execute()` runs — a JSON-RPC `-32001` (MCP `UNAUTHORIZED`) error whose `data` carries `{ tool, providers: ['github'], authUrl }` (no auth-check boilerplate)"
10
+ - 'Trusting the framework to handle token refresh, expiration, and the connect/authorize URL'
11
+ ---
12
+
13
+ # Tool With Single Auth Provider
14
+
15
+ Tool requiring a single OAuth provider via the `authProviders: ['github']` string shorthand — credentials loaded before `execute()` runs.
16
+
17
+ The shorthand form. By the time `execute()` runs, the user has completed the OAuth flow and credentials are in the vault — `this.authProviders.headers('github')` returns `{ Authorization: 'Bearer …' }` ready to forward.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/list-repos.tool.ts
23
+ import { Tool, ToolContext, z } from '@frontmcp/sdk';
24
+
25
+ const inputSchema = {
26
+ visibility: z.enum(['all', 'public', 'private']).default('all'),
27
+ perPage: z.number().int().min(1).max(100).default(30),
28
+ };
29
+ const outputSchema = {
30
+ repos: z.array(z.object({ fullName: z.string(), stars: z.number().int(), private: z.boolean() })),
31
+ };
32
+
33
+ @Tool({
34
+ name: 'list_repos',
35
+ description: 'List GitHub repos the authenticated user has access to',
36
+ inputSchema,
37
+ outputSchema,
38
+ authProviders: ['github'], // shorthand — single, required provider
39
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
40
+ })
41
+ export class ListReposTool extends ToolContext {
42
+ async execute(input: { visibility: 'all' | 'public' | 'private'; perPage: number }) {
43
+ const headers = await this.authProviders.headers('github');
44
+
45
+ const url = new URL('https://api.github.com/user/repos');
46
+ url.searchParams.set('visibility', input.visibility);
47
+ url.searchParams.set('per_page', String(input.perPage));
48
+
49
+ const response = await this.fetch(url, { headers });
50
+ const data = (await response.json()) as Array<{ full_name: string; stargazers_count: number; private: boolean }>;
51
+
52
+ return {
53
+ repos: data.map((r) => ({
54
+ fullName: r.full_name,
55
+ stars: r.stargazers_count,
56
+ private: r.private,
57
+ })),
58
+ };
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## What This Demonstrates
64
+
65
+ - Declaring a single required OAuth provider with the `authProviders: ['github']` shorthand
66
+ - Reading pre-formatted credentials via `await this.authProviders.headers('github')`
67
+ - Letting the framework reject calls whose required credential is missing **before** `execute()` runs — a JSON-RPC `-32001` (MCP `UNAUTHORIZED`) error whose `data` carries `{ tool, providers: ['github'], authUrl }` (no auth-check boilerplate)
68
+ - Trusting the framework to handle token refresh, expiration, and the connect/authorize URL
69
+
70
+ ## What you don't have to write
71
+
72
+ ```typescript
73
+ // ❌ unnecessary — the framework already did all of this before execute() ran:
74
+ const cred = await this.authProviders.get('github');
75
+ if (!cred) {
76
+ this.fail(new PublicMcpError('No GitHub auth — please sign in', { authUrl: '…' }));
77
+ }
78
+ const accessToken = cred.credential.accessToken;
79
+ if (cred.expiresAt && Date.now() >= cred.expiresAt) {
80
+ /* refresh */
81
+ }
82
+ const headers = { Authorization: `Bearer ${accessToken}` };
83
+ ```
84
+
85
+ The single line `await this.authProviders.headers('github')` covers it all.
@@ -0,0 +1,105 @@
1
+ ---
2
+ name: 14-tool-with-multiple-auth-providers
3
+ level: advanced
4
+ description: 'Tool with the full `authProviders` mapping form — one required provider with explicit scopes, one optional provider with an alias, and graceful degradation when the optional creds are missing.'
5
+ tags: [auth-providers, oauth, scopes, optional-auth, this.authProviders.headers]
6
+ features:
7
+ - 'Using the object form of `authProviders` to set `required`, `scopes`, and `alias`'
8
+ - Declaring required OAuth scopes that the server advertises in its Protected Resource Metadata (`scopes_supported`) so clients request them
9
+ - "Resolving an optional provider via `await this.authProviders.headers('cloud')` (returns an empty object `{}` when absent)"
10
+ - "Branching the tool's behavior — full deploy when both providers are present; preview-only when the cloud provider is missing"
11
+ - "The required `github` provider gating the call: when its credential is missing the framework aborts before `execute()` with `-32001` and `data: { tool, providers: ['github'], authUrl }`; the optional `aws`/`cloud` provider never gates"
12
+ ---
13
+
14
+ # Tool With Multiple Auth Providers
15
+
16
+ Tool with the full `authProviders` mapping form — one required provider with explicit scopes, one optional provider with an alias, and graceful degradation when the optional creds are missing.
17
+
18
+ The full form unlocks scopes, optional providers, and aliases. Use it when the simple shorthand isn't enough.
19
+
20
+ ## Code
21
+
22
+ ```typescript
23
+ // src/apps/main/tools/deploy-app.tool.ts
24
+ import { Tool, ToolContext, z } from '@frontmcp/sdk';
25
+
26
+ const inputSchema = {
27
+ repo: z.string().regex(/^[^/]+\/[^/]+$/),
28
+ environment: z.enum(['staging', 'production']),
29
+ dryRun: z.boolean().default(false),
30
+ };
31
+ const outputSchema = {
32
+ deploymentId: z.string(),
33
+ url: z.string().url(),
34
+ mode: z.enum(['preview', 'deployed']),
35
+ };
36
+
37
+ @Tool({
38
+ name: 'deploy_app',
39
+ description: 'Build and deploy a repo to cloud',
40
+ inputSchema,
41
+ outputSchema,
42
+ authProviders: [
43
+ { name: 'github', required: true, scopes: ['repo', 'workflow'] }, // required + scoped
44
+ { name: 'aws', required: false, alias: 'cloud' }, // optional, aliased
45
+ ],
46
+ annotations: { destructiveHint: true, idempotentHint: false, openWorldHint: true },
47
+ })
48
+ export class DeployAppTool extends ToolContext {
49
+ async execute(input: { repo: string; environment: 'staging' | 'production'; dryRun: boolean }) {
50
+ const githubHeaders = await this.authProviders.headers('github');
51
+ const cloudHeaders = await this.authProviders.headers('cloud'); // {} when AWS not connected
52
+ const hasCloud = Object.keys(cloudHeaders).length > 0;
53
+
54
+ // 1. Build artifact from the repo (always works — we have GitHub creds)
55
+ const buildId = await this.triggerBuild(input.repo, githubHeaders);
56
+
57
+ // 2. Deploy — only if cloud creds are present
58
+ if (!hasCloud || input.dryRun) {
59
+ return {
60
+ deploymentId: `preview-${buildId}`,
61
+ url: `https://preview.example.com/${buildId}`,
62
+ mode: 'preview' as const,
63
+ };
64
+ }
65
+
66
+ const deploymentId = await this.deployToCloud(buildId, input.environment, cloudHeaders);
67
+ return {
68
+ deploymentId,
69
+ url: `https://${input.environment}.example.com/${deploymentId}`,
70
+ mode: 'deployed' as const,
71
+ };
72
+ }
73
+
74
+ private async triggerBuild(_repo: string, _headers: Record<string, string>): Promise<string> {
75
+ return 'b_42';
76
+ }
77
+ private async deployToCloud(_buildId: string, _env: string, _headers: Record<string, string>): Promise<string> {
78
+ return 'd_99';
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## What This Demonstrates
84
+
85
+ - Using the object form of `authProviders` to set `required`, `scopes`, and `alias`
86
+ - Declaring required OAuth scopes that the server advertises in its Protected Resource Metadata (`scopes_supported`) so clients request them
87
+ - Resolving an optional provider via `await this.authProviders.headers('cloud')` (returns an empty object `{}` when absent)
88
+ - Branching the tool's behavior — full deploy when both providers are present; preview-only when the cloud provider is missing
89
+ - The required `github` provider gating the call: when its credential is missing the framework aborts before `execute()` with `-32001` and `data: { tool, providers: ['github'], authUrl }`; the optional `aws`/`cloud` provider never gates
90
+
91
+ ## Field reference (from auth-providers.md)
92
+
93
+ | Field | Default | Meaning |
94
+ | ---------- | -------- | -------------------------------------------------------------------------------------------- |
95
+ | `name` | — | Provider name — must match a registered credential provider |
96
+ | `required` | `true` | If `true`, the tool errors (`-32001`) before `execute()` runs when the credential is missing |
97
+ | `scopes` | — | OAuth scopes — advertised via PRM `scopes_supported` so clients know to request them |
98
+ | `alias` | = `name` | Local name for the provider — useful when two tools use the same provider differently |
99
+
100
+ ## When to use the object form
101
+
102
+ - Need scopes (`required: true, scopes: ['repo']`) — must use the object form
103
+ - Need optional providers (`required: false`) — must use the object form
104
+ - Need to alias the provider name (rare) — must use the object form
105
+ - Just one always-required provider with default scopes → the `authProviders: ['github']` shorthand is shorter
@@ -0,0 +1,115 @@
1
+ ---
2
+ name: 15-tool-with-credential-vault
3
+ level: advanced
4
+ description: "Tool that reads a user-supplied static credential (a Slack webhook URL) from the per-session encrypted credential vault — the pattern for credentials that aren't OAuth."
5
+ tags: [auth-providers, credential-vault, slack-webhook, encryption-at-rest]
6
+ features:
7
+ - "Declaring a vault-backed auth provider with `authProviders: ['slack-webhook']`"
8
+ - "Reading the user's pasted-in credential via `await this.authProviders.headers('slack-webhook')` — same API as OAuth"
9
+ - Letting the framework handle per-session AES-256-GCM encryption at rest (Redis or memory store)
10
+ - Knowing when to pick the vault (static secrets the user knows) vs OAuth (delegated identity)
11
+ ---
12
+
13
+ # Tool With Credential Vault
14
+
15
+ Tool that reads a user-supplied static credential (a Slack webhook URL) from the per-session encrypted credential vault — the pattern for credentials that aren't OAuth.
16
+
17
+ For credentials that aren't OAuth — webhook URLs, API keys the user pastes in, custom tokens — use a vault-backed auth provider. Same API as OAuth from the tool's perspective; the framework handles per-session encryption and key derivation.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/send-to-slack.tool.ts
23
+ import { PublicMcpError, Tool, ToolContext, z } from '@frontmcp/sdk';
24
+
25
+ const inputSchema = {
26
+ channel: z.string().regex(/^#/).describe('Slack channel, e.g. #ops'),
27
+ text: z.string().min(1).max(4_000),
28
+ username: z.string().optional(),
29
+ iconEmoji: z.string().optional(),
30
+ };
31
+ const outputSchema = { sent: z.boolean(), channel: z.string() };
32
+
33
+ @Tool({
34
+ name: 'send_to_slack',
35
+ description: 'Send a message to a Slack channel via the user-supplied incoming-webhook URL',
36
+ inputSchema,
37
+ outputSchema,
38
+ authProviders: ['slack-webhook'], // vault-backed provider — see auth skill
39
+ annotations: { destructiveHint: false, idempotentHint: false, openWorldHint: true },
40
+ })
41
+ export class SendToSlackTool extends ToolContext {
42
+ async execute(input: { channel: string; text: string; username?: string; iconEmoji?: string }) {
43
+ const headers = await this.authProviders.headers('slack-webhook');
44
+ // `headers` is a plain `Record<string, string>` (NOT a `Headers` object), e.g.:
45
+ // { 'x-slack-webhook-url': 'https://hooks.slack.com/services/T.../B.../...' }
46
+ // Read values with string-index access (`headers['x-...']`), not `headers.get(...)`.
47
+ // The framework reads the value from the vault, decrypts with the per-session AES-256-GCM
48
+ // key, and never returns the raw value — it's only available indirectly via these headers.
49
+
50
+ // Reading the key directly is safe here because we declared `authProviders: ['slack-webhook']`
51
+ // (required: true by default), so the framework already rejected the call before `execute()`
52
+ // if the vault entry was missing — by the time we read the header, the provider is guaranteed
53
+ // to have produced it. If you ever switch the provider to `required: false`, `headers` will be
54
+ // an empty object `{}` when no credential is set, so guard with
55
+ // `if (!webhookUrl) this.fail(new PublicMcpError('No Slack webhook configured'))` instead.
56
+ const webhookUrl = headers['x-slack-webhook-url'];
57
+ const response = await this.fetch(webhookUrl, {
58
+ method: 'POST',
59
+ headers: { 'content-type': 'application/json' },
60
+ body: JSON.stringify({
61
+ channel: input.channel,
62
+ text: input.text,
63
+ username: input.username,
64
+ icon_emoji: input.iconEmoji,
65
+ }),
66
+ });
67
+
68
+ if (!response.ok) {
69
+ this.fail(new PublicMcpError(`Slack webhook returned ${response.status}`));
70
+ }
71
+
72
+ return { sent: true, channel: input.channel };
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## What This Demonstrates
78
+
79
+ - Declaring a vault-backed auth provider with `authProviders: ['slack-webhook']`
80
+ - Reading the user's pasted-in credential via `await this.authProviders.headers('slack-webhook')` — same API as OAuth
81
+ - Letting the framework handle per-session AES-256-GCM encryption at rest (Redis or memory store)
82
+ - Knowing when to pick the vault (static secrets the user knows) vs OAuth (delegated identity)
83
+
84
+ ## Vault vs OAuth — when to pick which
85
+
86
+ | Vault-backed | OAuth |
87
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
88
+ | User pastes in a webhook URL, API key, or token they own | User clicks "Connect to GitHub" — provider issues a token to your server |
89
+ | Static — doesn't refresh, doesn't expire | Refresh token + access token + expiration |
90
+ | Per-session — gone when the session ends (vs. Redis store, then it survives) | Long-lived — usually persists across sessions |
91
+ | For "bring your own" integrations (Slack webhooks, custom API keys) | For "log in with" integrations (GitHub, Google, Microsoft) |
92
+
93
+ ## How vault-backed providers are configured
94
+
95
+ Server side, in the `auth` skill:
96
+
97
+ ```typescript
98
+ @FrontMcp({
99
+ auth: {
100
+ providers: [
101
+ {
102
+ name: 'slack-webhook',
103
+ kind: 'vault',
104
+ fields: [
105
+ { name: 'webhook-url', type: 'url', label: 'Slack incoming-webhook URL', required: true },
106
+ ],
107
+ },
108
+ ],
109
+ },
110
+ })
111
+ ```
112
+
113
+ The framework renders the credential UI based on `fields`, encrypts the user's input, stores it per-session, and exposes it back to tools via `this.authProviders.headers('slack-webhook')`.
114
+
115
+ See the `auth` skill for full vault configuration, encryption-key management, Redis-backed storage, and the credential UI.
@@ -0,0 +1,71 @@
1
+ ---
2
+ name: 16-tool-with-rate-limit
3
+ level: intermediate
4
+ description: 'Tool with `rateLimit: { maxRequests, windowMs }` capping invocations per session per minute — the protection for expensive / external-API-billed operations.'
5
+ tags: [throttling, rate-limit, abuse-protection]
6
+ features:
7
+ - "Capping the tool to N invocations per windowMs, partitioned per session via `partitionBy: 'session'`"
8
+ - "Letting the framework reject over-limit calls with `RateLimitError` (code `'RATE_LIMIT_EXCEEDED'`, HTTP status 429) carrying a retry-after hint in its message that clients can back off against"
9
+ - 'Combining `rateLimit` with `annotations.openWorldHint: true` so clients know the tool talks to billed external services'
10
+ - Sizing the limit against upstream quota / billing — not just "what feels reasonable"
11
+ ---
12
+
13
+ # Tool With Rate Limit
14
+
15
+ Tool with `rateLimit: { maxRequests, windowMs }` capping invocations per session per minute — the protection for expensive / external-API-billed operations.
16
+
17
+ The first throttle to reach for. Caps invocations over time. The default partition is `'global'` (one shared limit); this example sets `partitionBy: 'session'` so a runaway agent loop on one session can't burn through a quota that other sessions need.
18
+
19
+ ## Code
20
+
21
+ ```typescript
22
+ // src/apps/main/tools/translate.tool.ts
23
+ import { Tool, ToolContext, z } from '@frontmcp/sdk';
24
+
25
+ const inputSchema = {
26
+ text: z.string().min(1).max(2_000),
27
+ targetLang: z.string().regex(/^[a-z]{2}$/, 'ISO 639-1 code'),
28
+ };
29
+ const outputSchema = { translated: z.string(), sourceLang: z.string() };
30
+
31
+ @Tool({
32
+ name: 'translate',
33
+ description: 'Translate text via the external translation API (billed per call)',
34
+ inputSchema,
35
+ outputSchema,
36
+ rateLimit: { maxRequests: 60, windowMs: 60_000, partitionBy: 'session' }, // 60 calls / minute / session
37
+ authProviders: ['translate-api'],
38
+ annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
39
+ })
40
+ export class TranslateTool extends ToolContext {
41
+ async execute(input: { text: string; targetLang: string }) {
42
+ const headers = await this.authProviders.headers('translate-api');
43
+ const response = await this.fetch('https://api.translate.example/v1/translate', {
44
+ method: 'POST',
45
+ headers: { ...headers, 'content-type': 'application/json' },
46
+ body: JSON.stringify({ text: input.text, target: input.targetLang }),
47
+ });
48
+ const data = (await response.json()) as { translated: string; detectedSource: string };
49
+ return { translated: data.translated, sourceLang: data.detectedSource };
50
+ }
51
+ }
52
+ ```
53
+
54
+ > **Testing.** The framework throws `RateLimitError` (HTTP 429, code `'RATE_LIMIT_EXCEEDED'`) for over-limit calls. Tests that assert the rate-limit fires after N calls live in the dedicated `testing` skill — the canonical pattern uses `@frontmcp/testing`'s `TestServer` + Playwright `test`/`expect` fixtures.
55
+
56
+ ## What This Demonstrates
57
+
58
+ - Capping the tool to N invocations per windowMs, partitioned per session via `partitionBy: 'session'`
59
+ - Letting the framework reject over-limit calls with `RateLimitError` (code `'RATE_LIMIT_EXCEEDED'`, HTTP status 429) carrying a retry-after hint in its message that clients can back off against
60
+ - Combining `rateLimit` with `annotations.openWorldHint: true` so clients know the tool talks to billed external services
61
+ - Sizing the limit against upstream quota / billing — not just "what feels reasonable"
62
+
63
+ ## Sizing the limit
64
+
65
+ - **External API quotas** — set well below the upstream quota. If GitHub allows 5,000/hour authenticated, a per-session limit of 30/min (1,800/hr) gives room for multiple sessions to share the budget.
66
+ - **Billed services** — lower. A `translate` call may cost $0.02. 60/min × 60min × 24h × 1 session = $1,728/day worst case.
67
+ - **Pure rate concern, not cost** — 100/min is generous for almost anything.
68
+
69
+ ## Global vs per-session
70
+
71
+ The default partition is `'global'` — one limit shared across all callers. For a limit that's tracked independently per session, set `partitionBy: 'session'` (so a runaway loop on one session can't burn through everyone's budget). See the `throttling` reference for the full set of `partitionBy` strategies (`'global' | 'session' | 'userId' | 'ip'` or a custom function).