# FlowMCP Specification v3.0.0 > Complete specification for FlowMCP — a schema-driven normalization layer that transforms REST APIs, local databases, and workflows into MCP-compatible tools for AI agents. > > This file concatenates all 17 specification documents into a single file for LLM consumption. > Source: https://github.com/FlowMCP/flowmcp-spec ## Table of Contents 1. Overview 2. Schema Format 3. Parameters 4. Shared Lists 5. Output Schema 6. Security Model 7. Agents 8. MCP Tasks 9. Migration Guide 10. Validation Rules 11. Tests 12. Preload & Caching 13. Prompt Architecture 14. Resources 15. Skills 16. Catalog 17. ID Schema --- # FlowMCP Specification v3.0.0 — Overview FlowMCP is a **Tool Catalog with pre-built API templates** and a **Knowledge Base for API workflows**. It unifies access to APIs through two equal channels: 1. **CLI** — Direct access to Tools, Resources, Prompts, and Skills 2. **MCP/A2A Server** — Agents and MCP clients can use FlowMCP as a server (compatible with OpenClaw) This document provides the conceptual foundation, positioning, terminology, and document index for the v3.0.0 specification. --- ## The Problem FlowMCP Solves Not every data source is a clean REST API. The real world is messy — some APIs have quirks, some tasks require combining multiple APIs, and some data lives behind websites with no API at all. FlowMCP provides a solution for each scenario: | Data Source Type | Challenge | FlowMCP Solution | |------------------|-----------|-------------------| | Complete REST API | Standard endpoints, predictable responses | **Tool** (deterministic) | | API with peculiarities | Rate limits, pagination, unusual auth, response quirks | **Tool + Prompt** | | Multiple APIs combined | Cross-provider workflows, data enrichment chains | **Agent-Prompt** (Workflow) | | Website without API | Data locked in HTML, no programmatic access | **Prompt** (Instructions) | The key insight: not everything can be solved deterministically. A clean API call is deterministic — combine three APIs or scrape a website, and you need instructions for an LLM. FlowMCP covers both sides. --- ## What FlowMCP Offers FlowMCP provides two categories of primitives: deterministic building blocks that always behave the same way, and non-deterministic guidance that helps LLMs combine those building blocks effectively. ```mermaid flowchart TD A[FlowMCP] --> B[Deterministic] A --> C[Non-Deterministic] B --> D[Tools — API endpoint wrappers] B --> E[Resources — Local SQLite + Markdown data] C --> F[Prompts — Explanatory namespace descriptions] C --> G[Skills — Instructional multi-step workflows] G --> H[Tool Combinatorics — How to chain tools across providers] ``` FlowMCP provides **four primitives**: Tools, Resources, Prompts, and Skills. Tools and Resources are deterministic — same input always produces the same result. Prompts and Skills are non-deterministic — they guide LLMs on which tools to call, in which order, how to pass results between them, and when to fall back to alternative providers. Prompts are explanatory (describing a namespace or workflow), while Skills are instructional (step-by-step procedures with typed inputs and outputs). Together, they encode knowledge that would take hours to figure out manually. --- ## Partial Validatability Different primitives have different levels of validatability. FlowMCP embraces this spectrum rather than pretending everything is fully testable: | Primitive | Validatable? | What Can Be Validated | |-----------|-------------|----------------------| | **Tools** | Completely | Schema structure, parameter types, output format, live API tests (minimum 3 deterministic test cases) | | **Resources** | Completely | SQLite schema creation, query execution, parameter binding, result format | | **Prompts** | Partially | Tool references resolve, parameter syntax `{{type:name}}` is valid, `references` entries exist | | **Skills** | Partially | Placeholder syntax `{{type:name}}` is valid, `requires.tools` and `requires.resources` resolve, typed input/output structure | | **Agents** | Partially | Manifest structure (`agent.mjs` with `export const agent`), tool references exist, `expectedTools` (deterministic), `expectedContent` (partially — LLM output varies) | This is a feature, not a limitation. Tools and Resources are the deterministic anchor. Prompts, Skills, and Agents build on that anchor but acknowledge that LLM behavior introduces variability. --- ## Core Principles ### 1. Everything would be possible without FlowMCP FlowMCP saves time through pre-built templates. Every tool, prompt, and agent definition encodes knowledge that a developer *could* acquire by reading API docs, experimenting with endpoints, and writing integration code. FlowMCP packages that work so it does not need to be repeated. ### 2. Providers AND Agents are important Providers deliver data — one namespace per API source, model-neutral, reusable. Agents bundle tools from multiple providers for a specific task — purpose-driven, model-specific, opinionated. Neither is more important than the other. Providers are the building blocks, Agents are the compositions. ### 3. LLM-First The specification must be written so an LLM can import it as plaintext and write tools itself. Schema files are `.mjs` with named exports — no build steps, no binary formats, no complex inheritance. An LLM reading a schema file should immediately understand what it does. ### 4. Token efficiency Correct structure upfront saves LLM time and tokens at runtime. Shared lists avoid repeating enum values across schemas. Prompt placeholders reference tools by ID instead of duplicating descriptions. Agent manifests declare exactly which tools are needed — no discovery overhead. ### 5. Seamlessly extensible From local `.env` auth today to OAuth in the future. The architecture does not lock into a single auth mechanism. API keys in environment variables work now. OAuth flows, token refresh, and delegated auth can be added without breaking existing schemas. --- ## LLM-First Design Philosophy ### Open Structures Instead of Strict Hierarchies Traditional API frameworks optimize for machine enforcement — strict types, deep inheritance, access control layers. FlowMCP optimizes for LLM comprehension: | Aspect | Traditional Architecture | LLM-First Architecture | |--------|------------------------|----------------------| | File format | JSON/YAML with strict schema | `.mjs` with named exports — readable as plaintext | | Composition | Import chains, class hierarchies | Flat references by ID, no nesting | | Access control | Roles, permissions, scopes | None — FlowMCP is local, the user controls access | | Categorization | Enforced taxonomy | Efficiency categorization, not access control | | Extension | Plugin APIs, hook systems | Add a new `.mjs` file, reference it in registry | ### Why This Works FlowMCP is **local software**. It runs on the developer's machine or in their infrastructure. There is no multi-tenant permission system because there is only one tenant. The provider/agent categorization exists for **efficiency** — helping LLMs find the right tool quickly — not for access control. ### Consequences for Architecture Because the system is open and local, an LLM can create new agents at runtime by combining existing provider tools: ```mermaid flowchart LR A[LLM receives task] --> B[Reads available providers] B --> C[Selects relevant tools] C --> D[Composes ad-hoc agent] D --> E[Executes tool chain] F[Shared Lists] --> C G[Provider-Prompts] --> D ``` Shared lists and provider-prompts are available as context at every step. The LLM does not need to discover capabilities through trial and error — it reads the catalog. --- ## Three-Level Architecture FlowMCP organizes its catalog in three levels. The root level holds shared resources. Provider and Agent levels are peers that both reference root-level data: ```mermaid flowchart TD R[Root Level — Shared] R --> P[Provider Level] R --> AG[Agent Level] subgraph Root SL[Shared Lists — exclusively root] SH[Shared Helpers] REG[registry.json] end subgraph Providers P1[etherscan/ — tools, resources, prompts] P2[coingecko/ — tools, resources, prompts] P3[defillama/ — tools, resources, prompts] end subgraph Agents A1[token-analyst — tools + prompts + model + systemPrompt] A2[wallet-auditor — tools + prompts + model + systemPrompt] end R --> Root P --> Providers AG --> Agents ``` ### Root Level - **Shared Lists** — Reusable, versioned value lists (EVM chains, country codes, trading pairs). Shared lists live exclusively at root level and are injected into schemas at load-time. - **Shared Helpers** — Utility functions available to all schemas via dependency injection. - **registry.json** — The catalog manifest listing all providers, agents, and shared lists. ### Provider Level One API provider per namespace. Each provider directory contains: - **Tools** — Deterministic API endpoint wrappers (`main.tools`) - **Resources** — Local SQLite data access (`main.resources`) - **Prompts** — Model-neutral guidance for using this provider's tools (`main.prompts`) Provider-level prompts are **model-neutral** — they describe how to use the provider's tools without assuming a specific LLM. Any model can benefit from them. ### Agent Level A complete, purpose-driven definition that bundles tools from multiple providers for a specific task. Each agent includes: - **Tools** — Cherry-picked from multiple providers - **Prompts** — Model-specific, tested against a specific LLM - **Model** — The target LLM (e.g. `claude-sonnet-4-20250514`, `gpt-4o`) - **System Prompt** — The agent's persona and behavioral instructions Agent-level prompts are **model-specific** — they are written and tested for a particular LLM, leveraging its strengths and working around its weaknesses. --- ## Terminology | Term | Definition | |------|-----------| | **Schema** | A `.mjs` file with two named exports: `main` (static) and optionally `handlers` (factory function). Defines tools, resources, and/or prompts. | | **Tool** | A single API endpoint within a schema (formerly called "Route" in v2). Maps to the MCP `server.tool` primitive. Each tool has parameters, a method, a path, and optional handlers. Defined in `main.tools`. | | **Route** | Deprecated alias for Tool. `main.routes` is accepted in v3.0.0 with a deprecation warning but will be removed in v3.2.0. Schemas must not define both `tools` and `routes`. | | **Resource** | Local data access via SQLite databases (in-memory or file-based) and Markdown documents. Maps to the MCP `server.resource` primitive. Defined in `main.resources`. See `13-resources.md`. | | **Provider-Prompt** | A model-neutral prompt explaining a single namespace. Describes how to use one provider's tools effectively without assuming a specific LLM model. | | **Agent-Prompt** | A model-specific prompt tested against a specific LLM model. Contains tool combinatorics, chaining instructions, and fallback strategies. | | **Skill** | A self-contained instruction set for AI agents. Maps to the MCP `server.prompt` primitive. Defined as a `.mjs` file with `export const skill` containing typed metadata, input/output declarations, and Markdown instructions with `{{type:name}}` placeholders. Schema-scoped — references tools and resources within the same schema. See `14-skills.md`. | | **Content Placeholder** | `{{type:name}}` syntax for dynamic content in prompts and skills. Types: `{{tool:name}}` references a tool, `{{resource:name}}` references a resource, `{{input:key}}` references an input parameter. Replaces the deprecated `[[...]]` syntax from earlier revisions. | | **Namespace** | Provider identifier, lowercase letters only (e.g. `etherscan`, `coingecko`). Groups schemas by data source. | | **Handler** | An async function returned by the `handlers` factory. Performs pre- or post-processing for a tool or resource query. Receives dependencies via injection. | | **Modifier** | Handler subtype: `preRequest` transforms input before the API call, `postRequest` transforms output after the API call (or after a resource query). | | **Shared List** | A reusable, versioned value list (e.g. EVM chain identifiers, country codes) referenced by schemas and injected at load-time. | | **Agent** | A complete, purpose-driven definition with tools, prompts, skills, model, and behavior. Defined as `agent.mjs` with `export const agent` containing all metadata, tool references, model binding, system prompt, and tests. Bundles tools from multiple providers for a specific task. Replaces "Group" from v2. See `06-agents.md`. | | **Catalog** | A named directory containing a `registry.json` manifest with shared lists, provider schemas, and agent definitions. The top-level organizational unit. | | **Main Export** | `export const main = {...}` — the declarative, JSON-serializable part of a schema. Contains `tools`, `resources`, and `prompts`. Hashable for integrity verification. Schemas use `export const main`; agents use `export const agent` (see Agent). | | **Handlers Export** | `export const handlers = ({ sharedLists, libraries }) => ({...})` — factory function receiving injected dependencies. Subject to security scanning. | --- ## Specification Document Index | Document | Title | Description | |----------|-------|-------------| | `00-overview.md` | Overview | Vision, architecture, terminology, design principles (this document) | | `01-schema-format.md` | Schema Format | File structure, main/handlers split, tool definitions, naming conventions | | `02-parameters.md` | Parameters | Position block, Z block validation, shared list interpolation, API key injection, resource and prompt parameters | | `03-shared-lists.md` | Shared Lists | List format, versioning, field definitions, filtering, resolution lifecycle | | `04-output-schema.md` | Output Schema | Response type declarations, field mapping, flattening rules | | `05-security.md` | Security Model | Zero-import policy, library allowlist, static scan, dependency injection | | `06-agents.md` | Agents | Agent manifest format (`agent.mjs` with `export const agent`), tool cherry-picking, model binding, system prompts, integrity verification | | `07-tasks.md` | Tasks | Deferred — MCP Tasks integration placeholder | | `08-migration.md` | Migration | v1.2.0 to v2.0.0 and v2.0.0 to v3.0.0 migration guides, backward compatibility | | `09-validation-rules.md` | Validation Rules | Complete validation checklist for schemas, lists, agents, resources, and prompts | | `10-tests.md` | Tests | Test format for tools and resources, design principles, response capture lifecycle, output schema generation | | `12-prompt-architecture.md` | Prompt Architecture | Provider-Prompts (model-neutral), Agent-Prompts (model-specific), placeholder syntax, cross-schema composition | | `13-resources.md` | Resources | SQLite resource format, queries, parameters, SQL security, handler integration | | `14-skills.md` | Skills | Skill `.mjs` format (`export const skill`), `{{type:name}}` placeholders, typed input/output, versioning, validation rules | | `15-catalog.md` | Catalog | Catalog manifest, registry.json, named catalogs, import flow | | `16-id-schema.md` | ID Schema | ID format `namespace/type/name`, short form, resolution rules | --- ## Design Principles ### 1. Deterministic over clever Same input always produces the same API call. No randomness, no caching heuristics, no adaptive behavior inside the schema layer. If a schema's `preRequest` handler receives the same `payload`, it must produce the same `struct` every time. ### 2. Declare over code Maximize the `main` block, minimize handlers. Every field that can be expressed declaratively (URL patterns, parameter types, enum values) must live in `main`. Handlers exist only for transformations that cannot be expressed as static data — never for logic that could be a parameter default or a path template. ### 3. Inject over import Schemas receive data through dependency injection, never import. A handler that needs EVM chain data does not `import` a chain list — it receives `sharedLists.evmChains` via the factory function. Libraries are declared in `requiredLibraries` and injected by the runtime from an allowlist. Schema files contain zero import statements. ### 4. Hash over trust Integrity verification through SHA-256 hashes. The `main` block is hashable because it is pure JSON-serializable data. Agents store hashes of their member tools. Changes to a schema invalidate the hash, signaling that review is needed. ### 5. Constrain over permit Security by default, explicit opt-in for capabilities. Schema files have zero import statements — all dependencies are declared in `requiredLibraries` and injected from a runtime allowlist. The security scanner rejects schemas with forbidden patterns at load-time, before any tool is exposed to an AI client. --- ## Version History | Version | Revision | Date | Changes | |---------|----------|------|---------| | 1.0.0 | — | 2025-06 | Initial schema format. Flat structure with inline parameters and direct URL construction. | | 1.2.0 | — | 2025-11 | Added handlers (preRequest/postRequest), Zod-based parameter validation, modifier pattern for input/output transformation. | | 2.0.0 | — | 2026-02 | Two-export format (`main` + `handlers` factory). Dependency injection via allowlist. Shared lists for reusable values. Output schema declarations. Zero-import security model. Groups with integrity hashes. Maximum routes reduced from 10 to 8. | | 3.0.0 | 1.0 | 2026-03 | Four primitives: Tools (renamed from Routes), Resources (SQLite), Prompts (model-neutral/model-specific guidance), Skills (`.mjs` instructional workflows). `main.routes` deprecated in favor of `main.tools`. `flowmcp-skill/1.0.0` versioning for skills. Group type discriminators for resources and skills. | | 3.0.0 | 8.0 | 2026-03 | Three-level architecture (Root/Provider/Agent). Groups renamed to Agents with `agent.mjs` manifest format (`export const agent`). Prompt architecture with Provider-Prompts (model-neutral) and Agent-Prompts (model-specific). Catalog with registry.json. Unified placeholder syntax `{{type:name}}` (replaces deprecated `[[...]]`). ID schema `namespace/type/name`. Test minimum increased to 3. Agent tests with `expectedTools`/`expectedContent`. | | 3.1.0 | 1.0 | 2026-03 | Resources: Two SQLite modes (`in-memory` with `readonly: true`, `file-based` with WAL). Origin system (`global`, `project`, `inline`) replaces pseudo-paths. `better-sqlite3` replaces `sql.js`. `getSchema` MUST for both modes. `freeQuery` auto-injected. Max queries increased to 8. Block patterns removed. `source: 'markdown'` resource type with parameter-based access. Folder renamed `data/` to `resources/`. All fields required. Prompts: `contentFile` field for Provider-Prompts (content in external file). `references` field required (empty array when none). `about` convention (SHOULD) for Provider-Schemas and Agent-Manifests. | --- ## What Changed in v3.1.0 The v3.1.0 release enhances Resources and Prompts with production-ready features: ### Resources - **Two SQLite modes** — `mode: 'in-memory'` (readonly via `better-sqlite3` `readonly: true`) and `mode: 'file-based'` (writable via WAL mode). Clear separation instead of implicit read-only. - **Origin system** — `origin: 'global'`, `origin: 'project'`, `origin: 'inline'` replace pseudo-paths (`~/.flowmcp/data/`, `./data/`). Explicit storage locations with clear resolution rules. - **`better-sqlite3` runtime** — Replaces `sql.js` as the unified SQLite runtime. Native C bindings, real `readonly: true` flag, WAL mode for concurrent writes. - **`getSchema` is MUST** — Required for both modes (previously SHOULD). Schema authors must define it. CLI uses it to create databases for `file-based` mode. - **`freeQuery` auto-injected** — Runtime automatically adds freeQuery. SELECT-only for in-memory, all statements for file-based. - **Max queries increased to 8** — 7 schema-defined (including getSchema) + 1 auto-injected freeQuery. - **Block patterns removed** — `readonly: true` handles in-memory security at DB level. No more SQL pattern matching. - **Markdown resources** — `source: 'markdown'` with parameter-based access (section, lines, search). Intended for API documentation shipped with schemas. - **Folder renamed** — `resources/` replaces `data/`. - **All fields required** — No defaults, no optional fields. - **Backup strategy** — Automatic `.bak` copy before first write for file-based databases. ### Prompts - **`contentFile` for Provider-Prompts** — Provider-Prompt definitions live in `main.prompts`. Content is loaded from external `.mjs` files via the new `contentFile` field. Content files export `export const content`. - **`references` required** — The `references` field is now required for both prompt types. Empty array `[]` when no references. - **`about` convention** — Reserved prompt name (SHOULD) for both Provider-Schemas and Agent-Manifests. Entry point to a namespace or agent. The migration path for existing resources is documented in the schema migration notes. Existing schemas using `database` paths and `data/` folders need to adopt the origin system. --- ## What Changed in v3.0.0 The v3.0.0 release transforms FlowMCP from a tool catalog into a complete API knowledge platform covering all four primitives (Tools, Resources, Prompts, Skills): - **Tools replace Routes** — `main.tools` is the primary key. `main.routes` is accepted as a deprecated alias with a warning (removed in v3.2.0). Schemas must not define both `tools` and `routes`. - **Resources** — Embedded SQLite databases for local, deterministic data access. Defined in `main.resources`. See `13-resources.md`. - **Skills** — Self-contained instruction sets for AI agents. Defined as `.mjs` files with `export const skill` and `{{type:name}}` placeholders. Schema-scoped, maps to MCP `server.prompt` primitive. See `14-skills.md`. - **Groups renamed to Agents** — Groups evolve into full agent manifests (`agent.mjs` with `export const agent`) with model binding, system prompts, and purpose-driven tool selection. See `06-agents.md`. - **Prompt Architecture** — Two-tier prompt system: Provider-Prompts (model-neutral, single namespace) and Agent-Prompts (model-specific, multi-provider workflows). Unified `{{type:name}}` placeholder syntax (replaces deprecated `[[...]]`). See `12-prompt-architecture.md`. - **Catalog with registry.json** — Named catalogs with a manifest file listing all providers, agents, and shared lists. Enables import and distribution. See `15-catalog.md`. - **ID Schema** — Unified `namespace/type/name` format for referencing tools, resources, prompts, and skills across the catalog. Short form for common cases. See `16-id-schema.md`. - **Test minimum increased to 3** — Every tool must have at least 3 deterministic test cases (up from 1 in v2). - **Agent tests** — `expectedTools` validates which tools the agent selects (deterministic). `expectedContent` validates LLM output (partially deterministic). - **0-tool schemas are valid** — Resource-only, prompt-only, or skill-only schemas are allowed. - **Three-level architecture** — Root (shared lists, helpers, registry), Provider (one API per namespace), Agent (purpose-driven compositions). The migration path from v2.0.0 to v3.0.0 is documented in `08-migration.md`. --- ## What Changed in v2.0.0 The v2.0.0 release restructures the schema format around a fundamental insight: **the declarative parts of a schema should be separable from the executable parts**. This enables: - **Integrity hashing** of the `main` block without including function bodies - **Security scanning** of the `handlers` block as an isolated concern - **Shared lists** that inject reusable data at load-time instead of hardcoding enum values - **Output schemas** that declare expected response shapes for downstream consumers - **Groups** that compose tools across schemas with verifiable integrity The migration path from v1.2.0 to v2.0.0 is documented in `08-migration.md`. --- # FlowMCP Specification v3.0.0 — Schema Format This document defines the structure of a FlowMCP schema file, the two named exports (`main` and `handlers`), tool definitions, resource declarations, skill references, naming conventions, and constraints. --- ## Schema File Structure A schema is a `.mjs` file with **two separate named exports**: ```javascript // 1. 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, see 13-resources.md */ }, skills: { /* optional, see 14-skills.md */ } } // 2. Factory function — receives injected dependencies, returns handler objects export 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: ```javascript // Schema without handlers — only the main export export const main = { namespace: 'coingecko', name: 'SimplePrice', description: 'Get current price of coins in any supported currency', version: '3.0.0', root: 'https://api.coingecko.com/api/v3', tools: { /* ... */ } } ``` ### Why Two Separate Exports - **`main` can be hashed and validated without calling any function.** The runtime reads the static export, serializes it via `JSON.stringify()`, and computes its hash — no code execution required. - **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 function. - **`requiredLibraries` declares 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 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 | Field | Type | Description | |-------|------|-------------| | `namespace` | `string` | Provider identifier, lowercase letters only (`/^[a-z]+$/`). 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 `3.\d+.\d+` (semver, major must be `3`). Version `2.\d+.\d+` is accepted during migration. | | `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. | ### Optional Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `docs` | `string[]` | `[]` | Documentation URLs for the API provider. Informational only. | | `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`. | | `skills` | `object` | `{}` | Skill references. Keys are skill names in lowercase-hyphen format. Maximum 4 skills. See `14-skills.md`. | ### 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 #### `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`). Only lowercase ASCII letters are allowed — no numbers, hyphens, or underscores. ```javascript // Valid namespace: 'etherscan' namespace: 'coingecko' namespace: 'defillama' // Invalid namespace: 'defi-llama' // hyphen not allowed namespace: 'CoinGecko' // uppercase not allowed namespace: 'web3_data' // underscore not allowed ``` #### `root` The base URL is prepended to every route's `path`. It must use HTTPS and must not end with a slash: ```javascript // 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 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`). ```javascript requiredServerParams: [ 'ETHERSCAN_API_KEY' ] ``` #### `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. ```javascript // Schema needs ethers.js for address checksumming requiredLibraries: [ 'ethers' ] // Schema needs no libraries requiredLibraries: [] ``` See `05-security.md` for the default allowlist and how to extend it. #### `headers` Default headers sent with every request from this schema. Common use cases include Accept headers or API versioning: ```javascript headers: { 'Accept': 'application/json', 'X-API-Version': '2024-01' } ``` #### `sharedLists` References to shared lists that this schema uses. Each entry specifies the list reference, version, and optional filter: ```javascript 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 Each key in `tools` is the tool name in camelCase. The tool name becomes part of the fully qualified MCP tool name. ```javascript 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 | 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. At least 1 per tool. See `10-tests.md`. | ### Tool Field Details #### `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. #### `path` The path is appended to `root` to form the complete URL. It may contain `{{key}}` placeholders that are replaced by `insert` parameters: ```javascript // Static path path: '/api' // Path with insert 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 runtime validates this at load-time. --- ## 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: ```javascript 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) 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: ```javascript 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 | Handler | When | Input | Must Return | |---------|------|-------|-------------| | `preRequest` | Before the API call | `{ struct, payload }` | `{ struct, payload }` | | `postRequest` | After the API call | `{ response, struct, payload }` | `{ response }` | ### 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 1. **Handlers are optional.** Tools without handlers make direct API calls using the constructed URL and parameters. Most tools should not need handlers. 2. **Schema files must NOT contain import statements.** No `import`, no `require`, no dynamic `import()`. All dependencies are injected through the factory function. The security scanner rejects any schema containing import statements. See `05-security.md`. 3. **Handlers must NOT access restricted globals.** The following are forbidden: `fetch`, `fs`, `process`, `eval`, `Function`, `setTimeout`, `setInterval`, `XMLHttpRequest`, `WebSocket`. See `05-security.md` for the complete list. 4. **`sharedLists` provides resolved list data.** If a schema references shared lists in `main.sharedLists`, the resolved data is available inside the factory closure via `sharedLists['listName']` or `sharedLists.listName`. The data is deep-frozen — mutations throw. 5. **`libraries` provides approved npm packages.** If a schema declares `main.requiredLibraries`, the loaded modules are available inside the factory closure via `libraries['packageName']` or `libraries.packageName`. 6. **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. 7. **Return shape must match the handler type.** `preRequest` must return `{ struct, payload }`. `postRequest` must return `{ response }`. Missing keys cause a runtime error. 8. **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.md` for details. --- ## Naming Conventions | Element | Convention | Pattern | Example | |---------|-----------|---------|---------| | Namespace | Lowercase letters only | `^[a-z]+$` | `etherscan`, `coingecko` | | 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 | 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`. | | Max skills per schema | 4 | Keeps skill count manageable. See `14-skills.md`. | | Version major | `3` | Must match `3.\d+.\d+`. Version `2.\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]+$` | Letters only. No numbers, hyphens, or underscores. | | 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 The following diagram shows how the runtime loads a schema file, validates it, and registers its routes as MCP tools: ```mermaid 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{main.skills exists?} L -->|Yes| M[Load and validate skill .mjs files] L -->|No| N[Done] M --> O[Register skills as MCP prompts] O --> N ``` **Step-by-step:** 1. **Read file as string** — the raw source is read before any execution. 2. **Static security scan** — the string is scanned for forbidden patterns (`import`, `require`, `eval`, etc.). If any match, the file is rejected. See `05-security.md`. 3. **Dynamic import** — the file is imported via `import()`. 4. **Extract `main` export** — the named `main` export is read. 5. **Validate `main` block** — JSON-serializability, required fields, version format, namespace pattern, tool limits. If `main.routes` is found, it is treated as `main.tools` with a deprecation warning. If both `tools` and `routes` are present, the schema is rejected. 6. **Resolve `sharedLists`** — shared list references are resolved to data. Data is deep-frozen. 7. **Load `requiredLibraries`** — each library is checked against the allowlist and loaded via `import()`. Unapproved libraries reject the schema. 8. **Call `handlers()`** — if the `handlers` export exists, the factory function is called with `{ sharedLists, libraries }`. The returned handler objects are registered per tool (and per resource query, if applicable). 9. **Register tools** — each tool is exposed as an MCP tool. Tools with handlers use pre/post processing; tools without handlers make direct API calls. 10. **Load skills** — if `main.skills` is present, each referenced `.mjs` skill file is loaded via `import()`, validated, and registered as an MCP prompt. See `14-skills.md`. --- ## Complete Example A full schema for Etherscan with two tools, one handler using `sharedLists`, and no required libraries: ```javascript 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: [] } } ], 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 1. **Two separate exports** — `main` is a static data object, `handlers` is a factory function receiving `{ sharedLists }`. 2. **Two tools** (`getContractAbi`, `getSourceCode`) sharing the same `root` and `path` but differing by query parameters. 3. **Fixed parameters** (`module`, `action`) that are invisible to the user — they are sent automatically. 4. **User parameters** (`address`) with length validation for Ethereum addresses. 5. **Server parameters** (`apikey`) injected from the environment via `{{SERVER_PARAM:ETHERSCAN_API_KEY}}`. 6. **A shared list reference** (`evmChains`) filtered to chains that have an Etherscan alias. 7. **`requiredLibraries: []`** — this schema needs no external npm packages. 8. **A `postRequest` handler** on `getSourceCode` that flattens the nested API response into a cleaner object. 9. **No handler** on `getContractAbi` — the raw API response is returned directly. 10. **An output schema** on `getContractAbi` declaring the expected response shape. 11. **Zero import statements** — the schema file has no imports. Dependencies (`sharedLists`) are injected through the factory function. --- # FlowMCP Specification v3.0.0 — Parameters This document defines the parameter format for FlowMCP schema tools, resources, and skills. Each tool parameter describes where a value is placed in the API request (`position`) and how it is validated (`z`). Resource parameters use the same `position` + `z` system but without a `location` field. Skill input uses a simpler format. --- ## Parameter Structure Each parameter is an object with two required blocks: ```javascript { position: { key: 'address', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: [ 'min(42)', 'max(42)' ] } } ``` - **`position`** defines where the value goes in the HTTP request. - **`z`** defines how the value is validated before the request is made. Both blocks are required. A parameter without `position` or `z` is invalid and rejected at load-time. --- ## Position Block The `position` block controls where the parameter's value is placed in the constructed API request. | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | `string` | Yes | Parameter name. For user-facing parameters, this is the input field name exposed to the AI client. | | `value` | `string` | Yes | `{{USER_PARAM}}` for user input, `{{SERVER_PARAM:KEY_NAME}}` for server params, or a fixed string. | | `location` | `string` | Yes | Where the value is placed: `insert`, `query`, or `body`. | ### Location Types | Location | Description | URL Effect | Example | |----------|-------------|------------|---------| | `insert` | Inserted into the URL path at the `{{key}}` placeholder position | `root + path` with `{{key}}` replaced | `/api/v1/{{address}}/txs` becomes `/api/v1/0xABC.../txs` | | `query` | Added as a URL query parameter | `?key=value` appended to the URL | `?address=0xABC...&module=contract` | | `body` | Added to the JSON request body | `{ "key": "value" }` in the POST/PUT body | `{ "address": "0xABC..." }` | ### Location Rules 1. **`insert` parameters** require a matching `{{key}}` placeholder in the route's `path`. If no placeholder matches, the runtime raises a load-time error. 2. **`query` parameters** are appended to the URL in array order. Duplicate keys are allowed (for APIs that accept repeated query params like `?id=1&id=2`). 3. **`body` parameters** are only valid for `POST` and `PUT` routes. A `body` parameter on a `GET` or `DELETE` route causes a load-time validation error. 4. **Multiple locations** in the same route are valid. A route can have `insert`, `query`, and `body` parameters simultaneously (though `body` still requires `POST`/`PUT`). ### Value Types | Value Pattern | Description | Visible to User | |---------------|-------------|-----------------| | `{{USER_PARAM}}` | Value provided by the user at call-time | Yes | | `{{SERVER_PARAM:KEY_NAME}}` | Value injected from server environment | No | | Any other string | Fixed value, sent automatically | No | --- ## Z Block (Validation) The `z` block defines validation constraints that are enforced before the API request is made. The name `z` references Zod, the validation library used by the runtime. | Field | Type | Required | Description | |-------|------|----------|-------------| | `primitive` | `string` | Yes | Base type declaration with optional inline values. | | `options` | `string[]` | Yes | Array of validation constraints. Can be empty `[]`. | ### Primitive Types | Primitive | Description | JS Equivalent | Example | |-----------|-------------|---------------|---------| | `string()` | Any string value | `z.string()` | `'string()'` | | `number()` | Numeric value (integer or float) | `z.number()` | `'number()'` | | `boolean()` | True or false | `z.boolean()` | `'boolean()'` | | `enum(A,B,C)` | Exactly one of the listed values | `z.enum(['A','B','C'])` | `'enum(mainnet,testnet,devnet)'` | | `array()` | Array of values | `z.array()` | `'array()'` | | `object()` | Nested object | `z.object()` | `'object()'` | #### Enum Specifics Enum values are comma-separated inside the parentheses. No spaces around commas. Values are case-sensitive: ```javascript // Valid primitive: 'enum(GET,POST,PUT,DELETE)' primitive: 'enum(mainnet,testnet)' primitive: 'enum(1,5,137)' // Invalid primitive: 'enum(GET, POST)' // no spaces after commas primitive: 'enum()' // at least one value required ``` ### Validation Options Options are applied in array order after the primitive type check. | Option | Description | Applies To | Example | |--------|-------------|------------|---------| | `min(n)` | Minimum value (number) or minimum length (string) | `number`, `string` | `'min(1)'`, `'min(0)'` | | `max(n)` | Maximum value (number) or maximum length (string) | `number`, `string` | `'max(100)'`, `'max(256)'` | | `length(n)` | Exact length (string) or exact item count (array) | `string`, `array` | `'length(42)'` | | `optional()` | Parameter is not required. Omitted parameters are excluded from the request. | all | `'optional()'` | | `default(value)` | Default value used when the parameter is omitted. Implies `optional()`. | all | `'default(100)'`, `'default(desc)'` | #### Option Rules 1. **`optional()` and `default()`** — A parameter with `default(value)` is implicitly optional. Specifying both `optional()` and `default()` is allowed but redundant. 2. **`min()` and `max()` semantics** — For `number()`, these constrain the numeric value. For `string()`, these constrain the string length. For other types, they are ignored. 3. **`length()` semantics** — For `string()`, this is character count. For `array()`, this is item count. For other types, it is ignored. 4. **Multiple options** — Options are combined with AND logic. `[ 'min(1)', 'max(100)' ]` means the value must be >= 1 AND <= 100. > **Note:** Regular expressions are intentionally excluded from validation options. AI agents work poorly with regex patterns, and type-level validation combined with `min()`/`max()`/`length()` constraints covers the vast majority of use cases. --- ## Shared List Interpolation When a parameter's enum values come from a shared list, use the `{{listName:fieldName}}` syntax inside the `enum()` primitive: ```javascript { position: { key: 'chainName', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'enum({{evmChains:etherscanAlias}})', options: [] } } ``` At load-time, the runtime resolves `{{evmChains:etherscanAlias}}` by: 1. Finding the shared list named `evmChains` (declared in `main.sharedLists`) 2. Applying any filter defined in the shared list reference 3. Extracting the `etherscanAlias` field from each entry 4. Replacing the interpolation placeholder with the comma-separated values The result at runtime is equivalent to: ```javascript primitive: 'enum(ETHEREUM_MAINNET,POLYGON_MAINNET,ARBITRUM_MAINNET,OPTIMISM_MAINNET)' ``` ### Interpolation Rules 1. **`{{listName:fieldName}}` is only allowed inside `enum()`**. Using it in `string()`, `number()`, or any other primitive is a load-time error. 2. **The referenced list must be declared in `main.sharedLists`.** If a parameter references `{{evmChains:etherscanAlias}}` but `main.sharedLists` does not include an entry with `name: 'evmChains'`, the runtime rejects the schema. 3. **If the shared list reference has a `filter`, only matching entries are used.** A filter `{ field: 'hasEtherscan', value: true }` means only entries where `hasEtherscan === true` contribute values. 4. **The `fieldName` must exist in the list's `meta.fields`.** If the list does not define a field called `etherscanAlias`, the runtime raises a load-time error. 5. **Interpolation happens at load-time, not at call-time.** The enum values are resolved once when the schema is loaded. Shared list updates require a schema reload. 6. **Mixed static and interpolated values** are allowed: ```javascript primitive: 'enum(custom,{{evmChains:etherscanAlias}})' ``` This prepends `custom` to the resolved list values. --- ## Fixed Parameters Parameters with a fixed `value` (not `{{USER_PARAM}}` and not `{{SERVER_PARAM:...}}`) are invisible to the user. They are sent automatically with every request: ```javascript { position: { key: 'module', value: 'contract', location: 'query' }, z: { primitive: 'string()', options: [] } } ``` Fixed parameters are common for APIs that use query parameters for routing (like Etherscan's `module` and `action` parameters). They let a single `root` + `path` combination serve multiple routes differentiated by fixed query values. ### Fixed Parameter Rules 1. Fixed parameters are **not exposed to the AI client** in the tool's input schema. 2. The `z` block still applies — the fixed value must pass validation. This is checked at load-time. 3. Fixed parameters are processed in array order alongside user parameters. --- ## API Key Injection API keys and other server-level secrets are injected via the `{{SERVER_PARAM:KEY_NAME}}` syntax: ```javascript { position: { key: 'apikey', value: '{{SERVER_PARAM:ETHERSCAN_API_KEY}}', location: 'query' }, z: { primitive: 'string()', options: [] } } ``` ### Injection Rules 1. **The `KEY_NAME` must be declared in `main.requiredServerParams`.** If a parameter references `{{SERVER_PARAM:ETHERSCAN_API_KEY}}` but `requiredServerParams` does not include `'ETHERSCAN_API_KEY'`, the runtime raises a load-time error. 2. **Server parameters are invisible to the AI client.** They do not appear in the tool's input schema. 3. **The runtime resolves `{{SERVER_PARAM:KEY_NAME}}`** by reading the corresponding environment variable. If the variable is not set, the tool is not exposed (the schema loads but its tools are hidden). 4. **Server parameters are never logged.** The runtime must not include server parameter values in error messages, debug output, or response data. --- ## Parameter Ordering Parameters are defined as an array, and the array order matters: 1. **`insert` parameters** are applied to the `path` template in array order. If a path has two placeholders (`/api/{{chainId}}/{{address}}`), the first `insert` parameter fills `{{chainId}}` and the second fills `{{address}}` — matched by `key`, not by position. 2. **`query` parameters** are appended to the URL in array order. While URL query parameter order is generally insignificant, consistent ordering aids debugging and cache behavior. 3. **`body` parameters** are assembled into a JSON object. Array order determines key order in the serialized JSON (though JSON key order is not semantically significant). 4. **Mixed locations** are processed in a single pass. The runtime iterates the array once, routing each parameter to its location. --- ## Prompt Placeholder Syntax FlowMCP uses the `{{type:name}}` placeholder syntax for **prompt content** — it appears in skill content fields, Provider-Prompts, and Agent-Prompts to reference registered primitives and accept user input at runtime. The same `{{...}}` curly-brace syntax is used in schema parameters (see previous sections), but with different type prefixes that distinguish the two contexts. In schema `main` blocks (`main.tools`, `main.resources`), the `{{...}}` syntax controls HTTP request construction and SQL parameter binding — using variants like `{{USER_PARAM}}`, `{{SERVER_PARAM:KEY}}`, and `{{listName:fieldName}}`. In prompt content, the `{{type:name}}` syntax references tools, resources, skills, and user input parameters. ```mermaid flowchart LR A["{{...}} in Schema"] --> B["Schema Parameters"] B --> C["main.tools — HTTP requests"] B --> D["main.resources — SQL queries"] E["{{type:name}} in Prompts"] --> F["Prompt Content"] F --> G["{{tool:name}} — tool references"] F --> H["{{resource:name}} — resource references"] F --> I["{{skill:name}} — skill references"] F --> J["{{input:key}} — user input"] ``` The diagram shows that `{{...}}` in schema definitions uses `USER_PARAM`, `SERVER_PARAM`, and shared list interpolation variants, while `{{type:name}}` in prompt content uses typed prefixes (`tool:`, `resource:`, `skill:`, `input:`) to reference primitives and accept user input. ### Placeholder Types The `type:` prefix determines what the placeholder references: | Placeholder | Syntax | Resolves To | Example | |-------------|--------|-------------|---------| | Tool | `{{tool:name}}` | A tool in the same schema's `main.tools` | `{{tool:getContractAbi}}` | | Resource | `{{resource:name}}` | A resource in the same schema's `main.resources` | `{{resource:chainList}}` | | Skill | `{{skill:name}}` | Another skill in the same schema's `main.skills` | `{{skill:quick-check}}` | | Input | `{{input:key}}` | An input parameter from the skill's `input` array | `{{input:address}}` | ### Tool References (`{{tool:name}}`) A `{{tool:name}}` placeholder references a tool defined in the same schema's `main.tools`. The runtime resolves the reference and injects the tool's description or metadata into the rendered prompt content. ``` {{tool:getContractAbi}} ← references a tool in the same schema {{tool:getSourceCode}} ← references another tool in the same schema {{tool:simplePrice}} ← references a tool by its camelCase name ``` When the runtime encounters a tool placeholder, it: 1. Looks up the tool name in the same schema's `main.tools` 2. Verifies the tool exists (validated at load time) 3. Injects the tool's description or metadata into the rendered content ### Input Parameters (`{{input:key}}`) An `{{input:key}}` placeholder is a **user-input parameter**. The value is provided by the user when the prompt is invoked. Parameter keys follow camelCase conventions and must match an entry in the skill's `input` array. ``` {{input:chainId}} ← user provides a chain identifier {{input:address}} ← user provides a contract address {{input:token}} ← user provides a token symbol {{input:startDate}} ← user provides a date ``` Parameters are runtime values — the prompt cannot render fully until the user supplies them. The MCP client collects parameter values and passes them to the runtime for substitution. ### Side-by-Side Comparison ``` Analyze the token {{input:token}} on chain {{input:chainId}}. First, fetch the current price using {{tool:simplePrice}}. Then retrieve the contract ABI via {{tool:getContractAbi}}. Check the local data with {{resource:verifiedContracts}}. ``` In this example: - `{{input:token}}` and `{{input:chainId}}` are **input parameters** — the user provides `"ETH"` and `"1"` at invocation - `{{tool:simplePrice}}` and `{{tool:getContractAbi}}` are **tool references** — resolved to tools in the same schema - `{{resource:verifiedContracts}}` is a **resource reference** — resolved to a resource in the same schema ### Composable References Prompts can include content from other skills via `{{skill:name}}` placeholders and the `references[]` array. This enables prompt composition — one prompt can incorporate another prompt's content without duplication. ```javascript { name: 'full-token-analysis', content: ` Perform a complete token analysis for {{input:token}}. ## Price Data Use {{tool:simplePrice}} to fetch current pricing. ## On-Chain Metrics Use {{tool:getContractAbi}} to inspect the contract. ## Quick Summary For a brief version, follow {{skill:quick-summary}}. `, references: [ 'coingecko/prompt/price-guide', 'etherscan/prompt/contract-patterns' ] } ``` Referenced prompts are resolved by their ID using the ID Schema. The runtime loads each referenced prompt and makes its content available to the AI agent alongside the primary prompt. Referenced prompts are **not** inlined into the content — they are provided as additional context that the agent can draw from. ### Integration with Schema `{{...}}` Syntax The two uses of `{{...}}` serve distinct layers: | Aspect | Schema `{{...}}` | Prompt `{{type:name}}` | |--------|------------------|------------------------| | **Context** | Schema `main` blocks (`main.tools`, `main.resources`) | Prompt `content` fields | | **Purpose** | HTTP request construction, SQL parameter binding, shared list interpolation | Tool/resource/skill references and user input in prompts | | **Variants** | `{{USER_PARAM}}`, `{{SERVER_PARAM:KEY}}`, `{{listName:fieldName}}` | `{{tool:name}}`, `{{resource:name}}`, `{{skill:name}}`, `{{input:key}}` | | **Resolution time** | Load-time (shared lists) or call-time (user/server params) | Prompt render time | | **Appears in** | `position.value`, `z.primitive` | Skill and prompt `content` fields | A schema author uses `{{USER_PARAM}}` and `{{SERVER_PARAM:KEY}}` when defining how a tool's parameters map to API requests. A prompt author uses `{{tool:name}}` and `{{input:key}}` when writing instructions that reference tools or accept user input. ### Validation Rules | Code | Severity | Rule | |------|----------|------| | PH001 | error | `{{type:name}}` content must not be empty (e.g., `{{tool:}}` is invalid) | | PH002 | error | Tool references (`{{tool:name}}`) must resolve to a tool in the same schema's `main.tools` | | PH003 | error | Input parameter keys (`{{input:key}}`) must match `^[a-zA-Z][a-zA-Z0-9]*$` and exist in the skill's `input` array | | PH004 | error | Prompt placeholders (`{{tool:...}}`, `{{resource:...}}`, `{{skill:...}}`, `{{input:...}}`) are only valid in prompt `content` fields, not in schema `main` blocks | #### Validation Examples ``` flowmcp validate prompt.mjs PH001 error Empty placeholder {{tool:}} found at line 12 PH003 error Input parameter key "123abc" does not match ^[a-zA-Z][a-zA-Z0-9]*$ 2 errors, 0 warnings ``` ``` flowmcp validate prompt.mjs PH002 error Reference {{tool:nonExistent}} does not resolve to a registered tool 1 error, 0 warnings ``` --- ## Complete Examples ### Example 1: Simple Query Parameter with Length Validation A parameter that accepts an Ethereum address and validates its length: ```javascript { position: { key: 'contractAddress', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: [ 'min(42)', 'max(42)' ] } } ``` **Behavior:** - The AI client sees an input field named `contractAddress` of type `string`. - The user must provide a string of exactly 42 characters (e.g., `0x` followed by 40 hex characters). - The value is appended as `?contractAddress=0x...` to the URL. - If the length constraint fails, the runtime returns a validation error before making any HTTP request. ### Example 2: Enum Parameter with Shared List Interpolation A parameter that lets the user select an EVM chain, with valid values pulled from a shared list: ```javascript // In main.sharedLists: // { name: 'evmChains', version: '1.0.0', filter: { field: 'hasEtherscan', value: true } } { position: { key: 'chain', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'enum({{evmChains:slug}})', options: [ 'default(ethereum)' ] } } ``` **Behavior:** - At load-time, `{{evmChains:slug}}` resolves to the `slug` field of all entries in the `evmChains` list where `hasEtherscan` is `true`. - The effective primitive becomes something like `enum(ethereum,polygon,arbitrum,optimism,base)`. - The AI client sees a dropdown/enum input with these chain names. - If omitted, the default value `ethereum` is used. - Invalid values (e.g. `solana`) are rejected with a validation error. ### Example 3: Body Parameter with Nested Object A parameter for a POST endpoint that accepts a complex query object: ```javascript // Route: method: 'POST', path: '/api/v1/query' { position: { key: 'query', value: '{{USER_PARAM}}', location: 'body' }, z: { primitive: 'object()', options: [] } } ``` Combined with fixed body parameters: ```javascript // Full parameters array for the route [ { position: { key: 'version', value: '2', location: 'body' }, z: { primitive: 'string()', options: [] } }, { position: { key: 'query', value: '{{USER_PARAM}}', location: 'body' }, z: { primitive: 'object()', options: [] } }, { position: { key: 'limit', value: '{{USER_PARAM}}', location: 'body' }, z: { primitive: 'number()', options: [ 'optional()', 'default(100)', 'min(1)', 'max(1000)' ] } } ] ``` **Behavior:** - The resulting request body is: ```json { "version": "2", "query": { "sql": "SELECT * FROM ..." }, "limit": 100 } ``` - `version` is fixed — the user never sees it. - `query` is a user-provided object (the AI client passes it as JSON). - `limit` is optional with a default of `100`, constrained between `1` and `1000`. - Only `POST` and `PUT` tools can have `body` parameters. --- ## Parameter Visibility Summary | Value Pattern | Visible to AI Client | Appears in Input Schema | Source | |---------------|---------------------|------------------------|--------| | `{{USER_PARAM}}` | Yes | Yes | User provides at call-time | | `{{SERVER_PARAM:KEY}}` | No | No | Environment variable | | Fixed string | No | No | Hardcoded in schema | Only `{{USER_PARAM}}` parameters are exposed to the AI client. Fixed and server parameters are implementation details hidden from the tool consumer. --- ## Resource Parameters Resource parameters use the same `position` + `z` system as tool parameters, with one key difference: **resource parameters have no `location` field**. Tool parameters need `location` (`query`, `body`, `insert`) because they are placed into HTTP requests. Resource parameters are bound to SQL `?` placeholders — their position is determined by array order, not by an HTTP request structure. ### Resource Parameter Structure ```javascript { position: { key: 'symbol', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(1)' ] } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `position.key` | `string` | Yes | Parameter name exposed to the AI client. | | `position.value` | `string` | Yes | Must be `'{{USER_PARAM}}'` for user-provided values, or a fixed string. | | `z.primitive` | `string` | Yes | Zod-based type declaration. Same primitives as tool parameters, except `array()` and `object()` are not supported. | | `z.options` | `string[]` | Yes | Validation constraints. Same options as tool parameters. | ### Key Differences from Tool Parameters | Aspect | Tool Parameters | Resource Parameters | |--------|----------------|---------------------| | `location` field | Required (`query`, `body`, `insert`) | Forbidden — no HTTP request | | `{{SERVER_PARAM:...}}` | Supported | Not supported — no API keys needed | | `array()` / `object()` | Supported | Not supported — SQL accepts scalars only | | Binding order | Determined by `location` | Determined by array index (first param = first `?`) | The number of parameters must match the number of `?` placeholders in the SQL statement. A mismatch is a validation error. See `13-resources.md` for the complete resource specification. --- ## Skill Input Skills use a simpler input format than tool parameters. Skill input is an array of objects in the `skill.input` field of the `.mjs` skill file. Skill inputs are referenced in the skill's `content` via `{{input:key}}` placeholders. ### Skill Input Structure ```javascript input: [ { key: 'address', type: 'string', description: 'Ethereum contract address', required: true }, { key: 'network', type: 'enum', description: 'Target network', required: true, values: [ 'ethereum', 'polygon' ] }, { key: 'verbose', type: 'boolean', description: 'Include detailed breakdown', required: false } ] ``` ### Skill Input Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | `string` | Yes | Parameter name. Must match `^[a-z][a-zA-Z0-9]*$` (camelCase). Referenced via `{{input:key}}` in content. | | `type` | `string` | Yes | One of: `string`, `number`, `boolean`, `enum`. | | `description` | `string` | Yes | What this parameter means. Must not be empty. | | `required` | `boolean` | Yes | Whether the parameter is mandatory. | | `values` | `string[]` | Conditional | Required when `type` is `enum`. Forbidden otherwise. | ### Key Differences from Tool and Resource Parameters | Aspect | Tool Parameters | Resource Parameters | Skill Input | |--------|----------------|---------------------|-------------| | Format | `position` + `z` blocks | `position` + `z` blocks (no `location`) | `key` + `type` + `description` + `required` | | Purpose | HTTP request construction | SQL parameter binding | AI agent context | | Validation | Zod-based at runtime | Zod-based at runtime | Not runtime-validated (informational) | | Types | Full Zod primitives | Scalar Zod primitives only | `string`, `number`, `boolean`, `enum` | | Constraints | `min()`, `max()`, `length()`, `optional()`, `default()` | Same as tools | None (description only) | See `14-skills.md` for the complete skill specification. --- # FlowMCP Specification v2.0.0 — Shared Lists Shared lists eliminate duplication of common value sets across schemas. Instead of every Etherscan schema maintaining its own chain list, they reference a single `evmChains` shared list. This document defines the list format, field definitions, dependency model, schema referencing, runtime injection, and validation rules. --- ## Purpose Many schemas across different providers need the same sets of values — EVM chain identifiers, fiat currency codes, token standards, country codes. Without shared lists, each schema duplicates these values inline, leading to: - **Inconsistency** — one schema uses `'eth'`, another uses `'ETH'`, a third uses `'ethereum'` - **Maintenance burden** — adding a new chain means updating dozens of schemas - **No single source of truth** — no way to verify which values are canonical Shared lists solve this by providing versioned, validated, centrally maintained value sets that schemas reference by name. ```mermaid flowchart LR A[Shared List: evmChains] --> B[Schema: etherscan/contracts.mjs] A --> C[Schema: moralis/tokens.mjs] A --> D[Schema: defillama/protocols.mjs] B --> E[filter: etherscanAlias exists] C --> F[filter: moralisChainSlug exists] D --> G[filter: defillamaSlug exists] ``` The diagram shows how a single shared list feeds multiple schemas, each applying its own filter to extract only the entries relevant to that provider. --- ## List Definition Format A shared list is a `.mjs` file that exports a `list` object with two top-level keys: `meta` and `entries`. ```javascript export const list = { meta: { name: 'evmChains', version: '1.0.0', description: 'Unified EVM chain registry with provider-specific aliases', fields: [ { key: 'alias', type: 'string', description: 'Canonical chain alias' }, { key: 'chainId', type: 'number', description: 'EVM chain ID' }, { key: 'etherscanAlias', type: 'string', optional: true, description: 'Etherscan API chain parameter' }, { key: 'moralisChainSlug', type: 'string', optional: true, description: 'Moralis chain slug' }, { key: 'defillamaSlug', type: 'string', optional: true, description: 'DeFi Llama chain identifier' } ], dependsOn: [] }, entries: [ { alias: 'ETHEREUM_MAINNET', chainId: 1, etherscanAlias: 'ETH', moralisChainSlug: 'eth', defillamaSlug: 'Ethereum' }, { alias: 'POLYGON_MAINNET', chainId: 137, etherscanAlias: 'POLYGON', moralisChainSlug: 'polygon', defillamaSlug: 'Polygon' } ] } ``` The file must export exactly one `list` constant. No other exports are permitted. The file must not contain imports, function definitions, or dynamic expressions. --- ## Meta Block The `meta` block describes the list identity and structure. | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Unique list identifier (camelCase) | | `version` | `string` | Yes | Semver version | | `description` | `string` | Yes | What this list contains | | `fields` | `array` | Yes | Field definitions for entries | | `dependsOn` | `array` | No | Dependencies on other lists | ### Naming Convention List names use camelCase and must be globally unique across the entire list registry. The name should describe the collection, not a single entry: - `evmChains` (not `evmChain`) - `fiatCurrencies` (not `fiatCurrency`) - `isoCountryCodes` (not `countryCode`) ### Versioning Lists follow strict semver. A version bump is required when: - **Patch** (`1.0.1`) — correcting a typo in an existing entry, fixing a wrong `chainId` - **Minor** (`1.1.0`) — adding new entries, adding new optional fields - **Major** (`2.0.0`) — removing entries, removing fields, renaming fields, changing field types Schemas pin to a specific version. If a list bumps its major version, all referencing schemas must update their `version` field in the `sharedLists` reference. --- ## Field Definition Each entry in `meta.fields` describes one field that entries can or must contain. | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | `string` | Yes | Field name | | `type` | `string` | Yes | `string`, `number`, `boolean` | | `description` | `string` | Yes | What this field represents | | `optional` | `boolean` | No | If `true`, entries may omit this field | ### Type Constraints Only three primitive types are supported: | Type | JavaScript equivalent | Example values | |------|----------------------|----------------| | `string` | `typeof x === 'string'` | `'ETH'`, `'Ethereum'`, `'0x1'` | | `number` | `typeof x === 'number'` | `1`, `137`, `42161` | | `boolean` | `typeof x === 'boolean'` | `true`, `false` | Complex types (objects, arrays, nested structures) are not supported in shared list entries. Lists are flat by design — each entry is a single-level key-value map. ### Required vs Optional Fields Fields without `optional: true` are required. Every entry must include all required fields. Optional fields may be omitted entirely or set to `null`. This distinction is what enables provider-specific columns — `etherscanAlias` is optional because not every chain has an Etherscan explorer. --- ## Dependencies Between Lists Lists can declare dependencies on other lists using `meta.dependsOn`. This enables hierarchical data sets where child lists reference parent lists. ```javascript export const list = { meta: { name: 'germanBundeslaender', version: '1.0.0', description: 'German federal states', fields: [ { key: 'name', type: 'string', description: 'State name' }, { key: 'code', type: 'string', description: 'State code' }, { key: 'countryRef', type: 'string', description: 'Reference to parent country' } ], dependsOn: [ { ref: 'isoCountryCodes', version: '1.0.0', condition: { field: 'alpha2', value: 'DE' } } ] }, entries: [ { name: 'Bayern', code: 'BY', countryRef: 'DE' }, { name: 'Berlin', code: 'BE', countryRef: 'DE' }, { name: 'Hamburg', code: 'HH', countryRef: 'DE' } ] } ``` ### Dependency Object | Field | Type | Required | Description | |-------|------|----------|-------------| | `ref` | `string` | Yes | Name of the parent list | | `version` | `string` | Yes | Required version of the parent list | | `condition` | `object` | No | Filter condition on the parent list | ### Dependency Rules 1. **`ref` must resolve** — the referenced list name must exist in the list registry 2. **Version pinning** — `version` pins the dependency to a specific semver version; the runtime rejects mismatches 3. **`condition` is optional** — when present, it filters the parent list to a subset; when absent, the full parent list is available 4. **No circular dependencies** — if list A depends on list B, then list B must not depend on list A (directly or transitively) 5. **Maximum depth: 3 levels** — a list can depend on a list that depends on another list, but no deeper; this prevents resolution complexity and keeps the dependency graph shallow ```mermaid flowchart TD A[isoCountryCodes] --> B[germanBundeslaender] A --> C[usStates] B --> D[germanLandkreise] D -.->|FORBIDDEN: depth 4| E[germanGemeinden] ``` The diagram shows valid dependency chains up to depth 3, and a forbidden depth-4 dependency. ### Condition Format The `condition` object supports a single equality check: | Field | Type | Description | |-------|------|-------------| | `field` | `string` | Field name in the parent list to check | | `value` | `string` or `number` or `boolean` | Expected value | The condition acts as a semantic assertion: "this child list only makes sense when the parent list contains an entry matching this condition." The runtime verifies the condition at load-time — if the parent list has no entry where `field === value`, the dependency is unresolvable and the load fails. --- ## Referencing from Schemas Schemas reference shared lists in the `main.sharedLists` array. This declares which lists the schema needs at runtime. ```javascript main: { sharedLists: [ { ref: 'evmChains', version: '1.0.0', filter: { key: 'etherscanAlias', exists: true } } ] } ``` ### Reference Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `ref` | `string` | Yes | List name to reference | | `version` | `string` | Yes | Required list version | | `filter` | `object` | No | Filter entries before injection | A schema may reference multiple shared lists. Each reference is resolved independently. ### Filter Types Filters reduce the list to only the entries relevant to the schema. Three filter types are supported: | Filter | Description | Example | |--------|-------------|---------| | `{ key, exists: true }` | Only entries where field exists and is not `null` | `{ key: 'etherscanAlias', exists: true }` | | `{ key, value }` | Only entries where field equals value | `{ key: 'mainnet', value: true }` | | `{ key, in: [...] }` | Only entries where field is in the provided list | `{ key: 'chainId', in: [1, 137, 42161] }` | #### Exists Filter The `exists` filter selects entries where the specified field is present and not `null`. This is the most common filter type — it selects all entries that have a provider-specific alias. ```javascript filter: { key: 'etherscanAlias', exists: true } // Selects: { alias: 'ETHEREUM_MAINNET', etherscanAlias: 'ETH', ... } // Rejects: { alias: 'SOLANA_MAINNET', etherscanAlias: null, ... } ``` #### Value Filter The `value` filter selects entries where the specified field equals an exact value. ```javascript filter: { key: 'mainnet', value: true } // Selects: { alias: 'ETHEREUM_MAINNET', mainnet: true, ... } // Rejects: { alias: 'GOERLI_TESTNET', mainnet: false, ... } ``` #### In Filter The `in` filter selects entries where the specified field matches any value in the provided array. ```javascript filter: { key: 'chainId', in: [1, 137, 42161] } // Selects: entries with chainId 1, 137, or 42161 // Rejects: all other chainIds ``` ### No Filter When `filter` is omitted, all entries from the list are injected. This is appropriate when the schema needs the complete list. --- ## Runtime Injection At load-time, the core runtime resolves shared list references and injects them into the `handlers` factory function as the `sharedLists` parameter. ### Resolution Lifecycle ```mermaid flowchart TD A[Schema declares sharedLists] --> B[Resolve list by name + version] B --> C{List found?} C -->|No| D[Load error: list not found] C -->|Yes| E{Version matches?} E -->|No| F[Load error: version mismatch] E -->|Yes| G[Apply filter] G --> H[Inject into sharedLists for factory] H --> I[Build reverse-lookup index] ``` The diagram shows the resolution pipeline from schema declaration through list lookup, version verification, filtering, and injection. ### Step 1: Resolve The runtime looks up each `ref` in the list registry. If the list does not exist, the schema fails to load with an error. ### Step 2: Version Check The runtime verifies that the registry version matches the schema's declared `version`. A mismatch is a hard error — the schema must be updated to reference the correct version. ### Step 3: Filter If a `filter` is declared, the runtime applies it to the list's `entries` array, producing a subset. If no filter is declared, all entries are passed through. ### Step 4: Inject The filtered entries are collected and passed to the `handlers` factory function as `sharedLists`: ```javascript // The runtime calls the factory with resolved lists export const handlers = ( { sharedLists, libraries } ) => ({ getGasOracle: { preRequest: async ( { struct, payload } ) => { // sharedLists.evmChains is available via closure const chain = sharedLists.evmChains .find( ( entry ) => { const match = entry.etherscanAlias === payload.chainName return match } ) // ... return { struct, payload } } } }) ``` ### Parameter Interpolation Shared lists can be interpolated into parameter enum values using the `{{listName:fieldName}}` syntax: ```javascript parameters: [ { name: 'chain', type: 'string', description: 'Target blockchain', enum: '{{evmChains:etherscanAlias}}' } ] ``` At load-time, the runtime replaces `'{{evmChains:etherscanAlias}}'` with the array of `etherscanAlias` values from the filtered `evmChains` list — for example `['ETH', 'POLYGON', 'ARBITRUM']`. This ensures the parameter's enum values always match the shared list without manual synchronization. --- ## Reverse-Lookup Index The core runtime builds a reverse index that maps each list and field combination to the schemas that reference it. This enables impact analysis when a list is updated. ### Index Structure ``` evmChains:etherscanAlias -> [ 'etherscan/contracts.mjs::getContractAbi', 'etherscan/gas.mjs::getGasOracle' ] evmChains:moralisChainSlug -> [ 'moralis/tokens.mjs::getTokenBalance', 'moralis/nft.mjs::getNftsByWallet' ] isoCountryCodes:alpha2 -> [ 'compliance/kyc.mjs::verifyIdentity' ] ``` ### CLI Access The reverse index is queryable via the CLI: ```bash flowmcp list-refs evmChains ``` Output: ``` evmChains (v1.0.0) — 15 entries, 4 fields Referenced by: etherscan/contracts.mjs::getContractAbi (filter: etherscanAlias exists) etherscan/gas.mjs::getGasOracle (filter: etherscanAlias exists) moralis/tokens.mjs::getTokenBalance (filter: moralisChainSlug exists) defillama/protocols.mjs::getProtocolTvl (filter: defillamaSlug exists) ``` This allows schema authors to understand the blast radius of a list change before publishing an update. --- ## List Registry All shared lists are tracked in `_lists/_registry.json`. This file is auto-generated by the CLI and must not be edited manually. ```json { "specVersion": "2.0.0", "lists": [ { "name": "evmChains", "version": "1.0.0", "file": "_lists/evm-chains.mjs", "entryCount": 15, "hash": "sha256:def456..." } ] } ``` ### Registry Fields | Field | Type | Description | |-------|------|-------------| | `specVersion` | `string` | FlowMCP specification version | | `lists[].name` | `string` | List name (must match `meta.name` in the file) | | `lists[].version` | `string` | List version (must match `meta.version` in the file) | | `lists[].file` | `string` | Relative path to the `.mjs` file | | `lists[].entryCount` | `number` | Number of entries (for quick reference) | | `lists[].hash` | `string` | SHA-256 hash of the serialized list for integrity verification | ### Registry Invariants - Every `.mjs` file in the `_lists/` directory must have a corresponding entry in `_registry.json` - Every entry in `_registry.json` must point to an existing `.mjs` file - The `hash` must match the current file content; a mismatch indicates an unregistered change - The CLI command `flowmcp validate-lists` verifies all three invariants --- ## Validation Rules The following rules are enforced when loading shared lists: ### 1. Unique Names `meta.name` must be unique across all lists in the registry. Two lists with the same name but different versions are a version conflict error, not two separate lists. ### 2. Required Field Completeness Every entry in `entries` must include all fields from `meta.fields` that do not have `optional: true`. Missing required fields are a validation error. ### 3. Optional Field Handling Fields with `optional: true` may be omitted from an entry entirely, or may be set to `null`. Both representations are equivalent at runtime. ### 4. Type Matching The value of each field in an entry must match the `type` declared in `meta.fields`. A `chainId` declared as `number` must be a number in every entry — string values like `'1'` are rejected. ### 5. Dependency Resolution All entries in `meta.dependsOn` must resolve to existing lists with matching versions. The runtime validates dependencies before the list is made available. ### 6. No Version Conflicts If two schemas reference the same list with different versions, this is a hard error. The schema author must reconcile the versions. The runtime does not support loading multiple versions of the same list simultaneously. ### 7. Security Scan Compliance The list file must pass the security scan defined in `05-security.md`. Specifically: - No `import` or `require` statements - No function definitions or function expressions - No dynamic expressions (`eval`, template literals with expressions, computed properties) - No references to `process`, `fs`, `fetch`, or other runtime APIs - The file must contain only static data: strings, numbers, booleans, arrays, and plain objects --- ## File Conventions ### Directory Structure ``` _lists/ _registry.json evm-chains.mjs fiat-currencies.mjs iso-country-codes.mjs token-standards.mjs ``` ### Naming - File names use kebab-case: `evm-chains.mjs` - List names use camelCase: `evmChains` - The file name should be the kebab-case equivalent of the list name ### One List Per File Each `.mjs` file contains exactly one shared list. Combining multiple lists in a single file is not supported. --- # FlowMCP Specification v2.0.0 — Output Schema Output schemas make tool responses predictable. AI clients can know in advance what shape the data will have, enabling structured reasoning without parsing guesswork. This document defines the output declaration format, supported types, the response envelope, handler interaction, and validation rules. --- ## Purpose Without output schemas, an AI client calling a FlowMCP tool receives an opaque blob of JSON. The client must infer the structure from context, previous calls, or the tool description — all unreliable strategies. Output schemas solve this by declaring the expected response shape at the route level: - **AI clients** can pre-allocate structured reasoning about the response fields - **Schema validators** can verify that handler output matches the declaration - **Documentation generators** can produce accurate response tables automatically - **Type-aware consumers** can generate TypeScript interfaces or Zod schemas from the output definition ```mermaid flowchart LR A[Route Definition] --> B[output.schema] B --> C[AI Client: knows fields in advance] B --> D[Validator: checks handler output] B --> E[Docs: auto-generated tables] ``` The diagram shows how a single output schema declaration serves three consumers: AI clients, validators, and documentation tools. --- ## Route-Level Output Definition Each route can optionally define an `output` field alongside its `method`, `path`, `description`, and `parameters`: ```javascript routes: { getTokenPrice: { method: 'GET', path: '/simple/price', description: 'Get current token price', parameters: [ /* ... */ ], output: { mimeType: 'application/json', schema: { type: 'object', properties: { id: { type: 'string', description: 'Token identifier' }, symbol: { type: 'string', description: 'Token symbol' }, price: { type: 'number', description: 'Current price in USD' }, marketCap: { type: 'number', description: 'Market capitalization', nullable: true }, volume24h: { type: 'number', description: 'Trading volume (24h)' } } } } } } ``` The `output` field lives in the `main` block and is therefore part of the hashable, JSON-serializable schema surface. It must not contain functions or dynamic expressions. --- ## Output Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `mimeType` | `string` | Yes | Response content type | | `schema` | `object` | Yes | Simplified JSON Schema describing the `data` field | Both fields are required when `output` is present. If a route does not declare `output`, the entire field is omitted (not set to `null` or `{}`). --- ## Supported MIME-Types | MIME-Type | Description | Schema `type` | |-----------|-------------|---------------| | `application/json` | JSON response (default) | `object` or `array` | | `image/png` | PNG image, base64-encoded | `string` with `format: 'base64'` | | `text/plain` | Plain text response | `string` | ### MIME-Type to Schema Mapping The `mimeType` constrains which `schema.type` values are valid: - `application/json` requires `type: 'object'` or `type: 'array'` - `image/png` requires `type: 'string'` with `format: 'base64'` - `text/plain` requires `type: 'string'` A mismatch between `mimeType` and `schema.type` is a validation error. --- ## Standard Response Envelope Every FlowMCP tool response is wrapped in a standard envelope. This envelope is the same for all routes and does not need per-route definition. ### Success Response ```javascript { status: true, messages: [], data: { /* described by output.schema */ } } ``` ### Error Response ```javascript { status: false, messages: [ 'E001 getTokenPrice: API returned 404' ], data: null } ``` ### Envelope Fields | Field | Type | Description | |-------|------|-------------| | `status` | `boolean` | `true` on success, `false` on error | | `messages` | `array` | Empty on success, error descriptions on failure | | `data` | `object` or `null` | Response payload on success, `null` on error | The `output.schema` describes **only the `data` field** when `status: true`. Schema authors do not declare the envelope — it is implicit and standardized across all tools. --- ## Simplified JSON Schema Subset FlowMCP uses a deliberately constrained subset of JSON Schema. This avoids the complexity of full JSON Schema while covering the needs of API response descriptions. ### Supported Keywords | Keyword | Description | Example | |---------|-------------|---------| | `type` | Value type | `'string'`, `'number'`, `'boolean'`, `'object'`, `'array'` | | `properties` | Object properties | `{ name: { type: 'string' } }` | | `items` | Array item schema | `{ type: 'object', properties: {...} }` | | `description` | Human-readable description | `'Current price in USD'` | | `nullable` | Can be `null` | `true` | | `enum` | Allowed values | `['active', 'inactive']` | | `format` | Special format hint | `'base64'`, `'date-time'`, `'uri'` | ### Unsupported Keywords The following JSON Schema keywords are intentionally excluded: - `$ref` — no schema references; output schemas are self-contained - `oneOf`, `anyOf`, `allOf` — no union types; keep schemas simple - `required` — all declared properties are informational, not enforced - `additionalProperties` — APIs may return extra fields; the schema describes the guaranteed minimum - `pattern` — no regex validation on output fields - `minimum`, `maximum` — no range validation on output fields ### Type Values | Type | JavaScript equivalent | Description | |------|----------------------|-------------| | `string` | `typeof x === 'string'` | Text value | | `number` | `typeof x === 'number'` | Numeric value (integer or float) | | `boolean` | `typeof x === 'boolean'` | True or false | | `object` | Plain object | Nested structure with `properties` | | `array` | Array | Collection with `items` schema | --- ## Object Responses The most common response type. The `schema` declares an object with named properties: ```javascript output: { mimeType: 'application/json', schema: { type: 'object', properties: { id: { type: 'string', description: 'Token identifier' }, symbol: { type: 'string', description: 'Token symbol' }, price: { type: 'number', description: 'Current price in USD' }, marketCap: { type: 'number', description: 'Market capitalization', nullable: true }, volume24h: { type: 'number', description: 'Trading volume (24h)' } } } } ``` ### Nested Objects Properties can themselves be objects, up to 4 levels deep: ```javascript output: { mimeType: 'application/json', schema: { type: 'object', properties: { token: { type: 'object', description: 'Token metadata', properties: { name: { type: 'string', description: 'Token name' }, contract: { type: 'object', description: 'Contract details', properties: { address: { type: 'string', description: 'Contract address' }, verified: { type: 'boolean', description: 'Verification status' } } } } } } } } ``` --- ## Array Responses For routes that return lists, the schema uses `type: 'array'` with an `items` definition: ```javascript output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Protocol name' }, tvl: { type: 'number', description: 'Total value locked in USD' } } } } } ``` ### Array of Primitives Arrays can also contain primitive types: ```javascript output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'string', description: 'Contract address' } } } ``` ### Objects Containing Arrays Properties within objects can be arrays: ```javascript output: { mimeType: 'application/json', schema: { type: 'object', properties: { total: { type: 'number', description: 'Total result count' }, results: { type: 'array', description: 'List of matching tokens', items: { type: 'object', properties: { symbol: { type: 'string', description: 'Token symbol' }, price: { type: 'number', description: 'Current price' } } } } } } } ``` --- ## Image Responses For routes that return images (charts, QR codes, visual data), the schema declares a base64-encoded string: ```javascript output: { mimeType: 'image/png', schema: { type: 'string', format: 'base64', description: 'Chart image as base64-encoded PNG' } } ``` The runtime base64-encodes the binary response and places it in the `data` field of the envelope. AI clients that support image rendering can decode and display the image. --- ## Text Responses For routes that return plain text: ```javascript output: { mimeType: 'text/plain', schema: { type: 'string', description: 'Raw contract source code' } } ``` --- ## Nullable Fields Fields that may be `null` in a successful response must declare `nullable: true`: ```javascript properties: { marketCap: { type: 'number', description: 'Market capitalization', nullable: true }, website: { type: 'string', description: 'Project website URL', nullable: true } } ``` Without `nullable: true`, a `null` value in the response triggers a validation warning. This distinction helps AI clients differentiate between "field not available for this entry" (nullable) and "field should always be present" (not nullable). --- ## Enum Values in Output Output fields can declare `enum` to restrict values to a known set: ```javascript properties: { status: { type: 'string', description: 'Verification status', enum: ['verified', 'unverified', 'pending'] } } ``` This helps AI clients reason about possible values without inspecting raw data. --- ## Format Hints The `format` keyword provides additional semantic information about a string field: | Format | Description | Example value | |--------|-------------|---------------| | `base64` | Base64-encoded binary data | `'iVBORw0KGgo...'` | | `date-time` | ISO 8601 date-time | `'2026-02-16T12:00:00Z'` | | `uri` | Valid URI | `'https://etherscan.io/address/0x...'` | Format is informational — the runtime does not validate format compliance. It exists to give AI clients and documentation generators better context. --- ## When Output Schema is Omitted If a route does not define an `output` field: - The response is treated as `application/json` by default - The `data` field is passed through without schema validation - AI clients cannot rely on a specific shape - This is valid but **discouraged** for new schemas Omitting the output schema is acceptable for: - Legacy schemas migrating from v1.x - Routes with highly variable response shapes (rare) - Exploratory schemas during development For production schemas, the output schema should always be declared. --- ## Output Schema and Handlers When a route has a `postRequest` handler, the output schema describes the **final** response after handler transformation, not the raw API response. ```mermaid flowchart LR A[API Response] --> B[postRequest Handler] B --> C[Transformed Data] C --> D{Matches output.schema?} D -->|Yes| E[Return in envelope] D -->|No| F[Validation warning] ``` The diagram shows the validation point: the output schema is checked against the handler's return value, not the raw API response. ### Implications for Schema Authors - The `output.schema` must describe the shape **after** `postRequest` transforms the data - If `postRequest` flattens nested API responses, the schema describes the flat structure - If `postRequest` renames fields, the schema uses the new names - If no `postRequest` handler exists, the schema describes the raw API response directly ### Example: Handler Transforms Response ```javascript // Raw API response from CoinGecko: // { "bitcoin": { "usd": 45000, "usd_market_cap": 850000000000 } } // postRequest handler (inside factory) flattens it: export const handlers = ( { sharedLists, libraries } ) => ({ getTokenPrice: { postRequest: async ( { response, struct, payload } ) => { const [ id ] = Object.keys( response ) const { usd, usd_market_cap } = response[ id ] return { response: { id, price: usd, marketCap: usd_market_cap } } } } }) // Output schema describes the FLATTENED result: output: { mimeType: 'application/json', schema: { type: 'object', properties: { id: { type: 'string', description: 'Token identifier' }, price: { type: 'number', description: 'Price in USD' }, marketCap: { type: 'number', description: 'Market capitalization in USD' } } } } ``` --- ## Validation Rules The following rules are enforced when validating output schemas: ### 1. MIME-Type Restriction `mimeType` must be one of the supported types: `application/json`, `image/png`, `text/plain`. Unknown MIME-types are rejected. ### 2. Type-MIME Consistency `schema.type` must be compatible with the declared `mimeType`: | `mimeType` | Allowed `schema.type` | |------------|-----------------------| | `application/json` | `object`, `array` | | `image/png` | `string` (with `format: 'base64'`) | | `text/plain` | `string` | ### 3. Properties Restriction `properties` is only valid when `type` is `'object'`. Declaring `properties` on a `string` or `array` type is a validation error. ### 4. Items Restriction `items` is only valid when `type` is `'array'`. Declaring `items` on a `string` or `object` type is a validation error. ### 5. Nesting Depth Limit Maximum nesting depth is 4 levels. This prevents overly complex schemas that are difficult for AI clients to reason about: ``` Level 1: output.schema (root) Level 2: output.schema.properties.token Level 3: output.schema.properties.token.properties.contract Level 4: output.schema.properties.token.properties.contract.properties.address ``` A 5th level is rejected. ### 6. Nullable Semantics `nullable: true` means the field can be `null` in a successful response (when `status: true`). It does not mean the field can be absent — absent fields are not described by the schema at all. ### 7. Non-Blocking Validation Output schema validation is **non-blocking**. A mismatch between the actual handler output and the declared schema produces a validation **warning**, not an error. The response is still delivered to the client. This design choice reflects the reality that external APIs may change their response shapes without notice. A strict error would break the tool even though the data might still be usable. The warning is logged and surfaced to schema maintainers for review. ```mermaid flowchart TD A[Handler returns data] --> B{Schema declared?} B -->|No| C[Pass through, no check] B -->|Yes| D{Data matches schema?} D -->|Yes| E[Return data] D -->|No| F[Log warning] F --> G[Return data anyway] ``` The diagram shows the non-blocking validation flow: mismatches produce warnings but do not prevent the response from being delivered. --- # FlowMCP Specification v2.0.0 — Security Model FlowMCP enforces a layered security model that prevents schema files from accessing the network, filesystem, or process environment. All potentially dangerous operations are restricted to the trusted core runtime. Dependencies are injected through a factory function pattern, and external libraries are gated by an allowlist. --- ## Trust Boundary FlowMCP enforces a strict trust boundary between the core runtime and schema handlers: ```mermaid flowchart LR subgraph trusted ["TRUSTED ZONE — flowmcp-core"] A[Static scan — ban all imports] B[Load and validate main block] C[Resolve shared lists] D[Load libraries from allowlist] E[Execute fetch] F[Validate input/output] end subgraph restricted ["RESTRICTED ZONE — Schema Handlers"] G["Receives: struct, payload (per-call)"] H["Receives: sharedLists, libraries (via factory)"] I[sharedLists is frozen — read-only] J[libraries are approved packages only] K["NO: import, require, eval, fs, process"] L[CAN: use injected libraries and sharedLists] end A --> B --> C --> D --> E --> F D --> H E --> G ``` **Trusted Zone (flowmcp-core):** - Reads schema file as raw string and runs static security scan - Loads and validates the `main` export (JSON-serializable, required fields, constraints) - Resolves shared list references and deep-freezes the data - Loads libraries from the allowlist and injects them into the factory function - Executes HTTP fetch (handlers never fetch directly) - Validates input parameters and output schema **Restricted Zone (schema handlers):** - Receives `sharedLists` and `libraries` through the factory function (once at load time) - Receives `struct`, `payload`, and `response` per-call through handler parameters - Transforms data (restructure, filter, compute) - Returns modified data - Cannot access network, filesystem, process, or global scope - Cannot import or require any module --- ## Static Security Scan Before a schema is loaded, the **raw file content** (as a string) is scanned for forbidden patterns. This happens before `import()` to prevent code execution. Since all dependencies are injected through the factory function, schema files should have **zero import statements**. This makes the scan simpler and more restrictive than in previous versions. ### Forbidden Patterns (Entire File) | Pattern | Reason | |---------|--------| | `import ` | No imports — all dependencies are injected | | `require(` | No CommonJS imports | | `eval(` | Code injection | | `Function(` | Code injection | | `new Function` | Code injection | | `fs.` | Filesystem access | | `node:fs` | Filesystem access | | `fs/promises` | Filesystem access | | `process.` | Process access | | `child_process` | Shell execution | | `globalThis.` | Global scope access | | `global.` | Global scope access | | `__dirname` | Path leaking | | `__filename` | Path leaking | | `setTimeout` | Async side effects | | `setInterval` | Async side effects | ### Scan Implementation The scan runs in four sequential steps before the schema file is dynamically imported: ``` 1. Read file as raw string (before import) 2. Scan entire file for all forbidden patterns 3. If any pattern matches -> reject file with error message(s) 4. If clean -> proceed with dynamic import() ``` Because schema files have zero import statements and all dependencies are injected, there is no need to distinguish between "main block region" and "handler block region". The entire file is scanned uniformly against the same forbidden pattern list. --- ## Library Allowlist The runtime maintains an allowlist of approved npm packages. Only packages on this list can be declared in `main.requiredLibraries` and injected into the `handlers()` factory function. ### Default Allowlist The following packages are built into `flowmcp-core` as approved: ```javascript const DEFAULT_ALLOWLIST = [ 'ethers', 'moment', 'indicatorts', '@erc725/erc725.js', 'ccxt', 'axios' ] ``` ### User-Extended Allowlist Users can extend the allowlist in their `.flowmcp/config.json`: ```json { "security": { "allowedLibraries": [ "custom-lib", "another-lib" ] } } ``` The effective allowlist is the union of the default allowlist and the user-extended allowlist. ### Library Loading Sequence The runtime loads libraries through a strict sequence: ```mermaid flowchart TD A[Read main.requiredLibraries] --> B{Each library on allowlist?} B -->|Yes| C["Load via dynamic import()"] B -->|No| D[Reject schema — SEC013] C --> E[Package into libraries object] E --> F["Inject into handlers( { sharedLists, libraries } )"] ``` **Step-by-step:** 1. **Read `main.requiredLibraries`** — extract the list of declared packages. 2. **Check each against the allowlist** — every entry must appear in the default or user-extended allowlist. 3. **Reject unapproved libraries** — if any library is not on the allowlist, the schema is rejected with error code SEC013. 4. **Load approved libraries** — each approved library is loaded via dynamic `import()`. 5. **Package into `libraries` object** — loaded modules are keyed by package name. 6. **Inject into factory function** — the `libraries` object is passed to `handlers( { sharedLists, libraries } )`. ### Why an Allowlist - **Prevents arbitrary code execution.** Without an allowlist, a schema could declare any npm package and execute arbitrary code through it. - **Auditable.** The list of approved packages is explicit and reviewable. - **Extensible.** Users can add packages they trust to their local configuration. - **Fail-closed.** Unknown packages are rejected by default. --- ## Forbidden Patterns in Shared List Files Shared list files have an **even stricter** scan. They must only export a plain data object: | Allowed | Forbidden | |---------|-----------| | `export const list = { meta: {...}, entries: [...] }` | Any function definition | | String/number/boolean/null values | `async`, `await`, `function`, `=>` | | Arrays and objects | Any of the schema forbidden patterns | | Comments (`//`, `/* */`) | Template literals with expressions | Shared lists are pure data. They contain no logic, no transformations, and no computed values. The static scan enforces this by rejecting any file that contains function syntax or arrow expressions. --- ## Handler Security Constraints Even after passing the static scan, handlers are constrained at runtime: 1. **No `fetch` access**: Handlers cannot call `fetch()`. The runtime executes fetch and passes the response to `postRequest`. 2. **No side effects**: Handlers receive data and return data. No logging, no file writes, no timers. 3. **`sharedLists` is read-only**: Shared list data is deep-frozen via `Object.freeze()`. Mutations throw a `TypeError`. 4. **`libraries` contains only allowlisted packages**: Even if a handler tries to access a non-injected package, it is not available in scope. 5. **Return value required**: Handlers must return the expected shape or the runtime throws. ### Handler Function Signatures Handlers receive exactly the parameters they need. Per-call parameters are passed directly — no `userParams` or `context` wrapper: ```javascript // preRequest — modify the request before fetch preRequest: async ( { struct, payload } ) => { // struct: the request structure (url, headers, body) // payload: resolved route parameters // sharedLists + libraries: available via factory closure return { struct, payload } } // postRequest — transform the response after fetch postRequest: async ( { response, struct, payload } ) => { // response: parsed JSON from the API // struct + payload: same as preRequest // sharedLists + libraries: available via factory closure return { response } } ``` Handlers cannot: - Call `fetch()` or any network function - Access `process`, `global`, or `globalThis` - Import or require other modules - Create timers or async side effects - Modify `sharedLists` (frozen) - Access server parameters / API keys (injected by runtime into URL/headers only) --- ## API Key Protection API keys are never exposed to handler code: ```mermaid flowchart LR A[".env: ETHERSCAN_API_KEY=abc123"] --> B[Core Runtime] B --> C["URL: ?apikey=abc123"] B -.->|NOT passed| D[Handler Factory] D --> E["Only sharedLists + libraries"] B -.->|NOT passed| F[Handler Per-Call] F --> G["Only struct + payload + response"] ``` - `requiredServerParams` values are injected into URL/headers by the runtime - The `handlers()` factory function receives `sharedLists` and `libraries` only — no server parameters - Per-call handler parameters contain `struct`, `payload`, and `response` only — no server parameters - Actual key values are substituted by the runtime during URL construction ### Key Injection Flow ``` 1. Schema declares requiredServerParams: [ 'ETHERSCAN_API_KEY' ] 2. Runtime reads ETHERSCAN_API_KEY from .env 3. Parameter template: '{{SERVER_PARAM:ETHERSCAN_API_KEY}}' 4. Runtime substitutes into URL: 'https://api.etherscan.io/api?apikey=abc123' 5. Handler receives response — never sees the key value 6. Factory function receives sharedLists + libraries — never sees the key value ``` This ensures that even a compromised handler (one that somehow bypasses the static scan) cannot extract API keys from the execution context. --- ## Threat Model | Threat | Mitigation | |--------|------------| | Schema imports a module | Static scan blocks `import`/`require` — schema files have zero imports | | Schema requests unapproved library | Blocked by allowlist — SEC013 error, schema rejected | | Schema reads filesystem | Static scan blocks `fs`, `node:fs`, `fs/promises` | | Schema executes shell commands | Static scan blocks `child_process` | | Schema accesses environment | Static scan blocks `process.` | | Schema exfiltrates data via fetch | Handlers cannot call `fetch()` — runtime owns all network access | | Schema modifies global state | Static scan blocks `globalThis`/`global.` | | Handler mutates shared list data | `sharedLists` is deep-frozen — mutations throw `TypeError` | | Handler accesses non-injected library | Not available in scope — only `libraries` object contents are accessible | | Shared list contains executable code | Stricter scan blocks all functions, arrows, async/await | | Schema leaks API keys | Keys injected by runtime into URL/headers, never passed to factory or handlers | | Schema uses eval or Function constructor | Static scan blocks `eval(`, `Function(`, `new Function` | | Schema creates async side effects | Static scan blocks `setTimeout`/`setInterval` | | Schema accesses file paths | Static scan blocks `__dirname`/`__filename` | | Schema declares library not on allowlist | Runtime rejects schema before loading handlers | | Schema disguises import as string manipulation | Static scan operates on raw file string — any occurrence of `import ` is caught regardless of context | --- ## Security Scan Error Format When a scan fails, the error message follows the standard format: ``` SEC001 etherscan/SmartContractExplorer.mjs: Forbidden pattern "import " found at line 3 SEC002 etherscan/SmartContractExplorer.mjs: Forbidden pattern "process." found at line 47 ``` All violations in a single file are reported together (the scan does not stop at the first match). This allows schema authors to fix all issues in one pass. ### Error Codes | Code | Category | |------|----------| | SEC001-SEC099 | Static scan failures | | SEC100-SEC199 | Runtime constraint violations | | SEC200-SEC299 | Shared list scan failures | ### Error Code Details **SEC001-SEC099 — Static Scan Failures** | Code | Description | |------|-------------| | SEC001 | Forbidden `import` statement found | | SEC002 | Forbidden `require()` call found | | SEC003 | Forbidden `eval()` call found | | SEC004 | Forbidden `Function()` constructor found | | SEC005 | Forbidden filesystem access (`fs.`, `node:fs`, `fs/promises`) | | SEC006 | Forbidden `process.` access found | | SEC007 | Forbidden `child_process` access found | | SEC008 | Forbidden global scope access (`globalThis.`, `global.`) | | SEC009 | Forbidden path variable (`__dirname`, `__filename`) | | SEC010 | Forbidden `new Function` found | | SEC011 | Forbidden timer (`setTimeout`, `setInterval`) | | SEC012 | Reserved | | SEC013 | Unapproved library in `requiredLibraries` — not on allowlist | **SEC100-SEC199 — Runtime Constraint Violations** | Code | Description | |------|-------------| | SEC100 | Handler attempted to call `fetch()` | | SEC101 | Handler returned invalid shape | | SEC102 | Handler attempted to mutate frozen `sharedLists` | | SEC103 | Library loading failed for approved package | | SEC104 | Factory function `handlers()` threw during initialization | **SEC200-SEC299 — Shared List Scan Failures** | Code | Description | |------|-------------| | SEC200 | Function definition found in shared list | | SEC201 | Arrow function found in shared list | | SEC202 | Async/await keyword found in shared list | | SEC203 | Template literal with expression found in shared list | | SEC204 | Forbidden pattern (same as SEC001-SEC011) found in shared list | --- # FlowMCP Specification v3.0.0 — Agents An Agent is a complete, purpose-driven definition that bundles tools from multiple providers for a specific task. Agents replace Groups from v2. Where Groups were simple tool lists, Agents are full compositions with a model binding, system prompt, tests, prompts, skills, and optional resources. This document defines the agent manifest format, tool cherry-picking, model binding, system prompts, integrity verification, and validation rules. --- ## Purpose A typical FlowMCP catalog contains hundreds of tools across dozens of providers. A developer working on a crypto research task needs tools from CoinGecko (prices), Etherscan (on-chain data), and DeFi Llama (TVL data) — but not the other 200 tools in the catalog. An Agent selects exactly the tools needed, binds them to a specific LLM, defines how the LLM should behave, and includes tests that verify the composition works. ```mermaid flowchart LR A[agent.mjs] --> B[Tool Selection] A --> C[Model Binding] A --> D[System Prompt] A --> E[Tests] A --> F[Prompts] A --> G[Skills] A --> H[Resources] B --> I["coingecko-com/tool/simplePrice"] B --> J["etherscan-io/tool/getContractAbi"] B --> K["defillama-com/tool/getProtocolTvl"] C --> L["anthropic/claude-sonnet-4-5-20250929"] D --> M[Persona + behavioral instructions] E --> N["3+ end-to-end test cases"] F --> O[Explanatory namespace descriptions] G --> P[Instructional multi-step workflows] H --> Q[Own SQLite databases] ``` The diagram shows how an agent manifest connects seven concerns: which tools to use, which model to target, how the model should behave, how to verify the composition, what explanatory prompts to provide, what instructional skills to include, and what own resources to bring. --- ## Agent Manifest Format Each agent is defined by an `agent.mjs` file inside its own directory under `agents/`. The manifest is an ES module exporting `export const agent` containing all metadata, tool references, configuration, and tests. Where provider schemas export `main`, agent manifests export `agent` to clearly distinguish the two. ```javascript export const agent = { name: 'crypto-research', description: 'Cross-provider crypto analysis agent', version: 'flowmcp/3.0.0', model: 'anthropic/claude-sonnet-4-5-20250929', systemPrompt: 'You are a crypto research agent. You analyze token prices, on-chain data, and DeFi protocol metrics. Always provide sources for your data. When comparing across chains, normalize values to USD.', tools: { 'coingecko-com/tool/simplePrice': null, 'coingecko-com/tool/getCoinMarkets': null, 'etherscan-io/tool/getContractAbi': null, 'etherscan-io/tool/getTokenBalances': null, 'defillama-com/tool/getProtocolTvl': null }, resources: {}, prompts: { 'about': { file: './prompts/about.mjs' } }, skills: { 'token-deep-dive': { file: './skills/token-deep-dive.mjs' }, 'portfolio-analysis': { file: './skills/portfolio-analysis.mjs' } }, tests: [ { _description: 'Basic token lookup', input: 'What is the current price of Ethereum?', expectedTools: ['coingecko-com/tool/simplePrice'], expectedContent: ['current price', 'USD'] }, { _description: 'Cross-provider analysis', input: 'Compare TVL of Aave on Ethereum vs Arbitrum', expectedTools: ['defillama-com/tool/getProtocolTvl'], expectedContent: ['TVL', 'Ethereum', 'Arbitrum'] }, { _description: 'Multi-tool wallet analysis', input: 'Show top token holdings in vitalik.eth', expectedTools: ['etherscan-io/tool/getTokenBalances', 'coingecko-com/tool/simplePrice'], expectedContent: ['token', 'balance'] } ], maxRounds: 5, maxTokens: 4096, sharedLists: ['evmChains'], inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Research question' } }, required: ['query'] } } ``` --- ## Manifest Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Agent name. Must match `^[a-z][a-z0-9-]*$`. Must match the agent directory name. | | `description` | `string` | Yes | Human-readable description of the agent's purpose. | | `version` | `string` | Yes | Must be `flowmcp/3.0.0`. Declares which spec version this agent conforms to. | | `model` | `string` | Yes | Target LLM in OpenRouter syntax (`provider/model-name`). Must contain `/`. | | `systemPrompt` | `string` | Yes | Agent persona and behavioral instructions. Sent as the system message in every conversation. | | `tools` | `object` | Yes | Tool references as object. Keys with `/` are external references (value must be `null`). Keys without `/` are inline tool definitions (value is the tool definition object). Non-empty. See [Slash Rule](#slash-rule). | | `tests` | `array` | Yes | Minimum 3 agent tests. See [Agent Tests](#agent-tests). | | `maxRounds` | `number` | No | Maximum tool-call rounds per conversation. Default: `10`. | | `maxTokens` | `number` | No | Maximum tokens per LLM response. Default: `4096`. | | `prompts` | `object` | No | Explanatory prompts. Keys with `/` are external provider-prompt references (value must be `null`). Keys without `/` are inline declarations (value must have a `file` key pointing to an `.mjs` file that exports `export const content`). See [Slash Rule](#slash-rule). | | `skills` | `object` | No | Instructional skills. Keys must NOT contain `/` — skills are model-specific and cannot be externally referenced. Each value must have a `file` key pointing to an `.mjs` file that exports `export const skill`. | | `resources` | `object` | No | Own resources (SQLite databases). Keys with `/` are external provider-resource references (value must be `null`). Keys without `/` are inline resource definitions. See [Slash Rule](#slash-rule). | | `sharedLists` | `string[]` | No | Names of shared lists the agent needs. Resolved from the catalog's `_lists/` directory. | | `inputSchema` | `object` | No | JSON Schema defining the agent's input format. | ### Field Details #### `name` The agent name serves as both the identifier and the directory name. It must be unique within a catalog. ``` crypto-research <- valid defi-monitor <- valid wallet-auditor <- valid CryptoResearch <- INVALID (uppercase) crypto research <- INVALID (space) ``` #### `version` The version field uses the format `flowmcp/X.Y.Z` (not semver of the agent itself). It declares which FlowMCP specification the manifest conforms to. This allows the runtime to apply the correct validation rules. ``` flowmcp/3.0.0 <- valid 3.0.0 <- INVALID (missing flowmcp/ prefix) flowmcp/2.0.0 <- INVALID (agents are a v3 concept) ``` #### `model` The model field uses OpenRouter syntax: `provider/model-name`. The `/` separator is required and distinguishes the model provider from the model identifier. The model determines which LLM the agent is tested with and optimized for. Agent prompts and skills are model-specific — a prompt tuned for Claude may not work well with GPT-4o and vice versa. ``` anthropic/claude-sonnet-4-5-20250929 <- valid openai/gpt-4o <- valid google/gemini-2.0-flash <- valid claude-sonnet <- INVALID (no provider prefix) ``` #### `systemPrompt` The system prompt contains the agent's persona and behavioral instructions. It is sent as the system message at the start of every conversation. The system prompt should: - Define the agent's role and expertise - Set behavioral guidelines (tone, format, sources) - Specify how to handle edge cases - Reference the available tools by describing capabilities, not by listing tool names ```javascript export const agent = { systemPrompt: 'You are a crypto research agent specializing in token analysis and DeFi protocol comparison. Always cite data sources. When comparing metrics across chains, normalize to USD. If data is unavailable for a chain, state this explicitly rather than guessing.' } ``` #### `tools` Tools are declared as an object. External tools from provider schemas use keys with `/` (value `null`). Inline tools defined by the agent use keys without `/` (value is the tool definition). See the [Slash Rule](#slash-rule) for details. ```javascript export const agent = { tools: { // External tools — referenced by full ID, value is null 'coingecko-com/tool/simplePrice': null, 'etherscan-io/tool/getContractAbi': null, 'defillama-com/tool/getProtocolTvl': null, // Inline tool — no slashes in key, value is the definition 'customAnalysis': { method: 'POST', path: '/api/analyze', description: 'Custom analysis endpoint owned by this agent', parameters: [] } } } ``` See [Tool Cherry-Picking](#tool-cherry-picking) for external tool resolution details. #### `maxRounds` The maximum number of tool-call rounds the agent may execute in a single conversation turn. A "round" is one cycle of: LLM generates a tool call, runtime executes it, result is returned to the LLM. Default is `10`. Set lower for agents that should answer quickly, higher for agents that perform complex multi-step analysis. #### `maxTokens` The maximum number of tokens the LLM may generate per response. Default is `4096`. This controls response length, not total context. #### `prompts` Explanatory prompts declared as an object. Each key is the prompt name, each value must have a `file` key pointing to an `.mjs` file. Prompt files export `export const content` — a string containing the explanatory text. Agent-level prompts describe what the agent does and how its providers work together. The `about` prompt is a convention (SHOULD) that explains the agent's capabilities. ```javascript export const agent = { prompts: { // External provider prompt — slash key, value null 'coingecko-com/prompt/about': null, // Inline agent prompt — no slash, value has file key 'research-guide': { file: './prompts/research-guide.mjs' } } } ``` External prompt references (slash keys with `null` value) import prompts from provider schemas without copying them. Inline prompts use the format defined in `12-prompt-architecture.md` and use the `{{...}}` placeholder syntax for dynamic content. #### `skills` Instructional skills declared as an object. Each key is the skill name, each value must have a `file` key pointing to an `.mjs` file. Skill files export `export const skill` — an object containing the skill definition with content, input parameters, output description, and optionally external requirements. Agent-level skills are **model-specific** — they are written and tested for the LLM specified in `model`. They describe multi-step workflows, tool chaining strategies, and fallback logic. ```javascript export const agent = { skills: { // Skills are inline-only — no slash keys allowed (model-specific) 'token-deep-dive': { file: './skills/token-deep-dive.mjs' }, 'portfolio-analysis': { file: './skills/portfolio-analysis.mjs' } } } ``` Skills cannot be externally referenced because they are model-specific (`testedWith` required). A skill written for Claude may not work with GPT-4o. Skill files follow the format defined in `14-skills.md`. #### systemPrompt vs prompts vs skills The three content layers serve different purposes: | Field | Type | Purpose | Example | |-------|------|---------|---------| | `systemPrompt` | String | **Persona** — who the agent is and how it behaves | "You are a crypto research agent. Always cite sources." | | `prompts` | Object | **Explanatory** — what the agent can do, how providers relate | `about`: "This agent combines CoinGecko for prices, Etherscan for on-chain data, and DeFi Llama for TVL." | | `skills` | Object | **Instructional** — step-by-step workflows the agent follows | `token-deep-dive`: "1) Get price, 2) Check on-chain activity, 3) Check DeFi exposure, 4) Generate report" | At runtime, `systemPrompt` is always included as the system message. Prompts and skills are loaded and made available via MCP `server.prompt` — the LLM accesses them on demand. #### `resources` Own resources the agent brings — typically SQLite databases with curated context data. External resources from provider schemas can be referenced via slash keys (value `null`). Inline resources follow the same format as provider schema resources. Resource paths resolve across three levels: | Level | Path | Use Case | |-------|------|----------| | **Global** | `~/.flowmcp/` | Shared databases for all projects | | **Project** | `.flowmcp/` | Project-specific databases | | **Inline** | Relative to agent directory | Database bundled with the agent | ```javascript export const agent = { resources: { // External provider resource — slash key, value null 'offeneregister/resource/companiesDb': null, // Inline agent resource — no slash, value is the definition 'token-metadata': { source: { type: 'sqlite', path: 'token-metadata.db' }, queries: { 'search-tokens': { sql: "SELECT * FROM tokens WHERE symbol LIKE '%' || {{input:query}} || '%'" } } } } } ``` #### `sharedLists` Names of shared lists the agent needs. These are resolved from the catalog's `_lists/` directory at load time. Shared lists provide reusable value sets (EVM chain IDs, country codes, trading pairs) that the agent's prompts and system prompt may reference. #### `inputSchema` An optional JSON Schema that defines the expected input format when invoking the agent. This allows callers to validate their input before sending it to the agent. --- ## Slash Rule The slash rule is a uniform convention across all four agent primitives (`tools`, `prompts`, `resources`, `skills`). It determines whether an entry is an external reference or an inline definition: ```mermaid flowchart TD A[Key in object] --> B{Contains slash?} B -->|Yes| C["External reference — value MUST be null"] B -->|No| D["Inline definition — value is the definition object"] C --> E["'coingecko-com/tool/simplePrice': null"] D --> F["'customAnalysis': { method: 'POST', ... }"] ``` | Key Pattern | Value | Meaning | Example | |-------------|-------|---------|---------| | Contains `/` | `null` | External reference to a provider primitive | `'coingecko-com/tool/simplePrice': null` | | Contains `/` | non-`null` | **ERROR** (AGT020) | `'coingecko-com/tool/simplePrice': { ... }` | | No `/` | object | Inline definition owned by the agent | `'customAnalysis': { method: 'POST', ... }` | ### Per-Primitive Rules | Primitive | External (slash + null) | Inline (no slash) | Notes | |-----------|------------------------|-------------------|-------| | `tools` | Yes | Yes (tool definition object) | Most agents use external tools only | | `prompts` | Yes | Yes (`{ file: './...' }`) | Can reference provider prompts | | `resources` | Yes | Yes (resource definition) | Can reference provider resources | | `skills` | **No** (AGT014) | Yes (`{ file: './...' }`) | Model-specific, cannot be shared | Skills are the exception: they cannot have slash keys because they are model-specific (`testedWith` required). A skill written for Claude may produce incorrect results with GPT-4o. --- ## Tool Cherry-Picking Agents select specific tools from multiple providers. This is the key difference from loading entire schemas — an agent includes only the tools it needs, reducing context size and improving LLM focus. ### How Tool References Are Resolved ```mermaid flowchart TD A["Read agent.mjs tools{}"] --> B["For each key where value is null"] B --> C["Parse key: namespace/type/name"] C --> D["Look up namespace in catalog registry"] D --> E{Namespace found?} E -->|No| F["Error: AGT007 — namespace not registered"] E -->|Yes| G["Find schema containing tool name"] G --> H{Tool found?} H -->|No| I["Error: AGT007 — tool not found in namespace"] H -->|Yes| J["Load tool definition from schema"] J --> K["Add to agent's active tools"] ``` The diagram shows how each external tool reference (slash key with `null` value) in the manifest is resolved against the catalog registry. The runtime parses the key, looks up the namespace, finds the schema containing the tool, and loads the tool definition. Inline tools (keys without slashes) are used directly from the manifest. ### Resolution Steps 1. **Partition** — separate tool keys into external (contains `/`, value is `null`) and inline (no `/`, value is object) 2. **Parse external** — split each external key on `/` into namespace, type, and name (see `16-id-schema.md`) 3. **Find namespace** — locate the provider namespace in the catalog's `registry.json` 4. **Find schema** — within the namespace, find the schema file that contains the named tool 5. **Load tool** — extract the tool definition from the provider schema's `main.tools[name]` 6. **Register inline** — add inline tool definitions directly to the agent's active tool set 7. **Register external** — add resolved external tools to the agent's active tool set ### Cross-Provider Composition An agent can combine tools from any number of providers: ```javascript export const agent = { tools: { 'coingecko-com/tool/simplePrice': null, 'coingecko-com/tool/getCoinMarkets': null, 'etherscan-io/tool/getContractAbi': null, 'etherscan-io/tool/getTokenBalances': null, 'defillama-com/tool/getProtocolTvl': null, 'defillama-com/tool/getProtocolChainTvl': null } } ``` This agent uses 6 external tools from 3 providers. The runtime resolves each external tool key independently and collects `requiredServerParams` from all involved schemas. ### Server Params Collection Each provider schema declares its own `requiredServerParams` (API keys). When an agent activates, the runtime collects all unique params across all referenced schemas: ``` Agent "crypto-research" requires: - COINGECKO_API_KEY (from coingecko-com schemas) - ETHERSCAN_API_KEY (from etherscan-io schemas) - (none) (defillama-com has no requiredServerParams) Checking .env... COINGECKO_API_KEY=set, ETHERSCAN_API_KEY=set All server params available. Agent ready. ``` If any required param is missing, activation fails with a clear error identifying which schemas need which params. --- ## Model Binding The `model` field binds the agent to a specific LLM. This binding has three implications: ### 1. Test Execution Agent tests are executed against the specified model. The `expectedTools` and `expectedContent` assertions are validated using the bound model's behavior. A test suite that passes with `anthropic/claude-sonnet-4-5-20250929` may fail with `openai/gpt-4o` because different models make different tool selection decisions. ### 2. Prompt and Skill Optimization Agent-level prompts (in the `prompts/` directory) and skills (in the `skills/` directory) are written for the specific model. They leverage the model's strengths and work around its weaknesses. A skill that works well with Claude's structured thinking may not translate to GPT-4o's different reasoning style. ### 3. Runtime Model Selection When the agent is invoked, the runtime uses the `model` field to select which LLM to call. The model string uses OpenRouter syntax, enabling routing through OpenRouter or direct provider APIs. ```mermaid flowchart LR A[Agent invoked] --> B[Read model field] B --> C["anthropic/claude-sonnet-4-5-20250929"] C --> D{Route via OpenRouter?} D -->|Yes| E[OpenRouter API] D -->|No| F[Direct Anthropic API] E --> G[LLM processes prompt + tools] F --> G ``` The diagram shows how the model field determines LLM routing at runtime. --- ## System Prompt The `systemPrompt` field is the agent's core behavioral definition. It is sent as the system message in every conversation, before any user input or tool results. ### What the System Prompt Should Contain | Aspect | Purpose | Example | |--------|---------|---------| | Role | Define the agent's identity | "You are a crypto research agent" | | Expertise | Scope the agent's knowledge | "specializing in token analysis and DeFi protocols" | | Behavior | Set interaction guidelines | "Always cite data sources" | | Format | Define output expectations | "Present comparisons in tables" | | Edge cases | Handle missing data | "If data is unavailable, state this explicitly" | ### What the System Prompt Should NOT Contain - **Tool names or IDs** — the LLM discovers available tools through the MCP tool list, not the system prompt - **API-specific details** — tool descriptions handle this - **Shared list values** — these are injected at runtime - **Prompt or skill content** — prompts and skills are separate files with their own lifecycle ### System Prompt, Prompts, and Skills The system prompt works alongside prompts and skills but serves a different purpose: | Layer | Scope | Model-specific? | Content | |-------|-------|-----------------|---------| | System Prompt | Agent-wide | Yes | Persona, behavior, format | | Prompts | Explanatory | No | How providers and agent work | | Skills | Instructional | Yes | Tool combinatorics, chaining, workflows | At runtime, the system prompt is always included. Prompts and skills are loaded and made available via MCP — see `12-prompt-architecture.md` and `14-skills.md`. --- ## Agent Tests Agent tests validate end-to-end behavior: given a natural language input, does the agent invoke the correct tools and produce a response containing the expected content? Tests are defined inline in the manifest's `tests` array. ### Test Format ```javascript export const agent = { tests: [ { _description: 'Basic token lookup', input: 'What is the current price of Ethereum?', expectedTools: ['coingecko-com/tool/simplePrice'], expectedContent: ['current price', 'USD'] } ] } ``` ### Test Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `_description` | `string` | Yes | What this test demonstrates | | `input` | `string` | Yes | Natural language prompt (as a user would ask) | | `expectedTools` | `string[]` | Yes | Tool IDs that should be called (deterministic check) | | `expectedContent` | `string[]` | No | Substrings the response text must contain (case-insensitive) | ### Three-Level Test Model Agent tests operate on three levels of determinism, consistent with the model defined in `10-tests.md`: ```mermaid flowchart TD A[Agent Test] --> B["Level 1: Tool Usage"] A --> C["Level 2: Content"] A --> D["Level 3: Quality"] B --> E["Deterministic — expectedTools vs actual tool calls"] C --> F["Semi-deterministic — expectedContent vs response text"] D --> G["Subjective — human review or LLM-as-Judge"] ``` | Level | Assertion | Determinism | Method | |-------|-----------|-------------|--------| | Tool Usage | `expectedTools[]` | Deterministic | Compare expected tool IDs against actual tool calls | | Content | `expectedContent[]` | Semi-deterministic | Case-insensitive substring match against response text | | Quality | (not automated) | Subjective | Human review or LLM-as-Judge | **Tool Usage** is the strongest assertion. Given a well-scoped prompt, which tools the agent calls is deterministic. "What is the current price of Ethereum?" must invoke a price tool — there is no ambiguity about which tool category to use. **Content** assertions are semi-deterministic. LLM output varies across runs, but factual elements like "current price" or "USD" should appear in any correct response. **Quality** is outside the scope of automated validation. It exists in the model for completeness — teams may evaluate response quality through human review or LLM-as-Judge. ### Minimum Test Count Every agent must have at least 3 tests (validation rule AGT008). Three tests ensure coverage across: 1. **Basic case** — a straightforward single-tool query 2. **Edge case** — a question requiring multiple tools or complex reasoning 3. **Cross-cutting case** — a question that combines data from multiple providers ### Test Design Guidelines The same principles from `10-tests.md` apply: - **Express the breadth** — each test should demonstrate a different capability - **Teach through examples** — reading the tests should reveal what the agent can do - **No personal data** — use public, well-known entities - **Reproducible** — prefer stable queries over time-sensitive ones --- ## Integrity Verification Agents store hashes of their member tools to detect when underlying tool definitions change. When a tool's schema is updated (new parameters, changed output format, modified path), the hash mismatch signals that the agent needs review. ### Per-Tool Hash Each tool's hash is calculated from its `main` block definition — the declarative, JSON-serializable part: ``` toolHash = SHA-256( JSON.stringify( { namespace: 'etherscan-io', version: '3.0.0', tool: { name: 'getContractAbi', method: 'GET', path: '/api', parameters: [ /* full parameter definitions */ ], output: { /* output schema if present */ } }, sharedListRefs: [ { ref: 'evmChains', version: '1.0.0' } ] } ) ) ``` The hash input includes: - `namespace` from the provider schema - `version` from the provider schema - The tool definition (name, method, path, parameters, output) - Shared list references the tool uses Handler code is **excluded** from the hash. A handler change (e.g., improved response transformation) does not invalidate the agent because it does not change the tool's interface. ### Agent Hash The agent hash is calculated from its sorted tool references and their individual hashes: ``` agentHash = SHA-256( JSON.stringify( Object.keys( tools ) .filter( ( key ) => tools[ key ] === null ) .sort() .map( ( toolId ) => { const hash = getToolHash( toolId ) return { id: toolId, hash } } ) ) ) ``` Sorting ensures deterministic output regardless of the order tools appear in the manifest. ### Hash Storage The agent hash is stored in the manifest alongside the tool list: ```javascript export const agent = { name: 'crypto-research', tools: { 'coingecko-com/tool/simplePrice': null, 'etherscan-io/tool/getContractAbi': null }, hash: 'sha256:a1b2c3d4e5f6...' } ``` The `hash` field is optional in the manifest. When present, the runtime verifies it on activation. When absent, the runtime calculates and stores it on first activation. ### Verification Flow ```mermaid flowchart TD A[Activate agent] --> B[Read agent.mjs] B --> C[Resolve each tool reference] C --> D[Calculate per-tool hashes] D --> E[Calculate agent hash] E --> F{hash field present?} F -->|No| G[Store calculated hash in manifest] F -->|Yes| H{Hashes match?} H -->|Yes| I[Agent activated — integrity verified] H -->|No| J[Warning: hash mismatch] J --> K[Report which tools changed] K --> L[User reviews changes] L --> M[Recalculate and update hash] M --> I ``` The diagram shows the verification flow from activation through hash comparison to either success or mismatch resolution. ### Verification CLI ```bash flowmcp agent verify crypto-research ``` Output on success: ``` Agent "crypto-research": 5 tools, all hashes valid ``` Output on hash mismatch: ``` Agent "crypto-research": HASH MISMATCH - etherscan-io/tool/getContractAbi: expected sha256:abc... got sha256:def... - Tool parameters changed (new optional parameter added) Recommendation: Review changes and run `flowmcp agent rehash crypto-research` ``` --- ## Directory Structure Each agent lives in its own directory under `agents/` in the catalog: ``` agents/ └── crypto-research/ ├── agent.mjs ← export const agent ├── prompts/ │ └── about.mjs ← export const content └── skills/ ├── token-deep-dive.mjs ← export const skill └── portfolio-analysis.mjs ← export const skill ``` ### Directory Rules - The directory name must match `agent.name` - `agent.mjs` is required — it is the agent's entry point - `prompts/` is optional — only needed if the agent defines prompts - `skills/` is optional — only needed if the agent defines skills - Prompt file paths in `agent.prompts` are relative to the agent directory - Skill file paths in `agent.skills` are relative to the agent directory - No other files or subdirectories are expected (except resource files like `.db`) ### Relationship to Catalog The catalog's `registry.json` references each agent by its manifest path: ```javascript { "agents": [ { "name": "crypto-research", "description": "Cross-provider crypto analysis agent", "manifest": "agents/crypto-research/agent.mjs" } ] } ``` See `15-catalog.md` for the full catalog specification. --- ## Agent vs Group Migration Agents replace Groups from FlowMCP v2. The migration is conceptual — agents are not backward-compatible with groups because they serve a fundamentally different purpose. ### What Changed | Aspect | Group (v2) | Agent (v3) | |--------|-----------|------------| | Definition file | `.flowmcp/groups.json` | `agents/{name}/agent.mjs` | | Format | JSON | `.mjs` with `export const agent` | | Purpose | Tool list for activation | Complete agent definition | | Model binding | None | Required (`model` field) | | System prompt | None | Required (`systemPrompt` field) | | Tests | None | Required (minimum 3) | | Resources | None | Optional (own SQLite databases) | | Prompts | None | Optional (explanatory, as object) | | Skills | None | Optional (instructional, as object) | | Tool references | `namespace/file::tool` | `namespace/type/name` (ID schema) | | Location | Local to project (`.flowmcp/`) | Part of catalog (`agents/`) | | Sharing | Export/import JSON | Distributed via catalog registry | ### Migration Path Groups cannot be automatically converted to agents because agents require fields that groups do not have (model, systemPrompt, tests). The migration is manual: 1. **Create agent directory** — `agents/{group-name}/` 2. **Create agent.mjs** — use the group's tool list as a starting point for `export const agent` 3. **Convert tool references** — from `namespace/file::tool` to `namespace/type/name` format 4. **Add model** — choose the target LLM 5. **Add systemPrompt** — define the agent's persona 6. **Add tests** — write at least 3 agent tests 7. **Add prompts** — optionally create explanatory prompts (as object with `{ file: './...' }`) 8. **Add skills** — optionally create instructional skills (as object with `{ file: './...' }`) 9. **Add resources** — optionally add own SQLite databases 10. **Register in catalog** — add the agent to `registry.json` See `08-migration.md` for the complete v2-to-v3 migration guide. --- ## Agent Activation Lifecycle When an agent is activated, the runtime performs these steps: ```mermaid flowchart TD A[Activate agent] --> B[Read agent.mjs] B --> C[Validate manifest fields] C --> D[Resolve tool references] D --> E[Load provider schemas] E --> F[Static security scan per schema] F --> G[Collect requiredServerParams] G --> H{All server params in .env?} H -->|No| I[Error: missing server params] H -->|Yes| J[Resolve shared lists] J --> K[Verify integrity hashes] K --> L[Load agent resources] L --> M[Load agent prompts] M --> N[Load agent skills] N --> O[Register tools as MCP tools] O --> P[Register prompts as MCP prompts] P --> Q[Register skills as MCP prompts] Q --> R[Register resources as MCP resources] R --> S[Agent ready] ``` The diagram shows the full activation lifecycle from reading the manifest to the agent being ready for invocations. ### Activation Steps 1. **Read manifest** — import `agent.mjs` from the agent directory, access `export const agent` 2. **Validate** — check all required fields, format constraints, and version compatibility 3. **Resolve tools** — parse each tool ID and locate the corresponding provider schema 4. **Load schemas** — import the `.mjs` schema files for all referenced tools 5. **Security scan** — run the static security scanner on each loaded schema 6. **Collect params** — gather all `requiredServerParams` from all involved schemas 7. **Check env** — verify all required API keys are available in the environment 8. **Resolve lists** — load shared lists declared in `agent.sharedLists` 9. **Verify hashes** — compare stored hash against calculated hash (warn on mismatch) 10. **Load resources** — initialize agent-owned resources (SQLite databases) from `agent.resources` 11. **Load prompts** — import prompt files from `main.prompts`, each must export `export const content` 12. **Load skills** — import skill files from `main.skills`, each must export `export const skill` 13. **Register tools** — expose the agent's tools via MCP `server.tool` 14. **Register prompts** — expose the agent's prompts via MCP `server.prompt` 15. **Register skills** — expose the agent's skills via MCP `server.prompt` 16. **Register resources** — expose the agent's resources via MCP `server.resource` --- ## Validation Rules | Code | Severity | Rule | |------|----------|------| | AGT001 | error | `name` is required, must match `^[a-z][a-z0-9-]*$` | | AGT002 | error | `description` is required, must be a non-empty string | | AGT003 | error | `model` is required, must contain `/` (OpenRouter syntax) | | AGT004 | error | `version` must be `flowmcp/3.0.0` | | AGT005 | error | `systemPrompt` is required, must be a non-empty string | | AGT006 | error | `tools` is required, must be a non-empty object | | AGT007 | error | Each tool key containing `/` must be a valid ID format (`namespace/type/name`) and its value must be `null` | | AGT008 | error | `tests[]` is required, minimum 3 tests | | AGT009 | error | Each test must have an `input` field of type string | | AGT010 | error | Each test must have an `expectedTools` field as a non-empty array | | AGT011 | error | Each `expectedTools` entry must be a valid ID (contains `/`) | | AGT012 | warning | Tests should cover different tool combinations | | AGT013 | error | `prompts` if present must be an object. Keys with `/`: value must be `null` (external). Keys without `/`: value must have a `file` key | | AGT014 | error | `skills` if present must be an object. Keys must NOT contain `/` (skills are model-specific, inline-only). Each value must have a `file` key | | AGT015 | error | `resources` if present must be an object. Keys with `/`: value must be `null` (external). Keys without `/`: follows schema resource rules | | AGT016 | error | Referenced prompt and skill files must exist and have `.mjs` extension | | AGT017 | error | Prompt files must export `export const content` | | AGT018 | error | Skill files must export `export const skill` | | AGT019 | error | Inline tool keys (without `/`) must have a valid tool definition object as value (with `method`, `path`, `description`) | | AGT020 | error | Keys containing `/` with a non-`null` value are forbidden (slash keys must always be `null`) | ### Rule Details **AGT001** — The agent name is the primary identifier and must match the directory name. Invalid names prevent catalog resolution. **AGT002** — The description is displayed in catalog listings and agent discovery. It must be meaningful — empty strings are rejected. **AGT003** — The model field uses OpenRouter syntax where the `/` separates the provider from the model name. A model string without `/` cannot be routed to any provider. Examples: `anthropic/claude-sonnet-4-5-20250929`, `openai/gpt-4o`. **AGT004** — The version must be exactly `flowmcp/3.0.0`. This is not the agent's own version — it declares which FlowMCP specification the manifest conforms to. **AGT005** — The system prompt defines the agent's behavior. Without it, the agent has no persona or instructions. Empty strings are rejected because they provide no behavioral guidance. **AGT006** — An agent without tools has nothing to execute. The tools object must contain at least one entry (external or inline). **AGT007** — External tool keys (containing `/`) must follow the ID schema from `16-id-schema.md`. The full form `namespace/type/name` is required to ensure unambiguous resolution. The value for external keys must be `null`. **AGT008** — Three tests is the minimum for meaningful coverage: one basic case, one edge case, one cross-cutting case. This matches the tool test minimum from `10-tests.md`. **AGT009–AGT011** — These rules validate individual test fields. They correspond to the agent test validation rules TST009–TST011 defined in `10-tests.md`. Every test must have a natural language input and at least one expected tool call. **AGT012** — Tests should demonstrate breadth. If all three tests expect the same single tool, the test suite does not validate the agent's multi-tool orchestration capability. This is a warning, not an error, because some agents genuinely use only one tool. **AGT013** — The `prompts` field must be an object. Keys containing `/` are external provider-prompt references — their value must be `null`. Keys without `/` are inline prompt declarations — their value must have a `file` key pointing to a `.mjs` file. **AGT014** — The `skills` field must be an object. Keys must NOT contain `/` because skills are model-specific (`testedWith` required) and cannot be shared across agents targeting different models. Each value must have a `file` key pointing to a skill file. **AGT015** — The `resources` field must be an object. Keys containing `/` are external provider-resource references — their value must be `null`. Keys without `/` are inline resource definitions that must have a valid `source` and optional `queries`. **AGT019** — Inline tool keys (without `/`) must have a valid tool definition object as their value. The object must contain at minimum `method`, `path`, and `description` — the same fields required for tools in provider schemas. **AGT020** — A key containing `/` with a non-`null` value is always an error. This rule applies uniformly to `tools`, `prompts`, and `resources`. The slash in the key signals an external reference, which must have `null` as its value. **AGT016** — All files referenced in `prompts` and `skills` must exist on disk and must have the `.mjs` extension. Missing files prevent activation. **AGT017** — Prompt files must export a named constant `content` (`export const content`). This is a string containing the explanatory text, optionally with `{{...}}` placeholders. **AGT018** — Skill files must export a named constant `skill` (`export const skill`). This is an object containing the skill definition as specified in `14-skills.md`. ### Validation Command ```bash flowmcp validate-agent ``` The command runs all AGT rules and reports errors and warnings. An agent with any error-level violations cannot be activated. ### Validation Output Example ``` flowmcp validate-agent agents/crypto-research/ AGT001 pass name "crypto-research" matches pattern AGT002 pass description is non-empty AGT003 pass model "anthropic/claude-sonnet-4-5-20250929" contains / AGT004 pass version is flowmcp/3.0.0 AGT005 pass systemPrompt is non-empty AGT006 pass tools{} has 5 entries AGT007 pass all external tool keys are valid IDs with null values AGT008 pass tests[] has 3 entries (minimum: 3) AGT009 pass all tests have input field AGT010 pass all tests have expectedTools field AGT011 pass all expectedTools entries are valid IDs AGT012 pass tests cover 4 different tool combinations AGT013 pass prompts is valid object with file keys AGT014 pass skills is valid object with file keys AGT015 skip resources is empty AGT016 pass all referenced files exist and are .mjs AGT017 pass all prompt files export content AGT018 pass all skill files export skill AGT019 skip no inline tools defined AGT020 pass no slash keys with non-null values 0 errors, 0 warnings Agent is valid ``` --- ## Complete Example A fully specified agent manifest with directory structure: ### Directory ``` agents/ └── crypto-research/ ├── agent.mjs ← export const agent ├── prompts/ │ └── about.mjs ← export const content └── skills/ ├── token-deep-dive.mjs ← export const skill └── portfolio-analysis.mjs ← export const skill ``` ### agent.mjs ```javascript export const agent = { name: 'crypto-research', description: 'Cross-provider crypto analysis agent combining price data, on-chain analytics, and DeFi protocol metrics for comprehensive token and portfolio research', version: 'flowmcp/3.0.0', model: 'anthropic/claude-sonnet-4-5-20250929', systemPrompt: 'You are a crypto research agent specializing in token analysis, wallet forensics, and DeFi protocol comparison. Follow these guidelines:\n\n1. Always cite which data source provided each piece of information.\n2. When comparing metrics across chains, normalize values to USD.\n3. Present comparative data in tables when three or more items are compared.\n4. If data is unavailable for a requested chain or token, state this explicitly rather than guessing.\n5. For wallet analysis, always check both token balances and current prices to show USD values.', tools: { 'coingecko-com/tool/simplePrice': null, 'coingecko-com/tool/getCoinMarkets': null, 'etherscan-io/tool/getContractAbi': null, 'etherscan-io/tool/getTokenBalances': null, 'defillama-com/tool/getProtocolTvl': null }, resources: {}, prompts: { 'about': { file: './prompts/about.mjs' } }, skills: { 'token-deep-dive': { file: './skills/token-deep-dive.mjs' }, 'portfolio-analysis': { file: './skills/portfolio-analysis.mjs' } }, tests: [ { _description: 'Basic token lookup — single tool, single provider', input: 'What is the current price of Ethereum?', expectedTools: ['coingecko-com/tool/simplePrice'], expectedContent: ['current price', 'USD'] }, { _description: 'Cross-provider DeFi analysis — comparative query across chains', input: 'Compare the TVL of Aave on Ethereum vs Arbitrum', expectedTools: ['defillama-com/tool/getProtocolTvl'], expectedContent: ['TVL', 'Ethereum', 'Arbitrum'] }, { _description: 'Multi-tool wallet analysis — combines on-chain data with pricing', input: 'Show top token holdings in vitalik.eth', expectedTools: ['etherscan-io/tool/getTokenBalances', 'coingecko-com/tool/simplePrice'], expectedContent: ['token', 'balance'] } ], maxRounds: 5, maxTokens: 4096, sharedLists: ['evmChains'], inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Research question about tokens, wallets, or DeFi protocols' } }, required: ['query'] }, hash: 'sha256:a1b2c3d4e5f6789...' } ``` --- # FlowMCP Specification v2.0.0 — MCP Tasks ## Status **Deferred to v2.1.0.** This section is a placeholder. MCP Tasks enable long-running asynchronous operations (e.g. executing a Dune Analytics query that takes 30+ seconds). The MCP protocol defines a task lifecycle with creation, polling, completion, and cancellation. --- ## Why Deferred FlowMCP schemas describe how to interact with **external API async patterns** (job submission, status polling, result retrieval). The MCP Tasks protocol defines how the **MCP server itself** exposes async operations to AI clients. These are two complementary layers that need careful alignment. v2.1.0 will define: - Schema-level fields for declaring async routes - Mapping between external API status values and MCP Task states - Integration with the MCP Tasks protocol (`tasks/get`, `tasks/result`, `tasks/cancel`) --- ## Reference - [MCP Tasks Specification (2025-11-25)](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/tasks) - [SEP-1686: Tasks — GitHub Discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686) --- ## Reserved Fields Schema authors may include an `async` field in route definitions for forward compatibility. In v2.0.0, this field is **ignored** by the runtime but preserved for future use. --- # FlowMCP Specification v3.0.0 — Migration Guide This guide covers migrating schemas between FlowMCP versions. Section 1 covers v2.0.0 to v3.0.0 migration. Section 2 preserves the v1.2.0 to v2.0.0 guide for reference. --- ## Section 1: v2.0.0 to v3.0.0 The v3.0.0 migration is straightforward: rename `routes` to `tools` and update the version field. No structural changes to existing tool definitions are required. Resources and skills are new features that can be added incrementally. ### What Changes | Aspect | v2.0.0 | v3.0.0 | |--------|--------|--------| | Tool definitions key | `main.routes` | `main.tools` | | Version field | `'2.x.x'` | `'3.0.0'` | | Resources | Not available | Optional `main.resources` | | Skills | Not available | Optional `main.skills` | ### Automatic Migration ```bash flowmcp migrate ``` The `migrate` command performs two changes: 1. Renames `routes:` to `tools:` in the schema file 2. Updates `version` from `2.x.x` to `3.0.0` **The migration does NOT add resources or skills.** Those are new features that schema authors add when needed. #### Batch Migration ```bash flowmcp migrate --all ``` Migrates all `.mjs` schema files in the specified directory recursively. #### Dry Run ```bash flowmcp migrate --dry-run ``` Shows what would change without writing to disk. ### Migration Steps #### Step 1: Rename `routes` to `tools` **Before (v2.0.0):** ```javascript export const main = { namespace: 'etherscan', name: 'SmartContractExplorer', version: '2.0.0', root: 'https://api.etherscan.io', routes: { getContractAbi: { /* ... */ }, getSourceCode: { /* ... */ } } } ``` **After (v3.0.0):** ```javascript export const main = { namespace: 'etherscan', name: 'SmartContractExplorer', version: '3.0.0', root: 'https://api.etherscan.io', tools: { getContractAbi: { /* ... */ }, getSourceCode: { /* ... */ } } } ``` Tool definitions (method, path, parameters, output, tests) remain identical. Only the key name and version change. #### Step 2: Update Handler Keys (if applicable) Handler keys reference tool names. Since tool names do not change (only the container key from `routes` to `tools`), **no handler changes are needed**. ```javascript // Handlers remain identical — they reference tool names, not the container key export const handlers = ( { sharedLists, libraries } ) => ({ getSourceCode: { postRequest: async ( { response, struct, payload } ) => { // ... same as before } } }) ``` #### Step 3: Run Validation ```bash flowmcp validate ``` Validates the migrated schema against v3.0.0 rules. #### Step 4 (Optional): Add Resources If the schema would benefit from local, deterministic data, add a `resources` field to `main`. See `13-resources.md`. #### Step 5 (Optional): Add Skills If the schema has tools that compose well into multi-step workflows, add a `skills` field to `main` and create skill `.mjs` files. See `14-skills.md`. #### Step 6: Agent Migration: manifest.json → agent.mjs If you have agent definitions in `manifest.json` format, convert them to `agent.mjs` with `export const agent = { ... }`. 1. Create agent directory: `agents/{agent-name}/` 2. Create `agent.mjs` with `export const agent = { ... }` instead of `manifest.json` 3. Fields mapping: | Field | v2.0.0 (manifest.json) | v3.0.0 (agent.mjs) | |-------|------------------------|---------------------| | `name` | Same | Same | | `description` | Same | Same | | `model` | Same | Same | | `systemPrompt` | Same | Same | | `version` | `'2.0.0'` | `'flowmcp/3.0.0'` | | `tools` | Array of tool IDs | Object with full tool IDs as keys (`{ 'namespace/tool/name': null }`) | | `prompts` | Array | Object: `{ 'prompt-name': { file: './prompts/prompt-name.mjs' } }` | | `skills` | Not available | NEW Object: `{ 'skill-name': { file: './skills/skill-name.mjs' } }` | | `resources` | Not available | NEW Object: `{ 'resource-name': { file: './resources/resource-name.db' } }` | | `tests` | Array (inline) | Array stays inline, min 3 tests required | | `sharedLists` | Array of list names | Array of list names (same as before) | **Before (v2.0.0 manifest.json):** ```json { "name": "crypto-analyst", "description": "Analyzes crypto markets", "model": "claude-sonnet-4-20250514", "version": "2.0.0", "systemPrompt": "You are a crypto analyst...", "tools": [ "etherscan/getContractAbi" ], "prompts": [ { "name": "market-summary", "file": "./prompts/market-summary.mjs" } ], "tests": [ "analyze BTC", "check ETH gas" ], "sharedLists": [ "evmChains" ] } ``` **After (v3.0.0 agent.mjs):** ```javascript export const agent = { name: 'crypto-analyst', description: 'Analyzes crypto markets', model: 'claude-sonnet-4-20250514', version: 'flowmcp/3.0.0', systemPrompt: 'You are a crypto analyst...', tools: { 'etherscan/SmartContractExplorer/getContractAbi': null }, prompts: { 'market-summary': { file: './prompts/market-summary.mjs' } }, skills: { 'portfolio-check': { file: './skills/portfolio-check.mjs' } }, resources: { 'token-list': { file: './resources/token-list.db' } }, tests: [ 'analyze BTC', 'check ETH gas', 'compare SOL vs ETH' ], sharedLists: [ 'evmChains' ] } ``` #### Step 7: Prompt Migration: `[[...]]` → `{{type:name}}` The placeholder syntax in prompt and skill content changes from double-bracket to typed mustache syntax. | Old Syntax (v2.0.0) | New Syntax (v3.0.0) | Type | |----------------------|---------------------|------| | `[[coingecko/simplePrice]]` | `{{tool:simplePrice}}` | `tool` — references a tool by name | | `[[tokenSymbol]]` | `{{input:tokenSymbol}}` | `input` — references a user input parameter | | — | `{{resource:token-list}}` | `resource` — references a resource | | — | `{{prompt:market-summary}}` | `prompt` — references another prompt | **Available types:** | Type | Description | |------|-------------| | `tool` | References a tool name for dynamic invocation | | `input` | References a user-provided input parameter | | `resource` | References a resource definition | | `prompt` | References another prompt for composition | **Before (v2.0.0):** ```text Fetch the current price using [[coingecko/simplePrice]] for the token [[tokenSymbol]]. ``` **After (v3.0.0):** ```text Fetch the current price using {{tool:simplePrice}} for the token {{input:tokenSymbol}}. ``` ### Deprecation Timeline ```mermaid flowchart LR A["v3.0.0
routes = Alias + Warning"] --> B["v3.1.0
routes = Loud Warning"] B --> C["v3.2.0
routes = Error"] ``` | Version | Behavior | |---------|----------| | v3.0.0 | `main.routes` accepted as alias for `main.tools`. Emits deprecation warning. | | v3.1.0 | `main.routes` still accepted but emits loud warning on every load. | | v3.2.0 | `main.routes` rejected as a validation error. Only `main.tools` accepted. | **Important:** A schema defining BOTH `main.tools` AND `main.routes` is a validation error in all v3.x versions. ### v2.0.0 to v3.0.0 Backward Compatibility | Feature | v2.0.0 Schema (with `routes`) | v3.0.0 Schema (with `tools`) | |---------|-------------------------------|------------------------------| | Core v2.x runtime | Supported | Not supported | | Core v3.0 runtime | Supported (alias + warning) | Supported | | Core v3.2 runtime (future) | Rejected | Supported | ### v2.0.0 to v3.0.0 Migration Checklist Per schema: - [ ] `routes` renamed to `tools` - [ ] `version` updated to `'3.0.0'` - [ ] Validation passes (`flowmcp validate`) - [ ] Tests pass (`flowmcp test single`) - [ ] (Optional) Resources added if applicable - [ ] (Optional) Skills added if applicable Per agent: - [ ] Convert agent `manifest.json` to `agent.mjs` - [ ] Replace `[[...]]` placeholders with `{{type:name}}` in prompt/skill content - [ ] Convert agent prompts from Array to Object - [ ] Add `skills`/`resources` fields if needed --- ## Section 2: v1.2.0 to v2.0.0 This section preserves the original v1.2.0 to v2.0.0 migration guide. The FlowMCP core v2.0.0 supported both formats during a transition period. Legacy v1.2.0 format is no longer supported in v3.0.0. --- ## Schema Categories Existing schemas fall into three categories: | Category | % of Schemas | Migration Effort | Description | |----------|-------------|-----------------|-------------| | **Pure declarative** | ~60% | Automatic | No handlers, no imports. Only URL construction and parameters. | | **With handlers** | ~30% | Semi-automatic | Has `preRequest`/`postRequest` handlers but no imports. | | **With imports** | ~10% | Manual review | Imports shared data (chain lists, etc.) that must become shared list references. | --- ## Migration Steps ### Step 1: Wrap Existing Fields in `main` Block **Before (v1.2.0):** ```javascript export const schema = { namespace: 'etherscan', name: 'SmartContractExplorer', flowMCP: '1.2.0', root: 'https://api.etherscan.io/v2/api', requiredServerParams: [ 'ETHERSCAN_API_KEY' ], routes: { /* ... */ }, handlers: { /* ... */ } } ``` **After (v2.0.0):** ```javascript // Static, hashable — no imports export const main = { namespace: 'etherscan', name: 'SmartContractExplorer', version: '2.0.0', root: 'https://api.etherscan.io/v2/api', requiredServerParams: [ 'ETHERSCAN_API_KEY' ], requiredLibraries: [], routes: { /* ... */ } } // Factory function — receives injected dependencies export const handlers = ( { sharedLists, libraries } ) => ({ /* ... */ }) ``` Key changes: - Two separate named exports: `main` (static) and `handlers` (factory function) - `flowMCP: '1.2.0'` becomes `version: '2.0.0'` inside `main` - `handlers` is now a factory function receiving `{ sharedLists, libraries }` - New field `requiredLibraries` declares needed npm packages - Zero import statements — all dependencies are injected --- ### Step 2: Update Version Field | Before | After | |--------|-------| | `flowMCP: '1.2.0'` | `version: '2.0.0'` | The `version` field moves inside `main` and follows semver starting with `2.`. --- ### Step 3: Convert Imports to Shared List References **Before (v1.2.0):** ```javascript import { evmChains } from '../_shared/evm-chains.mjs' export const schema = { namespace: 'etherscan', // ... handlers: { getContractAbi: { preRequest: async ( { struct, payload } ) => { const chain = evmChains .find( ( c ) => c.alias === payload.chainName ) // ... } } } } ``` **After (v2.0.0):** ```javascript export const main = { namespace: 'etherscan', // ... sharedLists: [ { ref: 'evmChains', version: '1.0.0' } ], routes: { /* ... */ } } export const handlers = ( { sharedLists, libraries } ) => ({ getContractAbi: { preRequest: async ( { struct, payload } ) => { const chain = sharedLists.evmChains .find( ( c ) => c.alias === payload.chainName ) // ... return { struct, payload } } } }) ``` Key changes: - Remove `import` statement entirely (zero imports policy) - Add `sharedLists` reference in `main` - Access list via `sharedLists.evmChains` (injected by factory function) - The list data is the same — only the access mechanism changes --- ### Step 4: Add Output Schemas (Optional but Recommended) New in v2.0.0 — add `output` to routes for predictable responses: ```javascript routes: { getContractAbi: { // ... existing fields ... output: { mimeType: 'application/json', schema: { type: 'object', properties: { abi: { type: 'string', description: 'Contract ABI as JSON string' } } } } } } ``` This step is optional in v2.0.0 but will become recommended in v2.1.0. --- ### Step 5: Run Security Scan After migration, run the security scan: ```bash flowmcp validate --security ``` This verifies: - No forbidden patterns in the file - `main` block is JSON-serializable - Handler constraints are met - Shared list references are valid --- ### Step 6: Run Validation ```bash flowmcp validate ``` Full validation checks: - Required fields present - Namespace format valid - Version format valid - Route count within limits (max 8) - Parameter definitions complete - Output schema valid (if present) - Async fields valid (if present) --- ## Automatic Migration Tool A CLI command assists with migration: ```bash flowmcp migrate ``` The tool: 1. Reads the v1.2.0 schema 2. Wraps fields in `main` block 3. Updates version field 4. Detects imports and suggests shared list conversions 5. Writes the v2.0.0 schema to a new file (`.v2.mjs`) 6. Runs validation on the new file **Important**: The migration tool does NOT auto-convert imports. It flags them and creates TODO comments: ```javascript // TODO: Convert import to shared list reference // Original: import { evmChains } from '../_shared/evm-chains.mjs' // Suggested: main.sharedLists: [{ ref: 'evmChains', version: '1.0.0' }] ``` --- ## Backward Compatibility | Feature | v1.2.0 Schema | v2.0.0 Schema | |---------|--------------|--------------| | Core v1.x runtime | Supported | Not supported | | Core v2.0 runtime | Supported (legacy mode) | Supported | | Core v3.0 runtime | Not supported | Supported (alias + warning) | **Legacy mode** in Core v2.0: - Detects v1.2.0 format (no `main` wrapper, has `flowMCP` field) - Internally wraps in `main` block at load-time - Emits deprecation warning: `WARN: Schema uses v1.2.0 format. Run "flowmcp migrate " to upgrade.` - All features work except: shared list references, output schema, groups, async --- ## Common Migration Issues | Issue | Cause | Fix | |-------|-------|-----| | `SEC001: Forbidden pattern "import"` | Import statement still present | Convert to `sharedLists` reference | | `VAL003: "flowMCP" is not a valid field` | Old version field | Change to `version` inside `main` | | `VAL007: Route count exceeds 8` | v1.2.0 allowed 10 routes | Split schema into two files | | `VAL012: Handler references undefined route` | Route name mismatch after refactor | Align handler keys with route keys | --- ## Migration Checklist Per schema: - [ ] Fields wrapped in `main` block - [ ] `flowMCP: '1.2.0'` changed to `version: '2.0.0'` inside `main` - [ ] `handlers` at top level (sibling of `main`) - [ ] All `import` statements removed - [ ] Imported data converted to `sharedLists` references - [ ] Handler code uses `sharedLists` via factory injection instead of imported variables - [ ] Security scan passes - [ ] Full validation passes - [ ] All routes still functional (manual or automated test) --- # FlowMCP Specification v3.0.0 — Validation Rules This document defines all validation rules enforced by `flowmcp validate`. Each rule has a code, severity, and description. --- ## Severity Levels | Severity | Description | Effect | |----------|-------------|--------| | `error` | Must fix before use | Schema cannot be loaded | | `warning` | Should fix | Schema loads with warnings | | `info` | Nice to have | Informational only | --- ## Schema Structure Rules | Code | Severity | Rule | |------|----------|------| | VAL001 | error | Schema must export `main` as named export | | VAL002 | error | `main` must be an object | | VAL003 | error | `main` must not contain unknown fields | | VAL004 | error | `handlers` (if exported) must be a function | | VAL005 | warning | `handlers` function must return an object with keys matching tool names | --- ## Main Block — Required Fields | Code | Severity | Rule | |------|----------|------| | VAL010 | error | `main.namespace` is required and must be a string | | VAL011 | error | `main.namespace` must match `^[a-z]+$` | | VAL012 | error | `main.name` is required and must be a string | | VAL013 | error | `main.description` is required and must be a string | | VAL014 | error | `main.version` is required and must match `^3\.\d+\.\d+$` (version `^2\.\d+\.\d+$` accepted with deprecation warning) | | VAL015 | error | `main.root` is required when `main.tools` is non-empty. Optional for resource-only or skill-only schemas. | | VAL016 | error | `main.tools` (or deprecated `main.routes`) must be an object. May be empty `{}` if `main.resources` or `main.skills` is defined. | | VAL017 | error | Schema must not define both `main.tools` and `main.routes` simultaneously | | VAL018 | warning | `main.routes` is deprecated. Use `main.tools` instead. | --- ## Main Block — Optional Fields | Code | Severity | Rule | |------|----------|------| | VAL020 | error | `main.docs` (if present) must be an array of strings | | VAL021 | error | `main.tags` (if present) must be an array of strings | | VAL022 | error | `main.requiredServerParams` (if present) must be an array of strings | | VAL023 | error | `main.headers` (if present) must be a plain object | | VAL024 | error | `main.sharedLists` (if present) must be an array of objects | | VAL025 | error | `main.requiredLibraries` (if present) must be an array of strings | | VAL026 | error | Each entry in `requiredLibraries` must be on the runtime allowlist | --- ## Tool Rules | Code | Severity | Rule | |------|----------|------| | VAL030 | error | Tool name must match `^[a-z][a-zA-Z0-9]*$` | | VAL031 | error | Maximum 8 tools per schema | | VAL032 | error | `tool.method` is required and must be `GET`, `POST`, `PUT`, or `DELETE` | | VAL033 | error | `tool.path` is required and must be a string starting with `/` | | VAL034 | error | `tool.description` is required and must be a string | | VAL035 | error | `tool.parameters` is required and must be an array | | VAL036 | warning | `tool.output` is recommended for new schemas | | VAL037 | info | `tool.async` is a reserved field (not executed in v3.0.0) | --- ## Parameter Rules | Code | Severity | Rule | |------|----------|------| | VAL040 | error | Each parameter must have `position` and `z` objects | | VAL041 | error | `position.key` is required and must be a string | | VAL042 | error | `position.value` is required and must be a string | | VAL043 | error | `position.location` must be `insert`, `query`, or `body` | | VAL044 | error | `z.primitive` is required and must be a valid primitive type | | VAL045 | error | `z.options` must be an array of strings | | VAL046 | error | `enum()` values must not be empty | | VAL047 | error | Shared list interpolation `{{listName:fieldName}}` is only allowed in `enum()` | | VAL048 | error | Referenced shared list must be declared in `main.sharedLists` | | VAL049 | error | Referenced field must exist in the shared list's `meta.fields` | | VAL050 | error | `insert` parameters must have a corresponding `{{key}}` in `route.path` | --- ## Output Schema Rules | Code | Severity | Rule | |------|----------|------| | VAL060 | error | `output.mimeType` must be a supported MIME-Type | | VAL061 | error | `output.schema` must be a valid schema definition | | VAL062 | error | `output.schema.type` must match MIME-Type expectations | | VAL063 | warning | Nested depth should not exceed 4 levels | | VAL064 | error | `properties` is only valid when `type` is `object` | | VAL065 | error | `items` is only valid when `type` is `array` | --- ## Shared List Reference Rules | Code | Severity | Rule | |------|----------|------| | VAL070 | error | `sharedLists[].ref` is required and must be a string | | VAL071 | error | `sharedLists[].version` is required and must be semver | | VAL072 | error | Referenced list must exist in the list registry | | VAL073 | error | Referenced list version must match or be compatible | | VAL074 | error | `filter` (if present) must have valid `key` field | | VAL075 | warning | Unused shared list reference (not used by any parameter or handler) | --- ## Resource Rules | Code | Severity | Rule | |------|----------|------| | RES001 | error | `source` must be `'sqlite'`. No other values are accepted. | | RES002 | error | `description` must be a non-empty string. | | RES003 | error | `database` must be a relative path ending with `.db`. | | RES004 | error | `database` path must not contain `..` segments. | | RES005 | error | Maximum 2 resources per schema. | | RES006 | error | Maximum 4 queries per resource. | | RES007 | error | Each query must have a `sql` field of type string. | | RES008 | error | Each query must have a `description` field of type string. | | RES009 | error | Each query must have a `parameters` array. | | RES010 | error | Each query must have an `output` object with `mimeType` and `schema`. | | RES011 | error | Each query must have at least 1 test. | | RES012 | error | SQL statement must begin with `SELECT` (case-insensitive, after whitespace trim). | | RES013 | error | SQL statement must not contain blocked patterns: `ATTACH DATABASE`, `LOAD_EXTENSION`, `PRAGMA`, `CREATE`, `ALTER`, `DROP`, `INSERT`, `UPDATE`, `DELETE`, `REPLACE`, `TRUNCATE`. | | RES014 | error | Number of parameters must match number of `?` placeholders in the SQL statement. | | RES015 | error | Resource parameters must not have a `location` field in `position`. | | RES016 | error | Resource parameters must not use `{{SERVER_PARAM:...}}` values. | | RES017 | error | Resource name must match `^[a-z][a-zA-Z0-9]*$` (camelCase). | | RES018 | error | Query name must match `^[a-z][a-zA-Z0-9]*$` (camelCase). | | RES019 | error | Resource parameter primitives must be scalar: `string()`, `number()`, `boolean()`, or `enum()`. No `array()` or `object()`. | | RES020 | warning | Database file should exist at validation time. Missing file produces a warning, not an error. | | RES021 | error | `output.schema.type` must be `'array'` for resource queries. | | RES022 | error | Test parameter values must pass the corresponding `z` validation. | | RES023 | error | Test objects must be JSON-serializable. | See `13-resources.md` for the complete resource specification. --- ## Skill Rules ### Structural Rules (Static Validation) | Code | Severity | Rule | |------|----------|------| | SKL001 | error | Skill file must export `skill` as a named export | | SKL002 | error | `skill.name` is required, must be a string, must match `^[a-z][a-z0-9-]{0,63}$` | | SKL003 | error | `skill.name` must match the key in `main.skills` that references this file | | SKL004 | error | `skill.version` is required and must be `'flowmcp-skill/1.0.0'` | | SKL005 | error | Each entry in `requires.tools` must exist as a key in `main.tools` | | SKL006 | error | Each entry in `requires.resources` must exist as a key in `main.resources` | | SKL007 | error | `skill.description` is required, must be a string, maximum 1024 characters | | SKL008 | error | Each `{{input:key}}` placeholder in `content` must have a matching entry in `skill.input` | | SKL009 | error | `input[].values` is required when `type` is `'enum'` and forbidden otherwise | | SKL010 | error | `skill.content` is required and must be a non-empty string | | SKL011 | error | `skill.output` is required and must be a non-empty string | | SKL012 | error | `input[].key` must match `^[a-z][a-zA-Z0-9]*$` (camelCase) | | SKL013 | error | `input[].type` must be one of: `string`, `number`, `boolean`, `enum` | | SKL014 | error | `input[].description` is required and must be a non-empty string | | SKL015 | error | `input[].required` must be a boolean | | SKL016 | error | `main.skills` entries: `file` must end with `.mjs` | | SKL017 | error | `main.skills` entries: referenced file must exist | | SKL018 | error | Maximum 4 skills per schema | ### Reference Rules (Cross-Validation) | Code | Severity | Rule | |------|----------|------| | SKL020 | warning | `{{tool:name}}` placeholder in content references a tool not listed in `requires.tools` | | SKL021 | warning | `{{resource:name}}` placeholder in content references a resource not listed in `requires.resources` | | SKL022 | error | `{{skill:name}}` placeholder references a skill not in `main.skills` | | SKL023 | error | `{{skill:name}}` target skill must not itself contain `{{skill:...}}` placeholders (1 level deep only) | | SKL024 | warning | Entry in `requires.tools` is not referenced via `{{tool:...}}` in content | | SKL025 | warning | Entry in `requires.resources` is not referenced via `{{resource:...}}` in content | See `14-skills.md` for the complete skill specification. --- ## `dependsOn` / `requires` Cross-Checking Rules | Code | Severity | Rule | |------|----------|------| | DEP001 | error | `requires.tools` entries in a skill must reference tools that exist in the same schema's `main.tools` | | DEP002 | error | `requires.resources` entries in a skill must reference resources that exist in the same schema's `main.resources` | | DEP003 | warning | Skill references a tool via `{{tool:name}}` in content but does not list it in `requires.tools` | | DEP004 | warning | Skill references a resource via `{{resource:name}}` in content but does not list it in `requires.resources` | --- ## Async (Task) Rules Async fields are reserved for future versions. If present, they are ignored by the runtime. No validation errors are raised for `async` fields in v3.0.0. --- ## Security Rules | Code | Severity | Rule | |------|----------|------| | SEC001 | error | Forbidden pattern found in schema file — no `import` statements allowed (see [05-security.md](./05-security.md)) | | SEC002 | error | `main` block contains non-serializable value (function, symbol, etc.) | | SEC003 | error | Shared list file contains forbidden pattern | | SEC004 | error | Shared list file contains executable code | | SEC005 | error | `requiredLibraries` contains unapproved package | --- ## Shared List Validation Rules | Code | Severity | Rule | |------|----------|------| | LST001 | error | List must export `list` as named export | | LST002 | error | `list.meta.name` is required and must be unique | | LST003 | error | `list.meta.version` is required and must be semver | | LST004 | error | `list.meta.fields` is required and must be a non-empty array | | LST005 | error | Each field must have `key`, `type`, and `description` | | LST006 | error | `list.entries` is required and must be a non-empty array | | LST007 | error | Each entry must have all required fields | | LST008 | error | Entry field types must match `meta.fields` type declarations | | LST009 | error | `dependsOn` references must resolve to existing lists | | LST010 | error | Circular dependencies are forbidden | | LST011 | error | Maximum dependency depth: 3 levels | --- ## Agent Validation Rules | Code | Severity | Rule | |------|----------|------| | AGT001 | error | `name` is required, must match `^[a-z][a-z0-9-]*$` | | AGT002 | error | `description` is required, must be a non-empty string | | AGT003 | error | `model` is required, must contain `/` (OpenRouter syntax) | | AGT004 | error | `version` must be `flowmcp/3.0.0` | | AGT005 | error | `systemPrompt` is required, must be a non-empty string | | AGT006 | error | `tools[]` is required, must be a non-empty array | | AGT007 | error | Each tool reference must be a valid ID format (`namespace/type/name`) | | AGT008 | error | `tests[]` is required, minimum 3 tests | | AGT009 | error | Each test must have an `input` field of type string | | AGT010 | error | Each test must have an `expectedTools` field as a non-empty array | | AGT011 | error | Each `expectedTools` entry must be a valid ID (contains `/`) | | AGT012 | warning | Tests should cover different tool combinations | | AGT013 | error | `prompts` (if present) must be an Object (not Array) | | AGT014 | error | `skills` (if present) must be an Object (not Array) | | AGT015 | error | `resources` (if present) must be an Object (not Array) | | AGT016 | error | Referenced prompt/skill files must exist and be `.mjs` files | | AGT017 | error | Prompt files must have `export const prompt` (with `content` or `contentFile`) | | AGT018 | error | Skill files must have `export const skill` (with `name`, `version`, `content`/`contentFile`, `requires`, `input`, `output`) | See `06-agents.md` for the complete agent specification. --- ## Prompt Validation Rules | Code | Severity | Rule | |------|----------|------| | PRM001 | error | `name` is required, must be a string, must match `^[a-z][a-z0-9-]*$` | | PRM002 | error | `version` is required and must be `'flowmcp-prompt/1.0.0'` | | PRM003 | error | Exactly one of `namespace` or `agent` must be set (not both, not neither) | | PRM004 | error | `testedWith` is required when `agent` is set, forbidden when `namespace` is set | | PRM005 | error | `testedWith` value must contain `/` (OpenRouter model ID format) | | PRM006 | error | Each `dependsOn` entry must resolve to an existing tool in the catalog | | PRM007 | error | Each `references[]` entry must resolve to an existing prompt in the catalog | | PRM008 | error | Referenced prompts must not themselves have `references[]` (one level deep only) | | PRM009 | error | `{{type:name}}` references in `content` must resolve to registered primitives (see PH002) | | PRM010 | error | `content` OR `contentFile` must be present (XOR — exactly one must be set) | See `12-prompt-architecture.md` for the complete prompt specification. --- ## Catalog Validation Rules | Code | Severity | Rule | |------|----------|------| | CAT001 | error | `registry.json` must exist in the catalog root directory | | CAT002 | error | `name` field must match the catalog directory name | | CAT003 | error | All `shared[].file` paths must resolve to existing files | | CAT004 | error | All `schemas[].file` paths must resolve to existing files | | CAT005 | error | All `agents[].manifest` paths must resolve to existing files | | CAT006 | warning | Orphaned files (exist in the catalog directory but are not listed in `registry.json`) should be reported | | CAT007 | error | `schemaSpec` must be a valid FlowMCP specification version | See `15-catalog.md` for the complete catalog specification. --- ## ID Validation Rules | Code | Severity | Rule | |------|----------|------| | ID001 | error | ID must contain at least one `/` separator | | ID002 | error | Namespace must match `^[a-z][a-z0-9-]*$` | | ID003 | error | ResourceType (if present) must be one of: `tool`, `resource`, `prompt`, `list` | | ID004 | error | Name must not be empty | | ID005 | warning | Short form should only be used in unambiguous contexts | | ID006 | error | Full form is required in `registry.json` and validation rules | See `16-id-schema.md` for the complete ID schema specification. --- ## Placeholder Validation Rules | Code | Severity | Rule | |------|----------|------| | PH001 | error | `{{type:name}}` content must not be empty | | PH002 | error | References (content containing `/`) must resolve to a registered tool, resource, or prompt in the catalog | | PH003 | error | Parameter names (content without `/`) must match `^[a-zA-Z][a-zA-Z0-9]*$` | | PH004 | error | `{{type:name}}` placeholders are only valid in prompt/skill `content` fields, not in schema `main` blocks | See `02-parameters.md` for the complete parameter and placeholder specification. --- ## Test Requirements | Code | Severity | Rule | |------|----------|------| | TST001 | error | Each tool must have at least 3 tests. Each resource query must have at least 3 tests. Each agent must have at least 3 tests. | | TST002 | error | Each test must have a `_description` field of type string | | TST003 | error | Each test must provide values for all required `{{USER_PARAM}}` parameters | | TST004 | error | Test parameter values must pass the corresponding `z` validation | | TST005 | error | Test objects must be JSON-serializable (no functions, no Date, no undefined) | | TST006 | error | Test objects must only contain keys that match `{{USER_PARAM}}` parameter keys or `_description` | | TST007 | warning | Tools/queries with enum or chain parameters should have tests covering multiple enum values | | TST008 | info | Consider adding tests that demonstrate optional parameter usage | | TST009 | error | Each agent test must have an `input` field of type string | | TST010 | error | Each agent test must have an `expectedTools` field as non-empty array | | TST011 | error | Each expectedTools entry must be a valid ID (contains `/`) | | TST012 | warning | Agent tests should cover different tool combinations | | TST013 | info | Consider adding expectedContent assertions for richer validation | See `10-tests.md` for the complete test specification including format, design principles, and the response capture lifecycle. --- ## Validation Output Format The CLI displays results grouped by severity with the rule code, severity, location, and description: ``` flowmcp validate etherscan/contracts.mjs VAL014 error main.version: Must match ^3\.\d+\.\d+$ (found "1.2.0") VAL031 error tools: Maximum 8 tools exceeded (found 10) VAL036 warning getContractAbi: output schema is recommended TST001 warning getContractAbi: No tests found 2 errors, 2 warnings Schema cannot be loaded (has errors) ``` When all rules pass: ``` flowmcp validate etherscan/contracts.mjs 0 errors, 0 warnings Schema is valid ``` With security flag: ``` flowmcp validate --security etherscan/contracts.mjs SEC001 error Line 3: Forbidden pattern "import" detected SEC002 error main.handlers.preRequest: Non-serializable value (function) 2 errors, 0 warnings Schema cannot be loaded (has errors) ``` --- # FlowMCP Specification v3.0.0 — Tests Tests are executable examples embedded in tool and resource query definitions. For agents, tests are prompts with expected tool usage and content assertions. They serve three purposes: they document what a tool or resource query can do, they provide the input data needed to capture real responses, and those captured responses are the basis for generating accurate output schemas. This document defines the test format for both tools and resources, design principles, the response capture lifecycle, and validation rules. --- ## Purpose Without tests, a tool or resource query is a black box. The `description` field says what it does in prose, the `parameters` array says what inputs it accepts — but neither shows a concrete usage example with real values. Tests fill this gap. ```mermaid flowchart LR A[Tests] --> B[Learning: what is possible] A --> C[Execution: call API or query DB] A --> D[Agent Tests: verify tool usage] C --> E[Capture: store response] E --> F[Generate: output schema] D --> G[Verify: expectedTools + expectedContent] ``` The diagram shows the five roles of a test: teaching consumers what the tool or resource can do, executing real API calls (for tools) or database queries (for resources), capturing the response, generating the output schema from that response, and — for agents — verifying that the correct tools are invoked and that responses contain expected content. ### Tests as Learning Instrument A developer or AI agent reading a schema should understand a tool's or resource query's capabilities **by reading the tests alone**. Well-designed tests express the breadth of what the tool or query can do — different parameter combinations, edge cases, different data categories. They are not regression tests — they are **executable documentation**. ### Tests as Output Schema Source Output schemas describe the shape of `data` in a successful response. The only reliable source for this shape is a **real API response**. Tests provide the parameter values needed to make real API calls. The captured responses are fed into the `OutputSchemaGenerator` to produce accurate output schemas. ```mermaid flowchart TD A[Test] -->|parameter values| B[API Call or DB Query] B -->|real response| C[Response Capture] C -->|JSON structure| D[OutputSchemaGenerator] D -->|schema definition| E[output.schema in main block] ``` The diagram shows how tests feed the output schema generation pipeline: test parameters drive real API calls, responses are captured, and the generator derives the output schema from the actual data structure. --- ## Tool Test Format Tests are defined as an array inside each tool, alongside `method`, `path`, `description`, `parameters`, and `output`: ```javascript tools: { getSimplePrice: { method: 'GET', path: '/simple/price', description: 'Fetch current price for one or more coins', parameters: [ { position: { key: 'ids', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'array()', options: [] } }, { position: { key: 'vs_currencies', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: [] } } ], tests: [ { _description: 'Single coin in single currency', ids: ['bitcoin'], vs_currencies: 'usd' }, { _description: 'Multiple coins in multiple currencies', ids: ['bitcoin', 'ethereum', 'solana'], vs_currencies: 'usd,eur,gbp' } ], output: { /* ... */ } } } ``` The `tests` array is part of the `main` block and therefore JSON-serializable. It must not contain functions, Date objects, or any non-serializable values. --- ## Test Fields Each test object contains `_description` and parameter values: | Field | Type | Required | Description | |-------|------|----------|-------------| | `_description` | `string` | Yes | Human-readable explanation of what this test demonstrates | | `{paramKey}` | matches parameter type | Yes (per required param) | Value for each `{{USER_PARAM}}` parameter, keyed by the parameter's `position.key` | ### `_description` The description explains **what this specific test demonstrates** — not what the tool does (that is the tool's `description`), but what this particular parameter combination shows. ```javascript // Good — explains the specific scenario { _description: 'ERC-20 token on Ethereum mainnet', ... } { _description: 'Native token on Layer 2 chain (Arbitrum)', ... } { _description: 'Wallet with high transaction volume', ... } // Bad — generic, does not explain what makes this test different { _description: 'Test getTokenPrice', ... } { _description: 'Basic test', ... } { _description: 'Test 1', ... } ``` ### Parameter Values Each `{{USER_PARAM}}` parameter's `position.key` becomes a key in the test object. The value must pass the parameter's `z` validation: ```javascript // Parameter definition { position: { key: 'chain_id', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'number()', options: ['min(1)'] } } // Test value — must be a number >= 1 { _description: 'Ethereum mainnet', chain_id: 1 } ``` Optional parameters (`optional()` or `default()` in z options) may be omitted from test objects. If omitted, the runtime uses the default value or excludes the parameter. Fixed parameters (value is not `{{USER_PARAM}}`) and server parameters (`{{SERVER_PARAM:...}}`) are **never included in test objects** — they are handled automatically by the runtime. --- ## Design Principles ### 1. Express the Breadth Tests should cover the **range of what is possible** with the tool or resource query. A tool that accepts a chain ID should test multiple chains. A tool that accepts different asset types should test each type. The goal is not exhaustive coverage but representative variety. ```javascript // Good — shows breadth of chains and token types tests: [ { _description: 'ERC-20 token on Ethereum', chain_id: 1, contract: '0x6982508145454Ce325dDbE47a25d4ec3d2311933' }, { _description: 'Native token on Polygon', chain_id: 137, contract: '0x0000000000000000000000000000000000001010' }, { _description: 'Stablecoin on Arbitrum', chain_id: 42161, contract: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831' } ] // Bad — repetitive, same pattern three times tests: [ { _description: 'Test token 1', chain_id: 1, contract: '0xaaa...' }, { _description: 'Test token 2', chain_id: 1, contract: '0xbbb...' }, { _description: 'Test token 3', chain_id: 1, contract: '0xccc...' } ] ``` ### 2. Teach Through Examples Someone reading the tests should learn what the tool or resource query can do without reading the documentation. Each test teaches one capability or variation: ```javascript // The tests teach: this route handles single/multiple coins, different currencies, // and can mix coin types tests: [ { _description: 'Single coin in USD', ids: ['bitcoin'], vs_currencies: 'usd' }, { _description: 'Multiple coins in single currency', ids: ['bitcoin', 'ethereum'], vs_currencies: 'eur' }, { _description: 'Single coin in multiple currencies', ids: ['solana'], vs_currencies: 'usd,eur,gbp' } ] ``` ### 3. No Personal Data Tests must never contain personal data. All test values must be **publicly known, verifiable, and non-sensitive**: | Allowed | Not Allowed | |---------|-------------| | Public smart contract addresses | Private wallet addresses with real holdings | | Well-known token contracts (USDC, WETH) | Personal wallet addresses | | Public blockchain data (block numbers, tx hashes) | Email addresses, names, phone numbers | | Standard chain IDs (1, 137, 42161) | API keys, tokens, passwords | | Published document IDs (government open data) | Session tokens, auth credentials | | Generic search keywords | Personal identifiers | ```javascript // Good — public, well-known contract addresses { _description: 'USDC on Ethereum', contract: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' } // Bad — personal wallet { _description: 'My wallet', address: '0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18' } ``` ### 4. Reproducible Results Tests should produce **consistent, verifiable results** when executed. Prefer: - Well-established tokens/contracts over newly deployed ones - Historical data queries (specific block numbers) over latest-block queries when possible - Stable endpoints over experimental ones The API response may change over time (prices update, new data appears), but the **structure** of the response should remain stable. This structural stability is what makes captured responses useful for output schema generation. --- ## Test Count Guidelines ### Tool Test Count | Scenario | Minimum | Recommended | |----------|---------|-------------| | Tool with no parameters | 3 | 3 | | Tool with 1-2 parameters | 3 | 3-5 | | Tool with enum/chain parameters | 3 | 4-6 (different enum values) | | Tool with multiple optional parameters | 3 | 4-6 (with/without optionals) | **Minimum: 3 tests per tool is required.** A tool with fewer than 3 tests is a validation error (TST001). Three tests ensure breadth coverage: one basic case, one edge case, and one cross-cutting case. The recommended count depends on the parameter variety — more diverse parameters benefit from more tests. ### Resource Query Test Count | Scenario | Minimum | Recommended | |----------|---------|-------------| | Query with no parameters | 3 | 3 | | Query with 1-2 parameters | 3 | 3-5 | | Query with enum parameters | 3 | 4-6 (different enum values) | **Minimum: 3 tests per resource query is required.** Three tests ensure breadth coverage: one basic case, one edge case, and one cross-cutting case. Resource query tests execute against the bundled database, so they are always runnable without API keys or network access. Results are deterministic. **Maximum: No hard limit**, but tests should be purposeful. Each test must demonstrate something different. Duplicate or near-duplicate tests waste execution time during response capture. --- ## Response Capture Lifecycle Tests are the starting point for a pipeline that ends with accurate output schemas: ```mermaid flowchart TD A[1. Read tests from tool/query] --> B[2. Resolve server params from env] B --> C[3. Execute API call per test] C --> D[4. Record full response with metadata] D --> E[5. Feed response to OutputSchemaGenerator] E --> F[6. Write output schema to definition] ``` The diagram shows the six-step lifecycle from test definition to output schema generation. ### Step 1: Read Tests The runtime reads the `tests` array from the tool or resource query definition and extracts parameter values for each test. ### Step 2: Resolve Server Params If the schema has `requiredServerParams`, the corresponding environment variables are loaded. Tests that require API keys cannot run without the appropriate `.env` file. ### Step 3: Execute API Call For each test, the runtime constructs the full API request (URL, headers, body) using the test's parameter values and executes it. A delay between calls (default: 1 second) prevents rate limiting. ### Step 4: Record Response The full response is recorded with metadata: ```javascript { namespace: 'coingecko', routeName: 'getSimplePrice', testIndex: 0, _description: 'Single coin in USD', userParams: { ids: ['bitcoin'], vs_currencies: 'usd' }, responseTime: 234, timestamp: '2026-02-17T10:30:00Z', response: { status: true, messages: [], data: { /* actual API response after handler transformation */ } } } ``` The `response.data` field contains the data **after handler transformation** (postRequest, executeRequest). This is critical because the output schema describes the final shape, not the raw API response. ### Step 5: Generate Output Schema The `OutputSchemaGenerator` analyzes the `response.data` structure and produces a schema definition: ```javascript // From response.data: [{ id: 'bitcoin', prices: { usd: 45000 } }] // Generator produces: { type: 'array', items: { type: 'object', properties: { id: { type: 'string', description: '' }, prices: { type: 'object', description: '', properties: { usd: { type: 'number', description: '' } } } } } } ``` ### Step 6: Write Output Schema The generated schema is written back into the tool's or query's `output` field. If it already has an `output` schema, the captured response can be used to **validate** the existing schema against reality. --- ## Test Execution Modes ### Capture Mode All tests are executed against the real API. Responses are stored as files for inspection and output schema generation. This is the primary use case — run during schema development to build output schemas. ``` capture/ └── {timestamp}/ └── {namespace}/ ├── {routeName}-0.json ├── {routeName}-1.json └── metrics.json ``` ### Validation Mode Tests are executed and the actual response structure is compared against the declared `output.schema`. Mismatches produce warnings (non-blocking, per output schema spec). This mode verifies that output schemas remain accurate over time. ### Dry-Run Mode Tests are validated for correctness (parameter types, required fields, _description presence) without making API calls. Used during `flowmcp validate` to check test definitions statically. --- ## Connection to Output Schema The output schema (defined in `04-output-schema.md`) describes the `data` field of a successful response. Tests are the mechanism that produces the real data from which output schemas are derived. | Without Tests | With Tests | |---------------|------------| | Output schema must be written by hand | Output schema is generated from real responses | | Schema author guesses the response shape | Schema author captures the actual shape | | Output schema may drift from reality | Output schema is verified against reality | | No way to detect API changes | Response capture detects structural changes | **Tests and output schemas are complementary.** Tests provide the input, response capture provides the data, and the generator produces the schema. Maintaining one without the other is incomplete. --- ## Complete Example ### Tool Test Example A tool with well-designed tests that demonstrate breadth: ```javascript export const main = { namespace: 'chainlist', name: 'Chainlist Tools', description: 'Query EVM chain metadata from Chainlist', version: '3.0.0', root: 'https://chainlist.org/rpcs.json', tools: { getChainById: { method: 'GET', path: '/', description: 'Returns detailed information for a chain given its numeric chainId', parameters: [ { position: { key: 'chain_id', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'number()', options: ['min(1)'] } } ], tests: [ { _description: 'Ethereum mainnet — most widely used L1', chain_id: 1 }, { _description: 'Polygon PoS — popular L2 sidechain', chain_id: 137 }, { _description: 'Arbitrum One — optimistic rollup L2', chain_id: 42161 }, { _description: 'Base — Coinbase L2 on OP Stack', chain_id: 8453 } ], output: { mimeType: 'application/json', schema: { type: 'object', properties: { chainId: { type: 'number', description: 'Numeric chain identifier' }, name: { type: 'string', description: 'Human-readable chain name' }, nativeCurrency: { type: 'object', description: 'Native currency details', properties: { name: { type: 'string', description: 'Currency name' }, symbol: { type: 'string', description: 'Currency symbol' }, decimals: { type: 'number', description: 'Decimal places' } } }, rpc: { type: 'array', description: 'Available RPC endpoints' }, explorers: { type: 'array', description: 'Block explorer URLs' } } } } }, getChainsByKeyword: { method: 'GET', path: '/', description: 'Returns all chains matching a keyword substring', parameters: [ { position: { key: 'keyword', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: ['min(2)'] } }, { position: { key: 'limit', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'number()', options: ['min(1)', 'optional()'] } } ], tests: [ { _description: 'Search for Ethereum-related chains', keyword: 'Ethereum' }, { _description: 'Search for Binance chains with limit', keyword: 'BNB', limit: 3 }, { _description: 'Search for testnet chains', keyword: 'Sepolia' } ], output: { /* ... */ } } } } ``` ### What these tests demonstrate **`getChainById` tests teach:** - The tool works with well-known L1 chains (Ethereum) - It works with L2 sidechains (Polygon) - It works with rollup L2s (Arbitrum) - It works with newer OP Stack chains (Base) - All test values are public chain IDs — no personal data **`getChainsByKeyword` tests teach:** - The tool does substring matching on chain names - The `limit` parameter is optional (first test omits it) - Different keyword patterns produce different result sets - Testnet chains are also searchable --- ## Resource Query Tests Resource queries use the same test format as tools. Each test provides parameter values for a query execution against the bundled SQLite database. ### Resource Test Format ```javascript queries: { bySymbol: { sql: 'SELECT * FROM tokens WHERE symbol = ? COLLATE NOCASE', description: 'Find tokens by ticker symbol', parameters: [ { position: { key: 'symbol', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(1)' ] } } ], output: { /* ... */ }, tests: [ { _description: 'Well-known stablecoin (USDC)', symbol: 'USDC' }, { _description: 'Major L1 token (ETH)', symbol: 'ETH' }, { _description: 'Case-insensitive match (lowercase)', symbol: 'wbtc' } ] } } ``` ### Resource Test Differences from Tool Tests | Aspect | Tool Tests | Resource Tests | |--------|-----------|----------------| | Execution target | External API over network | Local SQLite database | | API keys required | Often yes (`requiredServerParams`) | Never | | Network required | Yes | No | | Result determinism | Response may vary over time | Always deterministic (bundled data) | | Test count minimum | 3 per tool | 3 per query | | Server params in test | Never included | Not applicable | ### Resource Test Design Principles The same four design principles apply (express breadth, teach through examples, no personal data, reproducible results). Resource tests have an additional advantage: **results are always reproducible** because the data is bundled in the `.db` file. Tests serve as documentation of what data the database contains. ```javascript // Good — shows different lookup strategies and data coverage tests: [ { _description: 'Well-known stablecoin (USDC)', symbol: 'USDC' }, { _description: 'Major DeFi token (UNI)', symbol: 'UNI' }, { _description: 'Case-insensitive match (lowercase)', symbol: 'weth' } ] // Bad — only one test, does not show data variety tests: [ { _description: 'Test query', symbol: 'ETH' } ] ``` See `13-resources.md` for the complete resource specification including query definitions, parameter binding, and handler integration. --- ## Agent Tests Agent tests validate end-to-end behavior at the agent level. Instead of testing individual tool calls with parameter values, agent tests provide a **natural language prompt** and assert which tools the agent should invoke and what content the response should contain. ### Agent Test Format ```json { "tests": [ { "_description": "Basic token lookup", "input": "What is the current price of Ethereum?", "expectedTools": ["coingecko-com/tool/simplePrice"], "expectedContent": ["current price", "24h change"] } ] } ``` ### Agent Test Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `_description` | `string` | Yes | What this test demonstrates | | `input` | `string` | Yes | Natural language prompt (as a user would ask) | | `expectedTools` | `string[]` | Yes | Tool IDs that should be called (deterministic check) | | `expectedContent` | `string[]` | No | Content assertions against response text | The `input` field contains a prompt exactly as a human user would type it. The `expectedTools` array lists the tool IDs (in `namespace/tool/name` format) that the agent must invoke to answer the prompt. The optional `expectedContent` array contains substrings that the final response text must include (case-insensitive matching). ### Three-Level Test Model Agent test validation operates on three levels, each with a different degree of determinism: ```mermaid flowchart LR T[Agent Test] --> A[Tool Usage — deterministic] T --> B[Content — assertions] T --> C[Quality — manual/LLM-as-Judge] ``` | Level | Checkable | Method | |-------|-----------|--------| | Tool Usage | Yes — deterministic | expectedTools[] against actual tool calls | | Content | Partially — assertions | expectedContent[] against response text (case-insensitive) | | Quality | No — subjective | Human review or LLM-as-Judge | **Tool Usage** is the strongest assertion. Given a well-scoped prompt, the set of tools an agent should call is deterministic. If the prompt is "What is the current price of Ethereum?", the agent must call the price tool — there is no ambiguity. **Content** assertions are semi-deterministic. The response text will vary across LLM runs, but certain factual elements (like "current price" or "24h change") should always appear if the agent answered correctly. **Quality** is subjective and cannot be validated by the spec runtime. It is included in the model for completeness — teams may use LLM-as-Judge or human review to evaluate response quality beyond the scope of automated checks. ### Consistency: Tool Tests vs Agent Tests | Aspect | Tool Test | Agent Test | |--------|-----------|------------| | Minimum | 3 | 3 | | `_description` | Required | Required | | Input | Parameter values | Natural language prompt | | Output | API response (deterministic) | Tool calls + text (partially deterministic) | | Validation | Schema match | expectedTools + expectedContent | Both test types share the same `_description` convention and the same minimum of 3 tests. The difference is in **what they test**: tool tests validate a single API call with concrete parameter values, while agent tests validate the agent's ability to select and orchestrate the right tools for a given user intent. ### Complete Agent Test Example A crypto-research agent with three tests demonstrating breadth: ```json { "tests": [ { "_description": "Single token price lookup — basic case", "input": "What is the current price of Ethereum?", "expectedTools": ["coingecko-com/tool/simplePrice"], "expectedContent": ["current price", "USD"] }, { "_description": "Cross-chain token comparison — edge case with multiple tools", "input": "Compare the TVL of Aave on Ethereum vs Arbitrum", "expectedTools": [ "defillama-com/tool/getProtocolTvl", "defillama-com/tool/getProtocolChainTvl" ], "expectedContent": ["TVL", "Ethereum", "Arbitrum"] }, { "_description": "Wallet analysis — cross-cutting case combining on-chain data", "input": "Show me the top 5 token holdings in vitalik.eth", "expectedTools": [ "etherscan-io/tool/getTokenBalances", "coingecko-com/tool/simplePrice" ], "expectedContent": ["token", "balance"] } ] } ``` **What these tests demonstrate:** - **Test 1** (basic case): A simple single-tool lookup. Validates that the agent correctly routes a straightforward price question to the CoinGecko price tool. - **Test 2** (edge case): A comparative question requiring multiple calls to the same provider. Validates that the agent can decompose a comparison into separate data fetches. - **Test 3** (cross-cutting case): A question that requires data from multiple providers (on-chain balances + price data). Validates that the agent can orchestrate tools across different namespaces. --- ## Validation Rules | Code | Severity | Rule | |------|----------|------| | TST001 | error | Each tool must have at least 3 tests. Each resource query must have at least 3 tests. Each agent must have at least 3 tests. | | TST002 | error | Each test must have a `_description` field of type string | | TST003 | error | Each test must provide values for all required `{{USER_PARAM}}` parameters | | TST004 | error | Test parameter values must pass the corresponding `z` validation | | TST005 | error | Test objects must be JSON-serializable (no functions, no Date, no undefined) | | TST006 | error | Test objects must only contain keys that match `{{USER_PARAM}}` parameter keys or `_description` | | TST007 | warning | Tools/queries with enum or chain parameters should have tests covering multiple enum values | | TST008 | info | Consider adding tests that demonstrate optional parameter usage | | TST009 | error | Each agent test must have an `input` field of type string | | TST010 | error | Each agent test must have an `expectedTools` field as non-empty array | | TST011 | error | Each expectedTools entry must be a valid ID (contains `/`) | | TST012 | warning | Agent tests should cover different tool combinations | | TST013 | info | Consider adding expectedContent assertions for richer validation | --- # FlowMCP Specification v2.0.0 — Preload & Caching This document defines the optional `preload` field on route level. It signals that a route returns a static or slow-changing dataset and that the runtime may cache the response locally. --- ## Motivation Some API endpoints return complete, rarely changing datasets (e.g. all hospitals in Germany, all memorial stones in Berlin). Fetching these on every call wastes bandwidth and time. The `preload` field lets schema authors declare caching intent so the CLI and other runtimes can cache responses transparently. --- ## The `preload` Field `preload` is an **optional object** on route level, alongside `method`, `path`, `description`, `parameters`, `output`, and `tests`. ```javascript routes: { getLocations: { method: 'GET', path: '/locations.json', description: 'All hospital locations in Germany', parameters: [], preload: { enabled: true, ttl: 604800, description: 'All hospital locations in Germany (~760KB)' }, output: { /* ... */ }, tests: [ /* ... */ ] } } ``` ### Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `enabled` | `boolean` | Yes | Whether caching is allowed for this route. Must be `true` to activate caching. | | `ttl` | `number` | Yes | Cache time-to-live in seconds. Must be a positive integer. | | `description` | `string` | No | Human-readable note shown on cache hit (e.g. dataset size, update frequency). | ### Semantics - **`enabled: true`** signals that the route's response is safe to cache. The runtime decides whether to actually cache (caching is always optional). - **`enabled: false`** explicitly disables caching even if present. Equivalent to omitting `preload` entirely. - **`ttl`** defines the maximum age of a cached response in seconds before it must be re-fetched. Common values: | TTL | Duration | Use Case | |-----|----------|----------| | `3600` | 1 hour | Frequently updated data | | `86400` | 1 day | Daily snapshots | | `604800` | 1 week | Weekly releases, semi-static data | | `2592000` | 30 days | Static reference data | --- ## Validation Rules These rules extend the existing validation rule set from `09-validation-rules.md`: | Code | Severity | Rule | |------|----------|------| | `VAL060` | error | If `preload` is present, it must be a plain object. | | `VAL061` | error | `preload.enabled` must be a boolean. | | `VAL062` | error | `preload.ttl` must be a positive integer (> 0). | | `VAL063` | warning | `preload.description` if present must be a string. | | `VAL064` | info | Routes with `preload.enabled: true` and no parameters are ideal cache candidates. | | `VAL065` | warning | Routes with `preload.enabled: true` and required parameters should document caching behavior — the cache key must include parameter values. | --- ## Cache Key When a route has parameters, the cache key must include the parameter values to avoid serving stale data for different inputs. The recommended cache key format is: ``` {namespace}/{routeName}/{paramHash}.json ``` Where `paramHash` is a deterministic hash of the sorted, JSON-serialized user parameters. For routes with no parameters (or only optional parameters that were omitted), the cache key simplifies to: ``` {namespace}/{routeName}.json ``` --- ## Runtime Behavior ### Cache Storage The recommended cache directory is `~/.flowmcp/cache/`. Each cached response is stored as a JSON file with metadata: ```json { "meta": { "fetchedAt": "2026-02-17T12:00:00.000Z", "expiresAt": "2026-02-24T12:00:00.000Z", "ttl": 604800, "size": 760123, "paramHash": null }, "data": { } } ``` ### Cache Flow ```mermaid flowchart TD A[flowmcp call tool-name] --> B{preload.enabled?} B -->|No| C[Normal fetch] B -->|Yes| D{Cache file exists?} D -->|No| E[Fetch + store in cache] D -->|Yes| F{Cache expired?} F -->|No| G[Return cached response] F -->|Yes| E E --> H[Return fresh response] ``` ### User Overrides Runtimes should support these override mechanisms: | Flag | Behavior | |------|----------| | `--no-cache` | Skip cache entirely, always fetch fresh | | `--refresh` | Fetch fresh and update cache | ### Cache Management Commands Runtimes should provide cache management: | Command | Description | |---------|-------------| | `cache status` | List all cached responses with size, age, expiry | | `cache clear` | Remove all cached responses | | `cache clear {namespace}` | Remove cached responses for a specific namespace | ### User Communication | Event | Message | |-------|---------| | Cache hit | `Cached (fetched: {date}, expires: {date})` | | Cache miss | `Fetching fresh data...` → `Cached for {ttl human}` | | Cache expired | `Cache expired, refreshing...` | | Force refresh | `Refreshing cache...` | --- ## Schema Author Guidelines ### When to Use Preload Use `preload` when: - The endpoint returns a complete, static or slow-changing dataset - The response is larger than ~10KB - The data doesn't change based on time-of-day or real-time events - Multiple calls with the same parameters return identical results ### When NOT to Use Preload Do not use `preload` when: - The data changes frequently (live prices, real-time feeds) - The response depends on authentication state - The endpoint has rate limits that make caching counterproductive (use rate limiting instead) ### TTL Selection Guide ```mermaid flowchart TD A[How often does this data change?] --> B{Daily or more?} B -->|Yes| C[ttl: 3600-86400] B -->|No| D{Weekly?} D -->|Yes| E[ttl: 604800] D -->|No| F{Monthly or less?} F -->|Yes| G[ttl: 2592000] F -->|No| H[Don't use preload] ``` --- ## Interaction with Other Features ### Handlers Handlers (`preRequest`, `postRequest`) still execute on cached data. The cache stores the raw API response before `postRequest` transformation. This ensures handler logic always runs on the most appropriate data format. **Alternative (simpler):** Cache the final transformed response after `postRequest`. This avoids re-running handlers on every cache hit but requires cache invalidation when handler logic changes. Runtimes should document which approach they use. ### Tests Route tests (`10-route-tests.md`) always bypass the cache to ensure they test the live API. The `--no-cache` flag is implied during test execution. ### Output Schema The output schema (`04-output-schema.md`) describes the response shape regardless of whether the response comes from cache or a live fetch. Caching does not affect the output contract. --- # FlowMCP Specification v3.0.0 — Prompt Architecture FlowMCP uses a two-tier prompt system to bridge deterministic tools with non-deterministic AI orchestration. **Provider-Prompts** explain how to use a single provider's tools effectively. **Agent-Prompts** compose tools from multiple providers into tested workflows. Both types use the `{{type:name}}` placeholder syntax for references and parameters. Provider-Prompts are defined in `main.prompts` with content loaded from external `.mjs` files via `contentFile`. Agent-Prompts are standalone `.mjs` files with `export const prompt = { ... }` containing inline content. --- ## Purpose Individual tools are deterministic — same input, same API call. But real-world tasks rarely involve a single tool. Analyzing a token requires price data from CoinGecko, on-chain metrics from Etherscan, and TVL data from DeFi Llama. An agent needs to know not just which tools exist, but in what order to call them, how to pass results between steps, and when to fall back to alternative providers. Prompts encode this knowledge. They are the non-deterministic layer that teaches LLMs **tool combinatorics** — which tools to call in which order, how to chain outputs to inputs, and what alternatives exist when a provider is unavailable. This is knowledge that would take hours to figure out manually by reading API docs and experimenting with endpoints. ```mermaid flowchart TD A[FlowMCP Prompts] --> B[Provider-Prompts] A --> C[Agent-Prompts] B --> D["Single namespace
Model-neutral
Any LLM can use them"] C --> E["Multi-provider
Model-specific
Tested with one LLM"] D --> F["How to use CoinGecko tools effectively"] E --> G["How to combine CoinGecko + Etherscan + DeFi Llama"] ``` The diagram shows the two tiers: Provider-Prompts are scoped to one namespace and work with any model. Agent-Prompts span multiple providers and are optimized for a specific model. --- ## Provider-Prompt vs Agent-Prompt The two prompt types serve different purposes and operate at different levels of the architecture. | Aspect | Provider-Prompt | Agent-Prompt | |--------|----------------|--------------| | Scope | Single namespace | Multi-provider | | Model dependency | Model-neutral — works with any LLM | Model-specific — tested against a particular LLM | | Scoping field | `namespace` | `agent` | | Tested with | Any model (no `testedWith` field) | Specific model (`testedWith` required) | | Location in catalog | `providers/{namespace}/prompts/` | `agents/{agent-name}/prompts/` | | Tool references | Own namespace tools only (bare names in `dependsOn`) | Tools from any provider (full ID format in `dependsOn`) | | Primary use case | Teach effective use of one provider's API | Orchestrate multi-provider workflows | ### When to Use Which **Provider-Prompts** are the right choice when the instructions are about a single API — how to paginate CoinGecko results, how to interpret Etherscan's response codes, or how to combine two endpoints from the same provider for richer data. **Agent-Prompts** are the right choice when the instructions span multiple providers — combining CoinGecko pricing with Etherscan contract data and DeFi Llama TVL metrics into a unified analysis workflow. --- ## Provider-Prompt vs Agent-Prompt: Structural Difference Provider-Prompts and Agent-Prompts differ structurally in how they handle content: | Aspect | Provider-Prompt | Agent-Prompt | |--------|----------------|--------------| | **Definition location** | In `main.prompts` (schema file) | Standalone `.mjs` file | | **Content location** | External file via `contentFile` | Inline in the file (`content` field) | | **Export** | Part of `main` export | `export const prompt = { ... }` | **Why the split?** Provider-Prompt definitions are compact metadata that belongs in the schema's `main` block (hashable, JSON-serializable). The actual prompt content is long text that would clutter the schema — so it lives in a separate file. Agent-Prompts are already standalone files, so content stays inline. ### Why `.mjs` Files Prompt files (both content files and Agent-Prompt files) use the same `.mjs` format as schema and skill files for three reasons: 1. **Consistent loading.** The runtime loads prompts via `import()` — the same mechanism used for schemas and skills. No separate parser needed. 2. **Static security scanning.** The same `SecurityScanner` that checks schema files also checks prompt files. The zero-import policy applies uniformly. 3. **Multiline content.** Template literals handle multiline prompt content naturally without escaping issues. --- ## Provider-Prompt Format A Provider-Prompt is scoped to a single namespace. It describes how to use that provider's tools effectively, without assuming any specific LLM model. The definition lives in `main.prompts`, and the content is loaded from an external file via `contentFile`. ### Definition in `main.prompts` ```javascript export const main = { namespace: 'coingecko', tools: { /* ... */ }, resources: { /* ... */ }, prompts: { priceComparison: { name: 'price-comparison', version: 'flowmcp-prompt/1.0.0', namespace: 'coingecko', description: 'Compare prices across multiple coins using CoinGecko data', dependsOn: [ 'simplePrice', 'coinMarkets' ], references: [], contentFile: './prompts/price-comparison.mjs' } } } ``` ### Content File The `contentFile` field points to a separate `.mjs` file that exports the prompt content: ```javascript // prompts/price-comparison.mjs export const content = ` Use {{tool:coingecko/simplePrice}} to fetch current prices for the requested coins. Then use {{tool:coingecko/coinMarkets}} to get market cap data. Compare the following metrics for {{input:coins}}: - Current price in {{input:currency}} - 24h price change - Market cap ranking If the user asks for historical data, note that simplePrice only returns current prices. Suggest using coinMarkets with the order parameter for trending analysis. ` ``` **Content file rules:** - Export must be `export const content` (not `export const prompt`) - No imports allowed (zero-import policy) - `{{type:name}}` placeholder syntax for references and parameters - Content must not be empty ### Provider-Prompt Characteristics - **`namespace` field** identifies the provider. Must match the provider's namespace in the catalog. - **`dependsOn` uses bare tool names** — since the scope is a single namespace, fully qualified IDs are unnecessary. `'simplePrice'` is sufficient because it can only refer to `coingecko/tool/simplePrice`. - **`references` array** lists other prompts to compose. Empty array when none. Required field. - **`contentFile` field** is a relative path to the `.mjs` file containing the prompt content. - **No `testedWith` field** — Provider-Prompts are model-neutral. Any LLM can benefit from them. - **No `agent` field** — the `namespace` field indicates this is a Provider-Prompt. - **`{{type:name}}` references** in content use the type prefix to distinguish tool references (`{{tool:coingecko/simplePrice}}`), resource references (`{{resource:name}}`), and input parameters (`{{input:coins}}`). References must target tools within the same namespace. --- ## Agent-Prompt Format An Agent-Prompt is scoped to an agent. It describes multi-provider workflows and is tested against a specific LLM model. ```javascript const content = ` First, get the contract details using {{tool:etherscan/getContractAbi}} for address {{input:address}}. Then fetch pricing data using {{tool:coingecko/simplePrice}}. For price comparison context, follow the approach in {{prompt:coingecko/price-comparison}}. Analyze the {{input:token}} considering: - Contract verification status - Current price and volume - Historical price trends If Etherscan returns an unverified contract, skip the ABI analysis and focus on the pricing data. Use {{tool:coingecko/coinMarkets}} as a fallback for additional market context. ` export const prompt = { name: 'token-deep-dive', version: 'flowmcp-prompt/1.0.0', agent: 'crypto-research', description: 'Deep analysis of a token across multiple data sources', testedWith: 'anthropic/claude-sonnet-4-5-20250929', dependsOn: [ 'coingecko/tool/simplePrice', 'coingecko/tool/coinMarkets', 'etherscan/tool/getContractAbi' ], references: [ 'coingecko/prompt/price-comparison' ], content } ``` ### Agent-Prompt Characteristics - **`agent` field** identifies the owning agent. Must match an agent name in the catalog. - **`dependsOn` uses full ID format** — since Agent-Prompts span multiple providers, each tool reference must be unambiguous. `'coingecko/tool/simplePrice'` specifies both the namespace and the tool name. - **`testedWith` is required** — documents which LLM model the prompt was tested and optimized for. - **No `namespace` field** — the `agent` field indicates this is an Agent-Prompt. - **`references` array** allows including content from other prompts (see [Composable Prompts](#composable-prompts)). - **`{{type:name}}` references** in `content` use type-prefixed placeholders to reference tools (`{{tool:...}}`), prompts (`{{prompt:...}}`), and parameters (`{{input:...}}`) across namespaces. --- ## Prompt Fields The `export const prompt` object contains all metadata and instructions. Some fields are shared across both types, others are exclusive to one type. | Field | Type | Provider-Prompt | Agent-Prompt | Description | |-------|------|----------------|--------------|-------------| | `name` | `string` | Required | Required | Kebab-case identifier. Must match `^[a-z][a-z0-9-]*$`. | | `version` | `string` | Required | Required | Must be `flowmcp-prompt/1.0.0`. | | `namespace` | `string` | Required | Forbidden | Provider namespace this prompt belongs to. | | `agent` | `string` | Forbidden | Required | Agent name this prompt belongs to. | | `description` | `string` | Required | Required | What the prompt teaches. Maximum 1024 characters. | | `testedWith` | `string` | Forbidden | Required | OpenRouter model ID (must contain `/`). | | `dependsOn` | `string[]` | Required | Required | Tool dependencies. Bare names for Provider-Prompts, full IDs for Agent-Prompts. | | `references` | `string[]` | Required | Required | Other prompts to compose. Full ID format. Empty array `[]` when none. | | `contentFile` | `string` | Required | Forbidden | Relative path to the content `.mjs` file. | | `content` | `string` | Forbidden | Required | Prompt instructions with `{{type:name}}` placeholders. Must not be empty. | **Key structural difference:** Provider-Prompts use `contentFile` (content in external file), Agent-Prompts use `content` (content inline). These fields are mutually exclusive — a prompt has either `contentFile` or `content`, never both. ### Field Details #### `name` The prompt name is the primary identifier. It is used in the catalog, in `{{prompt:name}}` placeholder references, and in MCP prompt registration. Only lowercase letters, numbers, and hyphens are allowed. ```javascript // Valid name: 'price-comparison' name: 'token-deep-dive' name: 'quick-check' // Invalid name: 'Price-Comparison' // uppercase not allowed name: '3d-analysis' // must start with letter name: 'my_prompt' // underscore not allowed ``` #### `version` The version string identifies the prompt format specification. In this release, the only valid value is `'flowmcp-prompt/1.0.0'`. The prefix `flowmcp-prompt/` distinguishes prompt versioning from schema versioning (`3.x.x`), skill versioning (`flowmcp-skill/1.0.0`), and shared list versioning. ```javascript // Valid version: 'flowmcp-prompt/1.0.0' // Invalid version: '1.0.0' // missing prefix version: 'flowmcp-prompt/2.0.0' // version 2.0.0 does not exist yet version: 'flowmcp-skill/1.0.0' // wrong prefix (this is a skill version) ``` #### `namespace` vs `agent` These two fields are mutually exclusive. Exactly one must be set — not both, not neither. The presence of `namespace` marks a Provider-Prompt. The presence of `agent` marks an Agent-Prompt. ```javascript // Provider-Prompt namespace: 'coingecko' // agent field must NOT be present // Agent-Prompt agent: 'crypto-research' // namespace field must NOT be present ``` #### `testedWith` Required for Agent-Prompts, forbidden for Provider-Prompts. Uses OpenRouter model syntax, which always contains a `/` separator between organization and model name. ```javascript // Valid testedWith: 'anthropic/claude-sonnet-4-5-20250929' testedWith: 'openai/gpt-4o' testedWith: 'google/gemini-2.0-flash' // Invalid testedWith: 'claude-sonnet' // missing organization prefix testedWith: 'gpt-4o' // must contain / ``` The `testedWith` field documents which model the prompt was optimized for. Other models may work but are not guaranteed to produce the same quality of results. This is especially relevant for complex multi-step workflows where models differ in their ability to chain tool calls and handle intermediate results. #### `dependsOn` Lists the tools that the prompt references. Every tool mentioned in the prompt's `content` via `{{tool:...}}` placeholders should appear in `dependsOn`. This enables validation — the runtime checks that all declared dependencies resolve to existing tools. **Provider-Prompts** use bare tool names (same namespace is implied): ```javascript // Provider-Prompt for coingecko dependsOn: [ 'simplePrice', 'coinMarkets' ] // Resolves to: coingecko/tool/simplePrice, coingecko/tool/coinMarkets ``` **Agent-Prompts** use full ID format (namespace is required): ```javascript // Agent-Prompt spanning coingecko and etherscan dependsOn: [ 'coingecko/tool/simplePrice', 'coingecko/tool/coinMarkets', 'etherscan/tool/getContractAbi' ] ``` #### `references` A required array of prompt IDs that this prompt composes. Use an empty array `[]` when the prompt does not reference other prompts. See [Composable Prompts](#composable-prompts) for full details. ```javascript // No references references: [] // Agent-Prompt referencing a Provider-Prompt references: [ 'coingecko/prompt/price-comparison' ] ``` #### `contentFile` (Provider-Prompt only) A relative path to the `.mjs` file containing the prompt content. Required for Provider-Prompts, forbidden for Agent-Prompts. The file must export `export const content = '...'`. ```javascript // Valid contentFile: './prompts/price-comparison.mjs' contentFile: './prompts/about.mjs' // Invalid contentFile: './prompts/about.js' // must be .mjs contentFile: '/absolute/path.mjs' // must be relative ``` The content file is loaded via `import()` at schema load-time. The runtime reads the `content` named export from the file. #### `content` (Agent-Prompt only) The prompt instructions that the AI agent follows. Contains `{{type:name}}` placeholders for tool references and user parameters. Must not be empty. Required for Agent-Prompts, forbidden for Provider-Prompts. See [Placeholder Syntax](#placeholder-syntax) for the full reference. --- ## Placeholder Syntax Prompt content uses the `{{type:name}}` placeholder syntax. The **type prefix** (before the colon) determines the category, and the **name** (after the colon) identifies the target. This is the same syntax used in Skills (`14-skills.md`), providing a unified placeholder system across all FlowMCP content primitives. ### Two Categories Placeholders fall into two categories based on their type prefix: | Category | Type Prefixes | Purpose | |----------|---------------|---------| | **References** | `tool:`, `resource:`, `prompt:` | Resolved at load-time against the catalog to a registered tool, resource, or prompt | | **Parameters** | `input:` | Value provided by the user at runtime | ### Reference Placeholders Reference placeholders point to registered primitives in the catalog: | Placeholder | Example | Meaning | |-------------|---------|---------| | `{{tool:name}}` | `{{tool:simplePrice}}` | Tool in the same namespace/agent | | `{{tool:namespace/name}}` | `{{tool:coingecko/simplePrice}}` | Tool in an explicit namespace | | `{{resource:name}}` | `{{resource:placesDb}}` | Resource in the same namespace/agent | | `{{resource:namespace/name}}` | `{{resource:etherscan/verifiedContracts}}` | Resource in an explicit namespace | | `{{prompt:name}}` | `{{prompt:about}}` | Prompt in the same namespace/agent | | `{{prompt:namespace/name}}` | `{{prompt:coingecko/price-comparison}}` | Prompt in an explicit namespace | **Namespace rule:** Without `/` after the type prefix, the reference targets the own schema or agent. With `/`, the reference targets an explicit namespace. ### Parameter Placeholders Parameter placeholders represent user-input values: ``` {{input:token}} <- user provides a token symbol {{input:address}} <- user provides a contract address {{input:currency}} <- user provides a currency code {{input:timeframeDays}} <- user provides a number of days ``` ### Example ``` Analyze the token {{input:token}} on chain {{input:chainId}}. First, fetch the current price using {{tool:coingecko/simplePrice}}. Then retrieve the contract ABI via {{tool:etherscan/getContractAbi}}. ``` In this example, `{{input:token}}` and `{{input:chainId}}` are parameters (type prefix `input:`). `{{tool:coingecko/simplePrice}}` and `{{tool:etherscan/getContractAbi}}` are references (type prefix `tool:`). ### Edge Case: Schema Parameter Placeholders The `{{type:name}}` content placeholder syntax coexists with schema parameter placeholders like `{{USER_PARAM}}` or `{{DYNAMIC_SQL}}` used in `main.tools` and `main.resources` blocks. There is no conflict because the content renderer only matches patterns with a **colon** (`:`): | Syntax | Context | Matched by Content Renderer? | |--------|---------|------------------------------| | `{{tool:simplePrice}}` | Prompt/Skill `content` | Yes — has colon | | `{{input:token}}` | Prompt/Skill `content` | Yes — has colon | | `{{USER_PARAM}}` | Schema `main` blocks | No — no colon, UPPER_CASE | | `{{DYNAMIC_SQL}}` | Schema `main` blocks | No — no colon, UPPER_CASE | **Regex for Content Renderer:** `\{\{(tool|resource|skill|prompt|input):([a-zA-Z/]+)\}\}` ### Migration Note Earlier versions of this specification used `[[...]]` bracket syntax for prompt placeholders (e.g., `[[coingecko/tool/simplePrice]]`, `[[token]]`). This has been replaced with the unified `{{type:name}}` syntax to align prompts with the same placeholder system used in Skills (`14-skills.md`). --- ## Tool Combinatorics The primary purpose of prompts is teaching LLMs **tool combinatorics** — the knowledge of how to combine multiple tools into effective workflows. This includes: ### Call Order Which tools to call first, second, third. Some tools depend on output from previous calls: ``` First call {{tool:coingecko/simplePrice}} to get the current price. Use the coin ID from the response to call {{tool:coingecko/coinMarkets}} for detailed market data. ``` ### Result Passing How to extract values from one tool's response and pass them to the next: ``` Extract the "id" field from {{tool:coingecko/coinList}} response. Pass that ID as the "ids" parameter to {{tool:coingecko/simplePrice}}. ``` ### Fallback Strategies What to do when a tool fails or returns incomplete data: ``` If {{tool:etherscan/getContractAbi}} returns "Contract source code not verified", skip the ABI analysis and use {{tool:etherscan/getContractCreation}} instead to get basic contract metadata. ``` ### Cross-Provider Enrichment How to combine data from different providers for richer analysis: ``` Get the token price from {{tool:coingecko/simplePrice}}. Get the contract details from {{tool:etherscan/getContractAbi}}. Get the protocol TVL from {{tool:defillama/getTvlProtocol}}. Cross-reference: if the token has high TVL but low price, it may indicate a yield farming opportunity. If the contract is unverified but has high volume, flag it as a potential risk. ``` This combinatoric knowledge is what would take hours to acquire manually — reading multiple API docs, experimenting with endpoints, discovering which response fields map to which request parameters, and building mental models of how different data sources complement each other. --- ## Composable Prompts The `references[]` array enables prompt composition — one prompt can incorporate another prompt's content without duplication. ### How It Works When a prompt declares `references`, the runtime loads each referenced prompt and makes its content available to the AI agent alongside the primary prompt. Referenced prompts are **not** inlined into the content — they are provided as additional context that the agent can draw from. ```javascript export const prompt = { name: 'token-deep-dive', agent: 'crypto-research', // ... references: [ 'coingecko/prompt/price-comparison' ], content: ` Perform a deep analysis of {{input:token}}. For price comparison methodology, follow {{prompt:coingecko/price-comparison}}. Then add on-chain analysis using {{tool:etherscan/getContractAbi}}. ` } ``` When the runtime renders `token-deep-dive`, it also loads `coingecko/prompt/price-comparison` and provides both to the agent. ### Composition Rules ```mermaid flowchart LR A["Agent-Prompt
token-deep-dive"] -->|references| B["Provider-Prompt
price-comparison"] B -.->|"NOT allowed to reference"| C["Another Prompt"] style C stroke-dasharray: 5 5 ``` The diagram shows that composition is limited to one level. The referencing prompt can include another prompt, but that referenced prompt cannot itself reference further prompts. | Rule | Description | |------|-------------| | **One level deep** | A referenced prompt must not itself have `references[]`. No chains: A -> B -> C is forbidden. | | **Agent -> Provider** | Agent-Prompts can reference Provider-Prompts (cross-scope). | | **Provider -> Provider** | Provider-Prompts can reference other prompts within the same namespace only. | | **Provider -> Agent** | Provider-Prompts cannot reference Agent-Prompts (Agent-Prompts are model-specific, Provider-Prompts are model-neutral). | | **Full ID format** | All entries in `references[]` use the full ID format: `namespace/prompt/name` or `agent/prompt/name`. | ### Why One Level Deep The one-level restriction prevents three problems: 1. **Circular references** — prompt A references prompt B references prompt A 2. **Context explosion** — each level adds content, which can exceed LLM context limits 3. **Unpredictable behavior** — deeply nested prompts become difficult to reason about and test --- ## `testedWith` Field The `testedWith` field documents which LLM model an Agent-Prompt was tested and optimized for. It uses the OpenRouter model identifier format. ### Format The value must contain a `/` separator between the organization and model name: ``` organization/model-name ``` ### Examples | Value | Organization | Model | |-------|-------------|-------| | `anthropic/claude-sonnet-4-5-20250929` | Anthropic | Claude Sonnet 4.5 | | `openai/gpt-4o` | OpenAI | GPT-4o | | `google/gemini-2.0-flash` | Google | Gemini 2.0 Flash | | `meta-llama/llama-3.1-405b-instruct` | Meta | Llama 3.1 405B | ### Implications - **Required for Agent-Prompts** — every Agent-Prompt must declare which model it was tested with. - **Forbidden for Provider-Prompts** — Provider-Prompts are model-neutral by design. - **Not a restriction** — the field documents testing history, not a runtime requirement. Other models may work, but the prompt author has only verified behavior with the declared model. - **Model-specific optimizations** — different models handle tool chaining, JSON parsing, and multi-step reasoning differently. An Agent-Prompt tested with Claude may structure instructions differently than one tested with GPT-4o. --- ## Directory Structure Provider-Prompt content files are stored in `prompts/` subdirectories alongside their provider's schema files. Agent-Prompt files are stored alongside their agent's manifest. ``` providers/ +-- coingecko/ +-- simple-price.mjs # Schema with tools + prompt definitions in main.prompts +-- coin-markets.mjs # Schema with tools +-- prompts/ | +-- about.mjs # Content file for main.prompts.about | +-- price-comparison.mjs # Content file for main.prompts.priceComparison +-- resources/ +-- coingecko-metadata.md # Markdown resource (inline) agents/ +-- crypto-research/ +-- agent.mjs # Agent manifest (with about definition) +-- prompts/ +-- token-deep-dive.mjs # Agent-Prompt (definition + content in one file) ``` ### File Organization Rules | Level | Directory | Contains | |-------|-----------|----------| | Provider | `providers/{namespace}/prompts/` | Content files for Provider-Prompts (referenced via `contentFile`) | | Agent | `agents/{agent-name}/prompts/` | Agent-Prompt files (standalone, definition + content) | - Provider-Prompt content filenames use kebab-case and match the prompt's `name` field: `price-comparison.mjs` contains the content for `name: 'price-comparison'`. - Provider-Prompt definitions live in the schema's `main.prompts`. Content files live in `prompts/`. - Agent-Prompts are standalone files with both definition and content. --- ## The `about` Convention ### Reserved Prompt Name `about` is a **reserved prompt name** as a convention (SHOULD, not MUST) for both Provider-Prompts and Agent-Prompts. It serves as the entry point to a namespace or agent — what it offers, how its tools relate, what resources are available. | Aspect | Provider-Schema | Agent-Manifest | |--------|----------------|----------------| | **Name** | `about` — reserved | `about` — reserved | | **Requirement** | SHOULD | SHOULD | | **Where** | `main.prompts.about` | In the agent manifest | | **When loaded** | Only on request | Only on request | | **Purpose** | Entry point to the namespace | Entry point to the agent | ### Provider `about` ```javascript // In main.prompts about: { name: 'about', version: 'flowmcp-prompt/1.0.0', namespace: 'pagespeed', description: 'Overview of PageSpeed Insights — tools, resources, workflows', dependsOn: [ 'runPagespeedAnalysis', 'getCoreWebVitals' ], references: [], contentFile: './prompts/about.mjs' } ``` ### Agent `about` ```javascript // In agent manifest or as separate file about: { name: 'about', version: 'flowmcp-prompt/1.0.0', agent: 'competitive-analysis', description: 'Overview of Competitive Analysis Agent — what it does, which providers it uses', testedWith: 'anthropic/claude-opus-4-6', dependsOn: [ 'tranco/resource/rankingDb', 'pagespeed/tool/runPagespeedAnalysis' ], references: [], content: '...' } ``` ### What `about` Is NOT - **Not a repetition** — does not restate tool descriptions that are already available - **Not a tutorial** — not a step-by-step guide for beginners - **Not API documentation** — that is what Markdown resources are for `about` is like an **index file**: entry point, context, relationships. What can this namespace/agent do? How do the tools relate? Are there resources? --- ## Prompts vs Skills Prompts and Skills are both non-deterministic guidance for AI agents, but they serve different purposes and use different formats. Skills remain as defined in `14-skills.md` — this document does not replace them. | Aspect | Prompts (`12-prompt-architecture.md`) | Skills (`14-skills.md`) | |--------|---------------------------------------|------------------------| | Export | `export const prompt` | `export const skill` | | Version prefix | `flowmcp-prompt/1.0.0` | `flowmcp-skill/1.0.0` | | Scope | Provider-level or Agent-level | Schema-level | | Placeholder syntax | `{{type:name}}` (unified syntax) | `{{type:name}}` (unified syntax) | | Tool references | `{{tool:namespace/name}}` or `{{tool:name}}` | `{{tool:name}}` (bare names within same schema) | | Input declaration | Parameters as `{{input:paramName}}` in content | Typed `input` array with key, type, description, required | | Cross-provider | Agent-Prompts can span providers | Not directly (only via group-level skills) | | Model binding | Agent-Prompts require `testedWith` | No model binding | | Composition | `references[]` array | `{{skill:name}}` placeholder | | Primary purpose | Tool combinatorics and workflow guidance | Structured instructions with typed inputs | Skills are schema-scoped instruction sets with explicit input typing and structured metadata. Prompts are catalog-level guidance focused on tool combinatorics and cross-provider workflows. Both map to the MCP `server.prompt` primitive. --- ## Validation Rules ### Structural Rules | Code | Severity | Rule | |------|----------|------| | PRM001 | error | `name` is required, must be a string, must match `^[a-z][a-z0-9-]*$` | | PRM002 | error | `version` is required and must be `'flowmcp-prompt/1.0.0'` | | PRM003 | error | Exactly one of `namespace` or `agent` must be set (not both, not neither) | | PRM004 | error | `testedWith` is required when `agent` is set, forbidden when `namespace` is set | | PRM005 | error | `testedWith` value must contain `/` (OpenRouter model ID format) | | PRM006 | error | Each `dependsOn` entry must resolve to an existing tool in the catalog | | PRM007 | error | Each `references[]` entry must resolve to an existing prompt in the catalog | | PRM008 | error | Referenced prompts must not themselves have `references[]` (one level deep only) | | PRM009 | error | `{{type:name}}` reference placeholders in prompt content must resolve to registered primitives | | PRM010 | error | Agent-Prompts: `content` is required and must be a non-empty string. Provider-Prompts: `content` is forbidden. | | PRM011 | error | Provider-Prompts: `contentFile` is required, must be a relative path ending with `.mjs`. Agent-Prompts: `contentFile` is forbidden. | | PRM012 | error | Content file must export `export const content` (not `export const prompt`). | | PRM013 | error | `references` is required and must be an array (empty `[]` when no references). | ### Rule Details **PRM001** — The name is the primary identifier. It appears in MCP prompt listings, ID references, and filenames. Kebab-case is enforced to ensure URL-safe, filesystem-safe identifiers. **PRM002** — The version string enables the validator to apply the correct rule set. Future versions of the prompt format will increment this value. **PRM003** — A prompt must be either a Provider-Prompt (has `namespace`) or an Agent-Prompt (has `agent`). Having both or neither is invalid. This rule enforces the two-tier architecture. **PRM004** — Provider-Prompts are model-neutral, so `testedWith` would be misleading. Agent-Prompts are model-specific, so `testedWith` is mandatory to document the testing context. **PRM005** — OpenRouter model IDs always contain a `/` between organization and model name (e.g., `anthropic/claude-sonnet-4-5-20250929`). A value without `/` indicates an incorrect format. **PRM006** — Every tool listed in `dependsOn` must exist in the catalog. For Provider-Prompts, bare names are resolved within the prompt's namespace. For Agent-Prompts, full IDs are resolved across the catalog. **PRM007** — Every prompt listed in `references[]` must exist in the catalog. References use full ID format (`namespace/prompt/name`). **PRM008** — If prompt A references prompt B, prompt B must not have its own `references[]` array. This enforces one-level-deep composition. **PRM009** — All `{{tool:...}}`, `{{resource:...}}`, and `{{prompt:...}}` reference placeholders in the `content` field must resolve to registered tools, resources, or prompts. Parameter placeholders (`{{input:...}}`) are not validated against the catalog — they are user inputs. **PRM010** — Agent-Prompts need inline content. Provider-Prompts get their content from an external file via `contentFile`. **PRM011** — Provider-Prompts must declare where their content lives via `contentFile`. The file must be a relative `.mjs` path. Agent-Prompts must not have `contentFile` because their content is inline. **PRM012** — Content files must use `export const content` as the named export. Using `export const prompt` would create ambiguity with the Agent-Prompt export pattern. **PRM013** — The `references` field is always required. When a prompt does not compose other prompts, an empty array `[]` must be provided. This makes the absence of references explicit rather than ambiguous. ### Validation Output Examples ``` flowmcp validate providers/coingecko/prompts/price-comparison.mjs 0 errors, 0 warnings Prompt is valid ``` ``` flowmcp validate agents/crypto-research/prompts/token-deep-dive.mjs PRM004 error Agent-Prompt "token-deep-dive" requires testedWith field PRM006 error dependsOn entry "etherscan/tool/nonExistent" does not resolve 2 errors, 0 warnings Prompt is invalid ``` ``` flowmcp validate providers/coingecko/prompts/bad-prompt.mjs PRM003 error Prompt "bad-prompt" has both namespace and agent set PRM005 error testedWith "claude-sonnet" must contain / 2 errors, 0 warnings Prompt is invalid ``` --- ## Loading Prompts are loaded as part of the catalog loading sequence. Provider-Prompts are loaded when their provider's schemas are loaded. Agent-Prompts are loaded when their agent's manifest is loaded. ### Loading Sequence ```mermaid flowchart TD A{Prompt type?} -->|Provider| B["Read main.prompts definitions"] A -->|Agent| C["Scan agents/{name}/prompts/ directory"] B --> D["Validate fields — PRM001-PRM013"] C --> E["Read each .mjs file as string"] E --> F[Static security scan] F --> G{"Security violations?"} G -->|Yes| H[Reject with SEC error] G -->|No| I["Dynamic import()"] I --> J[Extract prompt export] J --> D D --> K{"Has contentFile?"} K -->|Yes| L["Load content file via import()"] L --> M["Extract content export"] K -->|No| N["Use inline content"] M --> O{Provider or Agent?} N --> O O -->|Provider| P[Validate dependsOn against namespace tools] O -->|Agent| Q[Validate dependsOn against catalog tools] P --> R[Validate references against catalog prompts] Q --> R R --> S[Validate placeholder references in content] S --> T[Register as MCP prompt] ``` The diagram shows how prompt loading works for both types. Provider-Prompts are loaded from `main.prompts` definitions with content from external files. Agent-Prompts are loaded from standalone files. Both go through field validation and reference resolution. ### Security Prompt files are subject to the same zero-import security model as schema and skill files. The static security scan checks for all forbidden patterns listed in `05-security.md` before the file is loaded via `import()`. ```javascript // Allowed const content = `Instructions with {{tool:coingecko/simplePrice}}...` export const prompt = { /* metadata */ } // Forbidden — import statement (SEC001) import { something } from 'somewhere' // Forbidden — require call (SEC002) const lib = require( 'lib' ) ``` --- ## Complete Examples ### Provider-Prompt: CoinGecko Price Comparison **Schema definition** (in `providers/coingecko/simple-price.mjs`): ```javascript export const main = { namespace: 'coingecko', tools: { /* ... */ }, prompts: { priceComparison: { name: 'price-comparison', version: 'flowmcp-prompt/1.0.0', namespace: 'coingecko', description: 'Compare prices, market caps, and volumes across multiple coins using CoinGecko data', dependsOn: [ 'simplePrice', 'coinMarkets' ], references: [], contentFile: './prompts/price-comparison.mjs' } } } ``` **Content file** (`providers/coingecko/prompts/price-comparison.mjs`): ```javascript export const content = ` Use {{tool:coingecko/simplePrice}} to fetch current prices for the requested coins. Pass the coin IDs as a comma-separated string in the "ids" parameter and the target currency in the "vs_currencies" parameter. Then use {{tool:coingecko/coinMarkets}} to get detailed market data. Pass the same currency as "vs_currency" and set "order" to "market_cap_desc" for ranked results. Compare the following metrics for {{input:coins}}: - Current price in {{input:currency}} - 24h price change percentage - Market cap ranking - 24h trading volume Present the comparison as a Markdown table with one row per coin. Sort by market cap descending. Include a summary paragraph highlighting the top performer and any coins with unusual 24h volume relative to market cap. If simplePrice returns a coin ID that coinMarkets does not recognize, skip that coin in the comparison table and note it at the bottom of the report. ` ``` ### Agent-Prompt: Cross-Provider Token Analysis **File:** `agents/crypto-research/prompts/token-deep-dive.mjs` ```javascript const content = ` First, get the contract details using {{tool:etherscan/getContractAbi}} for address {{input:address}}. If the contract is verified, parse the ABI to identify the token standard (ERC-20, ERC-721, etc.) and extract key function signatures. Then fetch pricing data using {{tool:coingecko/simplePrice}} with the token's CoinGecko ID. If the token ID is unknown, try searching with the contract address or token symbol. For price comparison context, follow the approach in {{prompt:coingecko/price-comparison}}. Compare the target token against the top 3 tokens in the same category. Use {{tool:coingecko/coinMarkets}} to get 24h volume and market cap data for broader context. Analyze {{input:token}} considering: - Contract verification status (from Etherscan) - Token standard and key functions (from ABI) - Current price and 24h change (from CoinGecko) - Trading volume relative to market cap - Market cap ranking in category If {{tool:etherscan/getContractAbi}} returns "Contract source code not verified", note this as a risk factor but continue with the price analysis. Unverified contracts are not necessarily malicious but warrant caution. Produce a Markdown report with sections: Contract Overview, Price Analysis, Market Position, and Risk Assessment. ` export const prompt = { name: 'token-deep-dive', version: 'flowmcp-prompt/1.0.0', agent: 'crypto-research', description: 'Deep analysis of a token across multiple data sources combining on-chain and market data', testedWith: 'anthropic/claude-sonnet-4-5-20250929', dependsOn: [ 'coingecko/tool/simplePrice', 'coingecko/tool/coinMarkets', 'etherscan/tool/getContractAbi' ], references: [ 'coingecko/prompt/price-comparison' ], content } ``` ### What These Examples Demonstrate 1. **Provider-Prompt** — `price-comparison` definition lives in `main.prompts`, content in an external file via `contentFile`. Scoped to `coingecko`, uses bare tool names in `dependsOn`. 2. **Agent-Prompt** — `token-deep-dive` is a standalone file with inline `content`. Scoped to `crypto-research`, uses full IDs in `dependsOn`, requires `testedWith`. 3. **Placeholder syntax** — `{{tool:coingecko/simplePrice}}` is a tool reference (type prefix `tool:`), `{{input:token}}` is a parameter (type prefix `input:`). 4. **Composable prompts** — `token-deep-dive` references `coingecko/prompt/price-comparison` via the `references` array. 5. **Tool combinatorics** — both prompts describe call order, result passing, and fallback strategies. 6. **Fallback instructions** — both prompts handle edge cases (unrecognized coin IDs, unverified contracts). 7. **Content separation** — Provider-Prompt uses `export const content` in a separate file. Agent-Prompt defines `content` inline above the export. 8. **Zero imports** — no file contains import statements. 9. **`references` always present** — Provider-Prompt has `references: []`, Agent-Prompt has `references: [ 'coingecko/prompt/price-comparison' ]`. ### File Structure ``` providers/ +-- coingecko/ +-- simple-price.mjs # Schema with main.prompts definitions +-- coin-markets.mjs +-- prompts/ +-- price-comparison.mjs # Content file (export const content) agents/ +-- crypto-research/ +-- agent.mjs +-- prompts/ +-- token-deep-dive.mjs # Agent-Prompt (export const prompt) ``` --- # FlowMCP Specification v3.0.0 — Resources Resources provide local data access via SQLite databases and Markdown documents. They map to the MCP `server.resource` primitive and are defined in `main.resources` alongside `main.tools`. This document defines the resource format, two SQLite modes (in-memory and file-based), the origin-based storage system, Markdown resources, query definitions, parameter binding, handler integration, and validation rules. --- ## Purpose Tools fetch data from external APIs over the network — they depend on third-party availability, rate limits, and response format stability. Some use cases require data that is **local, deterministic, and always available**: token metadata lookups, chain ID mappings, contract registries, country code tables. Other use cases require **persistent local storage** for agent-generated data: analysis results, collected metrics, scraping output. Resources solve both by providing two SQLite modes and a Markdown document type: ```mermaid flowchart LR A[Schema Definition] --> B[Resource Declaration] B --> C{"source?"} C -->|sqlite| D{"mode?"} C -->|markdown| E[Markdown File] D -->|in-memory| F["better-sqlite3\nreadonly: true"] D -->|file-based| G["better-sqlite3\nWAL mode"] F --> H[Query Results] G --> H E --> I[Text Content] H --> J[MCP Resource Response] I --> J ``` The diagram shows the data flow from the schema's resource declaration through either SQLite (two modes) or Markdown into MCP resource responses. ### When to Use Resources | Use Case | Mechanism | Example | |----------|-----------|---------| | Live API data | Tool | Current token price from CoinGecko | | Static reference data | Resource (SQLite in-memory) | Token metadata by symbol or contract address | | Agent-generated data | Resource (SQLite file-based) | PageSpeed analysis results, collected metrics | | API documentation | Resource (Markdown) | DuneSQL syntax reference, API field descriptions | --- ## Resource Types FlowMCP supports two resource types, identified by the `source` field: ```mermaid flowchart TD R[main.resources] --> S["source: 'sqlite'"] R --> D["source: 'markdown'"] S --> SM["mode: 'in-memory'"] S --> SF["mode: 'file-based'"] SM --> SM1["Buffer copy in RAM\nSELECT only\nFile unchanged"] SF --> SF1["Direct file operations\nAll SQL statements\nChanges persistent"] D --> D1["Markdown file\nText loaded as string\nParameter-based access"] ``` | Source | Mode | Description | |--------|------|-------------| | `sqlite` | `in-memory` | DB opened with `readonly: true`. SELECT only. File remains unchanged. | | `sqlite` | `file-based` | DB opened with WAL mode. All SQL statements. Changes persistent on disk. | | `markdown` | — | Markdown file loaded as string. Parameter-based access (section, lines, search). | --- ## SQLite Resources ### Resource Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | `'sqlite'` | Yes | Resource type. Must be `'sqlite'` for SQLite resources. | | `mode` | `'in-memory'` or `'file-based'` | Yes | Access mode. Determines readonly vs. writable. | | `origin` | `'global'`, `'project'`, or `'inline'` | Yes | Storage location. See [Origin System](#origin-system). | | `name` | `string` | Yes | Filename with extension. Must end with `.db`. Convention: `{namespace}-{descriptive-name}.db`. | | `description` | `string` | Yes | What this resource provides. Appears in resource discovery. | | `queries` | `object` | Yes | Query definitions. Must include `getSchema`. Maximum 7 schema-defined queries. | All fields are required. There are no defaults and no optional fields. ### Mode: `in-memory` The database is opened with `better-sqlite3` using `readonly: true`. Only SELECT statements are allowed. The file on disk is never modified. ```javascript resources: { rankingDb: { source: 'sqlite', mode: 'in-memory', origin: 'global', name: 'tranco-ranking.db', description: 'Top 1M domain rankings from Tranco List', queries: { getSchema: { sql: "SELECT name, sql FROM sqlite_master WHERE type='table'", description: 'Returns the database schema (tables and their CREATE statements)', parameters: [], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Table name' }, sql: { type: 'string', description: 'CREATE TABLE statement' } } } } }, tests: [ { _description: 'Get all table definitions' } ] }, lookupDomain: { sql: 'SELECT rank, domain FROM rankings WHERE domain = ?', description: 'Look up the rank of a specific domain', parameters: [ { position: { key: 'domain', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(3)' ] } } ], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { rank: { type: 'number', description: 'Domain rank' }, domain: { type: 'string', description: 'Domain name' } } } } }, tests: [ { _description: 'Look up google.com', domain: 'google.com' }, { _description: 'Look up amazon.de', domain: 'amazon.de' }, { _description: 'Look up zalando.de', domain: 'zalando.de' } ] } } // freeQuery (SELECT only) is auto-injected by the runtime } } ``` | Aspect | Value | |--------|-------| | **Runtime** | `better-sqlite3` with `readonly: true` | | **Allowed statements** | SELECT only | | **File changes** | Never — readonly flag on DB level | | **Allowed origins** | `global` (recommended) or `project` | | **Use case** | Reference data, lookups, open data | | **getSchema** | **MUST** — defined by schema author in `queries` | | **freeQuery** | Auto-injected by runtime (SELECT only) | ### Mode: `file-based` The database is opened with `better-sqlite3` using WAL mode. All SQL statements are allowed. Changes are persistent on disk. Only `origin: 'project'` is allowed. ```javascript resources: { analysisDb: { source: 'sqlite', mode: 'file-based', origin: 'project', name: 'pagespeed-results.db', description: 'PageSpeed analysis results collected by the agent', queries: { getSchema: { sql: "SELECT name, sql FROM sqlite_master WHERE type='table'", description: 'Returns the database schema (tables and their CREATE statements)', parameters: [], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Table name' }, sql: { type: 'string', description: 'CREATE TABLE statement' } } } } }, tests: [ { _description: 'Get all table definitions' } ] }, getLatestResults: { sql: 'SELECT domain, score, created_at FROM results ORDER BY created_at DESC LIMIT ?', description: 'Get the most recent analysis results', parameters: [ { position: { key: 'limit', value: '{{USER_PARAM}}' }, z: { primitive: 'number()', options: [ 'min(1)', 'max(100)' ] } } ], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { domain: { type: 'string', description: 'Analyzed domain' }, score: { type: 'number', description: 'Performance score' }, created_at: { type: 'string', description: 'Analysis timestamp' } } } } }, tests: [ { _description: 'Get last 10 results', limit: 10 } ] } } // freeQuery (all statements) is auto-injected by the runtime } } ``` | Aspect | Value | |--------|-------| | **Runtime** | `better-sqlite3` with WAL mode | | **Allowed statements** | All — SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP | | **File changes** | Yes — persistent on disk | | **Only allowed origin** | `project` | | **Use case** | Analysis results, agent memory, data collection | | **getSchema** | **MUST** — defined by schema author. CLI derives CREATE TABLE from return value. | | **freeQuery** | Auto-injected by runtime (all statements) | | **DB does not exist** | CLI creates DB based on getSchema | | **Backup** | Automatic `.bak` copy before first write per session | | **Concurrent writes** | Supported via WAL mode | ### Mode Comparison | Aspect | `in-memory` | `file-based` | |--------|------------|-------------| | Runtime flag | `readonly: true` | WAL mode | | SELECT | Yes | Yes | | INSERT/UPDATE/DELETE | No | **Yes** | | CREATE/DROP TABLE | No | **Yes** | | Changes persistent | No | **Yes** | | Allowed origins | `global`, `project` | Only `project` | | DB must exist | Yes (warning if missing) | No (CLI creates via getSchema) | | getSchema | **MUST** (schema-defined) | **MUST** (schema-defined) | | Backup | Not needed | `.bak` before first write | | Concurrent access | Yes (readonly) | Yes (WAL mode) | ### Why Only Two Modes Only two modes exist. Either completely readonly (safe) or completely on disk (free). Clear separation: - **In-memory** = `better-sqlite3` with `readonly: true` — no write operations possible at all - **File-based** = `better-sqlite3` with WAL mode — full access, but only on project level Intermediate modes (append-only, insert-but-no-delete) are intentionally omitted: 1. **Hard to enforce** — SQL is too flexible for reliable pattern matching 2. **Misleading** — they suggest safety that cannot be guaranteed 3. **Counterproductive** — they create problems without solving real ones --- ## Origin System ### Three Locations, Explicitly Defined Instead of pseudo-paths (`~/.flowmcp/data/`, `./data/`), resources use an `origin` + `name` system. The origin determines where the file lives. The name determines the filename. The **base folder** is configurable. Default: `flowmcp`. Changeable via CLI flag. ```mermaid flowchart TD subgraph "origin: 'inline'" A["In schema directory\nproviders/{namespace}/resources/\ntranco-ranking.db"] end subgraph "origin: 'project'" B["In workspace\n.{base}/resources/\ntranco-ranking.db"] end subgraph "origin: 'global'" C["In user home\n~/.{base}/resources/\ntranco-ranking.db"] end ``` ### Path Resolution | Origin | Resolved Path | Who Creates | Description | |--------|---------------|-------------|-------------| | `inline` | `{schema-dir}/resources/{name}` | Schema author | Ships with the schema. Committed to repo. | | `project` | `.{base}/resources/{name}` | User or CLI | Project-local. Not committed. | | `global` | `~/.{base}/resources/{name}` | User (download) | System-level. Shared across projects. | ### Base Folder | Aspect | Value | |--------|-------| | **Default** | `flowmcp` | | **Configurable** | Yes, via CLI flag | | **Affects** | `project` and `global` origins | | **Example default** | `~/.flowmcp/resources/`, `.flowmcp/resources/` | | **Example custom** | `~/.myagent/resources/`, `.myagent/resources/` | ### Origin Rules per Resource Type | Origin | SQLite in-memory | SQLite file-based | Markdown | |--------|-----------------|-------------------|----------| | `inline` | **Not recommended** (data privacy) | **Not allowed** | **Yes** (recommended) | | `project` | Yes | **Yes** (only allowed origin) | Yes | | `global` | Yes (recommended) | **Not allowed** | Yes | **Why SQLite inline is not recommended:** SQLite databases may contain personal data, proprietary datasets, or large binary files. Committing them to a schema repository exposes data to all users of the catalog. Markdown documents are text, typically documentation, and safe to commit. **Why file-based is project-only:** Writable databases must be isolated to a single project. A writable global database could be corrupted by concurrent use from multiple projects. ### Name Field The `name` field contains the **complete filename including extension**: ```javascript // SQLite name: 'tranco-ranking.db' name: 'ofacsdn-sanctions.db' name: 'pagespeed-results.db' // Markdown name: 'duneanalytics-sql-reference.md' ``` | Part | Rule | Example | |------|------|---------| | Prefix | Namespace of the schema | `tranco` | | Separator | Hyphen | `-` | | Description | Kebab-case | `ranking` | | Extension | `.db` or `.md` | `.db` | | **Complete** | | `tranco-ranking.db` | The folder is always named `resources/` (not `data/`). --- ## Standard Queries ### `getSchema` — MUST (Schema-Defined) `getSchema` is **required** for both modes. The schema author defines it in the `queries` object. It is NOT auto-injected — it must be explicitly written in the schema. **Why MUST for both modes:** - **In-memory:** The agent needs the DB structure to formulate meaningful queries via freeQuery - **File-based:** The CLI needs the structure to create the DB and tables when the file does not exist **getSchema and CREATE TABLE:** The CLI can derive CREATE TABLE statements from the getSchema return value (table names, column names, column types). No separate `createSchema` query is needed. ```javascript getSchema: { sql: "SELECT name, sql FROM sqlite_master WHERE type='table'", description: 'Returns the database schema (tables and their CREATE statements)', parameters: [], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Table name' }, sql: { type: 'string', description: 'CREATE TABLE statement' } } } } }, tests: [ { _description: 'Get all table definitions' } ] } ``` ### `freeQuery` — Auto-Injected by Runtime `freeQuery` is automatically added by the runtime. Schema authors do **not** define it manually. | Aspect | in-memory | file-based | |--------|----------|-----------| | SELECT | Yes | Yes | | INSERT | No | Yes | | UPDATE | No | Yes | | DELETE | No | Yes | | CREATE TABLE | No | Yes | | DROP TABLE | No | Yes | | LIMIT default | 100 (max 1000) | 100 (max 1000) | For `in-memory`, the auto-injected freeQuery enforces SELECT-only via runtime checks. For `file-based`, all SQL statements are allowed. ### Query Limits | Type | Count | |------|-------| | Auto-injected | 1 (freeQuery) | | Schema-defined (including getSchema) | Max 7 | | **Total** | **Max 8** | ### Summary Table | Query | in-memory | file-based | |-------|----------|-----------| | `getSchema` | **MUST** (schema-defined) | **MUST** (schema-defined) | | `freeQuery` | Auto-injected (SELECT only) | Auto-injected (all statements) | | Domain queries | Schema-defined | Schema-defined | --- ## Markdown Resources ### `source: 'markdown'` Markdown resources provide text documents as MCP resources. They are intended for API documentation, syntax references, and other text content that an agent needs alongside tools. ```javascript resources: { sqlReference: { source: 'markdown', origin: 'inline', name: 'duneanalytics-sql-reference.md', description: 'Complete DuneSQL syntax reference — functions, datatypes, table catalog' } } ``` ### Markdown Resource Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | `'markdown'` | Yes | Resource type. Must be `'markdown'`. | | `origin` | `'global'`, `'project'`, or `'inline'` | Yes | Storage location. `inline` recommended. | | `name` | `string` | Yes | Filename with `.md` extension. | | `description` | `string` | Yes | What this document contains. | All fields are required. There is no `mode` field (Markdown is always read-only) and no `queries` field. ### Parameter-Based Access Markdown resources support parameter-based access for large documents. The runtime auto-injects access functions similar to how it auto-injects `freeQuery` for SQLite resources. ```mermaid flowchart LR A[Markdown Resource] --> B{"Size?"} B -->|"Small (< 100 KB)"| C["Load full document\n(default behavior)"] B -->|"Large (> 100 KB)"| D["Use access parameters"] D --> D1["section: '## Functions'"] D --> D2["lines: '11-33'"] D --> D3["search: 'CREATE TABLE'"] ``` | Parameter | Type | Description | |-----------|------|-------------| | *(none)* | — | Full document as string (default) | | `section` | `string` | Markdown heading name (e.g. `'## Functions'`) | | `lines` | `string` | Line range as `'from-to'` (e.g. `'11-33'`) | | `search` | `string` | Text search, returns matches with context lines | These access parameters are auto-injected by the runtime. Schema authors do not define them. ### Markdown Constraints | Aspect | Value | |--------|-------| | **File format** | Only `.md` (Markdown) | | **Max size** | ~2 MB (recommendation) | | **Behavior** | File is read, content returned as string | | **Recommended origin** | `inline` (committed with schema) | | **No queries field** | Text is loaded directly | | **Referenceable** | `{{resource:namespace/name}}` from prompts and skills | ### Why Only Markdown - **Uniform format** — no parser variety needed - **AI-friendly** — Markdown is the most natural format for LLMs - **Renderable** — can be displayed in documentation - **No ambiguity** — JSON/YAML would raise questions (parse or treat as text?) ### Resolved Path Example ``` {schema-dir}/resources/duneanalytics-sql-reference.md ``` --- ## Security Model ### In-Memory: Safe by Architecture | Layer | What Happens | |-------|-------------| | DB opened with `readonly: true` | `better-sqlite3` enforces read-only at DB level | | Only SELECT in freeQuery | Runtime enforces SELECT-only | | File unchanged | No write operations possible at all | No block patterns needed. `better-sqlite3` with `readonly: true` prevents all write operations at the database level. This is more reliable than pattern-matching on SQL strings. ### File-Based: Conscious Decision | Layer | What Happens | |-------|-------------| | Only project-level | No access to global or inline DBs | | All statements allowed | User has consciously chosen file-based | | CLI asks on creation | User must confirm | | Backup before first write | `.bak` copy as safety net | | WAL mode | Concurrent access safely possible | | Changes persistent | Intended — that is the purpose | No block patterns. Schema authors who choose `file-based` want to write. Restrictions would only suggest safety that cannot be guaranteed. ### Summary ```mermaid flowchart LR subgraph "in-memory" A["better-sqlite3\nreadonly: true"] --> B[SELECT only] B --> C[File unchanged] end subgraph "file-based" D["better-sqlite3\nWAL mode"] --> E[All statements] E --> F[Changes persistent] end subgraph "Protection" G["in-memory: DB-level readonly"] H["file-based: project-only + backup"] end ``` --- ## CLI: Database Creation ### Problem For `file-based` databases at project level, the DB must exist before the agent can write. But who creates it? ### Solution: CLI Creates on Demand ```mermaid flowchart TD A[Agent wants to write to DB] --> B{"DB exists?"} B -->|Yes| C[Write] B -->|No| D[Error returned to agent] D --> E[Agent calls CLI] E --> F{"Resource defined?\nsource: sqlite\nmode: file-based"} F -->|Yes| G["CLI asks user:\nCreate database {name}?"] G -->|"User: Yes"| H["Create DB + tables\nderived from getSchema"] G -->|"User: No"| I[Abort] F -->|No| J["Error: No writable resource defined"] ``` ### Rules | Rule | Value | |------|-------| | CLI creates DB | Only when `source: 'sqlite'` + `mode: 'file-based'` + DB missing | | User confirmation | **Required** — CLI always asks | | Path | Always `project`: `.{base}/resources/{name}` | | getSchema | **MUST** for file-based — CLI derives table structure from it | | Backup | `.bak` copy before first write (for existing DB) | ### Backup Strategy Before the first write operation per session, the runtime automatically creates a `.bak` copy: ``` .flowmcp/resources/pagespeed-results.db <- active DB .flowmcp/resources/pagespeed-results.db.bak <- backup before first write ``` | Aspect | Value | |--------|-------| | **When** | Before the first write statement per session | | **Where** | Same directory, `.bak` extension | | **Overwrite** | Yes — always the latest backup | | **Deletable** | Yes — `.bak` file can be deleted anytime | | **Recovery** | Rename `.bak` to `.db` | --- ## Runtime: better-sqlite3 ### One Runtime for Both Modes `better-sqlite3` is the unified runtime for all SQLite resources, replacing `sql.js`: | Aspect | sql.js (v3.0.0) | better-sqlite3 (v3.1.0) | |--------|-----------------|------------------------| | **In-memory** | `readFileSync` -> Buffer -> `new SQL.Database(buffer)` | `new Database(path, { readonly: true })` | | **File-based** | Not possible (RAM only) | `new Database(path)` + `pragma journal_mode = WAL` | | **Readonly** | No native support | `readonly: true` flag | | **WAL mode** | Not supported | Natively supported | | **Concurrent access** | No | Yes (with WAL mode) | | **Performance** | Slower (WebAssembly) | Faster (native C binding) | | **Installation** | Pure JS (no build needed) | Requires native build (node-gyp) | ### Why One Runtime - **No feature split** — no "this only works in this mode because different library" problems - **WAL mode** — concurrent writes only possible with `better-sqlite3` - **Real readonly** — `readonly: true` at DB level instead of pattern matching on SQL strings - **Performance** — native C binding instead of WebAssembly ### Trade-off: Native Build `better-sqlite3` requires `node-gyp` (C compiler). This is a trade-off: - **Pro:** Performance, WAL, real readonly, one runtime for everything - **Contra:** Native build needed, can cause issues on some systems Decision: The advantages outweigh the disadvantages. `node-gyp` is standard in Node.js projects. `better-sqlite3` is a core dependency (not optional). ### Code Examples ```javascript // in-memory import Database from 'better-sqlite3' const db = new Database( path, { readonly: true } ) const rows = db.prepare( 'SELECT * FROM rankings WHERE domain = ?' ).all( 'google.com' ) // file-based const db = new Database( path ) db.pragma( 'journal_mode = WAL' ) db.prepare( 'INSERT INTO results (domain, score) VALUES (?, ?)' ).run( 'google.com', 95 ) ``` --- ## Query Definition Each query defines a SQL prepared statement, its parameters, output schema, and tests. | Field | Type | Required | Description | |-------|------|----------|-------------| | `sql` | `string` | Yes | SQL prepared statement with `?` placeholders for parameter binding. | | `description` | `string` | Yes | What this query does. Appears in the MCP resource description. | | `parameters` | `array` | Yes | Parameter definitions using the `position` + `z` system. Can be empty `[]` for no-parameter queries. | | `output` | `object` | Yes | Output schema declaring expected result shape. Uses the same format as tool output schemas (see `04-output-schema.md`). | | `tests` | `array` | Yes | Executable test cases. At least 1 per query. | ### SQL Field A SQL prepared statement using `?` as the placeholder for bound parameters. Parameters are bound in the order they appear in the `parameters` array. ```javascript // Single parameter sql: 'SELECT * FROM tokens WHERE symbol = ? COLLATE NOCASE' // Multiple parameters — bound in array order sql: 'SELECT * FROM tokens WHERE address = ? AND chain_id = ?' // No parameters sql: 'SELECT DISTINCT chain_id, chain_name FROM tokens ORDER BY chain_id' ``` For `in-memory` resources, only `SELECT` statements and `WITH` (CTE) expressions are allowed in schema-defined queries. For `file-based` resources, all SQL statements are allowed. ### Dynamic SQL (`{{DYNAMIC_SQL}}`) For resources where the AI client needs to write its own SQL queries (e.g., exploratory data analysis), the special placeholder `{{DYNAMIC_SQL}}` signals that the SQL comes from the user at runtime. This is used by the auto-injected `freeQuery`. Schema authors do not normally need to use `{{DYNAMIC_SQL}}` directly — it is documented here for completeness. #### `{{DYNAMIC_SQL}}` Rules 1. **Runtime security checks** — for `in-memory`, user SQL must start with `SELECT` and must not contain write operations. For `file-based`, all statements are allowed. 2. **Automatic LIMIT** — the runtime appends `LIMIT {n}` to SELECT queries if no LIMIT clause is present. Default: 100, maximum: 1000. 3. **The `sql` parameter** — provides the user's SQL query. 4. **The `limit` parameter** — optional, controls the automatic LIMIT. --- ## Parameters Resource parameters use the same `position` + `z` system as tool parameters (see `02-parameters.md`), with one key difference: **resource parameters have no `location` field**. ### Why No `location` Tool parameters need `location` (`query`, `body`, `insert`) because they are placed into HTTP requests. Resource parameters are bound to SQL `?` placeholders — their position is determined by array order, not by an HTTP request structure. ### Parameter Structure ```javascript { position: { key: 'symbol', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(1)' ] } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `position.key` | `string` | Yes | Parameter name exposed to the AI client. | | `position.value` | `string` | Yes | Must be `'{{USER_PARAM}}'` for user-provided values, or a fixed string. | | `z.primitive` | `string` | Yes | Zod-based type declaration. Same primitives as tool parameters. | | `z.options` | `string[]` | Yes | Validation constraints. Same options as tool parameters. | ### Binding Order Parameters are bound to `?` placeholders in array order. The first parameter in the array binds to the first `?` in the SQL statement, the second to the second `?`, and so on. ```javascript // SQL: SELECT * FROM tokens WHERE address = ? AND chain_id = ? // ^ ^ // parameter[0] parameter[1] parameters: [ { position: { key: 'address', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(42)', 'max(42)' ] } }, { position: { key: 'chainId', value: '{{USER_PARAM}}' }, z: { primitive: 'number()', options: [ 'min(1)' ] } } ] ``` The number of parameters must match the number of `?` placeholders in the SQL statement. A mismatch is a validation error. ### Supported Primitives Resource parameters support scalar Zod primitives only: | Primitive | Description | Example | |-----------|-------------|---------| | `string()` | String value | `'string()'` | | `number()` | Numeric value | `'number()'` | | `boolean()` | Boolean value | `'boolean()'` | | `enum(A,B,C)` | One of the listed values | `'enum(ethereum,polygon,arbitrum)'` | The `array()` and `object()` primitives are not supported for resource parameters — SQL parameter binding accepts only scalar values. --- ## Handler Integration Resources support optional handlers for post-processing query results. Resource handlers are defined in the `handlers` export, nested under the resource name and query name: ```javascript export const handlers = ( { sharedLists, libraries } ) => ( { tokenLookup: { bySymbol: { postRequest: async ( { response, struct, payload } ) => { const enriched = response .map( ( row ) => { const { address, chain_id } = row const explorerUrl = `https://etherscan.io/token/${address}` return { ...row, explorerUrl } } ) return { response: enriched } } } } } ) ``` ### Handler Structure Resource handlers are nested one level deeper than tool handlers: ``` handlers +-- {resourceName} (tool handlers are at this level) +-- {queryName} +-- postRequest (same signature as tool postRequest) ``` ### Handler Type | Handler | When | Input | Must Return | |---------|------|-------|-------------| | `postRequest` | After query execution | `{ response, struct, payload }` | `{ response }` | Resource handlers only support `postRequest`. There is no `preRequest` for resources because there is no HTTP request to modify — the query is executed directly against the local database. ### Handler Rules 1. **Handlers are optional.** Queries without handlers return the raw SQL result rows directly. 2. **Only `postRequest` is supported.** Resource handlers transform query results, not query construction. 3. **Same security restrictions apply.** Resource handlers follow the same rules as tool handlers: no imports, no restricted globals, pure transformations only. See `05-security.md`. 4. **Return shape must match.** `postRequest` must return `{ response }`. --- ## Tests Resource queries use the same test format as tool tests (see `10-tests.md`). Each test provides parameter values for a query execution against the database. ```javascript tests: [ { _description: 'Well-known stablecoin (USDC)', symbol: 'USDC' }, { _description: 'Major L1 token (ETH)', symbol: 'ETH' }, { _description: 'Case-insensitive match (lowercase)', symbol: 'wbtc' } ] ``` ### Test Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `_description` | `string` | Yes | What this test demonstrates | | `{paramKey}` | matches parameter type | Yes (per required param) | Value for each `{{USER_PARAM}}` parameter | ### Test Count | Scenario | Minimum | Recommended | |----------|---------|-------------| | Query with no parameters | 1 | 1 | | Query with 1-2 parameters | 1 | 2-3 | | Query with enum parameters | 1 | 2-3 (different enum values) | Minimum: 1 test per query is required. A query without tests is a validation error. --- ## Execution Flow ```mermaid flowchart TD A[Schema Load] --> B{"source?"} B -->|sqlite| C{"mode?"} B -->|markdown| D[Read file as string] C -->|in-memory| E["Open DB with readonly: true"] C -->|file-based| F{"DB exists?"} F -->|Yes| G["Open DB with WAL mode"] F -->|No| H["CLI creates DB via getSchema"] H --> G E --> I[Receive query request] G --> I I --> J{"DYNAMIC_SQL?"} J -->|Yes| K[Runtime security check] J -->|No| L[Execute prepared statement] K --> M[Execute user SQL with LIMIT] L --> N[Return result rows] M --> N N --> O{"Handler exists?"} O -->|Yes| P[postRequest transforms rows] O -->|No| Q[Return rows directly] P --> R[Wrap in response envelope] Q --> R D --> S[Return text content] ``` --- ## Coexistence with Tools A schema can define both `tools` and `resources` in the same `main` export: ```javascript export const main = { namespace: 'tokens', name: 'TokenExplorer', description: 'Token data from API and local database', version: '3.0.0', root: 'https://api.coingecko.com/api/v3', tools: { getPrice: { method: 'GET', path: '/simple/price', description: 'Get current token price from CoinGecko API', parameters: [ /* ... */ ], tests: [ /* ... */ ] } }, resources: { tokenMetadata: { source: 'sqlite', mode: 'in-memory', origin: 'global', name: 'tokens-metadata.db', description: 'Token metadata from local database', queries: { getSchema: { /* ... */ }, bySymbol: { /* ... */ } } } } } ``` ### Coexistence Rules 1. **`tools` and `resources` are independent.** A schema can have tools only, resources only, or both. 2. **Limits are separate.** The 8-tool limit and 2-resource limit are independent constraints. 3. **Handlers are namespaced.** Tool handlers are keyed by tool name, resource handlers are keyed by resource name then query name. There is no collision because resource handlers are nested one level deeper. 4. **`root` is not required when a schema has only resources.** The `root` field provides the base URL for HTTP tools. A resource-only schema does not make HTTP calls and may omit `root`. --- ## Limits | Constraint | Value | Rationale | |------------|-------|-----------| | Max resources per schema | 2 | Keeps schemas focused. Resources should be tightly scoped to one data domain. | | Max schema-defined queries per resource | 7 | Including getSchema. Plus 1 auto-injected freeQuery = 8 total. | | Query name pattern | `^[a-z][a-zA-Z0-9]*$` | camelCase, consistent with tool names. | | Resource name pattern | `^[a-z][a-zA-Z0-9]*$` | camelCase, consistent with tool names. | | Database file extension | `.db` | Standardized file extension for SQLite databases. | | Markdown file extension | `.md` | Standardized file extension for Markdown documents. | | Resource folder name | `resources/` | Standardized folder name (not `data/`). | | `source` value | `'sqlite'` or `'markdown'` | Only these two types are supported. | | `mode` value (sqlite only) | `'in-memory'` or `'file-based'` | Two explicit modes. | | `origin` value | `'global'`, `'project'`, or `'inline'` | Three storage locations. | | `freeQuery` LIMIT | Default 100, max 1000 | Prevents unbounded result sets. | | All fields | Required | No defaults, no optional fields. | --- ## Hash Calculation Resource definitions participate in schema hash calculation with specific inclusion and exclusion rules: ### Included in Hash The following fields are part of the `main` export and therefore included in the schema hash (via `JSON.stringify()`): - Resource name (object key) - `source`, `mode`, `origin`, `name` - `description` - Query definitions (`sql`, `description`, `parameters`, `output`, `tests`) ### Excluded from Hash | Excluded | Reason | |----------|--------| | Database file contents | Data updates should not invalidate the schema hash. | | Markdown file contents | Same reasoning as database contents. | | Handler code | Consistent with tool handler exclusion. Handler functions are in the `handlers` export, not in `main`. | --- ## Naming Conventions | Element | Convention | Pattern | Example | |---------|-----------|---------|---------| | Resource name | camelCase | `^[a-z][a-zA-Z0-9]*$` | `tokenLookup`, `chainConfig` | | Query name | camelCase | `^[a-z][a-zA-Z0-9]*$` | `bySymbol`, `byAddress`, `getSchema` | | Parameter key | camelCase | `^[a-z][a-zA-Z0-9]*$` | `symbol`, `chainId` | | Database filename | kebab-case with namespace prefix | `^[a-z][a-z0-9-]*\.db$` | `tranco-ranking.db`, `ofacsdn-sanctions.db` | | Markdown filename | kebab-case with namespace prefix | `^[a-z][a-z0-9-]*\.md$` | `duneanalytics-sql-reference.md` | | Resource folder | `resources/` | Fixed | Not `data/` | --- ## Validation Rules The following rules are enforced when validating resource definitions: | Code | Severity | Rule | |------|----------|------| | RES001 | error | `source` must be `'sqlite'` or `'markdown'`. | | RES002 | error | `description` must be a non-empty string. | | RES003 | error | `mode` is required for `source: 'sqlite'` and must be `'in-memory'` or `'file-based'`. | | RES004 | error | `origin` is required and must be `'global'`, `'project'`, or `'inline'`. | | RES005 | error | `name` is required, must be a non-empty string with the correct extension (`.db` for sqlite, `.md` for markdown). | | RES006 | error | Maximum 2 resources per schema. | | RES007 | error | Maximum 7 schema-defined queries per SQLite resource (8 total with auto-injected freeQuery). | | RES008 | error | Each query must have a `sql` field of type string. | | RES009 | error | Each query must have a `description` field of type string. | | RES010 | error | Each query must have a `parameters` array. | | RES011 | error | Each query must have an `output` object with `mimeType` and `schema`. | | RES012 | error | Each query must have at least 1 test. | | RES013 | error | For `mode: 'in-memory'`, schema-defined SQL must begin with `SELECT` or `WITH` (CTE). | | RES014 | error | Number of parameters must match number of `?` placeholders in the SQL statement. | | RES015 | error | Resource parameters must not have a `location` field in `position`. | | RES016 | error | Resource parameters must not use `{{SERVER_PARAM:...}}` values. | | RES017 | error | Resource name must match `^[a-z][a-zA-Z0-9]*$` (camelCase). | | RES018 | error | Query name must match `^[a-z][a-zA-Z0-9]*$` (camelCase). | | RES019 | error | Resource parameter primitives must be scalar: `string()`, `number()`, `boolean()`, or `enum()`. | | RES020 | warning | Database file should exist at validation time. Missing file produces a warning. | | RES021 | error | `output.schema.type` must be `'array'` for resource queries. | | RES022 | error | Test parameter values must pass the corresponding `z` validation. | | RES023 | error | Test objects must be JSON-serializable. | | RES024 | error | SQLite resources must include a `getSchema` query. | | RES025 | error | `mode: 'file-based'` requires `origin: 'project'`. | | RES026 | error | `source: 'markdown'` must not have a `mode` field. | | RES027 | error | `source: 'markdown'` must not have a `queries` field. | | RES028 | warning | `source: 'sqlite'` with `origin: 'inline'` is not recommended (data privacy). | | RES029 | error | All resource fields are required. No field may be omitted. | --- ## Complete Example A full schema combining SQLite in-memory, SQLite file-based, and Markdown resources with tools and prompts: ```javascript export const main = { namespace: 'duneanalytics', name: 'Dune Analytics', description: 'Query blockchain data with DuneSQL', version: '3.0.0', tools: { executeQuery: { method: 'POST', path: '/api/v1/query', /* ... */ }, getExecutionResults: { method: 'GET', path: '/api/v1/execution/:id/results', /* ... */ }, getLatestResults: { method: 'GET', path: '/api/v1/query/:id/results', /* ... */ } }, resources: { sqlReference: { source: 'markdown', origin: 'inline', name: 'duneanalytics-sql-reference.md', description: 'DuneSQL syntax — functions, datatypes, tables' }, queryTemplates: { source: 'sqlite', mode: 'in-memory', origin: 'global', name: 'duneanalytics-templates.db', description: 'Pre-built query templates for common blockchain analytics', queries: { getSchema: { sql: "SELECT name, sql FROM sqlite_master WHERE type='table'", description: 'Returns the database schema', parameters: [], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Table name' }, sql: { type: 'string', description: 'CREATE TABLE statement' } } } } }, tests: [ { _description: 'Get all table definitions' } ] }, searchTemplates: { sql: 'SELECT name, description, sql FROM templates WHERE category = ?', description: 'Search query templates by category', parameters: [ { position: { key: 'category', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(1)' ] } } ], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Template name' }, description: { type: 'string', description: 'Template description' }, sql: { type: 'string', description: 'SQL query template' } } } } }, tests: [ { _description: 'Find DeFi templates', category: 'defi' }, { _description: 'Find NFT templates', category: 'nft' } ] } } } }, prompts: { about: { name: 'about', version: 'flowmcp-prompt/1.0.0', namespace: 'duneanalytics', description: 'Overview of Dune Analytics — tools, resources, DuneSQL workflow', dependsOn: [ 'executeQuery', 'getExecutionResults', 'getLatestResults' ], references: [], contentFile: './prompts/about.mjs' } } } ``` ### Directory Structure ``` providers/ +-- duneanalytics/ +-- analytics.mjs # Schema (main export) +-- prompts/ | +-- about.mjs # Content for about prompt +-- resources/ +-- duneanalytics-sql-reference.md # Markdown resource (inline) ``` The SQLite database `duneanalytics-templates.db` is not in the schema directory — its `origin: 'global'` places it at `~/.flowmcp/resources/duneanalytics-templates.db`. --- # FlowMCP Specification v3.0.0 — Skills Skills are reusable instructions for AI agents. They map to the MCP `server.prompt` primitive. Each skill is a `.mjs` file with a structured `export const skill` object that combines Markdown instructions with typed metadata. This document defines the skill file format, field specifications, placeholder syntax, schema integration, scope rules, security constraints, and validation rules. --- ## Purpose Tools define individual MCP tools. Group prompts define multi-step workflows that compose tools. Skills occupy a different layer — they are **self-contained instruction sets** that an AI agent can load and follow. A skill declares what tools and resources it needs, what input it expects, and what output it produces. The instructions themselves are Markdown content with placeholder references to tools, resources, and input parameters. ```mermaid flowchart LR A[Schema] --> B[Tools] A --> C[Resources = Data] A --> D[Skills = Instructions] D --> E["References tools via {{tool:name}}"] D --> F["References resources via {{resource:name}}"] D --> G["References input via {{input:key}}"] E --> H[AI Agent follows instructions] F --> H G --> H ``` The diagram shows that a schema contains tools (exposed as MCP tools), resources (exposed as MCP resources), and skills (exposed as MCP prompts). Skills reference tools and resources from the same schema through placeholders. The AI agent resolves these references and follows the instructions. ### Skills vs Group Prompts | Aspect | Group Prompts (`12-group-prompts.md`) | Skills | |--------|---------------------------------------|--------| | Scope | Group-level — references tools across schemas | Schema-level — references tools within the same schema | | Format | Markdown `.md` files in `.flowmcp/prompts/` | `.mjs` files with `export const skill` | | Metadata | Title and description in `groups.json` | Structured metadata in the skill file itself | | Input | Informal `## Input` section in Markdown | Typed `input` array with validation constraints | | Dependencies | Implicit — backtick tool references in workflow | Explicit — `requires.tools` and `requires.resources` arrays | | MCP Mapping | No direct MCP mapping | Maps to `server.prompt` primitive | | Loading | File read at runtime | Dynamic `import()` — consistent with schema loading | Group prompts are workflows that compose tools across schemas. Skills are schema-scoped instruction sets with typed metadata that map directly to the MCP prompt primitive. --- ## Skill File Format A skill is an ES module file (`.mjs`) with two parts: a `content` variable containing Markdown instructions, and an `export const skill` object containing structured metadata. ```javascript const content = ` ## Instructions Look up the contract ABI for {{input:address}} using {{tool:getContractAbi}}. Then retrieve the full source code using {{tool:getSourceCode}}. ## Analysis Compare the ABI function signatures against the source code. Identify any discrepancies between declared and implemented functions. ## Report Produce a Markdown report with: - Contract name and compiler version - Function count (from ABI vs source) - List of external calls - Security observations ` export const skill = { name: 'full-contract-audit', version: 'flowmcp-skill/1.0.0', description: 'Retrieve ABI and source code for a smart contract audit.', requires: { tools: [ 'getContractAbi', 'getSourceCode' ], resources: [], external: [] }, input: [ { key: 'address', type: 'string', description: 'Ethereum contract address (0x-prefixed, 42 characters)', required: true } ], output: 'Markdown report with ABI summary, source analysis, and security observations.', content } ``` ### Why `.mjs` Files Skill files use the same `.mjs` format as schema files for three reasons: 1. **Consistent loading.** The runtime loads skills via `import()` — the same mechanism used for schemas. No separate file parser or YAML library is needed. 2. **Static security scanning.** The same `SecurityScanner` that checks schema files also checks skill files. The zero-import policy applies uniformly. 3. **Multiline content.** Template literals in JavaScript handle multiline Markdown content naturally, without escaping issues that JSON or YAML would introduce. ### Content Variable Pattern The `content` field in the `skill` export is a string. By convention, this string is defined as a `const content` variable above the export, then referenced by name: ```javascript const content = ` ## Step 1 ... ` export const skill = { // ... content } ``` This pattern keeps the Markdown instructions visually separated from the metadata. The variable name must be `content`. Other names are permitted by the language but rejected by the validator — see validation rule SKL010. --- ## Skill Fields The `export const skill` object contains all metadata and instructions for the skill. ### Required Fields | Field | Type | Constraints | Description | |-------|------|-------------|-------------| | `name` | `string` | Must match `^[a-z][a-z0-9-]{0,63}$` | Unique identifier within the schema. Lowercase letters, numbers, and hyphens. Maximum 64 characters. | | `version` | `string` | Must be `'flowmcp-skill/1.0.0'` | Skill format version. See [Versioning](#versioning). | | `description` | `string` | Maximum 1024 characters | Human-readable explanation of what the skill does. Appears in MCP prompt listings. | | `output` | `string` | Must not be empty | Description of what the skill produces as a final artifact. | | `content` | `string` | Must not be empty | Markdown instructions for the AI agent. Contains placeholders referencing tools, resources, and input parameters. | ### Optional Fields | Field | Type | Default | Constraints | Description | |-------|------|---------|-------------|-------------| | `requires` | `object` | `{}` | See below | Declares dependencies on tools, resources, and external capabilities. | | `requires.tools` | `string[]` | `[]` | Each must be a tool name in the same schema's `main.tools` | Tools the skill needs. | | `requires.resources` | `string[]` | `[]` | Each must be a resource name in the same schema's `main.resources` | Resources the skill reads. | | `requires.external` | `string[]` | `[]` | Free-form strings | External capabilities the skill assumes (e.g. `'playwright'`, `'file-system'`). Informational — not validated against the runtime. | | `input` | `object[]` | `[]` | See below | Parameters the user provides when invoking the skill. | ### Field Details #### `name` The skill name is the primary identifier. It is used in the MCP prompt registration, in `{{skill:name}}` placeholder references, and in `main.skills` entries. Only lowercase letters, numbers, and hyphens are allowed. The name must start with a letter. ```javascript // Valid name: 'full-contract-audit' name: 'quick-check' name: 'tvl-comparison-report' // Invalid name: 'Full-Contract-Audit' // uppercase not allowed name: '3d-chart' // must start with letter name: 'my_skill' // underscore not allowed name: '' // empty not allowed ``` #### `version` The version string identifies the skill format specification. In this release, the only valid value is `'flowmcp-skill/1.0.0'`. The prefix `flowmcp-skill/` distinguishes skill versioning from schema versioning (`3.x.x`) and shared list versioning. ```javascript // Valid version: 'flowmcp-skill/1.0.0' // Invalid version: '1.0.0' // missing prefix version: 'flowmcp-skill/2.0.0' // version 2.0.0 does not exist yet version: '3.0.0' // this is a schema version, not a skill version ``` Why separate versioning: skill format changes (new fields, new placeholder types, new validation rules) happen independently of schema format changes. A skill at `flowmcp-skill/1.0.0` works with schemas at `3.0.0` or any future `3.x.x`. When the skill format changes in a breaking way, the version increments to `flowmcp-skill/2.0.0` — enabling validators to apply the correct rules and runtimes to support upgrade paths. #### `description` A human-readable summary of what the skill does. This text appears in MCP prompt listings and search results. Maximum 1024 characters. ```javascript // Good — explains what the skill does and what it produces description: 'Retrieve ABI and source code for a smart contract audit.' description: 'Compare TVL across DeFi protocols and generate a ranked summary table.' // Bad — too vague or too long description: 'A skill.' description: 'This skill does many things including...' // (1024+ chars) ``` #### `requires` The `requires` object declares what the skill depends on. All three sub-fields are optional. If `requires` is omitted entirely, the skill has no declared dependencies. ```javascript // Skill that needs two tools and no resources requires: { tools: [ 'getContractAbi', 'getSourceCode' ], resources: [], external: [] } // Skill that needs a tool and an external capability requires: { tools: [ 'getChainData' ], resources: [ 'chainList' ], external: [ 'playwright' ] } // Skill with no dependencies (informational skill) requires: {} ``` **`requires.tools`** — Each entry must be a tool name that exists in the same schema's `main.tools`. The validator checks this at load time (SKL005). These references tell the runtime which tools the skill needs and allow consumers to verify that all dependencies are available. **`requires.resources`** — Each entry must be a resource name that exists in the same schema's `main.resources`. The validator checks this at load time (SKL006). **`requires.external`** — Free-form strings declaring external capabilities. These are informational only — the runtime does not validate them against available capabilities. They help consumers understand what environment the skill expects. #### `input` An array of parameter definitions. Each parameter describes one piece of information the user must (or may) provide when invoking the skill. Input parameters are referenced in the `content` via `{{input:key}}` placeholders. ```javascript input: [ { key: 'address', type: 'string', description: 'Ethereum contract address (0x-prefixed, 42 characters)', required: true }, { key: 'chainName', type: 'enum', description: 'Target blockchain network', required: true, values: [ 'ethereum', 'polygon', 'arbitrum', 'base' ] }, { key: 'includeSource', type: 'boolean', description: 'Whether to include full source code in the report', required: false } ] ``` ### Input Parameter Fields Each object in the `input` array has the following fields: | Field | Type | Required | Constraints | Description | |-------|------|----------|-------------|-------------| | `key` | `string` | Yes | Must match `^[a-z][a-zA-Z0-9]*$` (camelCase) | Parameter name. Referenced via `{{input:key}}` in content. | | `type` | `string` | Yes | One of: `string`, `number`, `boolean`, `enum` | Parameter data type. | | `description` | `string` | Yes | Must not be empty | What this parameter means. | | `required` | `boolean` | Yes | Must be `true` or `false` | Whether the parameter is mandatory. | | `values` | `string[]` | Conditional | Required when `type` is `'enum'`. Must not be empty. | Allowed values for enum parameters. | ```javascript // String parameter — required { key: 'address', type: 'string', description: 'Contract address', required: true } // Number parameter — optional { key: 'depth', type: 'number', description: 'Analysis depth level (1-5)', required: false } // Boolean parameter — optional { key: 'verbose', type: 'boolean', description: 'Include detailed breakdown', required: false } // Enum parameter — required (values field is mandatory) { key: 'network', type: 'enum', description: 'Target network', required: true, values: [ 'ethereum', 'polygon' ] } ``` The `values` field is required when `type` is `'enum'` and forbidden when `type` is anything else. If a non-enum parameter includes `values`, the validator raises SKL009. #### `output` A string describing the expected output of the skill. This helps the AI agent understand what deliverable it should produce. ```javascript // Good — specific about format and content output: 'Markdown report with ABI summary, source analysis, and security observations.' output: 'JSON object with protocol names as keys and TVL values in USD.' // Bad — too vague output: 'A report.' output: 'Some data.' ``` #### `content` The Markdown instructions that the AI agent follows. This is the core of the skill — it tells the agent what to do, step by step. Content must not be empty. It may contain placeholders that reference tools, resources, other skills, and input parameters. See [Placeholders](#placeholders). --- ## Placeholders The `content` field supports four placeholder types. Placeholders use the `{{type:name}}` syntax and are resolved by the runtime when the skill is loaded as an MCP prompt. ### Placeholder Types | Placeholder | Syntax | Resolves To | Example | |-------------|--------|-------------|---------| | Tool | `{{tool:name}}` | A tool in the same schema's `main.tools` | `{{tool:getContractAbi}}` | | Resource | `{{resource:name}}` | A resource in the same schema's `main.resources` | `{{resource:chainList}}` | | Skill | `{{skill:name}}` | Another skill in the same schema's `main.skills` | `{{skill:quick-check}}` | | Input | `{{input:key}}` | An input parameter from the skill's `input` array | `{{input:address}}` | ### Placeholder Rules 1. **Tool placeholders** (`{{tool:name}}`) — the `name` must exist as a key in the same schema's `main.tools`. The validator checks this at load time (SKL005 via `requires.tools`). Every tool referenced via `{{tool:name}}` in content should be listed in `requires.tools`. 2. **Resource placeholders** (`{{resource:name}}`) — the `name` must exist as a key in the same schema's `main.resources`. The validator checks this at load time (SKL006 via `requires.resources`). Every resource referenced via `{{resource:name}}` in content should be listed in `requires.resources`. 3. **Skill placeholders** (`{{skill:name}}`) — the `name` must exist as a key in the same schema's `main.skills`. Skill-to-skill references are limited to **one level deep** — a skill referenced via `{{skill:name}}` must not itself contain `{{skill:...}}` placeholders. This prevents circular chains and unbounded nesting. See [Scope Rules](#scope-rules). 4. **Input placeholders** (`{{input:key}}`) — the `key` must exist in the skill's own `input` array. The validator checks this at load time (SKL008). ### Placeholder Examples ```javascript const content = ` ## Step 1: Resolve Contract Look up the ABI for contract {{input:address}} on {{input:network}} using {{tool:getContractAbi}}. ## Step 2: Fetch Source Retrieve the verified source code using {{tool:getSourceCode}}. ## Step 3: Cross-Reference Check {{resource:verifiedContracts}} for known audit reports on this contract. ## Step 4: Quick Summary If the user requested a brief summary, follow {{skill:quick-summary}} instead of producing the full report. ` ``` ### Unresolved Placeholders If a placeholder references a name that does not exist in the schema, the validator raises a warning (not an error). The placeholder is left as-is in the content — it becomes a literal string. This allows skills to contain informational references that the AI agent can interpret contextually, even if they do not resolve to an actual tool or resource. --- ## Schema Integration Skills are declared in the `main` export of a schema file and loaded from separate `.mjs` files. ### Declaration in `main.skills` The `main.skills` field is an optional object that maps skill names to file references: ```javascript export const main = { namespace: 'etherscan', name: 'SmartContractExplorer', description: 'Explore verified smart contracts on EVM-compatible chains', version: '3.0.0', root: 'https://api.etherscan.io', tools: { getContractAbi: { /* ... */ }, getSourceCode: { /* ... */ } }, skills: { 'full-contract-audit': { file: './skills/full-contract-audit.mjs' }, 'quick-summary': { file: './skills/quick-summary.mjs' } } } ``` ### `main.skills` Entry Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | key (skill name) | `string` | Yes | Must match `^[a-z][a-z0-9-]{0,63}$`. Must match the `name` field inside the skill file. | | `file` | `string` | Yes | Relative path from the schema file to the skill `.mjs` file. Must end with `.mjs`. | The key in `main.skills` must match the `name` field inside the referenced skill file. If they differ, the validator raises SKL003. ### File Organization Skills are stored in a `skills/` subdirectory relative to the schema file: ``` etherscan/ ├── SmartContractExplorer.mjs └── skills/ ├── full-contract-audit.mjs └── quick-summary.mjs ``` The `file` path in `main.skills` is relative to the schema file location: ```javascript // Schema at: etherscan/SmartContractExplorer.mjs skills: { 'full-contract-audit': { file: './skills/full-contract-audit.mjs' } } // Resolves to: etherscan/skills/full-contract-audit.mjs ``` ### MCP Registration Each skill is registered as an MCP prompt with the server. The fully qualified prompt name follows the pattern `namespace/schemaFile::skillName`: ``` etherscan/SmartContractExplorer::full-contract-audit etherscan/SmartContractExplorer::quick-summary ``` The runtime maps skill fields to MCP prompt fields: | Skill Field | MCP Prompt Field | |-------------|-----------------| | `name` | Prompt name (with namespace prefix) | | `description` | Prompt description | | `input` | Prompt arguments | | `content` (with resolved placeholders) | Prompt messages | --- ## Versioning ### Format ``` flowmcp-skill/1.0.0 ``` The version string has two parts separated by `/`: | Part | Value | Description | |------|-------|-------------| | Prefix | `flowmcp-skill` | Fixed identifier for the skill format | | Version | `1.0.0` | Semver version of the skill specification | ### Current Version The only valid version in this release is `flowmcp-skill/1.0.0`. ### Why Separate Versioning Skill format versioning is independent of schema format versioning (`3.x.x`) for three reasons: 1. **Independent evolution.** Skill fields, placeholder types, and validation rules change on a different cadence than schema fields. A new placeholder type does not require a schema version bump. 2. **Validation targeting.** The validator uses the version string to apply the correct rule set. A skill at `flowmcp-skill/1.0.0` is validated against the rules defined in this document. A future `flowmcp-skill/2.0.0` may add new required fields or placeholder types. 3. **Upgrade paths.** When breaking changes are introduced, the version increment signals that existing skills need migration. Tools can detect the version and offer automated migration. --- ## Validation Rules ### Structural Rules (Static Validation) These rules can be checked at load time by examining the skill file and the schema's `main` block. They run during `flowmcp validate`. | Code | Severity | Rule | |------|----------|------| | SKL001 | error | Skill file must export `skill` as a named export | | SKL002 | error | `skill.name` is required, must be a string, must match `^[a-z][a-z0-9-]{0,63}$` | | SKL003 | error | `skill.name` must match the key in `main.skills` that references this file | | SKL004 | error | `skill.version` is required and must be `'flowmcp-skill/1.0.0'` | | SKL005 | error | Each entry in `requires.tools` must exist as a key in `main.tools` | | SKL006 | error | Each entry in `requires.resources` must exist as a key in `main.resources` | | SKL007 | error | `skill.description` is required, must be a string, maximum 1024 characters | | SKL008 | error | Each `{{input:key}}` placeholder in `content` must have a matching entry in `skill.input` | | SKL009 | error | `input[].values` is required when `type` is `'enum'` and forbidden otherwise | | SKL010 | error | `skill.content` is required and must be a non-empty string | | SKL011 | error | `skill.output` is required and must be a non-empty string | | SKL012 | error | `input[].key` must match `^[a-z][a-zA-Z0-9]*$` (camelCase) | | SKL013 | error | `input[].type` must be one of: `string`, `number`, `boolean`, `enum` | | SKL014 | error | `input[].description` is required and must be a non-empty string | | SKL015 | error | `input[].required` must be a boolean | | SKL016 | error | `main.skills` entries: `file` must end with `.mjs` | | SKL017 | error | `main.skills` entries: referenced file must exist | | SKL018 | error | Maximum 4 skills per schema | ### Reference Rules (Cross-Validation) These rules validate references between skills, tools, and resources within the same schema. | Code | Severity | Rule | |------|----------|------| | SKL020 | warning | `{{tool:name}}` placeholder in content references a tool not listed in `requires.tools` | | SKL021 | warning | `{{resource:name}}` placeholder in content references a resource not listed in `requires.resources` | | SKL022 | error | `{{skill:name}}` placeholder references a skill not in `main.skills` | | SKL023 | error | `{{skill:name}}` target skill must not itself contain `{{skill:...}}` placeholders (1 level deep only) | | SKL024 | warning | Entry in `requires.tools` is not referenced via `{{tool:...}}` in content | | SKL025 | warning | Entry in `requires.resources` is not referenced via `{{resource:...}}` in content | ### Non-Validatable Aspects The following aspects cannot be checked by static validation. They depend on AI agent behavior at runtime: | Aspect | Why Not Validatable | |--------|-------------------| | Instruction quality | Whether the Markdown content produces good results is subjective | | Step completeness | Whether all necessary steps are covered requires domain knowledge | | Output accuracy | Whether the described output matches what the agent produces depends on agent capability | | Input sufficiency | Whether the declared inputs are enough to complete the task is domain-specific | | External availability | Whether `requires.external` capabilities are present depends on the runtime environment | --- ## Scope Rules Skills operate at the schema level by default. Cross-schema references are possible only through groups. ### Schema-Level Scope A skill can only reference tools and resources that belong to the same schema: ```javascript // Schema: etherscan/SmartContractExplorer.mjs export const main = { tools: { getContractAbi: { /* ... */ }, getSourceCode: { /* ... */ } }, skills: { 'contract-audit': { file: './skills/contract-audit.mjs' } } } // Skill: etherscan/skills/contract-audit.mjs // CAN reference: getContractAbi, getSourceCode (same schema) // CANNOT reference: getSimplePrice (different schema: coingecko) ``` ### Group-Level Scope When skills are used within a group context, they can reference tools from other schemas in the group using the fully qualified namespace prefix. This is a group-level feature, not a skill-level feature — the skill file itself always uses bare tool names. The group runtime resolves cross-schema references. ### Skill-to-Skill Scope A skill can reference another skill in the same schema via `{{skill:name}}`. This is limited to **one level deep**: ```javascript // Allowed: skill-a references skill-b // skill-a.mjs content: "For a quick version, follow {{skill:quick-summary}}" // quick-summary.mjs content: "Summarize the ABI using {{tool:getContractAbi}}" // Forbidden: skill-b references skill-c (would make skill-a -> skill-b -> skill-c) // quick-summary.mjs content: "See also {{skill:another-skill}}" // SKL023 error ``` The one-level-deep restriction prevents: - **Circular references** — skill A referencing skill B referencing skill A - **Deep nesting** — unbounded chains of skill references that are hard to follow - **Context explosion** — each level adds content, which can exceed context limits ### Scope Summary | Reference Type | Allowed Scope | Depth | |---------------|---------------|-------| | `{{tool:name}}` | Same schema only | N/A | | `{{resource:name}}` | Same schema only | N/A | | `{{skill:name}}` | Same schema only | 1 level deep | | `{{input:key}}` | Same skill only | N/A | --- ## Security Skills are subject to the same security model as schema files. The zero-import policy and static security scan apply uniformly. ### Static Security Scan Before a skill file is loaded via `import()`, the raw file content is scanned for all forbidden patterns listed in `05-security.md`: | Pattern | Reason | |---------|--------| | `import ` | No imports — skills are self-contained | | `require(` | No CommonJS imports | | `eval(` | Code injection | | `Function(` | Code injection | | `fs.` | Filesystem access | | `process.` | Process access | | All other patterns from `05-security.md` | Same rationale as schema files | ### What Skill Files Can Contain ```javascript // Allowed — const variable with template literal const content = `Markdown instructions...` // Allowed — export const with object literal export const skill = { /* metadata */ } // Allowed — comments // This skill audits smart contracts ``` ### What Skill Files Cannot Contain ```javascript // Forbidden — import statement (SEC001) import { something } from 'somewhere' // Forbidden — require call (SEC002) const lib = require( 'lib' ) // Forbidden — filesystem access (SEC005) const data = fs.readFileSync( 'file.txt' ) // Forbidden — eval (SEC003) eval( 'code' ) // Forbidden — process access (SEC006) const key = process.env.API_KEY ``` ### Security Scan Error Format Skill security violations use the same error codes as schema files (SEC001-SEC011) but include the skill file path: ``` SEC001 etherscan/skills/contract-audit.mjs: Forbidden pattern "import " found at line 1 ``` --- ## Limits | Constraint | Value | Rationale | |------------|-------|-----------| | Max skills per schema | 4 | Keeps schemas focused. Skills that grow beyond 4 indicate the schema should be split. | | Content must not be empty | Required | A skill without instructions has no purpose. | | Skill name max length | 64 characters | Prevents excessively long MCP prompt identifiers. | | Description max length | 1024 characters | Consistent with schema description limits. | | Skill-to-skill depth | 1 level | Prevents circular references and context explosion. | | Input parameter types | 4 (`string`, `number`, `boolean`, `enum`) | Matches the types available in MCP prompt arguments. | --- ## Loading Skills are loaded as part of the schema loading sequence defined in `01-schema-format.md`. The skill loading step occurs after the `main` block is validated and before tools are registered as MCP tools. ### Loading Sequence ```mermaid flowchart TD A[Validate main block] --> B{main.skills exists?} B -->|No| C[Skip skill loading] B -->|Yes| D[Read each skill file as string] D --> E[Static security scan per file] E --> F["Dynamic import() per file"] F --> G[Extract skill export] G --> H[Validate skill fields] H --> I[Validate requires.tools against main.tools] I --> J[Validate requires.resources against main.resources] J --> K[Validate placeholders in content] K --> L[Check skill-to-skill depth limit] L --> M[Register as MCP prompts] C --> N[Continue to tool registration] M --> N ``` The diagram shows how skill loading integrates into the existing schema loading pipeline. Skills are loaded after `main` validation and before tool registration. ### Step-by-Step 1. **Check `main.skills`** — if the field is absent or empty, skip skill loading entirely. 2. **Read each skill file as string** — the raw source is read before any execution, same as schema files. 3. **Static security scan** — the file string is scanned for forbidden patterns. If any match, the skill file is rejected. 4. **Dynamic import** — the file is imported via `import()`. 5. **Extract `skill` export** — the named `skill` export is read. 6. **Validate skill fields** — name, version, description, output, content, input parameters. 7. **Validate `requires.tools`** — each entry must exist in `main.tools`. 8. **Validate `requires.resources`** — each entry must exist in `main.resources`. 9. **Validate placeholders** — `{{input:key}}` references must match `input` entries; `{{tool:name}}` and `{{resource:name}}` references are checked against `requires` declarations. 10. **Check skill-to-skill depth** — if the content contains `{{skill:name}}`, the referenced skill must not itself contain `{{skill:...}}` placeholders. 11. **Register as MCP prompts** — each validated skill is exposed as an MCP prompt. ### No Additional Dependencies Skill loading requires no additional dependencies beyond what the schema runtime already provides: - No filesystem library — the runtime already reads files for schemas - No YAML parser — skills use `.mjs` format - No template engine — placeholder resolution is string replacement --- ## Complete Example A schema with two tools and one skill that composes them into a contract audit workflow: ### Schema File (`etherscan/SmartContractExplorer.mjs`) ```javascript 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', requiredServerParams: [ 'ETHERSCAN_API_KEY' ], tools: { getContractAbi: { method: 'GET', path: '/api', description: 'Returns the Contract ABI of a verified smart contract', parameters: [ /* ... */ ], tests: [ /* ... */ ] }, getSourceCode: { method: 'GET', path: '/api', description: 'Returns the Solidity source code of a verified smart contract', parameters: [ /* ... */ ], tests: [ /* ... */ ] } }, skills: { 'full-contract-audit': { file: './skills/full-contract-audit.mjs' } } } ``` ### Skill File (`etherscan/skills/full-contract-audit.mjs`) ```javascript const content = ` ## Step 1: Retrieve Contract ABI Call {{tool:getContractAbi}} with the contract address {{input:address}}. Parse the returned ABI JSON string into a structured object. Count the number of functions, events, and errors declared. ## Step 2: Retrieve Source Code Call {{tool:getSourceCode}} with the same address {{input:address}}. Extract the contract name, compiler version, and optimization settings. ## Step 3: Cross-Reference Analysis Compare the ABI function signatures against the source code: - Identify functions declared in ABI but missing from source - Identify internal functions not exposed in ABI - Flag any external calls (address.call, delegatecall) ## Step 4: Security Observations Review the source code for common patterns: - Reentrancy guards (nonReentrant modifier) - Access control (onlyOwner, role-based) - Upgrade patterns (proxy, UUPS) - Token approvals and transfers ## Step 5: Generate Report Produce a Markdown report with the following sections: - **Contract Overview**: name, compiler, optimization, address - **Interface Summary**: function/event/error counts from ABI - **Source Analysis**: external calls, modifiers, inheritance - **Security Notes**: observations from Step 4 - **Raw ABI**: the full ABI JSON for reference ` export const skill = { name: 'full-contract-audit', version: 'flowmcp-skill/1.0.0', description: 'Retrieve ABI and source code for a comprehensive smart contract audit report.', requires: { tools: [ 'getContractAbi', 'getSourceCode' ], resources: [], external: [] }, input: [ { key: 'address', type: 'string', description: 'Ethereum contract address (0x-prefixed, 42 characters)', required: true } ], output: 'Markdown report with contract overview, interface summary, source analysis, security observations, and raw ABI.', content } ``` ### What This Example Demonstrates 1. **Schema with skills** — the `main` export includes a `skills` field alongside `tools`. 2. **Skill file in `skills/` subdirectory** — follows the file organization convention. 3. **`requires.tools` declaration** — the skill explicitly lists `getContractAbi` and `getSourceCode` as dependencies. 4. **Tool placeholders** — `{{tool:getContractAbi}}` and `{{tool:getSourceCode}}` reference tools from the same schema. 5. **Input placeholders** — `{{input:address}}` references the skill's typed input parameter. 6. **Typed input** — the `address` parameter has type `string` and is required. 7. **Structured content** — the Markdown instructions follow a numbered step pattern. 8. **Content variable pattern** — the `content` variable is defined above the export and referenced by name. 9. **No import statements** — the skill file has zero imports, consistent with the zero-import policy. 10. **MCP mapping** — this skill registers as `etherscan/SmartContractExplorer::full-contract-audit` in the MCP prompt list. ### File Structure ``` etherscan/ ├── SmartContractExplorer.mjs # Schema with tools + skills declaration └── skills/ └── full-contract-audit.mjs # Skill file with content + metadata ``` --- # FlowMCP Specification v3.0.0 — Catalog A Catalog is the top-level organizational unit in FlowMCP v3. It is a named directory containing a `registry.json` manifest that describes all shared lists, provider schemas, and agent definitions within that directory. Multiple catalogs can coexist side by side, enabling community, company-internal, and experimental tool collections to operate independently. --- ## Purpose As FlowMCP grows beyond individual schemas and shared lists, a higher-level structure is needed to group related content into a cohesive, distributable unit. Without catalogs: - **No manifest** — the runtime must scan directories and infer relationships between schemas, lists, and agents - **No isolation** — company-internal schemas mix with community schemas in a flat namespace - **No import boundary** — importing from a remote registry requires knowledge of internal file structure Catalogs solve this by providing a single `registry.json` manifest that declares everything the directory contains. The runtime reads this manifest instead of scanning the filesystem, and import commands use it as the entry point for downloading and resolving dependencies. ```mermaid flowchart LR A[Catalog Directory] --> B[registry.json] B --> C[Shared Lists] B --> D[Provider Schemas] B --> E[Agent Definitions] C --> F[Runtime Resolution] D --> F E --> F F --> G[MCP Server] ``` The diagram shows how the catalog directory contains a manifest that references shared lists, provider schemas, and agent definitions. The runtime resolves all three through the manifest and exposes them via the MCP server. --- ## Catalog Structure A catalog is a named directory containing a `registry.json` manifest and three content areas: shared lists, provider schemas, and agent definitions. ``` schemas/v3.0.0/ └── flowmcp-community/ <- Named catalog directory ├── registry.json <- Catalog manifest ├── _lists/ <- Shared Lists (root level) │ ├── evm-chains.mjs │ ├── german-bundeslaender.mjs │ └── ... ├── _shared/ <- Shared Helpers (root level) ├── providers/ <- All provider schemas │ ├── aave/ │ │ └── reserves.mjs │ ├── coingecko-com/ │ │ ├── simple-price.mjs │ │ └── prompts/ <- Provider prompts (model-neutral) │ └── ... └── agents/ <- Agent definitions ├── crypto-research/ │ ├── agent.mjs <- Agent manifest (export const agent) │ ├── prompts/ <- Agent-specific prompts │ │ └── prompt-name.mjs │ ├── skills/ <- Agent-specific skills │ │ └── skill-name.mjs │ └── resources/ <- Agent-specific resources (optional) └── defi-monitor/ ├── agent.mjs ├── prompts/ ├── skills/ └── resources/ ``` ### Directory Conventions | Directory | Level | Purpose | |-----------|-------|---------| | `_lists/` | Root | Shared value lists consumed by all providers and agents | | `_shared/` | Root | Shared helper modules consumed by provider schemas | | `providers/` | Root | Provider schema directories, one per namespace | | `providers/{namespace}/` | Provider | Schema files for a single data source | | `providers/{namespace}/prompts/` | Provider | Model-neutral prompts for the provider's tools | | `agents/` | Root | Agent definition directories | | `agents/{agent-name}/` | Agent | Agent manifest, prompts, skills, and resources | | `agents/{agent-name}/prompts/` | Agent | Agent-specific prompts | | `agents/{agent-name}/skills/` | Agent | Agent-specific skills | | `agents/{agent-name}/resources/` | Agent | Agent-specific resources (optional) | ### Naming Rules - The catalog directory name must match the `name` field in `registry.json` - Directory names use kebab-case: `flowmcp-community`, `my-company-tools` - Provider namespace directories use kebab-case: `coingecko-com`, `defi-llama` - Agent directories use kebab-case: `crypto-research`, `defi-monitor` --- ## Multiple Catalogs Multiple catalogs can exist side by side under the same parent directory. Each catalog is fully self-contained — its `registry.json` references only files within its own directory tree. There are no cross-catalog dependencies. ``` schemas/v3.0.0/ ├── flowmcp-community/ <- Official community catalog │ └── registry.json ├── my-company-tools/ <- Company-internal catalog │ └── registry.json └── experimental/ <- Personal experiments └── registry.json ``` ### Isolation Guarantees 1. **No cross-catalog shared lists** — a schema in `my-company-tools` cannot reference a shared list defined in `flowmcp-community`. If both catalogs need the same list, each must include its own copy. 2. **No namespace collisions across catalogs** — two catalogs may contain providers with the same namespace (e.g. both have `etherscan/`). The runtime qualifies tool names with the catalog name to prevent collisions. 3. **Independent versioning** — each catalog has its own `version` field. Updating `flowmcp-community` to version `3.1.0` does not affect `my-company-tools` at version `3.0.0`. 4. **Independent import** — `flowmcp import-registry` targets a single catalog. Importing one catalog does not pull in others. --- ## Registry Manifest Format The `registry.json` file is the entry point for a catalog. It declares identity metadata and lists all shared lists, provider schemas, and agent definitions within the catalog. ```json { "name": "flowmcp-community", "version": "3.0.0", "description": "Official FlowMCP community catalog", "schemaSpec": "3.0.0", "shared": [ { "file": "_lists/evm-chains.mjs", "name": "evmChains" }, { "file": "_lists/german-bundeslaender.mjs", "name": "germanBundeslaender" } ], "schemas": [ { "namespace": "coingecko-com", "file": "providers/coingecko-com/simple-price.mjs", "name": "CoinGecko Simple Price", "requiredServerParams": [], "hasHandlers": false, "sharedLists": [] } ], "agents": [ { "name": "crypto-research", "description": "Cross-provider crypto analysis agent", "manifest": "agents/crypto-research/agent.mjs" } ] } ``` All file paths in `registry.json` are relative to the catalog root directory. Absolute paths and paths that escape the catalog directory (e.g. `../other-catalog/file.mjs`) are forbidden. --- ## Registry Fields ### Top-Level Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Catalog name. Must match the catalog directory name. Kebab-case. | | `version` | `string` | Yes | Catalog version (semver). Independent of schema spec version. | | `description` | `string` | Yes | Human-readable description of the catalog's purpose. | | `schemaSpec` | `string` | Yes | FlowMCP specification version this catalog conforms to (e.g. `"3.0.0"`). | | `shared` | `array` | Yes | Shared list references. May be empty (`[]`). | | `schemas` | `array` | Yes | Schema entries. May be empty (`[]`). | | `agents` | `array` | Yes | Agent entries. May be empty (`[]`). | ### Shared List Entry Fields Each object in the `shared` array describes one shared list file. | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | `string` | Yes | Relative path from catalog root to the `.mjs` list file. | | `name` | `string` | Yes | List name (camelCase). Must match `meta.name` in the referenced file. | ### Schema Entry Fields Each object in the `schemas` array describes one provider schema file. | Field | Type | Required | Description | |-------|------|----------|-------------| | `namespace` | `string` | Yes | Provider namespace (kebab-case). Groups schemas by data source. | | `file` | `string` | Yes | Relative path from catalog root to the `.mjs` schema file. | | `name` | `string` | Yes | Human-readable schema name. | | `requiredServerParams` | `string[]` | Yes | Server parameters (API keys) the schema requires. May be empty. | | `hasHandlers` | `boolean` | Yes | Whether the schema exports a `handlers` factory function. | | `sharedLists` | `string[]` | Yes | Names of shared lists this schema references. May be empty. | ### Agent Entry Fields Each object in the `agents` array describes one agent definition. | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `string` | Yes | Agent name (kebab-case). Must match the agent directory name. | | `description` | `string` | Yes | Human-readable description of the agent's purpose. | | `manifest` | `string` | Yes | Relative path from catalog root to the agent's `agent.mjs` file. | | `prompts` | `Object` | No | Agent-specific prompts exported by the agent. | | `skills` | `Object` | No | Agent-specific skills exported by the agent. | | `resources` | `Object` | No | Agent-specific resources exported by the agent. | --- ## Import Flow The import flow describes how a catalog is downloaded and activated locally. The v3 import flow extends the v2 flow by adding agent manifest resolution. ```mermaid flowchart LR A["flowmcp import-registry URL"] --> B[Download registry.json] B --> C[Download shared lists] B --> D[Download provider schemas] B --> E[Download agent manifests] E --> F[Agents available locally] F --> G["flowmcp import-agent crypto-research"] G --> H[Activate agent tools locally] ``` The diagram shows the two-phase import process: `import-registry` downloads the catalog contents, and `import-agent` activates a specific agent's tools locally. ### Phase 1: Catalog Import (`import-registry`) 1. **Download `registry.json`** from the remote URL. 2. **Validate manifest** — check required fields, verify `schemaSpec` compatibility. 3. **Download shared lists** — resolve each `shared[].file` path and download the `.mjs` files. 4. **Download provider schemas** — resolve each `schemas[].file` path and download the `.mjs` files. 5. **Download agent manifests** — resolve each `agents[].manifest` path and download the `agent.mjs` files (and associated prompt, skill, and resource files). 6. **Store locally** — write all downloaded files into the local catalog directory. ### Phase 2: Agent Activation (`import-agent`) 1. **Read agent manifest** — parse the locally stored `agent.mjs` for the named agent. 2. **Resolve tool dependencies** — identify which provider schemas the agent requires. 3. **Activate tools** — add the agent's required tools to the local project configuration. 4. **Register prompts** — make the agent's prompts available as MCP prompts. ### v2 vs v3 Comparison | Step | v2 (Current) | v3 (New) | |------|-------------|----------| | Download | Schemas + shared lists | Schemas + shared lists + agent manifests | | Agent setup | User creates groups manually | Pre-built agents available via `import-agent` | | Tool composition | Manual cherry-picking into groups | Agent manifest declares required tools | | Prompts | Group-level prompts only | Schema-level + agent-level prompts | --- ## Three-Level Architecture The catalog structure maps directly to the three-level architecture of FlowMCP: Root, Provider, and Agent. ```mermaid flowchart TD A[Catalog Root] --> B["_lists/ — Shared Lists"] A --> C["_shared/ — Shared Helpers"] A --> D["providers/ — Provider Schemas"] A --> E["agents/ — Agent Definitions"] D --> F["providers/aave/"] D --> G["providers/coingecko-com/"] E --> H["agents/crypto-research/"] E --> I["agents/defi-monitor/"] B --> F B --> G B --> H B --> I ``` The diagram shows how shared lists at the root level flow down to both providers and agents. Providers and agents consume shared lists but never define their own. ### Root Level The root level contains resources shared across all providers and agents: | Directory | Content | Consumed By | |-----------|---------|-------------| | `_lists/` | Shared value lists (`.mjs` files) | Providers, agents | | `_shared/` | Shared helper modules | Providers | | `registry.json` | Catalog manifest | Runtime, CLI | ### Provider Level Each provider directory contains schemas for a single data source: | Content | Description | |---------|-------------| | `*.mjs` schema files | Tool and resource definitions | | `prompts/` directory | Model-neutral prompts for the provider's tools | Providers reference shared lists from the root `_lists/` directory via `main.sharedLists`. They never define their own lists. ### Agent Level Each agent directory contains a manifest and prompts for a pre-built tool composition: | Content | Description | |---------|-------------| | `agent.mjs` | Agent manifest (`export const agent`), metadata, tool dependencies, configuration | | `prompts/` directory | Agent-specific prompts | | `skills/` directory | Agent-specific skills | | `resources/` directory | Agent-specific resources (optional) | Agents reference tools from providers and shared lists from root. Like providers, agents never define their own shared lists. ### Shared List Ownership Rule Shared lists are defined **exclusively** at the catalog root level in the `_lists/` directory. Neither providers nor agents define their own lists — they consume from root. This ensures: - **Single source of truth** — one canonical version of each list - **No duplication** — providers and agents reference the same list entries - **Consistent updates** — changing a list at root propagates to all consumers --- ## Validation Rules The following rules are enforced when loading and validating a catalog. | Code | Severity | Rule | |------|----------|------| | CAT001 | error | `registry.json` must exist in the catalog root directory | | CAT002 | error | `name` field must match the catalog directory name | | CAT003 | error | All `shared[].file` paths must resolve to existing files | | CAT004 | error | All `schemas[].file` paths must resolve to existing files | | CAT005 | error | All `agents[].manifest` paths must resolve to existing files | | CAT006 | warning | Orphaned files (exist in the catalog directory but are not listed in `registry.json`) should be reported | | CAT007 | error | `schemaSpec` must be a valid FlowMCP specification version | ### Rule Details **CAT001** — The `registry.json` file is the entry point for catalog resolution. Without it, the runtime cannot discover the catalog contents. A directory without `registry.json` is not a catalog. **CAT002** — The catalog name in the manifest must match the directory name. If the directory is `flowmcp-community/`, then `name` must be `"flowmcp-community"`. This prevents confusion when multiple catalogs coexist. **CAT003** — Every shared list declared in `shared[]` must have a corresponding `.mjs` file at the declared path. Missing files indicate a broken manifest that was not regenerated after file operations. **CAT004** — Every schema declared in `schemas[]` must have a corresponding `.mjs` file at the declared path. The validator checks file existence, not schema validity — schema-level validation is handled by the rules in `01-schema-format.md` and `09-validation-rules.md`. **CAT005** — Every agent declared in `agents[]` must have an `agent.mjs` at the declared path. Missing manifests prevent agent activation. **CAT006** — Files that exist in the catalog directory tree but are not referenced in `registry.json` may indicate forgotten schemas or leftover files from development. The validator reports these as warnings to help catalog authors maintain a clean manifest. **CAT007** — The `schemaSpec` field must reference a known FlowMCP specification version. This ensures the runtime applies the correct validation rules and loading behavior for the catalog's contents. Invalid version strings (e.g. `"latest"`, `"2.x"`) are rejected. ### Validation Command ```bash flowmcp validate-catalog ``` The command runs all CAT rules and reports errors and warnings. A catalog with any error-level violations cannot be imported or published. --- # FlowMCP Specification v3.0.0 — ID Schema A unified ID system for referencing Tools, Resources, and Prompts across the FlowMCP ecosystem. IDs must be unambiguous, human-readable, and resolvable. This document defines the ID format, component rules, short form conventions, resolution algorithm, placeholder integration, namespace governance, and validation rules. --- ## Purpose FlowMCP exposes three MCP primitives — Tools, Resources, and Skills (prompts) — across potentially hundreds of schemas from dozens of providers. As the ecosystem grows, references to these primitives appear in multiple contexts: group definitions, skill placeholders, registry entries, CLI commands, and cross-schema dependencies. Without a unified ID system, references are ambiguous. Does `simplePrice` refer to a tool, a resource, or a prompt? Which provider owns it? Is `evmChains` a tool name or a shared list? The ID schema solves this by defining a **structured, human-readable identifier format** that uniquely identifies any primitive in the ecosystem. Every tool, resource, prompt, and shared list has exactly one canonical ID. ```mermaid flowchart LR A[Namespace] --> B[/] B --> C[Resource Type] C --> D[/] D --> E[Name] E --> F["coingecko/tool/simplePrice"] ``` The diagram shows the three components of a full ID separated by `/` delimiters, forming a single unambiguous reference string. --- ## Format The canonical ID format is a three-segment string separated by `/`: ``` namespace/resourceType/name ``` ### Full Form Examples | ID | Description | |----|-------------| | `coingecko/tool/simplePrice` | Tool from CoinGecko provider | | `coingecko/resource/supported-coins` | Resource from CoinGecko | | `coingecko/prompt/price-comparison` | Prompt from CoinGecko | | `crypto-research/prompt/token-deep-dive` | Agent prompt | | `shared/list/evmChains` | Shared list reference | Each segment serves a distinct purpose: the namespace identifies the owner, the resource type discriminates the primitive kind, and the name identifies the specific item within that namespace and type. --- ## Components | Component | Pattern | Required | Description | |-----------|---------|----------|-------------| | namespace | `^[a-z][a-z0-9-]*$` | Yes | Provider or agent identifier. Lowercase letters, digits, and hyphens. Must start with a letter. | | resourceType | `tool`, `resource`, `prompt`, `list` | Context-dependent | Type discriminator. Required in full form. Omittable in short form when context is unambiguous (see [Short Form](#short-form)). | | name | `^[a-zA-Z][a-zA-Z0-9-]*$` | Yes | Resource name. camelCase for tools and resources, kebab-case for prompts. Must start with a letter. | ### Component Details #### Namespace The namespace identifies the owner of the primitive. It is derived from the provider's domain name or agent name and must be globally unique within a FlowMCP registry. ``` coingecko ← provider namespace etherscan ← provider namespace defilama ← provider namespace crypto-research ← agent namespace shared ← reserved namespace for shared lists ``` Namespace rules: - Lowercase letters, digits, and hyphens only - Must start with a letter - No dots, underscores, or uppercase characters - `shared` is a reserved namespace (see [Namespace Rules](#namespace-rules)) #### Resource Type The resource type discriminates between the four kinds of addressable primitives: | Type | Maps To | Defined In | |------|---------|-----------| | `tool` | MCP `server.tool` | `main.tools` | | `resource` | MCP `server.resource` | `main.resources` | | `prompt` | MCP `server.prompt` | `main.skills` | | `list` | Shared list | `list.meta.name` | #### Name The name identifies the specific primitive within its namespace and type. Naming conventions follow the same rules as schema element names (see `01-schema-format.md`): | Primitive | Convention | Example | |-----------|-----------|---------| | Tool | camelCase | `simplePrice`, `getContractAbi` | | Resource | camelCase | `tokenLookup`, `chainConfig` | | Prompt | kebab-case | `price-comparison`, `token-deep-dive` | | Shared List | camelCase | `evmChains`, `countryCodes` | --- ## Short Form When the resource type is unambiguous from context, the `resourceType` segment can be omitted: ``` coingecko/simplePrice ← tool (default in tool contexts) coingecko/price-comparison ← prompt (when in prompt context) ``` ### Short Form Rules | Context | Default Type | Example | |---------|-------------|---------| | `manifest.tools[]` | `tool` | `coingecko/simplePrice` | | CLI `flowmcp call` | `tool` | `flowmcp call coingecko/simplePrice` | | `{{type:name}}` placeholder in skill content | Determined by type prefix | `{{tool:simplePrice}}` resolves to tool | | Validation rules | Full form required | `coingecko/tool/simplePrice` | | `registry.json` | Full form required | `coingecko/tool/simplePrice` | | Group definitions | Full form required | `coingecko/tool/simplePrice` | ### When Short Form Is Allowed - In tool contexts (`manifest.tools[]`, CLI call commands), the default type is `tool` - In prompt content (`{{type:name}}` references), the type is determined by the placeholder prefix (`tool:`, `resource:`, `skill:`, `input:`) - Short form is a convenience — it does not change the canonical ID ### When Full Form Is Required - In `registry.json` entries — the registry is the source of truth and must be unambiguous - In validation rules and error messages — precision matters for debugging - In group definitions — groups compose primitives across schemas and types - In any context where multiple types share the same namespace --- ## Resolution How IDs are resolved to actual files, schemas, and internal references. ### Resolution Algorithm ```mermaid flowchart TD A["Receive ID string"] --> B["Split on /"] B --> C{Segment count?} C -->|3 segments| D["Full form: namespace / type / name"] C -->|2 segments| E["Short form: namespace / name"] C -->|1 or 0| F["Error: ID001"] E --> G["Infer type from context"] G --> D D --> H["Look up namespace in registry"] H --> I{Namespace found?} I -->|No| J["Error: namespace not registered"] I -->|Yes| K["Find schema with matching type + name"] K --> L{Match found?} L -->|No| M["Error: name not found in namespace"] L -->|Yes| N["Return file path + internal reference"] ``` The diagram shows the resolution flow from receiving an ID string through parsing, namespace lookup, and name matching to the final file path reference. ### Resolution Steps 1. **Parse** — split the ID string on `/` to extract segments. If three segments: namespace, type, name. If two segments: namespace, name (type inferred from context). If fewer than two segments: validation error ID001. 2. **Find** — look up the namespace in the loaded registry or schema catalog. The registry maps namespaces to schema file locations. 3. **Match** — within the namespace, find the schema, tool, resource, or prompt with the matching name and type. 4. **Return** — produce the resolved reference: file path to the schema file and the internal key path (e.g., `main.tools.simplePrice`). ### Resolution Context The resolution context determines how short-form IDs are expanded: | Caller | Context | Short Form Expansion | |--------|---------|---------------------| | CLI (`flowmcp call`) | Tool execution | `namespace/name` becomes `namespace/tool/name` | | Skill placeholder (`{{type:name}}`) | Content rendering | Type determined by placeholder prefix (`tool:`, `resource:`, `skill:`, `input:`) | | Group definition | Group loading | Full form required — no expansion | | Validator | Schema validation | Full form required — no expansion | --- ## Usage in Placeholders The ID schema connects to the `{{type:name}}` placeholder syntax used in skill content (see `14-skills.md`). Skill content uses typed placeholders with a `type:` prefix to reference tools, resources, skills, and input parameters. | Placeholder | Interpretation | |-------------|---------------| | `{{tool:getContractAbi}}` | Tool reference — resolved to a tool in the same schema's `main.tools` | | `{{resource:verifiedContracts}}` | Resource reference — resolved to a resource in the same schema's `main.resources` | | `{{skill:quick-summary}}` | Skill reference — resolved to a skill in the same schema's `main.skills` | | `{{input:address}}` | Input parameter — value provided by the user at runtime | ### Resolution in Skills When a skill's `content` field contains `{{tool:name}}`, `{{resource:name}}`, or `{{skill:name}}` placeholders, the runtime: 1. Parses the placeholder type prefix to determine the primitive kind 2. Resolves the name to a registered primitive within the same schema 3. Injects the primitive's description or metadata into the rendered content The ID schema provides the canonical identifier format (`namespace/type/name`) used in registries, group definitions, and cross-schema references. Within skill content, the `{{type:name}}` syntax references primitives scoped to the same schema. --- ## Namespace Rules Namespaces are the top-level organizational unit. They must be unique within a registry and follow strict governance rules. ### Namespace Assignment | Source | Namespace Derivation | Example | |--------|---------------------|---------| | API Provider | Domain-derived name | `coingecko`, `etherscan`, `defilama` | | Agent | Agent name | `crypto-research`, `defi-monitor` | | Shared resources | Reserved `shared` | `shared/list/evmChains` | ### Provider Namespaces Providers use their domain-derived name as the namespace. The derivation follows these rules: - Remove the TLD (`.com`, `.io`, `.org`, etc.) - Lowercase the remainder - Replace dots with hyphens - Remove `www.` prefix if present ``` api.coingecko.com → coingecko etherscan.io → etherscan defillama.com → defilama pro-api.coinmarketcap.com → coinmarketcap ``` ### Agent Namespaces Agents use their agent name as the namespace. Agent namespaces follow the same pattern constraints as provider namespaces (`^[a-z][a-z0-9-]*$`). ``` crypto-research ← agent that performs token research defi-monitor ← agent that monitors DeFi protocols ``` ### Reserved Namespaces | Namespace | Purpose | |-----------|---------| | `shared` | Shared lists referenced across schemas. Only `list` type is valid under this namespace. | The `shared` namespace is reserved by the FlowMCP specification. Schema authors must not use `shared` as a provider or agent namespace. --- ## Validation Rules | Code | Severity | Rule | |------|----------|------| | ID001 | error | ID must contain at least one `/` separator | | ID002 | error | Namespace must match `^[a-z][a-z0-9-]*$` | | ID003 | error | ResourceType (if present) must be one of: `tool`, `resource`, `prompt`, `list` | | ID004 | error | Name must not be empty | | ID005 | warning | Short form should only be used in unambiguous contexts | | ID006 | error | Full form is required in `registry.json` and validation rules | ### Validation Output Examples ``` flowmcp validate --id "coingecko/tool/simplePrice" 0 errors, 0 warnings ID is valid ``` ``` flowmcp validate --id "COINGECKO/tool/simplePrice" ID002 error Namespace "COINGECKO" must match ^[a-z][a-z0-9-]*$ 1 error, 0 warnings ID is invalid ``` ``` flowmcp validate --id "simplePrice" ID001 error ID must contain at least one "/" separator 1 error, 0 warnings ID is invalid ``` --- ## Examples ### Tool Reference ``` coingecko/tool/simplePrice ``` - **Namespace**: `coingecko` — the CoinGecko provider - **Type**: `tool` — an MCP tool (API endpoint) - **Name**: `simplePrice` — the specific tool name (camelCase) ### Resource Reference ``` coingecko/resource/supported-coins ``` - **Namespace**: `coingecko` — the CoinGecko provider - **Type**: `resource` — an MCP resource (SQLite data) - **Name**: `supported-coins` — the specific resource ### Prompt Reference ``` crypto-research/prompt/token-deep-dive ``` - **Namespace**: `crypto-research` — an agent namespace - **Type**: `prompt` — an MCP prompt (skill) - **Name**: `token-deep-dive` — the specific prompt (kebab-case) ### Shared List Reference ``` shared/list/evmChains ``` - **Namespace**: `shared` — reserved namespace - **Type**: `list` — a shared list - **Name**: `evmChains` — the specific list (camelCase) --- ## Relationship to Existing Identifiers The ID schema unifies several existing identification mechanisms: | Existing Mechanism | ID Schema Equivalent | Migration | |-------------------|---------------------|-----------| | `namespace/file::tool` (group format) | `namespace/tool/name` | Replace `file::tool` with `tool/name` | | `::resource::namespace/file::query` (group format) | `namespace/resource/name` | Replace prefix + `file::query` with `resource/name` | | Skill `requires.tools` entries | `namespace/tool/name` | Add namespace prefix | | Shared list `ref` field | `shared/list/name` | Wrap in `shared/list/` prefix | The ID schema provides a single, consistent format that replaces these context-specific referencing styles. Backward compatibility with existing formats is maintained during migration — see `08-migration.md`. ---