Skip to content

Ask the docs

Answers come only from these docs and are written by an AI model (Gemini), so check the linked pages. Your question is not kept. Do not include personal details or keys.

Make your product's tools available in Claude, ChatGPT and other assistants.

Your product doesn’t need to own the chat for someone to use it. An MCP server lets people work with your product from the assistant they’re already using, alongside the rest of their task.

Flow exposes your tools and skills over Streamable HTTP, with descriptors, authentication hooks and structured results.

The host runs the conversation and calls your tools. You do not need a model key for the server.

Start the server

From a Flow project, run:

Terminal
npm run mcp

Keep it running. The starter prints its MCP URL and a Claude Code connection command.

Connect a client

Run the connection command printed by the starter. At its default port, it is:

Terminal
claude mcp add --transport http bean-and-leaf http://127.0.0.1:8790/mcp

Use the printed URL if the server chose a different port. Restart the client if it does not discover the newly added connection.

Make a request

Ask the connected assistant:

Try this
What's on the café menu? Which drinks have oat milk?

Check it worked: the host calls get_menu and uses the returned menu to answer. If you added the tool from Write a tool, ask about the opening hours too.

The starter already includes src/mcp.ts. In an existing Node project, install Flow Framework (the MCP server SDK, @modelcontextprotocol/server, comes with it from 0.16.0), then point mcpServer at your tools and agent folder.

Terminal
npm i @flow-engineer/flow
src/mcp.ts
import { createServer } from "node:http";
import { fileURLToPath } from "node:url";
import { mcpServer } from "@flow-engineer/flow/mcp";
import { tools } from "./tools.ts";
export const mcp = mcpServer({
name: "my-product",
version: "1.0.0",
tools,
dir: fileURLToPath(new URL("../agent", import.meta.url)),
});
createServer((req, res) => void mcp.node(req, res)).listen(8790);

Run it with node src/mcp.ts. It serves /mcp and the tools-only plugin endpoint /mcp/plugin.

A project can be only this server, with no src/agent.ts and no model key. Export the tools beside mcp (export { tools }) so flow check checks them, and deploy it to Flow’s hosting like any agent (An MCP server alone), from @flow-engineer/flow 0.16.1.

Your definitionMCP representation
name, title, descriptionThe tool’s identifier and how to use it
inputJSON Schema for its arguments
outputValidated results, also sent as structured content
hintsRead-only, destructive, idempotent and open-world annotations
statusRunning and completion text for hosts that support it
agent/skills/Skill descriptions and a read_skill tool
(built in)A feedback tool, and instructions saying when to use it

Available in Framework 0.8.0. By default every tool is listed upfront. Mark an individual tool mcp: { deferred: true } to let assistants discover its schema when needed. This affects MCP only: WhatsApp and other chat agents still receive their normal tools, and the CLI keeps its ordinary commands.

For a minimal example, use this tool in the tools array passed to mcpServer above:

src/tools.ts
import { tool, z } from "@flow-engineer/flow";
export const inspectRecord = tool({
name: "inspect_record",
description: "Return a record identifier for inspection.",
input: z.object({ id: z.string() }),
output: z.object({ id: z.string() }),
signIn: "none",
hints: { readOnly: true },
mcp: { deferred: true },
run: ({ id }) => ({ id }),
});
export const tools = [inspectRecord];

Expected result: an MCP client’s tools/list contains search_tools and execute_tool, plus read_skill if the server has skills. It does not contain inspect_record upfront. With no deferred tools, neither generated wrapper is added.

The assistant discovers and invokes the operation in two calls:

search_tools arguments
{ "query": "inspect record", "limit": 1 }

Search returns { "tools": [...], "total": 1 } with matching deferred tools’ full descriptors: name, description, input and output schemas, annotations and metadata. It matches keywords in names, titles and descriptions; the default limit is five and the maximum is twenty. A search for something unrelated returns an empty list. total counts all matches, including those past the limit.

execute_tool arguments
{ "name": "inspect_record", "arguments": { "id": "record-1" } }

This returns structured content { "id": "record-1" }. Execute invokes one existing tool, validates its arguments and output, and preserves its formatting, verified identity, backend credentials, cancellation signal and idempotency key. Its target’s sign-in and scope checks still run before execution. It does not evaluate generated code.

An explicit server list replaces the tools’ flags:

const mcp = mcpServer({ name: "my-product", version: "1", tools, deferred: ["inspect_record"] });

Omit deferred to use the individual flags; use deferred: [] to put everything upfront. Unknown names are refused when constructing the server. A hosting service can choose this list from usage data before constructing each server handle. Selection stays fixed for that handle; automatic promotion and demotion are not part of this release. Executed operations emit traces under their own names, so usage can count direct and indirect calls together. This works on Flow hosting and on your own infrastructure.

