Build an MCP server
MCP serversMake 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.
Try the starter
Section titled “Try the starter”Start the server
From a Flow project, run:
npm run mcpKeep 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:
claude mcp add --transport http bean-and-leaf http://127.0.0.1:8790/mcpUse the printed URL if the server chose a different port. Restart the client if it does not discover the newly added connection.
These remote hosts need an endpoint they can reach over public HTTPS. Deploy your
server, then add its /mcp URL through the host’s MCP connection flow.
Use Flow hosting or your own Node hosting. A deployed /mcp is public:
configure sign-in before the tools access a user’s account. The
local 127.0.0.1 URL is for a client on your machine.
Add the URL as an HTTP MCP server in your client. Use a local URL for a local client or HTTPS for a remote one.
The server uses Streamable HTTP. A client must support that transport and, for account-protected tools, MCP authorization.
Make a request
Ask the connected assistant:
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.
Serve your own tools
Section titled “Serve your own tools”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.
npm i @flow-engineer/flowimport { 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.
What the host receives
Section titled “What the host receives”| Your definition | MCP representation |
|---|---|
name, title, description | The tool’s identifier and how to use it |
input | JSON Schema for its arguments |
output | Validated results, also sent as structured content |
hints | Read-only, destructive, idempotent and open-world annotations |
status | Running 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 |
Defer less-used tools
Section titled “Defer less-used tools”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:
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:
{ "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.
{ "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.
Choose exposure at deployment
Section titled “Choose exposure at deployment”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.
Add per-user sign-in
Section titled “Add per-user sign-in”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 setting | Use 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.
OAuth providers, verified identities and backend credentials.
Use Flow's OAuth bridgeConnect an existing login page when you do not have an OAuth server.
Give a slow login page more time
Section titled “Give a slow login page more time”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):
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.
Keep a tool to your own CLI
Section titled “Keep a tool to your own CLI”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.
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 returnclient: { id, kind: "cli" }fromverifyfor 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.
Hear from the agents that use it
Section titled “Hear from the agents that use it”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:
| Input | Meaning |
|---|---|
about | tooling: this MCP server or CLI itself (a tool that failed or is missing, confusing inputs or results). product: the service behind it |
message | What happened and what they expected |
tool | Optional: 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.
{ "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.
Customize or extend it
Section titled “Customize or extend it”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.
npx @flow-engineer/flow mcp updateGenerate native packages that connect to the server’s tools-only endpoint.
Thanks, we read every comment.