Skip to main content
Tools are typed actions that execute operations with side effects. They’re the primary way to enable an AI model to interact with external systems—calling APIs, modifying data, performing calculations, or triggering workflows.
This feature implements the MCP Tools specification. FrontMCP handles all protocol details automatically.
Nx users: Scaffold with nx g @frontmcp/nx:tool my-tool --project my-app. See Tool Generator.

Why Tools?

In the Model Context Protocol, tools serve a distinct purpose from resources and prompts: Tools are ideal for:
  • API integrations — call external services, webhooks, third-party APIs
  • Data mutations — create, update, delete records
  • Calculations — perform computations, transformations
  • System operations — file operations, process management
  • Workflows — trigger multi-step processes, orchestration

Creating Tools

Class Style

Use class decorators for tools that need dependency injection, lifecycle hooks, or complex logic:

Function Style

For simpler tools, use the functional builder:

Registering Tools

Add tools to your app via the tools array:
Tools can also be generated dynamically by adapters (e.g., OpenAPI adapter) or plugins.

Loading from npm or Remote Servers

You can mix local tool classes with tools loaded from npm packages or proxied from remote MCP servers:
Tool.esm() loads a single named tool from an npm package at runtime. Tool.remote() proxies a single tool from a remote MCP server. For loading entire apps (all tools, resources, prompts), use App.esm() or App.remote().

Input Schemas

Tools use Zod schemas for type-safe input validation. The schema is automatically converted to JSON Schema for MCP protocol compatibility. tools/list describes the input the caller sends, not the value after parsing: a field with .default() is not listed as required, and a field with .transform() is listed by its input type. The same JSON Schema is used for agents and skills.

Basic Types

With Descriptions

Optional and Default Values

Complex Types


Output Schemas

Optionally define an output schema for response validation:
A result that does not match outputSchema fails the call: the client gets an error result with _meta.code: "INVALID_OUTPUT", and nothing from the rejected result is sent. When the result matches, fields the schema does not declare are dropped, with one exception: a _meta object on the returned value is copied into the response’s _meta without validation, so keep secrets out of it. A number field holding Infinity, -Infinity, or NaN also fails with INVALID_OUTPUT, unless the server sets output: { allowNonFinite: true }. JSON cannot encode those values, so with that setting the client receives null in their place.

Output schema exposure

By default a structured outputSchema is advertised as the tool’s outputSchema (JSON Schema) in tools/list. The output policy lets you control where that schema surfaces. It is declarable on @Tool, @App, and @FrontMcp, and the effective value for a tool is resolved with a Tool > App > server > default cascade — mirroring the OpenAPI adapter’s outputSchema.mode.
When the schema is folded into the description ('description' / 'both'), schemaDescriptionFormat picks the rendering: 'summary' (a compact human-readable property list) or 'jsonSchema' (a fenced JSON Schema code block).
Set it once at the server level to apply a house style everywhere, then override per tool where needed:
Output-schema exposure only applies to structured object outputs (a Zod raw shape or a top-level z.object) — the same forms that get advertised as outputSchema. Primitive, media, multi-content, and union outputs flow through content and have nothing object-shaped to expose. Runtime output validation still applies to every form regardless of schemaMode.

Return Values

Tools support multiple return formats. The SDK automatically converts your return value to the MCP CallToolResult format.

Simple Returns

Without an outputSchema, the result is sent as JSON: in structuredContent, and as JSON text in content. A plain value (string, number, boolean or array) is wrapped as { value: … }:
To send plain text or a bare number, declare the primitive output type. The text content is then the value itself, and structuredContent is { content: value }:
execute() must return a value. A tool that returns undefined (for example, an async execute() with no return) fails with _meta.code: "FLOW_EXITED_WITHOUT_OUTPUT" (“Flow exited without producing output”). Return an explicit result, such as { ok: true }, from tools that only have side effects.

Full MCP Format

For complete control over the response, return the full CallToolResult structure:

