MCP Fusion/Core concepts/Tools
Tools
Define AI Capabilities with f.query, f.mutation and f.action: typed parameters, middleware, the Result monad and FSM state gating that removes tools the agent may not use.
Tools are what the agent calls. In Vinkius terms they are your connector's AI Capabilities. MCP Fusion gives you one fluent builder with three semantic entry points, and the semantics map to the MCP annotations the agent sees.
| Builder | Meaning | MCP annotation |
|---|---|---|
f.query() | Reads data | readOnlyHint: true |
f.mutation() | Writes data | Destructive hints per tool |
f.action() | Side effects, external calls | Destructive hints per tool |
A complete tool
export default f.mutation('billing.refund')
.describe('Refund an invoice by ID')
.withString('id', 'Invoice ID')
.withNumber('amount_cents', 'Amount to refund in CENTS')
.withOptionalEnum('reason', ['duplicate', 'customer_request', 'fraud'],
'Why the refund is happening')
.returns(InvoicePresenter)
.use(requireAuth)
.handle(async (input, ctx) => {
const result = await refunds.create(input, ctx.tenantId);
if (!result.ok) return result.response;
return result.value;
});Parameters
.withString(name, description),.withNumber,.withBoolean,.withEnumand the.withOptionalvariants build the Zod input schema- Model driven tools can take
.fromModel()and reuse a Model's fillable fields as parameters .toonDescription()compresses the tool description into a pipe format that costs about half the tokens.bindState('approved', 'DISCHARGE')connects the tool to an FSM state, covered below
Middleware
.use() attaches middleware to one tool. Middleware composes Express style and each middleware returns a partial context that merges into ctx, which is how auth, tenant resolution and rate limiting stay out of your handlers:
// A middleware derives context: the returned object merges into ctx,
// typed through the whole fluent chain.
const withUser = f.middleware(async (ctx) => {
const user = await auth.verify(ctx.request);
return { user };
});
f.query('billing.get_profile').use(withUser);See Routing for global middleware applied to every tool in a directory.
The Result monad
Handlers that can fail return a Result instead of throwing, and the framework turns a Failure into a structured tool response the agent can act on:
import { succeed, fail, toolError } from '@mcpfusion/core';
.handle(async (input) => {
const invoice = await db.invoices.findUnique({ where: { id: input.id } });
if (!invoice) {
return fail(toolError('NOT_FOUND', {
message: 'No invoice with that ID',
availableActions: ['billing.list_invoices'],
}));
}
return succeed(invoice);
});The availableActions list is self healing context: when the agent picked a wrong ID, the error teaches it what it can do instead. See Governance for how the same idea works across contract changes.
FSM state gating
Some tools must not exist in certain states. A discharge tool has no business being visible before an invoice is approved. .bindState() connects the tool to a workflow state and MCP Fusion physically removes it from tools/list while the state forbids it:
export default f.action('billing.discharge')
.describe('Discharge an approved invoice')
.bindState('approved', 'DISCHARGE')
.handle(async (input, ctx) => { /* ... */ });The client receives notifications/tools/list_changed as the state moves, so the agent's menu matches reality. This kills the classic hallucination where the model calls a step that is out of order. The state store is pluggable, with Redis and edge KV options, so gating works in serverless too.
State gating removes the tool, it does not hide it. The agent never sees a tool it may not call, so it never tries and never burns a turn on a rejection.
Next steps
- Routing: organize tools by file, group many actions into one tool
- Models and Presenters: shape what each tool returns
- Testing: test the full pipeline in memory
