Skip to content

Schema Format

A FlowMCP schema is a .mjs file with two separate named exports: a static main block and an optional handlers factory function. This separation enables integrity hashing, security scanning, and dependency injection.

// main export (required)
// Static, declarative, JSON-serializable — hashable without execution
export const main = {
namespace: 'provider',
name: 'SchemaName',
description: 'What this schema does',
version: '3.0.0',
root: 'https://api.provider.com',
tools: { /* ... */ },
resources: { /* ... */ }, // optional
skills: [ /* ... */ ] // optional
}
// handlers export (optional)
// Factory function — receives injected dependencies
export const handlers = ( { sharedLists, libraries } ) => ({
toolName: {
preRequest: async ( { struct, payload } ) => {
return { struct, payload }
},
postRequest: async ( { response, struct, payload } ) => {
return { response }
}
}
})
  • main can be hashed without calling any function. The runtime reads the static export, serializes it via JSON.stringify(), and computes its hash.
  • Handlers receive all dependencies through injection. Schema files have zero import statements.
  • requiredLibraries declares what npm packages the schema needs. The runtime loads them from a security allowlist and injects them.

All fields in main must be JSON-serializable. No functions, no dynamic values, no imports.

FieldTypeDescription
namespacestringProvider identifier, lowercase letters only (/^[a-z]+$/).
namestringSchema name in PascalCase (e.g. SmartContractExplorer).
descriptionstringWhat this schema does, 1-2 sentences.
versionstringMust match 3.\d+.\d+ (semver, major must be 3).
rootstringBase URL for all tools. Must start with https:// (no trailing slash).
toolsobjectTool definitions. Keys are camelCase tool names. Maximum 8 tools.
FieldTypeDefaultDescription
docsstring[][]Documentation URLs for the API provider.
tagsstring[][]Categorization tags for tool discovery.
requiredServerParamsstring[][]Environment variable names needed at runtime.
requiredLibrariesstring[][]npm packages needed by handlers (must be on allowlist).
headersobject{}Default HTTP headers applied to all tools.
sharedListsobject[][]Shared list references. See Shared Lists.
resourcesobject{}SQLite-based read-only data resources. See Resources.
skillsarray[]AI agent skill references. See Skills.

Only lowercase ASCII letters. No numbers, hyphens, or underscores:

// Valid
namespace: 'etherscan'
namespace: 'coingecko'
// Invalid
namespace: 'defi-llama' // hyphen not allowed
namespace: 'CoinGecko' // uppercase not allowed

Must use HTTPS with no trailing slash:

// Valid
root: 'https://api.etherscan.io'
root: 'https://pro-api.coingecko.com/api/v3'
// Invalid
root: 'http://api.etherscan.io' // must be HTTPS
root: 'https://api.etherscan.io/' // no trailing slash

Each key in tools is the tool name in camelCase.

FieldTypeRequiredDescription
methodstringYesHTTP method: GET, POST, PUT, DELETE.
pathstringYesURL path appended to root. May contain {{key}} placeholders.
descriptionstringYesWhat this tool does. Appears in tool description.
parametersarrayYesInput parameter definitions. Can be empty [].
testsarrayYesExecutable test cases. At least 1 per tool.
outputobjectNoOutput schema. See Output Schema.
preloadobjectNoCache configuration. See Preload.

The path supports {{key}} placeholders that are replaced by insert parameters:

// Static path
path: '/api'
// Single placeholder
path: '/api/v1/{{address}}/transactions'
// Multiple placeholders
path: '/api/v1/{{chainId}}/address/{{address}}/balances'

The handlers export is a factory function receiving injected dependencies:

export const handlers = ( { sharedLists, libraries } ) => {
const { ethers } = libraries
return {
getContractAbi: {
preRequest: async ( { struct, payload } ) => {
const checksummed = ethers.getAddress( payload.address )
return { struct, payload: { ...payload, address: checksummed } }
}
}
}
}
HandlerWhenInputMust Return
preRequestBefore the API call{ struct, payload }{ struct, payload }
postRequestAfter the API call{ response, struct, payload }{ response }
  1. Handlers are optional. Tools without handlers make direct API calls.
  2. Zero import statements. All dependencies are injected through the factory function.
  3. No restricted globals. fetch, fs, process, eval, Function, setTimeout are forbidden.
  4. sharedLists is read-only. Deep-frozen via Object.freeze(). Mutations throw TypeError.
  5. Handlers must be pure transformations. No side effects, no state mutations, no logging.
flowchart TD
A[Read schema file as string] --> B[Static security scan]
B --> C[Dynamic import]
C --> D[Extract main export]
D --> E[Validate main block]
E --> F[Resolve sharedLists]
F --> G[Load requiredLibraries from allowlist]
G --> H{handlers export exists?}
H -->|Yes| I["Call handlers( { sharedLists, libraries } )"]
H -->|No| J[Direct API call mode]
I --> K[Register tools as MCP tools]
J --> K
K --> L{resources defined?}
L -->|Yes| M[Load SQLite databases]
L -->|No| N{skills defined?}
M --> N
N -->|Yes| O[Load skill .mjs files]
N -->|No| P[Ready]
O --> P
ElementConventionPatternExample
NamespaceLowercase letters only^[a-z]+$etherscan
Schema namePascalCase^[A-Z][a-zA-Z0-9]*$SmartContractExplorer
Schema filenamePascalCase .mjs^[A-Z][a-zA-Z0-9]*\.mjs$SmartContractExplorer.mjs
Tool namecamelCase^[a-z][a-zA-Z0-9]*$getContractAbi
Parameter keycamelCase^[a-z][a-zA-Z0-9]*$contractAddress
Taglowercase with hyphens^[a-z][a-z0-9-]*$smart-contracts
ConstraintValueRationale
Max tools per schema8Keeps schemas focused. Split large APIs into multiple schemas.
Max resources per schema2Resources are supplementary data, not primary output.
Max skills per schema4Skills compose tools; keep schemas focused.
Version major3Must match 3.\d+.\d+.
Namespace pattern^[a-z]+$Letters only. No numbers, hyphens, or underscores.
Root URL protocolhttps://HTTP is not allowed.
main exportJSON-serializableMust survive JSON.parse( JSON.stringify() ) roundtrip.
Schema file importsZeroAll dependencies are injected.
export const main = {
namespace: 'etherscan',
name: 'SmartContractExplorer',
description: 'Explore verified smart contracts on EVM-compatible chains via Etherscan APIs',
version: '3.0.0',
root: 'https://api.etherscan.io',
docs: [ 'https://docs.etherscan.io/api-endpoints/contracts' ],
tags: [ 'smart-contracts', 'evm', 'abi' ],
requiredServerParams: [ 'ETHERSCAN_API_KEY' ],
requiredLibraries: [],
headers: { 'Accept': 'application/json' },
sharedLists: [
{ ref: 'evmChains', version: '1.0.0', filter: { key: 'etherscanAlias', exists: true } }
],
tools: {
getContractAbi: {
method: 'GET',
path: '/api',
description: 'Returns the Contract ABI of a verified smart contract',
parameters: [
{ position: { key: 'module', value: 'contract', location: 'query' }, z: { primitive: 'string()', options: [] } },
{ position: { key: 'action', value: 'getabi', location: 'query' }, z: { primitive: 'string()', options: [] } },
{ position: { key: 'address', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: [ 'min(42)', 'max(42)' ] } },
{ position: { key: 'apikey', value: '{{SERVER_PARAM:ETHERSCAN_API_KEY}}', location: 'query' }, z: { primitive: 'string()', options: [] } }
]
}
}
}
export const handlers = ( { sharedLists } ) => ({
getContractAbi: {
postRequest: async ( { response } ) => {
const [ first ] = response.result
return { response: { contractName: first.ContractName, sourceCode: first.SourceCode } }
}
}
})