Multiple Content Items

Return an array to include multiple content blocks:

Tool Metadata

Field descriptions:

Tool Examples

Provide examples to improve discoverability and help LLMs understand how to use your tools effectively.

CodeCall Discovery

Examples are indexed for semantic search with 2x weight, helping users find the right tools faster.

LLM Understanding

The codecall:describe tool returns up to 5 examples per tool to help LLMs understand usage patterns.
If you don’t provide examples, codecall:describe generates one from the tool’s intent (create, list, get, update, delete, search). Its arguments come only from the properties your input schema declares: a query-like property (or else the first filter property such as status) for a search, the pagination properties the tool really has for a list, the required properties, and the first value of an enum. Values stay within the property’s enum, const, bounds and length limits; a pattern or format is not generated, so an optional property that has one is left out. With nothing to go on the example passes {}; it never invents an argument like query. When you provide examples, codecall:describe returns only yours (up to 5), with no generated example next to them.

Tool Annotations

Annotations provide hints to clients about tool behavior:

Tool Context

Class-based tools have access to a rich execution context via this:

Using Providers

Inject services via the get() method:

Request Context

Class-based tools have access to the full request context (FrontMcpContext) including tracing, timing, and authentication:
this.context returns the active FrontMcpContext. For typed identity (roles, scopes, claims) prefer this.auth. See Request Context for complete API reference.

Progress Notifications

Send real-time updates to clients during long-running operations using MCP notifications.

Sending Messages

Use this.notify() to send log messages to the client:
Log levels are the eight MCP levels: 'debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'. A message is sent only at or above the level the client asked for: with logging/setLevel on a session, or with _meta["io.modelcontextprotocol/logLevel"] on a 2026-07-28 request. Until the client asks, notify() returns false.

Sending Progress

Use this.progress() for progress bar updates. This only works when the client includes a progressToken in the request:
Progress notifications are only sent if the client includes a progressToken in the request’s _meta field. If no token is provided, this.progress() returns false and silently succeeds.

Progress Token Access

The progressToken is automatically extracted from the request’s _meta field and made available via the ToolCallExtra type for advanced use cases:
For most use cases, simply use this.progress() which handles the token automatically.

Real-World Examples

Calculator Tool

API Integration Tool

Database Mutation Tool

File Operation Tool


MCP Protocol Integration

Tools integrate with the MCP protocol via two flows: When a client requests tools/call with a name and arguments:
  1. The SDK locates the tool by name
  2. Arguments are validated against the tool’s inputSchema
  3. The execute() method is called with the validated arguments
  4. The return value is validated against outputSchema (if provided) and converted to MCP CallToolResult format

Capabilities

FrontMCP automatically advertises tool capabilities during MCP initialization:
The SDK sets listChanged: true when you have any tools registered, enabling clients to receive real-time notifications when tools are dynamically added or removed.

Change Notifications

When tools change dynamically (e.g., via adapters or plugins), FrontMCP automatically sends notifications/tools/list_changed to connected clients. Clients that support this notification will refresh their tool list.
For the full protocol specification, see MCP Tools.

Tool UI

Tools can render visual widgets alongside their responses. This enables rich, interactive presentations of tool outputs—weather cards, order summaries, data tables, and more.

Basic UI Configuration

Add a ui property to attach a visual template:

Template Types

FrontMCP auto-detects your template type:
A string that contains both < and > is used as HTML. MDX is not compiled: JSX tags in a string reach the client as HTML, {output.field} expressions are not evaluated, and mdxComponents is ignored. Use a .tsx FileSource widget for anything interactive. Markdown strings are converted on the server (headings, paragraphs, fenced code, lists, bold, italic, inline code, links). Raw HTML in the Markdown is escaped, and a link survives only when its target starts with http:, https:, mailto:, / or #.
TypeScript: annotate the ctx parameter (TS7006). When you inline an HTML template as an arrow function, you need to annotate ctx. Because ui.template is a union of multiple callable shapes (TemplateBuilderFn | string | ((props: any) => any) | FileSource), TypeScript can’t pick a single contextual type for the arrow’s parameter, so template: (ctx) => … errors with Parameter 'ctx' implicitly has an 'any' type. Either annotate ctx: TemplateContext<MyInput, MyOutput> (imported from @frontmcp/sdk), or move the widget into its own .tsx file and use the FileSource form (template: { file: … }) — both avoid the inference gap.

