Skip to main content
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.
This page covers the schema format from the formal specification. See Parameters for parameter details and Security Model for security constraints.

The Two-Export Pattern

// Static, declarative, JSON-serializable — hashable without execution
export const main = {
    namespace: 'provider',
    name: 'SchemaName',
    description: 'What this schema does',
    version: '2.0.0',
    root: 'https://api.provider.com',
    routes: { /* ... */ }
}

Why Two Separate Exports

  • 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. The runtime resolves shared lists, loads approved libraries, and passes them into the handlers() factory.
  • requiredLibraries declares what npm packages the schema needs. The runtime loads them from a security allowlist and injects them.

The main Export

All fields in main must be JSON-serializable. No functions, no dynamic values, no imports. Everything must survive a JSON.parse( JSON.stringify( main ) ) roundtrip.

Required Fields

FieldTypeDescription
namespacestringProvider identifier, lowercase letters only (/^[a-z]+$/).
namestringSchema name in PascalCase (e.g. SmartContractExplorer).
descriptionstringWhat this schema does, 1-2 sentences.
versionstringMust match 2.\d+.\d+ (semver, major must be 2).
rootstringBase URL for all routes. Must start with https:// (no trailing slash).
routesobjectRoute definitions. Keys are camelCase route names. Maximum 8 routes.

Optional Fields

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 routes.
sharedListsobject[][]Shared list references. See Shared Lists.

Field Details

namespace

Only lowercase ASCII letters. No numbers, hyphens, or underscores:
// Valid
namespace: 'etherscan'
namespace: 'coingecko'
namespace: 'defillama'

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

root

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

requiredServerParams

Declares environment variables that must exist at runtime. Values are injected via {{SERVER_PARAM:KEY_NAME}} syntax:
requiredServerParams: [ 'ETHERSCAN_API_KEY' ]

requiredLibraries

Declares npm packages that handlers need. Must be on the runtime allowlist (see Security Model):
requiredLibraries: [ 'ethers' ]

Route Definition

Each key in routes is the route name in camelCase. The route name becomes part of the fully qualified tool name (namespace/schemaFile::routeName).

Route Fields

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

HTTP Methods

MethodBody AllowedTypical Use
GETNoRead operations, queries
POSTYesCreate operations, complex queries
PUTYesUpdate operations
DELETENoDelete operations

Path Templates

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'
Every {{key}} placeholder must have a corresponding parameter with location: 'insert'.

The handlers Export

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 } }
            }
        }
    }
}

Injected Dependencies

ParameterTypeDescription
sharedListsobjectResolved shared list data, keyed by list name. Deep-frozen (read-only).
librariesobjectLoaded npm packages from requiredLibraries, keyed by package name.

Handler Types

HandlerWhenInputMust Return
preRequestBefore the API call{ struct, payload }{ struct, payload }
postRequestAfter the API call{ response, struct, payload }{ response }

Handler Rules

  1. Handlers are optional. Routes 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.
  6. Return shape must match. preRequest returns { struct, payload }. postRequest returns { response }.

Runtime Loading Sequence

Naming Conventions

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
Route 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

Constraints

ConstraintValueRationale
Max routes per schema8Keeps schemas focused. Split large APIs into multiple schemas.
Version major2Must match 2.\d+.\d+.
Namespace pattern^[a-z]+$Letters only. No numbers, hyphens, or underscores.
Root URL protocolhttps://HTTP is not allowed.
Root URL trailing slashForbiddenroot must not end with /.
main exportJSON-serializableMust survive JSON.parse( JSON.stringify() ) roundtrip.
Schema file importsZeroAll dependencies are injected.

Complete Example

A full Etherscan schema with two routes, one handler, and shared list reference:
export const main = {
    namespace: 'etherscan',
    name: 'SmartContractExplorer',
    description: 'Explore verified smart contracts on EVM-compatible chains via Etherscan APIs',
    version: '2.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 } }
    ],
    routes: {
        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 } ) => {
            const { result } = response
            const [ first ] = result
            const { SourceCode, ABI, ContractName, CompilerVersion, OptimizationUsed } = first

            return {
                response: {
                    contractName: ContractName,
                    compilerVersion: CompilerVersion,
                    optimizationUsed: OptimizationUsed === '1',
                    sourceCode: SourceCode,
                    abi: ABI
                }
            }
        }
    }
})
This example demonstrates: two separate exports, fixed parameters (module, action), user parameters (address), server parameter injection (apikey), a shared list reference, an output schema, and a postRequest handler that flattens the response.