Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
f3c5b82
feat(skills): add create-tool-ui reference for the MCP Apps widget su…
frontegg-david May 28, 2026
8753aef
docs(skills): document @Tool({ ui }) option in create-tool reference …
frontegg-david May 28, 2026
10719da
docs(skills): note TS7006 workaround for ui.template `ctx` inference …
frontegg-david May 28, 2026
5b621dc
fix(uipack): surface actionable error when @frontmcp/ui is missing fo…
frontegg-david May 28, 2026
90687cc
fix(uipack): surface actionable error when relative FileSource path m…
frontegg-david May 28, 2026
629f9a3
fix(cli): exclude *.widget.tsx from server typecheck so React widgets…
frontegg-david May 28, 2026
bbf5049
feat: add Tool UI section with interactive widget support in create-t…
frontegg-david May 28, 2026
5095ac6
docs(uipack): correct contradictory Claude CDN guidance for tool UI w…
frontegg-david May 28, 2026
afe6a59
fix(uipack): make resourceMode:'inline' actually inline React for .ts…
frontegg-david May 28, 2026
cfea3a7
feat(uipack): implement resource-level _meta handling for widget reso…
frontegg-david May 28, 2026
09b03fe
Merge remote-tracking branch 'origin/feat/tool-ui-skill-suite' into f…
frontegg-david May 28, 2026
543f93a
fix(sdk): emit ui.csp on the widget resource so Claude actually honor…
frontegg-david May 28, 2026
04a1639
fix(uipack): host-detect resourceMode default; auto-inline React on C…
frontegg-david May 28, 2026
2bd9f64
chore(review): address code-review nits — tighter types and fix broke…
frontegg-david May 28, 2026
aadd4a8
chore(review): address code-review nits — tighter types and fix broke…
frontegg-david May 28, 2026
e587d91
chore(review): address code-review nits — tighter types and fix broke…
frontegg-david May 28, 2026
acbd416
Merge branch 'refs/heads/main' into refactoring-skills
frontegg-david May 29, 2026
7b61940
feat: add new skills for tool validation and output schema requirements
frontegg-david May 29, 2026
e386164
fix: improve error handling and documentation consistency in tool spe…
frontegg-david May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions libs/skills/__tests__/manifest.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Skills manifest validation tests.
*/

import { VALID_TARGETS, VALID_CATEGORIES, VALID_BUNDLES } from '../src/manifest';
import { VALID_BUNDLES, VALID_CATEGORIES, VALID_TARGETS } from '../src/manifest';