UI Configuration Options

Options with no effect yet. These pass validation but nothing reads them, so setting one changes nothing; startup logs a warning per tool that lists them: widgetDescription, widgetAccessible, displayMode, prefersBorder, sandboxDomain, contentSecurity, hydrate, runtimeOptions, mdxComponents, bundlingMode, uiType and htmlResponsePrefix. There is no dual HTML payload for Claude, so htmlResponsePrefix does nothing. Serving modes. 'direct-url' and 'custom-url' are not implemented: the widget is served inline, as with 'inline', and startup logs a warning. 'hybrid' pre-compiles the shell at startup and sends only a reference in each call’s _meta['ui/component'] ({ type, hash, toolName }) — no component code and no data. Sanitization. Markup you author (template literals, HTML strings, .tsx files) is trusted and not sanitized. Values interpolated into html`…` are escaped; plain string results are escaped only with escapeStringResults: true; Markdown strings are always escaped.
Build template markup with ctx.helpers.html. A plain string a template function returns is rendered as markup when it looks like HTML, so returning unescaped tool output injects tags. The html tagged template escapes interpolated values; ctx.helpers.trustedHtml(markup) marks markup you already made safe. Set ui.escapeStringResults: true (or @FrontMcp({ ui: { escapeStringResults: true } })) to escape plain string results now — FrontMCP 1.9 does so by default. See Trusted markup.
Widget resources carry no caller data. resources/read ui://widget/{toolName}.html returns the widget compiled at startup (servingMode: 'static', or the hybrid shell) or a data-free placeholder that receives the result through the bridge. An inline render, which embeds the call’s input and output, is returned only in that call’s _meta['ui/html']. The advertised URI percent-encodes the tool name (app:tool becomes ui://widget/app%3Atool.html); both forms read back. See building-tool-ui.
.tsx/.jsx widgets require @frontmcp/ui installed. When ui.template points at a .tsx/.jsx file (the recommended pattern for non-trivial widgets), FrontMCP injects an auto-generated React mount that imports McpBridgeProvider from @frontmcp/ui/react. Install @frontmcp/ui in the consuming project at the same version as @frontmcp/sdk — without it, server-side bundling fails.Bundling also needs esbuild. @frontmcp/uipack loads it on demand to bundle the widget when the tool is called, so install it as a runtime dependency where the server runs (npm install esbuild — in dependencies, not devDependencies). Projects created with frontmcp create already get it through the frontmcp package. Without it, the call fails with an error naming the widget.In the default resourceMode: 'cdn' mode, react / react-dom stay external and load from the CDN at runtime, so only @frontmcp/ui needs to be present on disk. When the framework selects resourceMode: 'inline' — either explicitly or via host detection (Claude, #456) — react and react-dom must also be resolvable from the consuming project so esbuild can bundle them into the widget; install them as devDependencies if your project doesn’t already pull them in. See building-tool-ui for the install snippet and the per-host trade-offs.
FileSource paths resolve against process.cwd(), not the tool file. A bare template: { file: './widget.tsx' } from src/tools/foo.tool.ts looks for <cwd>/widget.tsx, not src/tools/widget.tsx — and the mismatch only surfaces at tool-call time as ENOENT. Always anchor relative paths to the tool source with fileURLToPath(new URL('./widget.tsx', import.meta.url)) (from node:url), or pass an absolute path:
In a CommonJS project ("type": "commonjs"), import.meta.url is unavailable — anchor with join(__dirname, 'weather.widget.tsx') (from node:path) instead. Both forms are independent of process.cwd(), but not of the build: once the tool is compiled they resolve next to the compiled file, so the widget has to ship there.frontmcp build copies every *.widget.tsx / *.widget.jsx under the entry’s directory into the output. Targets that run tsc’s output as emitted get them at the same relative path, next to each compiled tool. Bundled targets (node, cli, lambda, vercel) get them directly next to the bundle, because every bundled module’s __dirname is the bundle’s directory — keep each widget beside the tool that uses it and give it a unique file name. Only widget files are copied, not other local files a widget imports. A plain tsc build copies nothing, so add a copy step; otherwise the call fails with an ENOENT error that names the path it looked for.
resourceMode is host-detected for .tsx widgets. When you leave resourceMode unset, FrontMCP picks the right default per connecting client: Claude auto-switches to 'inline' (React bundled in — #456), so the widget actually renders in Claude’s sandboxed iframe. Other hosts keep 'cdn' (smaller payload, fetched from esm.sh). Set the value explicitly to opt out of detection:
Detection only applies to per-call rendering (inline / hybrid / lean serving modes). servingMode: 'static' widgets compile at server startup with no client context — set resourceMode: 'inline' explicitly when a static widget needs to render in Claude.
ui.csp is honored by Claude (#455). FrontMCP now attaches ui.csp to the resources/read content item’s _meta.ui.csp (and _meta['ui/csp']), not just to the tool listing. Claude only reads CSP from the resource content — declarations on the tool were previously silently ignored. Use ui.csp.connectDomains for fetch/XHR/WebSocket allow-lists and ui.csp.resourceDomains for images / scripts / fonts / styles.
Widget files use the *.widget.tsx naming convention. .tsx/.jsx widgets are bundled separately by uipack/esbuild at render time. The tsconfig.json scaffolded by frontmcp init excludes **/*.widget.tsx / **/*.widget.jsx from the server typecheck so widgets don’t force the project to set jsx: 'react-jsx' or pull in @types/react. Running frontmcp init on an existing project also adds these excludes. For IDE typecheck of widget sources, add a sibling tsconfig.widget.json with jsx: 'react-jsx' and include: ['src/**/*.widget.tsx'].
The widget page follows the host theme. When an MCP Apps host sends theme: 'light' or 'dark' — in the ui/initialize result or a later ui/notifications/host-context-changed — the window.FrontMcpBridge runtime sets <meta name="color-scheme"> and <html data-theme> to it, so the frame’s background and form controls match the host and your CSS can use [data-theme="dark"]. getTheme() returns the same value and onContextChange listeners also fire for the handshake context. Nothing is written when the host sends no theme. A widget styled for light only can keep it with :root { color-scheme: light }, which takes precedence over the meta tag.

Using @frontmcp/ui Components

@frontmcp/ui is a React library: components come from @frontmcp/ui/components and bridge hooks from @frontmcp/ui/react. It has no card(), badge(), descriptionList(), button(), form() or input() functions.
get-order.widget.tsx
get-order.tool.ts

Testing Tool UI

Use @frontmcp/testing for E2E validation of rendered widgets:

Building Tool UI

Complete configuration options and examples

React SDK

Pre-built React components from @frontmcp/react

Best Practices

Do:
  • Use descriptive name and description fields to help models understand tool purpose
  • Define clear input schemas with .describe() on each field
  • Use appropriate annotations (destructiveHint, idempotentHint, etc.) to guide client behavior
  • Validate inputs thoroughly and return meaningful error messages
  • Keep tools focused on a single action or operation
Don’t:
  • Create tools for read-only data retrieval (use resources instead)
  • Skip input validation—always define a proper inputSchema
  • Ignore error handling—wrap external calls in try/catch
  • Create overly complex tools—split into multiple tools if needed
  • Expose sensitive operations without proper authentication checks