Write a tool
ToolsDefine a product capability once. Call it from MCP, a CLI or your messaging agent.
A tool is a TypeScript function with a typed input. It reads from your product or takes an action, then returns data the caller can use.
Tools are what make a conversation more than an answer box. They let someone check availability, move a booking or place an order against your real systems. Start with the task a person wants to complete, then expose the operations needed to do it.
This guide adds get_hours to the starter. You will call it from the CLI, then make the same
capability available in your MCP server and messaging agent.
Before you start
Section titled “Before you start”Create a project with the quickstart. The starter already imports tool and z
in src/tools.ts and exports a tools array.
Add a tool
Section titled “Add a tool”Add this definition inside the existing tools array in src/tools.ts. Keep the other
starter tools so its existing commands and test scenarios continue to work.
tool({ name: "get_hours", title: "Opening hours", description: "Get the opening and closing time and the business time zone.", input: z.object({}), output: z.object({ opens: z.string(), closes: z.string(), timeZone: z.string(), }), signIn: "none", hints: { readOnly: true, openWorld: false }, run: () => ({ opens: "08:00", closes: "18:00", timeZone: "America/New_York", }),}),This example returns fixed data so you can try it immediately. In your product, run can call
your API or database and return its result.
Call it
Section titled “Call it”Restart the MCP server to load your change, then call the tool in another terminal:
npm run mcpIn the second terminal:
npm run cli -- get-hours --jsonCheck it worked: the CLI returns the hours from your function.
{ "opens": "08:00", "closes": "18:00", "timeZone": "America/New_York"}No CLI configuration is needed for this new tool: unnamed commands use the tool’s name with
underscores changed to dashes. The MCP server lists get_hours too. Restart npm start to use
it in the playground, then ask “What time do you open?”
What each field does
Section titled “What each field does”| Field | Purpose |
|---|---|
name | The stable identifier used by the agent and MCP clients |
description | What the tool does and when a model should call it |
input | A Zod schema that validates the caller’s arguments |
run | The function that performs the operation |
title | A short name shown to people |
output | The result schema; MCP checks it and sends structured content |
signIn | Whether the operation needs a verified account |
hints | Whether it reads, writes, can overwrite, or reaches external systems |
Add input
Section titled “Add input”Describe the input a person would naturally ask for. Zod validates it before run is called.
For a tool that looks up a product, the input could be:
input: z.object({ product_id: z.string().describe("The product id returned by a search"), include_stock: z.boolean().default(false),}),Return the data a later tool call needs, such as an id or status. Keep credentials and internal implementation details out of the returned result.
Format tool results
Section titled “Format tool results”In Framework 0.8.0, a tool can provide Markdown for people while retaining structured data for
models and scripts. Add these fields to a tool whose run returns the matching object:
output: z.object({ order_id: z.string(), ready_by: z.string() }),format: (result) => `**Order ${result.order_id}** is ready by ${result.ready_by}.`,Expected result: { order_id: "BL-1040", ready_by: "10:30" } produces the text
**Order BL-1040** is ready by 10:30. MCP returns it in content alongside the data in
structuredContent; the CLI prints it by default. Messaging
models still receive the data. format requires output and runs after output validation.
Use the caller’s account
Section titled “Use the caller’s account”run receives a second argument, context, supplied by the framework. Use its verified
identity to find the account, rather than asking a model to provide an account id.
| Value | Use it for |
|---|---|
context.key | Your verified account key, or null without one |
context.person | The stable person identity |
context.credentials | Server-side access to a configured backend |
context.idempotencyKey | Deduplicating a backend write on retries |
context.signal | Cancelling work when the conversation closes |
context.records | The agent’s own small records, kept across conversations (records) |
context.events | The agent’s own business events side, once events.attach(agent) ran: save a person’s watch there (events) |
The authentication guide shows how to connect your existing login and credentials provider.
Share a status during a slow operation
Section titled “Share a status during a slow operation”Hosts that support status text can show a short line while a tool runs and when it finishes:
status: { running: "Checking availability…", done: "Availability checked",},For messaging, Flow handles acknowledgement and background execution. deadlineMs limits how
long a reply waits for one tool; it does not replace an API client’s own timeout.
Validate your change
Section titled “Validate your change”npx @flow-engineer/flow checkThis checks tool descriptors, skill references and CLI configuration. Exercise the tool as well: a valid schema does not establish that your API call returns the right result.
Thanks, we read every comment.