describe('manifest constants', () => {
it('should export valid targets', () => {
Expand All @@ -24,7 +24,8 @@ describe('manifest constants', () => {
expect(VALID_CATEGORIES).toContain('production');
expect(VALID_CATEGORIES).toContain('extensibility');
expect(VALID_CATEGORIES).toContain('observability');
expect(VALID_CATEGORIES).toHaveLength(9);
expect(VALID_CATEGORIES).toContain('development/create');
expect(VALID_CATEGORIES).toHaveLength(10);
});

it('should export valid bundles', () => {
Expand Down
337 changes: 300 additions & 37 deletions libs/skills/__tests__/skills-validation.spec.ts

Large diffs are not rendered by default.

26 changes: 26 additions & 0 deletions libs/skills/catalog/TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,29 @@ frontmatter `name`.

- [Documentation](https://docs.agentfront.dev/frontmcp/...)
- Related skills: `related-skill-a`, `related-skill-b`

---

## Alternative: `layout: 'component'`

The template above describes the **router layout** (`layout: 'router'`, the default) — a Scenario Routing Table SKILL.md, `references/<topic>.md` files, and examples grouped under `examples/<topic>/<example>.md`.

For per-thing skills (`create-tool`, `create-resource`, `create-prompt`, etc.) use the **component layout** instead. Opt in by setting `layout: component` in the manifest entry. Differences:

| Aspect | Router layout (default) | Component layout |
| -------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| SKILL.md frontmatter | Minimal — `name`, `description`, optional `priority`/`visibility` | **Rich** — multi-line `description:` with an explicit `Triggers:` list, `paths:` glob array, `when_to_use` block, top-level `priority`/`visibility`/`tags`/`category`. Designed for Claude Code's auto-discovery heuristics. |
| `examples/` | Grouped: `examples/<reference>/<example>.md` | Flat: `examples/<example>.md` |
| `rules/` | Not used | `rules/<rule>.md` — short DO/DON'T constraint files with `name`, `constraint`, `severity: required \| recommended` frontmatter |
| Manifest entry | `references[].examples[]` | Top-level `examples[]` + top-level `rules[]` |
| SKILL.md body | "Scenario Routing Table" pointing at references | "Scenario Routing Table" pointing at examples + a `Rules` table pointing at `rules/*.md` |

Every example file MUST still satisfy the same alignment invariants enforced by `skills-validation.spec.ts`:

- Frontmatter `description` = first paragraph after the H1.
- Frontmatter `features` = bullets under `## What This Demonstrates`.
- Manifest example entry `description` / `level` / `tags` / `features` = the file's frontmatter.

For component-layout skills the manifest sync extends to `rules[]`: the rule file's frontmatter `constraint` and `severity` must match the manifest entry.

See `create-tool/SKILL.md` for a complete working example of the component layout.
318 changes: 318 additions & 0 deletions libs/skills/catalog/create-tool/SKILL.md

Large diffs are not rendered by default.

112 changes: 112 additions & 0 deletions libs/skills/catalog/create-tool/examples/01-basic-class-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
name: 01-basic-class-tool
level: basic
description: Minimal class-based tool with Zod input/output schemas and types derived from the schemas. The foundation every other example builds on.
tags: [foundation, class-tool, output-schema, derived-types]
features:
- 'Extending `ToolContext` (no generics) and implementing `execute()`'
- 'Hoisting `inputSchema` / `outputSchema` to a sibling `.schema.ts` file'
- 'Deriving the `execute()` parameter and return types via `ToolInputOf<>` / `ToolOutputOf<>`'
- 'Using a Zod raw shape for `inputSchema` (not `z.object(...)`)'
- "Always defining `outputSchema` so the tool can't accidentally leak extra fields"
- 'Registering the tool in an `@App({ tools })`'
---

# Basic Class Tool

Minimal class-based tool with Zod input/output schemas and types derived from the schemas. The foundation every other example builds on.

The foundation. Two files (schema + tool), schemas as the single source of truth, derived `execute()` types, full output validation. Every other example in this skill builds on this shape.

## Files

```text
src/apps/main/tools/
├── greet-user.schema.ts # input/output schemas + derived types
├── greet-user.tool.ts # @Tool class, execute()
└── greet-user.tool.spec.ts # unit test
```

## Code

```typescript
// src/apps/main/tools/greet-user.schema.ts
import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';

export const inputSchema = {
name: z.string().min(1).describe('The name of the user to greet'),
};

export const outputSchema = {
greeting: z.string(),
};

export type GreetUserInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
export type GreetUserOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
```

```typescript
// src/apps/main/tools/greet-user.tool.ts
import { Tool, ToolContext } from '@frontmcp/sdk';

import { inputSchema, outputSchema, type GreetUserInput, type GreetUserOutput } from './greet-user.schema';

@Tool({
name: 'greet_user',
description: 'Greet a user by name',
inputSchema,
outputSchema,
})
export class GreetUserTool extends ToolContext {
async execute(input: GreetUserInput): Promise<GreetUserOutput> {
return { greeting: `Hello, ${input.name}!` };
}
}
```

```typescript
// src/apps/main/index.ts
import { App } from '@frontmcp/sdk';

import { GreetUserTool } from './tools/greet-user.tool';

@App({
name: 'main',
tools: [GreetUserTool],
})
export class MainApp {}
```

> **Testing.** Per-tool tests use the `@frontmcp/testing` surface — `TestServer` + Playwright-style `test` / `expect` fixtures, with `mcpMatchers` for response assertions. The full pattern lives in the dedicated `testing` skill; this skill stays focused on building the tool itself.

## What This Demonstrates

- Extending `ToolContext` (no generics) and implementing `execute()`
- Hoisting `inputSchema` / `outputSchema` to a sibling `.schema.ts` file
- Deriving the `execute()` parameter and return types via `ToolInputOf<>` / `ToolOutputOf<>`
- Using a Zod raw shape for `inputSchema` (not `z.object(...)`)
- Always defining `outputSchema` so the tool can't accidentally leak extra fields
- Registering the tool in an `@App({ tools })`

## Why each choice matters

- **No `ToolContext<typeof inputSchema>` generic** — input/output types are auto-inferred from the `@Tool` decorator at the class level. Adding the generic is a smell (see `rules/no-toolcontext-generics.md`).
- **Hoist only the schemas** (not the decorator config) to `<name>.schema.ts` — specs, sibling tools, and generated clients can `import { inputSchema, GreetUserInput }` without dragging the `@Tool` class along.
- **Use `ToolInputOf<>` / `ToolOutputOf<>`** instead of inline annotations like `execute(input: { name: string })`, which silently drift when the schema changes.
- **Raw Zod shape** for `inputSchema` — `{ name: z.string() }`, not `z.object({ name: z.string() })`. The framework wraps it internally.
- **`outputSchema` always present** — without it, returning `{ greeting, leakedSecret }` would expose `leakedSecret` to the client; with it, the field is stripped before the response leaves.
- **Register in `@App({ tools })`** (not directly in `@FrontMcp({ tools })`) — apps provide per-app lifecycle, auth, and hooks; top-level registration is the simple-server escape hatch.

## When to pick this shape over alternatives

- **Class** (this example) vs **function** (`tool({...})(handler)`) — pick class for anything with DI (`this.get`), lifecycle, hooks, or UI widgets. Pick function only for trivial pure-input tools. See [`02-basic-function-tool`](./02-basic-function-tool.md).
- **Sibling files** (this example) vs **folder-per-tool** — pick siblings for apps with ≤3 tools each. Promote to a folder once the tool has local helpers, fixtures, or its own error types. See [`references/file-layout.md`](../references/file-layout.md).

## Related rules

- [`rules/input-schema-is-raw-shape.md`](../rules/input-schema-is-raw-shape.md)
- [`rules/always-define-output-schema.md`](../rules/always-define-output-schema.md)
- [`rules/derive-execute-types.md`](../rules/derive-execute-types.md)
- [`rules/no-toolcontext-generics.md`](../rules/no-toolcontext-generics.md)
- [`rules/snake-case-tool-names.md`](../rules/snake-case-tool-names.md)
- [`rules/register-in-app.md`](../rules/register-in-app.md)
72 changes: 72 additions & 0 deletions libs/skills/catalog/create-tool/examples/02-basic-function-tool.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: 02-basic-function-tool
level: basic
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.'
tags: [foundation, function-tool, tool-builder]
features:
- 'Using the `tool({...})(handler)` builder for a one-liner'
- "Returning a primitive via `outputSchema: 'number'`"
- 'Registering the function-style tool in `@App({ tools })` exactly like a class tool'
- "When function-style is the right choice (and when it isn't)"
---

# Basic Function Tool

Function-style `tool({...})(handler)` for a tiny pure-input tool — pick this over a class only when the tool needs no DI / lifecycle / UI.

For trivial pure-input tools, the `tool()` function builder is a one-liner alternative to `@Tool` + class.

## Code

```typescript
// src/apps/main/tools/add-numbers.tool.ts
import { tool, z } from '@frontmcp/sdk';

export const AddNumbers = tool({
name: 'add_numbers',
description: 'Add two numbers',
inputSchema: {
a: z.number().describe('First number'),
b: z.number().describe('Second number'),
},
outputSchema: 'number',
})((input) => input.a + input.b);
```

```typescript
// src/apps/main/tools/add-numbers.tool.spec.ts
import { testTool } from '@frontmcp/testing';

import { AddNumbers } from './add-numbers.tool';

describe('AddNumbers', () => {
it('adds two numbers', async () => {
expect(await testTool(AddNumbers).call({ a: 2, b: 3 })).toBe(5);
});
});
```

```typescript
// src/apps/main/index.ts
import { App } from '@frontmcp/sdk';

import { AddNumbers } from './tools/add-numbers.tool';

@App({ name: 'main', tools: [AddNumbers] })
export class MainApp {}
```

## What This Demonstrates

- Using the `tool({...})(handler)` builder for a one-liner
- Returning a primitive via `outputSchema: 'number'`
- Registering the function-style tool in `@App({ tools })` exactly like a class tool
- When function-style is the right choice (and when it isn't)

## When to pick function-style over class

- ✅ Pure math / formatting / parsing — no DI, no lifecycle, no UI widget
- ❌ Anything that needs `this.get(TOKEN)` — promote to class
- ❌ Anything with a `ui:` widget — class + folder-per-tool layout is cleaner

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.fail` / `ctx.fetch`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
name: 03-tool-with-zod-shape-output
level: basic
description: Tool returning structured JSON declared via a Zod raw shape outputSchema — the recommended pattern for any complex output.
tags: [output-schema, zod-shape, structured-output]
features:
- 'Declaring `outputSchema` as a Zod raw shape `{ field: z.string(), … }`'
- 'Constraining values with `.int().min(0)` so invalid output is rejected at the boundary'
- "Letting unrelated fields returned by the implementation (e.g. an upstream API's extras) be stripped silently"
- "Deriving `OrderSummaryOutput` once so the type and runtime contract can't drift"
---

# Tool With Zod Shape Output

Tool returning structured JSON declared via a Zod raw shape outputSchema — the recommended pattern for any complex output.

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.

## Code

```typescript
// src/apps/main/tools/order-summary.schema.ts
import { ToolInputOf, ToolOutputOf, z } from '@frontmcp/sdk';

export const inputSchema = {
orderId: z.string().uuid().describe('Order UUID'),
};

export const outputSchema = {
orderId: z.string(),
customer: z.string(),
totalUsd: z.number(),
itemCount: z.number().int().min(0),
pendingCount: z.number().int().min(0),
status: z.enum(['pending', 'paid', 'shipped', 'delivered', 'cancelled']),
};

export type OrderSummaryInput = ToolInputOf<{ inputSchema: typeof inputSchema }>;
export type OrderSummaryOutput = ToolOutputOf<{ outputSchema: typeof outputSchema }>;
```

```typescript
// src/apps/main/tools/order-summary.tool.ts
import { Tool, ToolContext } from '@frontmcp/sdk';

import { ORDERS_REPO } from '../tokens';
import { inputSchema, outputSchema, type OrderSummaryInput, type OrderSummaryOutput } from './order-summary.schema';

@Tool({
name: 'order_summary',
description: 'Summary for an order — totals, item counts, and status.',
inputSchema,
outputSchema,
})
export class OrderSummaryTool extends ToolContext {
async execute(input: OrderSummaryInput): Promise<OrderSummaryOutput> {
const repo = this.get(ORDERS_REPO);
const order = await repo.findById(input.orderId);
// `order` may carry { …, internalNotes, paymentProviderRef, debug }
// — none of those are in outputSchema, so they're stripped before returning.
return {
orderId: order.id,
customer: order.customerName,
totalUsd: order.totalCents / 100,
itemCount: order.items.length,
pendingCount: order.items.filter((i) => i.status === 'pending').length,
status: order.status,
};
}
}
```

## What This Demonstrates

- Declaring `outputSchema` as a Zod raw shape `{ field: z.string(), … }`
- Constraining values with `.int().min(0)` so invalid output is rejected at the boundary
- Letting unrelated fields returned by the implementation (e.g. an upstream API's extras) be stripped silently
- Deriving `OrderSummaryOutput` once so the type and runtime contract can't drift
Loading
Loading