Skip to content

MCP Server Integration

Docs > Guides

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.

The integration path depends on your use case:

TransportUse CaseProtocol
stdioClaude Desktop, Claude Code, local AI appsStandard input/output
HTTP/SSEWeb services, remote clients, multi-tenantServer-Sent Events over HTTP
CLIQuick testing, agent modeflowmcp 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.

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 server
const 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 tool
async 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 stdio
const transport = new StdioServerTransport()
await server.connect( transport )

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 server
const 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 endpoint
app.get( '/sse', async ( req, res ) => {
const transport = new SSEServerTransport( '/messages', res )
await server.connect( transport )
} )
// Message endpoint
app.post( '/messages', async ( req, res ) => {
await transport.handlePostMessage( req, res )
} )
app.listen( 3000, () => {
console.log( 'MCP server running on http://localhost:3000' )
} )

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:

Terminal window
# Serve the whole configured schemaFolders[] catalog as an MCP server (stdio)
flowmcp run

To use FlowMCP schemas in Claude Desktop, add your server to claude_desktop_config.json:

{
"mcpServers": {
"flowmcp-crypto": {
"command": "node",
"args": [ "/path/to/your/server.mjs" ],
"env": {
"ETHERSCAN_API_KEY": "your-key-here",
"COINGECKO_API_KEY": "your-key-here"
}
}
}
}
{
"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

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 tools
const 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 (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 needs
export const main = {
// ...
requiredServerParams: [ 'ETHERSCAN_API_KEY', 'MORALIS_API_KEY' ],
// ...
}
// Server provides the values
const serverParams = {
ETHERSCAN_API_KEY: process.env.ETHERSCAN_API_KEY,
MORALIS_API_KEY: process.env.MORALIS_API_KEY
}
// serverParams flows into every prepareServerTool call
const { toolName, description, zod, func } = FlowMCP
.prepareServerTool( { main, handlerMap, serverParams, routeName } )

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 tool
const { 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
}
}
)

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 needed
const { status, data, dataAsString } = await func( { chainName: 'ETH' } )