Schema Format
A FlowMCP schema is a single .mjs module that describes how to reach an API and turn its endpoints into tools. It splits into two named exports: a static, JSON-serializable main block that holds all declarative configuration, and an optional handlers factory that transforms requests and responses. The sections below walk through both exports, the tool and field definitions inside main, the naming conventions, and the constraints the runtime enforces at load time.
Schema File Structure
Section titled “Schema File Structure”A schema is a .mjs file with two separate named exports:
// 1. Static, declarative, JSON-serializable — hashable without executionexport const main = { namespace: 'provider', name: 'SchemaName', description: 'What this schema does', version: '4.0.0', root: 'https://api.provider.com', tools: { /* ... */ }, resources: { /* optional, see 13-resources.md */ }}
// 2. Factory function — receives injected dependencies, returns handler objectsexport const handlers = ( { sharedLists, libraries } ) => ({ routeName: { preRequest: async ( { struct, payload } ) => { return { struct, payload } }, postRequest: async ( { response, struct, payload } ) => { return { response } } }})main is always required. It contains all declarative configuration — pure data, JSON-serializable, hashable for integrity verification without executing any code.
handlers is optional. It is a factory function that receives injected dependencies and returns handler objects for route-level pre- and post-processing. If a schema has no handlers, omit the export entirely:
// Schema without handlers — only the main exportexport const main = { namespace: 'coingecko', name: 'SimplePrice', description: 'Get current price of coins in any supported currency', version: '4.0.0', root: 'https://api.coingecko.com/api/v3', tools: { /* ... */ }}Why Two Separate Exports
Section titled “Why Two Separate Exports”maincan be hashed and validated without calling any function. The runtime reads the static export, serializes it viaJSON.stringify(), and computes its hash — no code execution required.- Handlers receive all dependencies through injection. Schema files have zero
importstatements. The runtime resolves shared lists, loads approved libraries, and passes them into thehandlers()factory function. requiredLibrariesdeclares what npm packages the schema needs. The runtime loads them from a security allowlist and injects them — schemas never import packages directly.
The main Export
Section titled “The main Export”All fields in main must be JSON-serializable. No functions, no dynamic values, no imports, no Date objects, no undefined values. The runtime serializes main via JSON.stringify() for hashing — anything that does not survive a JSON.parse( JSON.stringify( main ) ) roundtrip is invalid.
Required Fields
Section titled “Required Fields”| Field | Type | Description |
|---|---|---|
namespace | string | Provider identifier, lowercase letters and hyphens (/^[a-z][a-z0-9-]*$/). Groups schemas by data source. |
name | string | Human-readable schema name in PascalCase (e.g. SmartContractExplorer). |
description | string | What this schema does, 1-2 sentences. Appears in tool discovery. |
version | string | Must match pattern 4.\d+.\d+ (semver, major MUST be 4). Version 3.\d+.\d+ is accepted during migration. FlowMCP-Spec-Version (frozen by Major). |
schemaVersion | string | Schema-Content-Version, must match pattern \d+\.\d+\.\d+ (semver, free per schema). Bump rules defined in the grading specification. Initial value for migrated schemas: 1.0.0. |
root | string | Base URL for all tools. Must start with https:// (no trailing slash). Not required for resource-only schemas. |
tools | object | Tool definitions. Keys are tool names in camelCase. Maximum 8 tools. May be empty {} if the schema defines resources or skills. |
meta | object | Optional MCP integration metadata block per tool. When present it must be complete (all 6 fields). See 19-mcp-integration.md. |
Two Version Axes
Section titled “Two Version Axes”| Axis | Field | Range | Meaning |
|---|---|---|---|
| Spec-Version | version | 4.\d+.\d+ (Major frozen) | FlowMCP-Spec-Version |
| Schema-Version | schemaVersion | \d+.\d+.\d+ (semver, free) | Schema-Content-Version |
Example header:
export const main = { namespace: 'etherscan', name: 'GetContractEthereum', description: 'Fetch verified contract source by address', version: '4.0.0', schemaVersion: '1.0.0', root: 'https://api.etherscan.io', tools: { /* ... */ }}Note:
schemaHashis a derived-only field. It is computed by the grading toolchain (HashGenerator) from the canonical-JSON-serialised schema excluding theschemaHashfield itself, and stored only in grading entries,SourceSnapshots, andindex.json. It MUST NOT appear as a source field in.mjsschema files. TheFolderScannerflags an in-sourceschemaHashas defectSCN-012.
Mandatory Informational Fields
Section titled “Mandatory Informational Fields”These fields are required (MUST be present). They carry no functional effect on the runtime — the runtime does not validate URLs, interpret ToS, or classify licenses. Their purpose is to document provenance for schema authors and the grading pipeline.
Enforcement is phased: validation currently emits a Warning (VAL027, VAL028) for missing/invalid values; this will escalate to Error in a future minor release once existing schemas have been migrated. See
09-validation-rules.md.
| Field | Type | Description |
|---|---|---|
docs | string[] | Non-empty array of documentation URLs for the API provider. Every API has documentation — this MUST be filled. |
termsOfService | string | URL to the API provider’s Terms of Service — OR the sentinel value "no-tos-found" when the provider publishes no ToS. The field MUST be present; omitting it is a validation warning (escalates to error). FlowMCP does not classify or interpret ToS. See 23-license-and-tos.md. |
Sentinel contract for termsOfService: Use "no-tos-found" (exact string) when the provider has no published ToS. This signals a conscious author decision rather than an accidentally omitted field.
Optional Fields
Section titled “Optional Fields”| Field | Type | Default | Description |
|---|---|---|---|
termsOfServiceCheckedAt | string | null | null | ISO-Date (YYYY-MM-DD) when termsOfService URL was last verified by FlowMCP authors. |
termsOfServiceLanguage | string | null | null | Two-letter language code of the ToS document ('en', 'de', 'multi'). |
dataLicense | string | null | null | URL to the data license of API responses (e.g. CC-BY, Public Domain), if separately published by provider. |
dataLicenseName | string | null | null | Human-readable name of the data license (e.g. 'CC-BY-SA-4.0', 'Public Domain'). |
tags | string[] | [] | Categorization tags for tool discovery (e.g. ['defi', 'tvl']). |
requiredServerParams | string[] | [] | Environment variable names needed at runtime (e.g. ['ETHERSCAN_API_KEY']). |
requiredLibraries | string[] | [] | npm packages needed by handlers. Must be on the runtime allowlist. See 05-security.md. |
headers | object | {} | Default HTTP headers applied to all routes. Route-level headers override these. |
sharedLists | object[] | [] | Shared list references. See 03-shared-lists.md for format. |
resources | object | {} | Resource definitions. Keys are resource names in camelCase. Maximum 2 resources. See 13-resources.md. |
Deprecated Fields
Section titled “Deprecated Fields”| Field | Status | Replacement | Notes |
|---|---|---|---|
routes | Deprecated in v3.0.0 | tools | Accepted as alias with deprecation warning. Will produce loud warning in v3.1.0 and error in v3.2.0. A schema defining BOTH tools and routes is a validation error. |
Field Details
Section titled “Field Details”namespace
Section titled “namespace”The namespace is the primary grouping mechanism. It identifies the API provider and is used in the fully qualified tool name (namespace/schemaFile::routeName). Lowercase ASCII letters, digits, and hyphens are allowed — no underscores or uppercase.
The namespace is also the provider folder name (providers/<namespace>/), and the two MUST stay in sync — the folder↔namespace invariant VAL019 (see 09-validation-rules.md); the all-unparseable fallback and rename-on-parse rules are in 16-id-schema.md.
// Validnamespace: 'etherscan'namespace: 'coingecko'namespace: 'defillama'namespace: 'coingecko-com'namespace: 'etherscan-io'
// Invalidnamespace: 'CoinGecko' // uppercase not allowednamespace: 'web3_data' // underscore not allowedThe base URL is prepended to every route’s path. It must use HTTPS and MUST NOT end with a slash:
// Validroot: 'https://api.etherscan.io'root: 'https://pro-api.coingecko.com/api/v3'
// Invalidroot: 'http://api.etherscan.io' // must be HTTPSroot: 'https://api.etherscan.io/' // no trailing slashrequiredServerParams
Section titled “requiredServerParams”Declares environment variables that MUST be available at runtime. The runtime checks for their presence before exposing the schema’s tools. Values are injected into parameters via the {{SERVER_PARAM:KEY_NAME}} syntax (see 02-parameters.md).
requiredServerParams: [ 'ETHERSCAN_API_KEY' ]requiredLibraries
Section titled “requiredLibraries”Declares npm packages that handlers need. The runtime loads these from a security allowlist and injects them into the handlers() factory function. Schemas that declare unapproved libraries are rejected at load time.
// Schema needs ethers.js for address checksummingrequiredLibraries: [ 'ethers' ]
// Schema needs no librariesrequiredLibraries: []See 05-security.md for the default allowlist and how to extend it.
headers
Section titled “headers”Default headers sent with every request from this schema. Common use cases include Accept headers or API versioning:
headers: { 'Accept': 'application/json', 'X-API-Version': '2024-01'}sharedLists
Section titled “sharedLists”References to shared lists that this schema uses. Each entry specifies the list reference, version, and optional filter:
sharedLists: [ { ref: 'evmChains', version: '1.0.0', filter: { key: 'etherscanAlias', exists: true } }]See 03-shared-lists.md for the complete shared list specification.
Tool Definition
Section titled “Tool Definition”Each key in tools is the tool name in camelCase. The tool name becomes part of the fully qualified MCP tool name.
tools: { getContractAbi: { method: 'GET', path: '/api', description: 'Returns the ABI of a verified smart contract', parameters: [ /* see 02-parameters.md */ ], output: { /* see 04-output-schema.md */ } }, getSourceCode: { method: 'GET', path: '/api', description: 'Returns the source code of a verified smart contract', parameters: [ /* ... */ ] }}Tool Fields
Section titled “Tool Fields”| Field | Type | Required | Description |
|---|---|---|---|
method | string | Yes | HTTP method: GET, POST, PUT, DELETE. |
path | string | Yes | URL path appended to root. May contain {{key}} placeholders for insert parameters. |
description | string | Yes | What this tool does. Appears in tool description for the AI client. |
parameters | array | Yes | Input parameter definitions. Can be empty [] for no-input tools. |
output | object | No | Output schema declaring expected response shape. See 04-output-schema.md. |
preload | object | No | Cache configuration for static/slow-changing datasets. See 11-preload.md. |
tests | array | Yes | Executable test cases with real parameter values. Each tool MUST have at least 3 tests. See 10-tests.md. |
Tool Field Details
Section titled “Tool Field Details”method
Section titled “method”Only four HTTP methods are supported. The method determines how parameters with location: 'body' are handled:
| Method | Body Allowed | Typical Use |
|---|---|---|
GET | No | Read operations, queries |
POST | Yes | Create operations, complex queries |
PUT | Yes | Update operations |
DELETE | No | Delete operations |
If a GET or DELETE route has parameters with location: 'body', the runtime raises a validation error at load-time.
The path is appended to root to form the complete URL. It may contain {{key}} placeholders that are replaced by insert parameters:
// Static pathpath: '/api'
// Path with insert placeholderpath: '/api/v1/{{address}}/transactions'
// Multiple placeholderspath: '/api/v1/{{chainId}}/address/{{address}}/balances'Every {{key}} placeholder MUST have a corresponding parameter with location: 'insert'. The runtime validates this at load-time.
The handlers Export
Section titled “The handlers Export”The handlers export is a factory function that receives injected dependencies and returns an object mapping tool names (and optionally resource names) to handler definitions:
export const handlers = ( { sharedLists, libraries } ) => ({ getContractAbi: { preRequest: async ( { struct, payload } ) => { const chain = sharedLists.evmChains .find( ( c ) => c.alias === payload.chainName ) const updatedPayload = { ...payload, chainId: chain.chainId }
return { struct, payload: updatedPayload } }, postRequest: async ( { response, struct, payload } ) => { const { result } = response const parsed = JSON.parse( result )
return { response: parsed } } }})Injected Dependencies (Factory Parameters)
Section titled “Injected Dependencies (Factory Parameters)”The runtime calls handlers( { sharedLists, libraries } ) once at load time. The factory function receives:
| Parameter | Type | Description |
|---|---|---|
sharedLists | object | Resolved shared list data, keyed by list name. Deep-frozen (read-only). Mutations throw a TypeError. |
libraries | object | Loaded npm packages from requiredLibraries, keyed by package name. Only packages on the runtime allowlist are available. |
Example with both dependencies:
export const main = { // ... requiredLibraries: [ 'ethers' ], sharedLists: [ { ref: 'evmChains', version: '1.0.0', filter: { key: 'etherscanAlias', exists: true } } ], tools: { /* ... */ }}
export const handlers = ( { sharedLists, libraries } ) => { const { ethers } = libraries
return { getContractAbi: { preRequest: async ( { struct, payload } ) => { const checksummed = ethers.getAddress( payload.address ) const chain = sharedLists.evmChains .find( ( c ) => c.alias === payload.chainName )
return { struct, payload: { ...payload, address: checksummed } } } } }}Handler Types
Section titled “Handler Types”| Handler | Purpose |
|---|---|
preRequest | Transform request parameters before HTTP call |
executeRequest | Replace standard HTTP fetch entirely |
postRequest | Transform HTTP response after the call |
| Handler | When | Input | Must Return |
|---|---|---|---|
preRequest | Before the API call | { struct, payload } | { struct, payload } |
executeRequest | Replaces the HTTP call entirely | { struct, payload } | { response } |
postRequest | After the API call (or after executeRequest) | { response, struct, payload } | { response } |
executeRequest Semantics
Section titled “executeRequest Semantics”When executeRequest is defined for a tool, the runtime skips the standard HTTP fetch and calls the handler instead. The handler is responsible for producing a response. Common use cases include:
- XML/TRIAS APIs that cannot be parsed with the standard JSON pipeline
- CSW/OGC endpoints with non-standard response formats
- SQLite-backed schemas that resolve queries locally without HTTP
- Composite calls that MUST combine multiple upstream requests into one
export const handlers = ( { sharedLists, libraries } ) => ({ getXmlData: { executeRequest: async ( { struct, payload } ) => { const { xml2js } = libraries const rawResponse = await fetch( struct.url, { method: struct.method, headers: struct.headers } ) const text = await rawResponse.text() const parsed = await xml2js.parseStringPromise( text )
return { response: parsed } } }})Handler Per-Call Parameters
Section titled “Handler Per-Call Parameters”Each handler invocation receives per-call data through its function parameters:
| Parameter | Type | Description |
|---|---|---|
struct | object | The constructed URL, method, headers, and body. Mutable in preRequest. |
payload | object | The user’s validated input parameters as key-value pairs. |
response | object | The parsed JSON response from the API. Only available in postRequest. |
Server parameters (requiredServerParams) are handled by the runtime during URL construction and are never exposed to handlers. The old userParams and context parameters are replaced by the factory function injection pattern.
Handler Rules
Section titled “Handler Rules”-
Handlers are optional. Tools without handlers make direct API calls using the constructed URL and parameters. Most tools SHOULD NOT need handlers.
-
Schema files MUST NOT contain import statements. No
import, norequire, no dynamicimport(). All dependencies are injected through the factory function. The security scanner rejects any schema containing import statements. See05-security.md. -
Handlers MUST NOT access restricted globals. The following are forbidden:
fetch,fs,process,eval,Function,setTimeout,setInterval,XMLHttpRequest,WebSocket. See05-security.mdfor the complete list. -
sharedListsprovides resolved list data. If a schema references shared lists inmain.sharedLists, the resolved data is available inside the factory closure viasharedLists['listName']orsharedLists.listName. The data is deep-frozen — mutations throw. -
librariesprovides approved npm packages. If a schema declaresmain.requiredLibraries, the loaded modules are available inside the factory closure vialibraries['packageName']orlibraries.packageName. -
Handlers MUST be pure transformations. They receive data, transform it, and return data. No side effects, no state mutations outside the return value, no logging.
-
Return shape MUST match the handler type.
preRequestmust return{ struct, payload }.postRequestmust return{ response }. Missing keys cause a runtime error. -
Resource handlers are nested one level deeper. Tool handlers are keyed by tool name, resource handlers are keyed by resource name then query name. See
13-resources.mdfor details.
Naming Conventions
Section titled “Naming Conventions”| Element | Convention | Pattern | Example |
|---|---|---|---|
| Namespace | Lowercase letters, digits, hyphens | ^[a-z][a-z0-9-]*$ | etherscan, coingecko, coingecko-com |
| Schema name | PascalCase | ^[A-Z][a-zA-Z0-9]*$ | SmartContractExplorer |
| Schema filename | PascalCase .mjs | ^[A-Z][a-zA-Z0-9]*\.mjs$ | SmartContractExplorer.mjs |
| Tool name | camelCase | ^[a-z][a-zA-Z0-9]*$ | getContractAbi |
| Resource name | camelCase | ^[a-z][a-zA-Z0-9]*$ | tokenLookup |
| Skill name | lowercase-hyphen | ^[a-z][a-z0-9-]{0,63}$ | full-contract-audit |
| Parameter key | camelCase | ^[a-z][a-zA-Z0-9]*$ | contractAddress |
| Shared list name | camelCase | ^[a-z][a-zA-Z0-9]*$ | evmChains |
| Tag | lowercase with hyphens | ^[a-z][a-z0-9-]*$ | smart-contracts |
Constraints
Section titled “Constraints”| Constraint | Value | Rationale |
|---|---|---|
| Max tools per schema | 8 | Keeps schemas focused. Split large APIs into multiple schemas. |
| Max resources per schema | 2 | Keeps resource scope focused. See 13-resources.md. |
| Version major | 4 | Must match 4.\d+.\d+. Version 3.\d+.\d+ accepted during migration with warning. |
tools + routes simultaneously | Forbidden | A schema MUST use tools or routes, never both. Using both is a validation error. |
| Namespace pattern | ^[a-z][a-z0-9-]*$ | Lowercase letters, digits, hyphens. No underscores or uppercase. |
| Tool name pattern | ^[a-z][a-zA-Z0-9]*$ | camelCase, starts with lowercase letter. |
| Root URL protocol | https:// | HTTP is not allowed. All API calls go over TLS. |
| Root URL requirement | Conditional | Required when tools are defined. Optional for resource-only or skill-only schemas. |
| Root URL trailing slash | Forbidden | root MUST NOT end with /. |
main export | JSON-serializable | Must survive JSON.parse( JSON.stringify() ) roundtrip. |
| Schema file imports | Zero | Schema files MUST have no import statements. All dependencies are injected. |
requiredLibraries | Allowlist only | Every entry MUST be on the runtime allowlist. See 05-security.md. |
Runtime Loading Sequence
Section titled “Runtime Loading Sequence”The following diagram shows how the runtime loads a schema file, validates it, and registers its routes as MCP tools:
Step-by-step:
- Read file as string — the raw source is read before any execution.
- Static security scan — the string is scanned for forbidden patterns (
import,require,eval, etc.). If any match, the file is rejected. See05-security.md. - Dynamic import — the file is imported via
import(). - Extract
mainexport — the namedmainexport is read. - Validate
mainblock — JSON-serializability, required fields, version format, namespace pattern, tool limits. Ifmain.routesis found, it is treated asmain.toolswith a deprecation warning. If bothtoolsandroutesare present, the schema is rejected. - Resolve
sharedLists— shared list references are resolved to data. Data is deep-frozen. - Load
requiredLibraries— each library is checked against the allowlist and loaded viaimport(). Unapproved libraries reject the schema. - Call
handlers()— if thehandlersexport exists, the factory function is called with{ sharedLists, libraries }. The returned handler objects are registered per tool (and per resource query, if applicable). - Register tools — each tool is exposed as an MCP tool. Tools with
executeRequesthandlers replace the HTTP call; tools withpreRequest/postRequesthandlers use pre/post processing; tools without handlers make direct API calls.
Complete Example
Section titled “Complete Example”A full schema for Etherscan with two tools, one handler using sharedLists, and no required libraries:
export const main = { namespace: 'etherscan', name: 'SmartContractExplorer', description: 'Explore verified smart contracts on EVM-compatible chains via Etherscan APIs', version: '4.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: [] } } ], output: { mimeType: 'application/json', schema: { type: 'object', properties: { status: { type: 'string', description: 'API response status' }, message: { type: 'string', description: 'Status message' }, result: { type: 'string', description: 'Contract ABI as JSON string' } } } } }, getSourceCode: { method: 'GET', path: '/api', description: 'Returns the Solidity source code of a verified smart contract', parameters: [ { position: { key: 'module', value: 'contract', location: 'query' }, z: { primitive: 'string()', options: [] } }, { position: { key: 'action', value: 'getsourcecode', 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 } ) => ({ getSourceCode: { postRequest: async ( { response, struct, payload } ) => { const { result } = response const [ first ] = result const { SourceCode, ABI, ContractName, CompilerVersion, OptimizationUsed } = first const simplified = { contractName: ContractName, compilerVersion: CompilerVersion, optimizationUsed: OptimizationUsed === '1', sourceCode: SourceCode, abi: ABI }
return { response: simplified } } }})What this example demonstrates
Section titled “What this example demonstrates”- Two separate exports —
mainis a static data object,handlersis a factory function receiving{ sharedLists }. - Two tools (
getContractAbi,getSourceCode) sharing the samerootandpathbut differing by query parameters. - Fixed parameters (
module,action) that are invisible to the user — they are sent automatically. - User parameters (
address) with length validation for Ethereum addresses. - Server parameters (
apikey) injected from the environment via{{SERVER_PARAM:ETHERSCAN_API_KEY}}. - A shared list reference (
evmChains) filtered to chains that have an Etherscan alias. requiredLibraries: []— this schema needs no external npm packages.- A
postRequesthandler ongetSourceCodethat flattens the nested API response into a cleaner object. - No handler on
getContractAbi— the raw API response is returned directly. - An output schema on
getContractAbideclaring the expected response shape. - Zero import statements — the schema file has no imports. Dependencies (
sharedLists) are injected through the factory function.
Related
Section titled “Related”- ./00-overview.md — see chapter 00.
- ./02-parameters.md — see chapter 02.
- ./04-output-schema.md — see chapter 04.
- ./13-resources.md — see chapter 13.
- ./14-skills.md — see chapter 14.
- ./16-id-schema.md — see chapter 16.
- ./19-mcp-integration.md — see chapter 19.