Both /mcp and /mcp/plugin apply the policy; the plugin endpoint keeps excluding standalone skills. The CLI and mcpTools request the complete original catalog automatically. Other clients can request it using tools/list metadata:

{ "_meta": { "flow.engineer/full-tool-catalog": true } }

This returns the endpoint’s original descriptors without the generated wrappers. Use descriptor to customize the wrappers, including their names, or supply a tool named search_tools or execute_tool to replace the corresponding generated implementation. Custom SDK handlers remain available through extend.

Configure mcpServer({ auth: { issuer, scopes, verify } }) against your authorization server. Flow challenges calls that need sign-in, and the host starts its OAuth flow.

Tool settingUse it for
signIn: "required"Account data or actions; this is the default
signIn: "optional"A tool that supports both anonymous and signed-in callers
signIn: "none"Public information such as the example menu

Authentication must be configured on the server to enforce those requirements. The café starter has no accounts and runs without server authentication, so each of its tools says signIn: "none".

Use context.key for the verified account and context.credentials.get("backend") for backend access supplied by your credentials provider. Sharing a tool with a messaging agent does not import the assistant’s private chat history.

Available in Framework 0.10.1. The OAuth bridge waits 10 minutes by default for your login page to send its code back. If a first sign-up can take longer, for example with email verification, set login.pendingTtlSeconds to at least your login page’s own window (60 to 86400 seconds):

src/mcp.ts
const bridge = await oauthBridge({
// ...issuer, resources, storage, keys, tokens
login: { url: "https://example.com/login", exchange, active, pendingTtlSeconds: 3600 },
});

Expected result: a sign-in whose code comes back 30 minutes after the person reached your login page finishes, and the host or CLI gets its token. A sign-in older than the setting fails, and the person starts it again. When a different account signs in on your login page in the same browser, it replaces the earlier sign-in there.

Available in Framework 0.10.0. Mark a tool mcp: { clients: ["cli"] } to list and run it only for the CLI built with your agent’s defineCli. Use it for values that must not reach a third-party assistant’s transcript, such as revealing an API key. Chat agents ignore the setting, so give the agent and mcpServer the same tools.

src/tools.ts
import { tool, z } from "@flow-engineer/flow";
export const apiKeysReveal = tool({
name: "api_keys_reveal",
description: "Show one API key's value. Only when the person asks for it.",
input: z.object({ key_id: z.string() }),
output: z.object({ name: z.string(), key: z.string() }),
mcp: { clients: ["cli"] },
run: ({ key_id }, context) => backend.revealKey(key_id, context),
});

The server tells the CLI apart by the OAuth client its token was issued to, so it needs mcpServer({ auth }); without it the server refuses the tool when it is constructed.

  • With Flow’s OAuth bridge there is nothing else to configure. The bridge serves the CLI’s client at <origin>/.well-known/oauth-client/cli, and the CLI signs in as that client.
  • With your own authorization server, register one public client for the CLI (loopback redirects, no secret), set it as defineCli({ clientId }), and return client: { id, kind: "cli" } from verify for tokens issued to it.

Expected result: after acme auth login, acme api-keys-reveal --key-id <id> prints the key and acme tools lists it. Claude, ChatGPT or any other MCP client signed in to the same account does not see it in tools/list, the full catalog or search_tools, and calling it by name returns the same error as an unknown tool.

Every MCP server, and so every CLI, has a built-in feedback tool. Agents and people use it to tell you what failed, what is missing or what could be better:

InputMeaning
abouttooling: this MCP server or CLI itself (a tool that failed or is missing, confusing inputs or results). product: the service behind it
messageWhat happened and what they expected
toolOptional: the tool it is about

It needs no sign-in. Hosts are told when to use it in the server’s instructions. With traces on (always on Flow’s hosting), each piece of feedback appears in the Flow dashboard under Insights → Feedback, filtered by Tooling or Product. Feedback is sent even when trace content is off, because it was written for you.

feedback arguments
{ "about": "tooling", "message": "search_orders has no date filter, so I listed every order.", "tool": "search_orders" }

Expected result: the call answers Sent to the team behind Acme. Thank you., and the entry shows under Insights → Feedback within a minute.

To leave it out, pass mcpServer({ feedback: false }). To handle feedback yourself, add your own tool named feedback; it replaces the built-in one.

Override instructions and tool descriptors, add your own SDK handlers through extend, and keep custom routes beside the generated ones. The CLI uses the same endpoint.

Use tools from another MCP server

mcpTools({ name, url }) brings another server’s tools into your agent. Pin the descriptors in mcp.lock.json so a remote change goes through your checks before reaching customers.

Terminal
npx @flow-engineer/flow mcp update
Package the same tools and skills as a plugin

Generate native packages that connect to the server’s tools-only endpoint.