MCP Server Integration
FlowMCP schemas can be served as MCP tools through two transport modes: stdio for local AI applications like Claude Desktop, and HTTP/SSE for remote web applications. This guide covers both approaches.
Overview
Section titled “Overview”The integration path depends on your use case:
| Transport | Use Case | Protocol |
|---|---|---|
| stdio | Claude Desktop, Claude Code, local AI apps | Standard input/output |
| HTTP/SSE | Web services, remote clients, multi-tenant | Server-Sent Events over HTTP |
| CLI | Quick testing, agent mode | flowmcp run |
The v4 core exposes three static building blocks — FlowMCP.loadSchema, FlowMCP.prepareServerTool, and FlowMCP.fetch. You load a schema once, then register each of its tools on an MCP server. There is no single “activate” call: registration is an explicit loop, which is what gives you per-tool control.
Local Server (stdio)
Section titled “Local Server (stdio)”The stdio transport is used for local AI applications that launch the MCP server as a subprocess. This is the standard approach for Claude Desktop integration.
import { FlowMCP } from 'flowmcp-core'import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
// Create MCP serverconst server = new McpServer( { name: 'my-flowmcp-server', version: '1.0.0' })
// Server params (API keys from environment)const serverParams = { ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY}
// Load a schema and register every tool it declares as an MCP toolasync function registerSchema( { filePath } ) { const { status, main, handlerMap, messages } = await FlowMCP .loadSchema( { filePath } )
if( !status ) { console.error( `Failed to load ${filePath}:`, messages ) return }
Object.keys( main.tools ) .forEach( ( routeName ) => { const { toolName, description, zod, func } = FlowMCP .prepareServerTool( { main, handlerMap, serverParams, routeName } )
server.registerTool( toolName, { description, inputSchema: zod }, async ( args ) => { const { status: ok, data, dataAsString } = await func( args ) return { content: [ { type: 'text', text: dataAsString ?? JSON.stringify( data ) } ], isError: !ok } } ) } )}
await registerSchema( { filePath: './schemas/coingecko-ping.mjs' } )await registerSchema( { filePath: './schemas/etherscan-gas.mjs' } )
// Connect via stdioconst transport = new StdioServerTransport()await server.connect( transport )Remote Server (HTTP/SSE)
Section titled “Remote Server (HTTP/SSE)”For web applications and remote access, use the SSE transport with an HTTP server. The registerSchema helper from the stdio example is reused verbatim — only the transport differs:
import express from 'express'import { FlowMCP } from 'flowmcp-core'import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'
const app = express()
// Create MCP serverconst server = new McpServer( { name: 'my-remote-server', version: '1.0.0' })
const serverParams = { ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY}
// Load and register a schema (same loadSchema -> prepareServerTool -> registerTool loop)const { main, handlerMap } = await FlowMCP .loadSchema( { filePath: './schemas/etherscan-gas.mjs' } )
Object.keys( main.tools ) .forEach( ( routeName ) => { const { toolName, description, zod, func } = FlowMCP .prepareServerTool( { main, handlerMap, serverParams, routeName } )
server.registerTool( toolName, { description, inputSchema: zod }, async ( args ) => { const { status, data, dataAsString } = await func( args ) return { content: [ { type: 'text', text: dataAsString ?? JSON.stringify( data ) } ], isError: !status } } ) } )
// SSE endpointapp.get( '/sse', async ( req, res ) => { const transport = new SSEServerTransport( '/messages', res ) await server.connect( transport )} )
// Message endpointapp.post( '/messages', async ( req, res ) => { await transport.handlePostMessage( req, res )} )
app.listen( 3000, () => { console.log( 'MCP server running on http://localhost:3000' )} )CLI (flowmcp run)
Section titled “CLI (flowmcp run)”The fastest way to serve schemas is through the CLI. flowmcp run starts an MCP server (stdio transport) that exposes every tool from the schema folders configured in schemaFolders[] — no per-tool activation step:
# Serve the whole configured schemaFolders[] catalog as an MCP server (stdio)flowmcp runClaude Desktop Configuration
Section titled “Claude Desktop Configuration”To use FlowMCP schemas in Claude Desktop, add your server to claude_desktop_config.json:
Custom Server (stdio)
Section titled “Custom Server (stdio)”{ "mcpServers": { "flowmcp-crypto": { "command": "node", "args": [ "/path/to/your/server.mjs" ], "env": { "ETHERSCAN_API_KEY": "your-key-here", "COINGECKO_API_KEY": "your-key-here" } } }}FlowMCP CLI
Section titled “FlowMCP CLI”{ "mcpServers": { "flowmcp": { "command": "flowmcp", "args": [ "run" ] } }}The config file is located at:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Selecting Which Tools to Expose
Section titled “Selecting Which Tools to Expose”When you build your own server, you decide which tools reach the AI client at two points: which schema files you load, and which routes you register. There is no separate filter API in v4 — you simply skip the routes you do not want inside the registration loop:
// Expose only a subset of a schema's toolsconst exposeOnly = [ 'getGasOracle', 'getBalance' ]
const { main, handlerMap } = await FlowMCP .loadSchema( { filePath: './schemas/etherscan-gas.mjs' } )
Object.keys( main.tools ) .filter( ( routeName ) => exposeOnly.includes( routeName ) ) .forEach( ( routeName ) => { const { toolName, description, zod, func } = FlowMCP .prepareServerTool( { main, handlerMap, serverParams, routeName } )
server.registerTool( toolName, { description, inputSchema: zod }, async ( args ) => { const { status, data, dataAsString } = await func( args ) return { content: [ { type: 'text', text: dataAsString ?? JSON.stringify( data ) } ], isError: !status } } ) } )For the CLI path, the equivalent controls are the schemaFolders[] config (which schemas exist) and named selections (flowmcp selection).
Server Parameters
Section titled “Server Parameters”Server parameters (API keys, tokens) are injected at runtime and never exposed to the AI client. Declare them in the schema’s requiredServerParams and pass them to prepareServerTool when you register each tool:
// Schema declares what it needsexport const main = { // ... requiredServerParams: [ 'ETHERSCAN_API_KEY', 'MORALIS_API_KEY' ], // ...}
// Server provides the valuesconst serverParams = { ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY, MORALIS_API_KEY: process.env.MORALIS_API_KEY}
// serverParams flows into every prepareServerTool callconst { toolName, description, zod, func } = FlowMCP .prepareServerTool( { main, handlerMap, serverParams, routeName } )Registering a Single Route
Section titled “Registering a Single Route”For fine-grained control, register one route instead of looping over the whole schema:
const { main, handlerMap } = await FlowMCP .loadSchema( { filePath: './schemas/etherscan-gas.mjs' } )
// Prepare a single toolconst { toolName, description, zod, func } = FlowMCP .prepareServerTool( { main, handlerMap, serverParams, routeName: 'getGasOracle' } )
console.log( `Registering: ${toolName}` )// Output: "Registering: get_gas_oracle_etherscan"
server.registerTool( toolName, { description, inputSchema: zod }, async ( args ) => { const { status, data, dataAsString } = await func( args ) return { content: [ { type: 'text', text: dataAsString ?? JSON.stringify( data ) } ], isError: !status } })Inspecting a Tool Before Registering
Section titled “Inspecting a Tool Before Registering”prepareServerTool returns a plain object, so you can inspect a tool configuration without registering it — and even execute it directly through the returned func:
const { toolName, description, zod, func } = FlowMCP .prepareServerTool( { main, handlerMap, serverParams, routeName: 'getGasOracle' } )
console.log( 'Tool name:', toolName ) // e.g. "get_gas_oracle_etherscan"console.log( 'Description:', description )console.log( 'Zod input shape:', zod ) // raw shape { param: ZodType }
// Execute manually if neededconst { status, data, dataAsString } = await func( { chainName: 'ETH' } )