# FlowMCP Specification v4.2.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 23 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 18. Selections 19. Prefill 20. MCP Integration 21. Validation Strategy 22. Schema Lifecycle 23. Scoring Protocol --- # FlowMCP Specification v4.2.0 — Overview | Field | Value | |-------|-------| | Related | [01-schema-format.md](./01-schema-format.md), [06-agents.md](./06-agents.md), [17-selections.md](./17-selections.md), [15-catalog.md](./15-catalog.md) | 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 v4.2.0 specification. --- ## Conformance Language The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. Some specification files in `spec/v4.2.0/` are intentionally written in prose without normative keywords because they describe history, lifecycle, or conceptual background (this overview document, the migration guide, the schema lifecycle document). All other specification files use normative language and assume this conformance interpretation. References: - [RFC2119](https://www.rfc-editor.org/rfc/rfc2119) — Key words for use in RFCs to Indicate Requirement Levels - [RFC8174](https://www.rfc-editor.org/rfc/rfc8174) — Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words - [BCP 14](https://www.rfc-editor.org/info/bcp14) — Best Current Practice 14 (combined RFC2119 + RFC8174) --- ## 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] C --> I[Selections — Curated tool subsets for agent context] G --> H[Tool Combinatorics — How to chain tools across providers] ``` FlowMCP provides **five primitives**: Tools, Resources, Prompts, Skills, and Selections. Tools, Resources, and Prompts are defined in `main.tools`, `main.resources`, and `main.prompts`. **Skills are a top-level entity** that lives outside `main` and is scoped to a namespace, selection, or agent — never `main.skills` (forbidden in v4.0.0, see VAL016). Selections are curated subsets that bundle tools/resources/prompts/skills for agent loading. 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 | | **Selections** | Partially | Structure valid, all referenced Primitives resolvable (SEL001–SEL003) | | **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, selections] P2[coingecko/ — tools, resources, prompts, selections] P3[defillama/ — tools, resources, prompts, selections] 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 (including `type: 'namespace' \| 'selection' \| 'agent'`), input/output declarations, and Markdown instructions with `{{type:name}}` placeholders. Lives in `providers/{ns}/skills/`, `selections/{name}/skills/`, or `agents/{name}/skills/` depending on `type`. **NOT defined under `main.skills`** (forbidden in v4.0.0). 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 | | `17-selections.md` | Selections | Curated tool subsets for agent loading — `whenToUse`, reference resolution, SEL validation rules | | `18-prefill.md` | Prefill | Pre-execute tools before delivering a Skill — prefill array, execution order, result injection | | `19-mcp-integration.md` | MCP Integration | `meta` block for Tools (isReadOnly, isConcurrencySafe, isDestructive, searchHint, aliases, alwaysLoad) | | `20-validation-strategy.md` | Validation Strategy | Validation pipeline, grade system, quality thresholds, CI integration | | `21-schema-lifecycle.md` | Schema Lifecycle | Six lifecycle stages, API-test special rule for static schemas, partial schema policy | --- ## 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. `runSql` 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 ### v4.2.0 The v4.2.0 release adds remote-data resources, a fifth primitive, and richer agent validation: - **HTTP Resources** — `source: 'http'` references remote files (typically SQLite databases) fetched via HTTPS and cached locally. Validated by RES024 (HTTPS required). See `13-resources.md`. - **Selection primitive** — A fifth primitive: a named collection of Tools, Resources, Prompts, and Skills that belong together thematically, activated as a coherent set. See `17-selections.md`. - **Extended Agent rules** — `agent.selections` references MUST resolve to valid Selection IDs (AGT030). `elicitation.maxRounds` MUST be a positive integer (AGT031). See `06-agents.md`. ### v4.0.0 The v4.0.0 release establishes the major-4 baseline with a mandatory MCP integration layer: - **Required `meta` block per Tool** — Every Tool MUST declare `isReadOnly`, `isConcurrencySafe`, `isDestructive`, `searchHint`, `aliases`, and `alwaysLoad` (VAL100–VAL106). Missing `meta` is a validation error. - **Skills as a scoped primitive** — `main.skills` is removed. Skills are namespace-, selection-, or agent-scoped instead. See `14-skills.md`. - **Enum / Shared List enforcement** — Enum values matching a Shared List MUST use `{{listName:alias}}` rather than hardcoded duplicates (VAL107). - **Unified spec version** — Skills and agents adopt `flowmcp/4.0.0` as the unified version identifier. The migration path from v3.0.0 to v4.0.0 is documented in `08-migration.md`. ### 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. - **`runSql` auto-injected** — Runtime automatically adds runSql. SELECT-only for in-memory, all statements for file-based. - **Max queries increased to 8** — 7 schema-defined (including getSchema) + 1 auto-injected runSql. - **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. ### 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. In v4.0.0 Skills are namespace/selection/agent-scoped (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`. ### 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 v4.2.0 — Schema Format | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md) | | Related | [02-parameters.md](./02-parameters.md), [04-output-schema.md](./04-output-schema.md), [13-resources.md](./13-resources.md), [14-skills.md](./14-skills.md), [16-id-schema.md](./16-id-schema.md), [19-mcp-integration.md](./19-mcp-integration.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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: '4.0.0', root: 'https://api.provider.com', tools: { /* ... */ }, resources: { /* optional, see 13-resources.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: '4.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 and hyphens (`/^[a-z][a-z0-9-]*$/`). Groups schemas by data source. | | `name` | `string` | Human-readable schema name in PascalCase (e.g. `SmartContractExplorer`). | | `description` | `string` | What this schema does, 1-2 sentences. Appears in tool discovery. | | `version` | `string` | Must match pattern `4.\d+.\d+` (semver, major MUST be `4`). Version `3.\d+.\d+` is accepted during migration. **FlowMCP-Spec-Version** (frozen by Major). | | `schemaVersion` | `string` | **NEW in v4.1.1.** Schema-Content-Version, must match pattern `\d+\.\d+\.\d+` (semver, free per schema). Bump rules defined in the grading specification. Initial value for migrated schemas: `1.0.0`. | | `schemaHash` | `string` | **NEW in v4.1.1.** 8-character sha256-prefix (`[0-9a-f]{8}`) of canonical-JSON-serialised schema (excluding the `schemaHash` field itself). Used as stable identifier in `grading-data/schemas//--v.mjs` snapshots. Automatically generated. | | `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. | #### Two Version Axes (v4.1.1+) | Axis | Field | Range | Meaning | |------|-------|-------|---------| | Spec-Version | `version` | `4.\d+.\d+` (Major frozen) | FlowMCP-Spec-Version | | Schema-Version | `schemaVersion` | `\d+.\d+.\d+` (semver, free) | Schema-Content-Version | Example header (v4.1.1): ```javascript export const main = { namespace: 'etherscan', name: 'GetContractEthereum', description: 'Fetch verified contract source by address', version: '4.0.0', schemaVersion: '1.0.0', schemaHash: 'a1b2c3d4', root: 'https://api.etherscan.io', tools: { /* ... */ } } ``` ### Optional Fields | Field | Type | Default | Description | |-------|------|---------|-------------| | `docs` | `string[]` | `[]` | Documentation URLs for the API provider. Informational only. | | `termsOfService` | `string \| null` | `null` | URL to the API provider's Terms of Services. **Informational only — FlowMCP does not classify or interpret ToS.** See `23-license-and-tos.md`. | | `termsOfServiceCheckedAt` | `string \| null` | `null` | ISO-Date (YYYY-MM-DD) when `termsOfService` URL was last verified by FlowMCP authors. | | `termsOfServiceLanguage` | `string \| null` | `null` | Two-letter language code of the ToS document (`'en'`, `'de'`, `'multi'`). | | `dataLicense` | `string \| null` | `null` | URL to the data license of API responses (e.g. CC-BY, Public Domain), if separately published by provider. | | `dataLicenseName` | `string \| null` | `null` | Human-readable name of the data license (e.g. `'CC-BY-SA-4.0'`, `'Public Domain'`). | | `tags` | `string[]` | `[]` | Categorization tags for tool discovery (e.g. `['defi', 'tvl']`). | | `requiredServerParams` | `string[]` | `[]` | Environment variable names needed at runtime (e.g. `['ETHERSCAN_API_KEY']`). | | `requiredLibraries` | `string[]` | `[]` | npm packages needed by handlers. Must be on the runtime allowlist. See `05-security.md`. | | `headers` | `object` | `{}` | Default HTTP headers applied to all routes. Route-level headers override these. | | `sharedLists` | `object[]` | `[]` | Shared list references. See `03-shared-lists.md` for format. | | `resources` | `object` | `{}` | Resource definitions. Keys are resource names in camelCase. Maximum 2 resources. See `13-resources.md`. | | `meta` | `object` | — | **Required.** MCP integration metadata block for all tools. See `19-mcp-integration.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`). Lowercase ASCII letters, digits, and hyphens are allowed — no underscores or uppercase. ```javascript // Valid namespace: 'etherscan' namespace: 'coingecko' namespace: 'defillama' namespace: 'coingecko-com' namespace: 'etherscan-io' // Invalid 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 | Purpose | |---------|---------| | `preRequest` | Transform request parameters before HTTP call | | `executeRequest` | Replace standard HTTP fetch entirely | | `postRequest` | Transform HTTP response after the call | | Handler | When | Input | Must Return | |---------|------|-------|-------------| | `preRequest` | Before the API call | `{ struct, payload }` | `{ struct, payload }` | | `executeRequest` | Replaces the HTTP call entirely | `{ struct, payload }` | `{ response }` | | `postRequest` | After the API call (or after executeRequest) | `{ response, struct, payload }` | `{ response }` | #### `executeRequest` Semantics When `executeRequest` is defined for a tool, the runtime skips the standard HTTP fetch and calls the handler instead. The handler is responsible for producing a response. Common use cases include: - **XML/TRIAS APIs** that cannot be parsed with the standard JSON pipeline - **CSW/OGC endpoints** with non-standard response formats - **SQLite-backed schemas** that resolve queries locally without HTTP - **Composite calls** that MUST combine multiple upstream requests into one ```javascript export const handlers = ( { sharedLists, libraries } ) => ({ getXmlData: { executeRequest: async ( { struct, payload } ) => { const { xml2js } = libraries const rawResponse = await fetch( struct.url, { method: struct.method, headers: struct.headers } ) const text = await rawResponse.text() const parsed = await xml2js.parseStringPromise( text ) return { response: parsed } } } }) ### Handler Per-Call Parameters 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, digits, hyphens | `^[a-z][a-z0-9-]*$` | `etherscan`, `coingecko`, `coingecko-com` | | Schema name | PascalCase | `^[A-Z][a-zA-Z0-9]*$` | `SmartContractExplorer` | | Schema filename | PascalCase `.mjs` | `^[A-Z][a-zA-Z0-9]*\.mjs$` | `SmartContractExplorer.mjs` | | Tool name | camelCase | `^[a-z][a-zA-Z0-9]*$` | `getContractAbi` | | Resource name | camelCase | `^[a-z][a-zA-Z0-9]*$` | `tokenLookup` | | Skill name | lowercase-hyphen | `^[a-z][a-z0-9-]{0,63}$` | `full-contract-audit` | | Parameter key | camelCase | `^[a-z][a-zA-Z0-9]*$` | `contractAddress` | | Shared list name | camelCase | `^[a-z][a-zA-Z0-9]*$` | `evmChains` | | Tag | lowercase with hyphens | `^[a-z][a-z0-9-]*$` | `smart-contracts` | --- ## Constraints | Constraint | Value | Rationale | |------------|-------|-----------| | Max tools per schema | 8 | Keeps schemas focused. Split large APIs into multiple schemas. | | Max resources per schema | 2 | Keeps resource scope focused. See `13-resources.md`. | | Version major | `4` | Must match `4.\d+.\d+`. Version `3.\d+.\d+` accepted during migration with warning. | | `tools` + `routes` simultaneously | Forbidden | A schema MUST use `tools` or `routes`, never both. Using both is a validation error. | | Namespace pattern | `^[a-z][a-z0-9-]*$` | Lowercase letters, digits, hyphens. No underscores or uppercase. | | Tool name pattern | `^[a-z][a-zA-Z0-9]*$` | camelCase, starts with lowercase letter. | | Root URL protocol | `https://` | HTTP is not allowed. All API calls go over TLS. | | Root URL requirement | Conditional | Required when `tools` are defined. Optional for resource-only or skill-only schemas. | | Root URL trailing slash | Forbidden | `root` MUST NOT end with `/`. | | `main` export | JSON-serializable | Must survive `JSON.parse( JSON.stringify() )` roundtrip. | | Schema file imports | Zero | Schema files MUST have no `import` statements. All dependencies are injected. | | `requiredLibraries` | Allowlist only | Every entry MUST be on the runtime allowlist. See `05-security.md`. | --- ## Runtime Loading Sequence 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 --> N[Done] ``` **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 `executeRequest` handlers replace the HTTP call; tools with `preRequest`/`postRequest` handlers use pre/post processing; tools without handlers make direct API calls. --- ## 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: '4.0.0', root: 'https://api.etherscan.io', docs: [ 'https://docs.etherscan.io/api-endpoints/contracts' ], tags: [ 'smart-contracts', 'evm', 'abi' ], requiredServerParams: [ 'ETHERSCAN_API_KEY' ], requiredLibraries: [], headers: { 'Accept': 'application/json' }, sharedLists: [ { ref: 'evmChains', version: '1.0.0', filter: { key: 'etherscanAlias', exists: true } } ], tools: { getContractAbi: { method: 'GET', path: '/api', description: 'Returns the Contract ABI of a verified smart contract', parameters: [ { position: { key: 'module', value: 'contract', location: 'query' }, z: { primitive: 'string()', options: [] } }, { position: { key: 'action', value: 'getabi', location: 'query' }, z: { primitive: 'string()', options: [] } }, { position: { key: 'address', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: [ 'min(42)', 'max(42)' ] } }, { position: { key: 'apikey', value: '{{SERVER_PARAM:ETHERSCAN_API_KEY}}', location: 'query' }, z: { primitive: 'string()', options: [] } } ], output: { mimeType: 'application/json', schema: { type: 'object', properties: { status: { type: 'string', description: 'API response status' }, message: { type: 'string', description: 'Status message' }, result: { type: 'string', description: 'Contract ABI as JSON string' } } } } }, getSourceCode: { method: 'GET', path: '/api', description: 'Returns the Solidity source code of a verified smart contract', parameters: [ { position: { key: 'module', value: 'contract', location: 'query' }, z: { primitive: 'string()', options: [] } }, { position: { key: 'action', value: 'getsourcecode', location: 'query' }, z: { primitive: 'string()', options: [] } }, { position: { key: 'address', value: '{{USER_PARAM}}', location: 'query' }, z: { primitive: 'string()', options: [ 'min(42)', 'max(42)' ] } }, { position: { key: 'apikey', value: '{{SERVER_PARAM:ETHERSCAN_API_KEY}}', location: 'query' }, z: { primitive: 'string()', options: [] } } ] } } } export const handlers = ( { sharedLists } ) => ({ getSourceCode: { postRequest: async ( { response, struct, payload } ) => { const { result } = response const [ first ] = result const { SourceCode, ABI, ContractName, CompilerVersion, OptimizationUsed } = first const simplified = { contractName: ContractName, compilerVersion: CompilerVersion, optimizationUsed: OptimizationUsed === '1', sourceCode: SourceCode, abi: ABI } return { response: simplified } } } }) ``` ### What this example demonstrates 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 v4.2.0 — Parameters | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [03-shared-lists.md](./03-shared-lists.md), [04-output-schema.md](./04-output-schema.md), [18-prefill.md](./18-prefill.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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 registered in the current scope (`selection.skills`, `agent.skills`, or the active namespace's `providers/{ns}/skills/`). `main.skills` is forbidden in v4.0.0. | `{{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 | | VAL107 | error | When enum values correspond to a Shared List, `{{listName:alias}}` MUST be used. Hardcoded enum values that duplicate a Shared List are forbidden. | #### 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 v4.2.0 — Shared Lists | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md), [02-parameters.md](./02-parameters.md) | | Related | [15-catalog.md](./15-catalog.md), [16-id-schema.md](./16-id-schema.md), [09-validation-rules.md](./09-validation-rules.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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. --- ## Alias-Mapping Pattern Shared Lists connect two worlds: - **User side**: readable aliases (e.g., `ethereum`, `polygon`, `bsc`) - **Provider side**: API-specific field values (e.g., `eth`, `0x89`, `56`) Schemas use Shared Lists not just for validation, but as a mapping layer: ```javascript // Schema parameter (user side): { name: 'chain', type: 'string', enum: '{{evmChains:alias}}' } // Handler (provider side): preRequest: ({ struct, payload }) => { const chain = payload.userParams.chain const moralisSlug = struct.sharedLists.evmChains.find(e => e.alias === chain)?.moralisChainSlug return { ...payload, builtPayload: { chain: moralisSlug } } } ``` **Important:** Schemas that hardcode enums instead of using Shared Lists with handler mapping fail at VAL107 (Error). The Alias-Mapping Pattern is the correct implementation. ### Why This Pattern Matters Without Alias-Mapping, every provider maintains its own chain list with provider-specific values. An LLM using multiple providers MUST know that `ethereum` for CoinGecko means `eth` for Moralis, `ETH` for Etherscan, and `Ethereum` for DeFi Llama. This knowledge cannot be embedded reliably in tool descriptions. With Alias-Mapping, every provider exposes the same canonical user-side alias (`ethereum`). The handler maps it to the provider-specific value at call time. The LLM only needs to know one alias per chain. ### Canonical Alias Convention The `alias` field in a Shared List entry is the **canonical user-side value**. It is: - Lowercase - Human-readable (not a chain ID number) - Stable across Shared List versions (never renamed without a major version bump) All schemas in the community catalog MUST use the `alias` field as the user-facing enum value when referencing EVM chains. --- ## 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 v4.2.0 — Output Schema | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md), [02-parameters.md](./02-parameters.md) | | Related | [10-tests.md](./10-tests.md), [22-scoring-protocol.md](./22-scoring-protocol.md), [19-mcp-integration.md](./19-mcp-integration.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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 v4.2.0 — Security Model | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [09-validation-rules.md](./09-validation-rules.md), [13-resources.md](./13-resources.md), [23-license-and-tos.md](./23-license-and-tos.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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 — SEC020] 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 SEC020. 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 — SEC020 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 `new Function` found | | SEC006 | Forbidden `process.` access found | | SEC007 | Forbidden `child_process` access found | | SEC008 | Forbidden `fs.` access found | | SEC009 | Forbidden `node:fs` import found | | SEC010 | Forbidden `fs/promises` import found | | SEC011 | Forbidden `globalThis.` access found | | SEC012 | Forbidden `global.` access found | | SEC013 | Forbidden `__dirname` path variable found | | SEC014 | Forbidden `__filename` path variable found | | SEC015 | Forbidden `setTimeout` timer found | | SEC016 | Forbidden `setInterval` timer found | | SEC020 | Unapproved library in `requiredLibraries` — not on allowlist | > `SEC017`–`SEC019` are pipeline-level checks defined in [09-validation-rules.md](./09-validation-rules.md). **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-SEC016) found in shared list | --- # FlowMCP Specification v4.2.0 — Agents | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [12-prompt-architecture.md](./12-prompt-architecture.md), [14-skills.md](./14-skills.md), [17-selections.md](./17-selections.md), [16-id-schema.md](./16-id-schema.md), [10-tests.md](./10-tests.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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/4.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/4.0.0`. Declares which spec version this agent conforms to (unified versioning across all FlowMCP primitives). | | `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. | | `selections` | `string[]` | No | Selection IDs to load when the agent starts. All referenced Selections MUST be resolvable. See [Selections](#selections). | | `elicitation` | `object` | No | MCP Elicitation configuration. See [Elicitation](#elicitation). | ### 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/4.0.0 <- valid (current spec) flowmcp/3.0.0 <- DEPRECATED (v3 agent, accepted with warning during migration) 4.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 `agent.skills` (and any referenced Selections' `selection.skills` and the active namespaces' `providers/{ns}/skills/`). Each MUST export `export const skill`. `main.skills` is forbidden in v4.0.0. 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` --- ## Selections The `agent.selections` field is an array of Selection IDs. When an agent loads, all referenced Selections are automatically loaded alongside the agent's tools and prompts. ```javascript export const agent = { name: 'evm-researcher', // ... other fields ... selections: [ 'evm-research/selection/contract-analysis', 'defi-research/selection/tvl-monitor' ] } ``` ### Selection Loading Behavior - Each Selection ID MUST follow the `namespace/selection/name` format (see `17-selections.md`) - Selections are loaded at agent startup as part of the activation sequence - If a referenced Selection cannot be loaded (missing file, resolution error), agent startup fails with validation rule AGT030 ### Validation Rule **AGT030:** All IDs in `agent.selections` must be resolvable Selection IDs. An unresolvable Selection causes agent startup to fail with a clear error identifying which Selection could not be loaded. --- ## Elicitation Elicitation enables an agent to request additional input from the user during a conversation using the MCP Elicitation Protocol. The `agent.elicitation` field configures this behavior. ```javascript export const agent = { name: 'wallet-analyzer', // ... other fields ... elicitation: { enabled: true, maxRounds: 3, requestedSchemas: [ { message: 'Please provide your wallet address:', requestedSchema: { type: 'object', properties: { address: { type: 'string', title: 'Wallet Address', description: 'Ethereum address (0x-prefixed, 42 chars)' } }, required: [ 'address' ] } } ] } } ``` ### MCP Elicitation Protocol Elicitation uses the MCP `elicitation/create` method to request input from the user through the MCP client: | Step | Direction | Payload | |------|-----------|---------| | 1. Request | Server → Client | `{ message: string, requestedSchema: JSONSchema }` | | 2. Response | Client → Server | `{ action: 'accept' \| 'decline' \| 'cancel', content: { ... } }` | **`requestedSchema` restrictions:** Only restricted JSON Schema is allowed — top-level properties only, no nesting beyond one level. Arrays and deeply nested objects are not permitted in the elicitation schema. **Client actions:** - `accept` — user provided the requested data; `content` contains the values - `decline` — user explicitly refused to provide the data - `cancel` — user cancelled the entire operation ### Elicitation Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `enabled` | `boolean` | Yes | Whether elicitation is active for this agent | | `maxRounds` | `number` | Yes | Maximum number of elicitation rounds per conversation. Must be a positive integer (≥ 1). | | `requestedSchemas` | `array` | No | Pre-defined elicitation request templates the agent MAY use | ### FlowMCP Policy The `maxRounds` field limits how many times the agent can request user input in a single conversation. After the limit is reached, the agent decides autonomously based on available information — it does not wait for additional user input. This prevents infinite elicitation loops and ensures that agents remain responsive even when users are unavailable. `elicitation.maxRounds` must be a positive integer (value ≥ 1); zero, negative values, and non-integers are rejected (AGT031, see the canonical table below). --- ## 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/4.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`) | | AGT031 | error | `elicitation.maxRounds` must be a positive integer (value ≥ 1). Zero, negative values, and non-integers are rejected. | ### 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/4.0.0`. This is not the agent's own version — it declares which FlowMCP specification the manifest conforms to (unified versioning — Schema, Selection, Agent, Skill, and Prompt all use the same `flowmcp/X.Y.Z` string). **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/4.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/4.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 v4.2.0 — MCP Tasks | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [04-output-schema.md](./04-output-schema.md), [10-tests.md](./10-tests.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). ## 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 v4.2.0 — Migration Guide | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [06-agents.md](./06-agents.md), [13-resources.md](./13-resources.md), [14-skills.md](./14-skills.md), [21-schema-lifecycle.md](./21-schema-lifecycle.md) | This guide covers migrating schemas between FlowMCP versions in chronological order. Section 1 covers v1.2.0 to v2.0.0 migration, Section 2 covers v2.0.0 to v3.0.0, and Section 3 covers v3.0.0 to v4.0.0. --- ## Section 1: 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 (v1.2.0 to v2.0.0) ### 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) --- ## Section 2: 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 3: v3.0.0 to v4.0.0 The v3.0.0 to v4.0.0 migration requires adding a required `meta` block to every Tool and removing `main.skills`. There is no automatic migration command — the changes require domain knowledge about each tool. ### What Changes | Step | What | How | |------|------|-----| | 1 | Remove `main.skills` | Move skills into a Selection or Agent Manifest | | 2 | Add meta block per Tool | Set all 6 fields explicitly, `alwaysLoad: false` as default | | 3 | Enum enforcement | Enums matching a Shared List MUST use `{{listName:alias}}` | | 4 | Update version to 4.0.0 | `main.version: '4.0.0'` | | 5 | Validate | `flowmcp validate` → PASS | | 6 | API-Test | `flowmcp test single` → min. 1 response | ### Breaking Changes - `main.skills` removal is a hard breaking change (no deprecation). Skills must be referenced via Selections or Agent Manifests. - `meta` block is required for every Tool (VAL100–VAL106). Missing `meta` is a validation error. ### Why No Automatic Migration The v3→v4 migration is more complex than v2→v3. Adding `meta` blocks requires domain knowledge about each tool (Is it read-only? Is it safe to call concurrently? What are good search keywords?). An automatic migration would generate incorrect defaults. Therefore: manual process guided by this document. ### Example: Before and After **Before (v3.0.0):** ```javascript export const schema = { main: { namespace: 'etherscan-io', name: 'contracts', version: '3.0.0' }, tools: { getAbi: { description: 'Get contract ABI', parameters: { address: { type: 'string', description: 'Contract address' } } } } } ``` **After (v4.0.0):** ```javascript export const schema = { main: { namespace: 'etherscan-io', name: 'contracts', version: '4.0.0' }, tools: { getAbi: { description: 'Get contract ABI', parameters: { address: { type: 'string', description: 'Contract address' } }, meta: { isReadOnly: true, isConcurrencySafe: true, isDestructive: false, searchHint: 'contract ABI ethereum smart contract', aliases: [ 'getContractAbi' ], alwaysLoad: false } } } } ``` --- # FlowMCP Specification v4.2.0 — Validation Rules | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [05-security.md](./05-security.md), [02-parameters.md](./02-parameters.md), [06-agents.md](./06-agents.md), [13-resources.md](./13-resources.md), [14-skills.md](./14-skills.md), [16-id-schema.md](./16-id-schema.md), [17-selections.md](./17-selections.md), [20-validation-strategy.md](./20-validation-strategy.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). This document defines all validation rules enforced by `flowmcp validate`. Each rule has a code, severity, and description. This file is the **central code registry** for FlowMCP v4.2.0. All validation, selection, agent, skill, resource, and placeholder codes (VAL/SEL/AGT/SKL/RES/DEP/SEC/LST/PRM/CAT/ID/PH/TST) are defined here. Other specification documents and downstream tooling reference this registry but do not redefine codes. --- ## 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][a-z0-9-]*$` | | 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 `^4\.\d+\.\d+$` (version `^3\.\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` is defined. `main.skills` is forbidden in v4.0.0 — skills are namespace-, selection-, or agent-scoped (see `14-skills.md`). | | 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 one of `'sqlite'`, `'markdown'`, or `'http'`. Other values are not accepted. See `13-resources.md` for the semantics of each source type. | | 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. | | RES024 | error | `source: 'http'` requires a `url` field. The URL MUST use HTTPS. (added in v4.2.0) | | RES036 | error | `source: 'http'` requires a `path` field (local cache file). Enforced by core (`ResourceDatabaseManager`). (added in v4.2.0) | `RES001` and `RES036` are enforced by core (`ResourceDatabaseManager`); all other RES codes are pipeline-level validation checks. 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 under which the skill is registered (`selection.skills`, `agent.skills`) or the file basename without `.mjs` for namespace-scoped skills. | | SKL004 | error | `skill.version` is required and MUST be `'flowmcp/4.0.0'` (unified spec version). | | 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 | Skill registration entries (`selection.skills`, `agent.skills`): `file` must end with `.mjs` | | SKL017 | error | Skill registration entries (`selection.skills`, `agent.skills`): referenced file MUST exist | | SKL018 | error | Maximum 4 skills per registration scope (selection or agent) | ### 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 registered in the current scope (`selection.skills`, `agent.skills`, or the active namespace). | | 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 | Static-scan codes (SEC001–SEC016, SEC020) are defined in [05-security.md](./05-security.md). See that table for the canonical static-scan and library-allowlist codes. | | SEC017 | error | `main` block contains non-serializable value (function, symbol, etc.) | | SEC018 | error | Shared list file contains forbidden pattern | | SEC019 | error | Shared list file contains executable code | | SEC020 | error | `requiredLibraries` contains unapproved package (see [05-security.md](./05-security.md)) | --- ## 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/4.0.0` (unified spec version). | | 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 | `agent.skills` (if present) must be an Object (not Array). Keys MUST NOT contain `/` (inline form), values are `{ file }` objects pointing to `agents/{name}/skills/*.mjs`. See VAL110 and `06-agents.md`. | | 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`) | | AGT030 | error | All IDs in `agent.selections` must be resolvable Selection IDs (added in v4.2.0) | | AGT031 | error | `elicitation.maxRounds` must be a positive integer (>= 1) (added in v4.2.0) | `AGT004`, `AGT030`, and `AGT031` are enforced by core at agent load/startup time; all other AGT codes are pipeline-level validation checks. See `06-agents.md` for the complete agent specification. --- ## MCP Integration Meta Rules (v4.2.0) | Code | Severity | Rule | |------|----------|------| | VAL100 | error | Every Tool MUST have a `meta` block | | VAL101 | error | `meta.isReadOnly` is required and MUST be a boolean | | VAL102 | error | `meta.isConcurrencySafe` is required and MUST be a boolean | | VAL103 | error | `meta.isDestructive` is required and MUST be a boolean | | VAL104 | error | `meta.searchHint` is required and MUST be a non-empty string | | VAL105 | error | `meta.aliases` is required and MUST be a string array | | VAL106 | error | `meta.alwaysLoad` is required and MUST be a boolean | | VAL107 | error | When enum values correspond to a Shared List, `{{listName:alias}}` MUST be used. Hardcoded enum values that duplicate a Shared List are forbidden. | | VAL110 | error | Slash-Rule: Keys in `selection.tools`, `selection.resources`, `selection.prompts`, `agent.tools`, and `agent.prompts` that act as references MUST contain a `/` (full ID form). Keys in `selection.skills` and `agent.skills` MUST NOT contain a `/` (inline form with `{ file }`). See `17-selections.md` for the full slash-rule matrix. | See `19-mcp-integration.md` for the complete meta block specification. --- ## Selection Validation Rules (v4.2.0) | Code | Severity | Rule | |------|----------|------| | SEL001 | error | `selection.whenToUse` is required and MUST be a non-empty string | | SEL002 | error | A Selection MUST reference at least 1 Primitive (tool, resource, prompt, or skill) | | SEL003 | error | All Primitive references in a Selection MUST be resolvable within the catalog | | SEL004 | info | If a Selection includes inline-skill objects, SkillValidator runs on each (recorded in the validation report). Optional — present only when inline skills exist. | See `17-selections.md` for the complete Selection 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 ^4\.\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 SEC017 error main.handlers.preRequest: Non-serializable value (function) 2 errors, 0 warnings Schema cannot be loaded (has errors) ``` --- # FlowMCP Specification v4.2.0 — Tests | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md), [02-parameters.md](./02-parameters.md) | | Related | [04-output-schema.md](./04-output-schema.md), [06-agents.md](./06-agents.md), [22-scoring-protocol.md](./22-scoring-protocol.md), [13-resources.md](./13-resources.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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. --- ## Skill Tests (v4.2.0) Skills have two test types: structural and one-shot. ### Primitive Test Overview | Primitive | Test Type | Minimum Count | |-----------|-----------|---------------| | Tools | Structural + API | 3 per tool | | Resources | Structural + Query | 3 per query | | Skills | Structural + One-Shot | 1 + 1 | | Agents | End-to-End | 3 | ### Structural Test for Skills The structural test validates that all internal references in the skill are resolvable: - All `{{tool:name}}` references exist in `main.tools` - All `{{resource:name}}` references exist in `main.resources` - All `{{input:key}}` placeholders match the skill's `input` array - No unresolvable `{{skill:name}}` references This test is deterministic and runs during `flowmcp validate`. ### One-Shot Test for Skills A Skill passes the **One-Shot Test** when an AI agent can execute the complete workflow described in the skill's `content` field without requiring additional ToolSearch roundtrips for disambiguation. **Definition:** The One-Shot Test is a probabilistic (LLM-eval) test. The skill is delivered to an AI agent as an MCP prompt. The agent executes it. If the agent can complete the workflow in one pass — calling the correct tools in the correct order, with correct parameters — the skill passes. **Empirical basis:** Testing in March 2026 (FlowMCP Harness CLI) showed: | Skill Type | 5-Run Success Rate | |------------|-------------------| | Enriched Skills (with embedded tool descriptions, parameter tables, example calls) | 5/5 (100%) | | Bare Skills (tool name only, no embedded metadata) | 0/5 (0%) | This demonstrates that One-Shot performance is directly correlated with information density in the skill's `content` field. A skill that only names tools without embedding their parameters forces the agent into a ToolSearch roundtrip and breaks the one-shot constraint. **Test format:** ```javascript // Structural test: deterministic // 1. Validate all references resolve // 2. Run: flowmcp validate providers/{namespace}/skills/skill-name.mjs // One-Shot test: probabilistic (LLM-eval) // 1. Load skill as MCP prompt // 2. Send to agent with a representative input // 3. Evaluate: did the agent complete the workflow in one pass? // 4. Repeat 3-5 times, require ≥ 4/5 success rate for PASS ``` **Implication for skill authoring:** Skills MUST be self-contained. Each skill's `content` must embed: - Parameter tables for every referenced tool - Allowed enum values for enum parameters - Example tool calls with placeholder values - Expected output format See `14-skills.md` (One-Shot Design Principle) for authoring guidelines. --- ## 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 v4.2.0 — Preload & Caching | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [13-resources.md](./13-resources.md), [10-tests.md](./10-tests.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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 v4.2.0 — Prompt Architecture | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [14-skills.md](./14-skills.md), [06-agents.md](./06-agents.md), [18-prefill.md](./18-prefill.md), [16-id-schema.md](./16-id-schema.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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 v4.2.0 — Resources | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md), [02-parameters.md](./02-parameters.md) | | Related | [04-output-schema.md](./04-output-schema.md), [11-preload.md](./11-preload.md), [05-security.md](./05-security.md), [19-mcp-integration.md](./19-mcp-integration.md), [14-skills.md](./14-skills.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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'"] R --> H["source: 'http'"] 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"] H --> H1["Remote file via HTTPS\nCached locally\nSame query interface as sqlite"] ``` | 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). | | `http` | — | Remote file fetched via HTTPS and cached locally. Same SQLite query interface as `sqlite` source. | --- ## 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. Maximum 7 schema-defined queries. `runSql` and `describeTables` are auto-injected by the runtime. | 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' } ] } } // runSql + describeTables are 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** | **OPTIONAL** — prefer auto-injected describeTables | | **runSql** | Auto-injected by runtime (SELECT only) | | **describeTables** | Auto-injected by runtime (AI-friendly schema discovery) | ### 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 } ] } } // runSql + describeTables are 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** | **OPTIONAL** — define only if CLI MUST bootstrap DB via CREATE TABLE | | **runSql** | Auto-injected by runtime (all statements) | | **describeTables** | Auto-injected by runtime (AI-friendly schema discovery) | | **DB does not exist** | CLI creates DB based on getSchema if defined, otherwise reports missing-DB warning | | **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 | OPTIONAL (rarely needed) | OPTIONAL (only for CLI DB bootstrap) | | runSql | Auto-injected (SELECT only) | Auto-injected (all statements) | | describeTables | Auto-injected (structured discovery) | Auto-injected (structured discovery) | | 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 --- ## SQLite-GTFS Resources ### Overview The `source: 'sqlite-gtfs'` resource type is a **top-level sub-type of `sqlite`** (Design C — top-level with inheritance). It denotes a SQLite database that has been produced by a FlowMCP add-on (such as `gtfs-sqlite-toolkit`) and carries a verified quality seal in its `meta` table. The seal guarantees that the database structure, validation status, and discoverable capabilities are conformant with a known add-on contract — which allows FlowMCP-CLI to automatically inject standard tools without the schema author writing any SQL by hand. `sqlite-gtfs` is **not** a fallback for `sqlite`. A database without a valid seal is rejected (see [Validation Rules](#validation-rules) — `RES032`). Users who want full manual control over a SQLite database continue to use `source: 'sqlite'`. ### Required Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | `'sqlite-gtfs'` | Yes | Resource type. Must be `'sqlite-gtfs'`. | | `mode` | `'file-based'` | Yes | Access mode. Only `'file-based'` is allowed. `'in-memory'` is rejected (`RES030`) because the seal verification depends on the persisted `meta` table. | | `path` | `string` | Yes | Absolute path to the converted SQLite database. May contain the path variable `${FLOWMCP_RESOURCES}` (see [Path Variable Support](#path-variable-support)). | | `addon` | `string` | Yes | Name of the FlowMCP add-on responsible for producing and interpreting the database (e.g. `gtfs-sqlite-toolkit`). The add-on is the authority over which standard tools are auto-injected. | ### Optional Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `addonVersion` | `string` | No | SemVer range pinning the add-on version (e.g. `>=0.1.0`). When omitted, the FlowMCP-CLI uses the version listed in its add-on registry. | | `addonSource` | `string` | No | Add-on source reference. Default: `github:FlowMCP/`. **NPM is not supported** — only `github:` paths are valid. FlowMCP is not published to the NPM registry. | ### Seal Requirement Every `sqlite-gtfs` resource MUST point to a database whose `meta` table contains the entry `qualitySeal === 'sqlite-gtfs'`. This is a hard requirement enforced by the validator (`RES032`). A database that does not carry the seal — whether because it was produced by an unverified converter, manually edited, or partially converted — is rejected at schema activation time. The seal is written by the converter (e.g. `MetaWriter` in `gtfs-sqlite-toolkit`) only when the conversion completes successfully and all pre-validation checks pass strictly. A failed or partial conversion produces `qualitySeal: null` and the database is not usable as a `sqlite-gtfs` resource. ### Required Meta Keys The `meta` table of a sealed database MUST contain the following ten keys. They support reproducibility, capability detection, and audit logging: | Key | Purpose | |-----|---------| | `qualitySeal` | Add-on identifier (here: `'sqlite-gtfs'`). The seal value. | | `specRevision` | FlowMCP spec revision the converter targeted (e.g. `'2026-04-27'`). Used for spec-drift warnings (`RES034`). | | `specUrl` | URL of the spec revision document. | | `converterVersion` | SemVer of the add-on that produced the database (e.g. `gtfs-sqlite-toolkit@0.1.0`). | | `capabilities` | JSON-encoded map of capability booleans (e.g. `basicLookup`, `routing`, `shapesVisualization`, `flexService`). Drives auto-injection. | | `validationReport` | JSON-encoded summary of pre-validation results (counts of errors / warnings / info). | | `buildDate` | ISO-8601 timestamp of the conversion run. | | `rowCounts` | JSON-encoded per-table row counts. | | `sourceUrl` | URL of the upstream data source the converter consumed (provider feed URL). | | `sourceHash` | SHA-256 hash of the source archive — supports content-addressed deduplication and replay. | ### Path Variable Support The `path` field MAY contain the variable `${FLOWMCP_RESOURCES}`. The FlowMCP-CLI resolves the variable from the `FLOWMCP_RESOURCES` environment variable, falling back to `~/.flowmcp/resources/` when the variable is unset. | Variable | Resolution | Default when unset | |----------|------------|--------------------| | `${FLOWMCP_RESOURCES}` | `process.env.FLOWMCP_RESOURCES` | `~/.flowmcp/resources/` | Path variables establish the `FLOWMCP_*` naming family — variable names mirror the spec primitive they reference (`main.resources` → `FLOWMCP_RESOURCES`). Future variables follow the same pattern (`FLOWMCP_LOGS`, `FLOWMCP_CACHE`). A path variable that cannot be resolved (no environment variable and no documented default) raises `RES035`. ### Schema Example A minimal POC schema demonstrating `sqlite-gtfs`: ```javascript export const schema = { namespace: 'gtfsde', name: 'gtfsde-transit-v2', version: '4.2.0', main: { resources: [ { source: 'sqlite-gtfs', // Design C mode: 'file-based', path: '${FLOWMCP_RESOURCES}/gtfs-de.db', // User-configurable addon: 'gtfs-sqlite-toolkit', // Authority over default methods addonVersion: '>=0.1.0', // optional addonSource: 'github:FlowMCP/gtfs-sqlite-toolkit' // NPM is not supported } ], tools: [ // OPTIONAL: schema-specific tools only. // Standard GTFS tools are auto-injected by the add-on. ] } } ``` When the FlowMCP-CLI activates this schema via `flowmcp add`, it (1) resolves the path variable, (2) verifies the seal, (3) reads `capabilities` from the `meta` table, (4) loads the add-on declared in `addon`, and (5) injects the standard toolset that the add-on derives from the active capabilities. ### Add-on Authority The `sqlite-gtfs` resource type **delegates the definition of standard tools to the add-on**. The spec describes the resource contract; the add-on (e.g. `gtfs-sqlite-toolkit`) owns the catalog of default methods — `searchStops`, `searchRoutes`, `getDepartures`, `getShapeForRoute`, `getFlexBookingRules`, and so on — and decides which ones become available based on the `capabilities` map. This split keeps the spec stable while letting add-ons evolve independently. New add-ons (for example a future `sqlite-netex` or `sqlite-trias`) follow the same `Design C` pattern: top-level source value, seal requirement, add-on reference, capability-driven auto-injection. ### Data Policy Note **Provider GTFS data MUST NOT be committed to the spec repository, the add-on repository, or any other public FlowMCP repository.** GTFS feeds carry third-party licensing terms that typically prohibit redistribution. Users provide their own GTFS data locally — either by downloading from the provider directly or by using the add-on's converter against a local source archive. The converted database lives under the user-controlled `${FLOWMCP_RESOURCES}` directory and is never published. The synthetic Mini-GTFS fixture shipped with `gtfs-sqlite-toolkit` (under a CC0 license) is the only GTFS-shaped data permitted in any public FlowMCP repository. It exists exclusively for testing and CI. --- ## 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` — OPTIONAL (Schema-Defined) Schema authors MAY define `getSchema` if they want to expose CREATE TABLE statements directly. For `in-memory` mode, `describeTables` provides AI-friendly structured discovery automatically (recommended). For `file-based` mode, `getSchema` is useful when CLI needs to construct the DB. **When to define `getSchema`:** - `mode: 'file-based'`: Only when the CLI MUST create the database on first run. The CLI derives CREATE TABLE statements from the getSchema return value (table names, column names, column types). - `mode: 'in-memory'`: Almost never — `describeTables` is sufficient. Define `getSchema` only if a downstream consumer needs the CREATE TABLE text directly (e.g. dump/migration tooling). **getSchema and CREATE TABLE:** When defined, the CLI can derive CREATE TABLE statements from the getSchema return value. No separate `createSchema` query is needed. ```javascript getSchema: { sql: "SELECT name, sql FROM sqlite_master WHERE type='table'", description: 'Returns CREATE TABLE statements for all tables (optional, for CLI bootstrap)', 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' } ] } ``` ### `runSql` — Auto-Injected by Runtime `runSql` is automatically added by the runtime. Schema authors do **not** define it manually. The SELECT-only enforcement for `in-memory` mode is unchanged — security comes from the `mode` field, not from the query name. | 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 runSql enforces SELECT-only via runtime checks. For `file-based`, all SQL statements are allowed. ### `describeTables` — Auto-Injected by Runtime `describeTables` is automatically added by the runtime alongside `runSql`. Schema authors do **not** define it manually. It returns the database structure as a flat row set, optimized for AI consumption (no CREATE TABLE parsing required). **Auto-Inject SQL:** ```sql SELECT m.name as table_name, p.name as column, p.type FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table' ``` **Return shape:** ```javascript [ { table_name: 'pools', column: 'pool_address', type: 'TEXT' }, { table_name: 'pools', column: 'token0', type: 'TEXT' }, { table_name: 'pools', column: 'token1', type: 'TEXT' }, { table_name: 'tokens', column: 'symbol', type: 'TEXT' }, { table_name: 'tokens', column: 'decimals', type: 'INTEGER' } ] ``` | Aspect | in-memory | file-based | |--------|----------|-----------| | Auto-injected | Yes | Yes | | Underlying SQL | `sqlite_master` JOIN `pragma_table_info` | identical | | Return format | Rows (table_name, column, type) | identical | | Use case | AI-friendly schema discovery | AI-friendly schema discovery | | Read-only safety | Yes (SQLite built-in metadata) | Yes (SQLite built-in metadata) | `describeTables` works in `readonly: true` mode because `sqlite_master` and `pragma_table_info` are read-only metadata functions baked into SQLite. They function identically with the `SQLITE_OPEN_READONLY` flag. **When to use `describeTables` vs `getSchema`:** | Aspect | `describeTables` (auto) | `getSchema` (optional, author-defined) | |--------|-----------------------|---------------------------------------| | Format | Structured rows | CREATE TABLE text | | Constraints (PRIMARY KEY, NOT NULL, FK) | Not included | Included | | AI-friendliness | High (immediate consumption) | Medium (regex parsing required) | | Use case | Default AI discovery | Author needs CREATE-text (e.g. CLI bootstraps file-based DB) | For `mode: 'in-memory'`, constraints are practically irrelevant — the agent only builds SELECTs. `describeTables` covers 100% of practical discovery use cases. For `mode: 'file-based'`, schema authors MAY still define `getSchema` if the CLI needs CREATE TABLE statements to bootstrap the database on first run. ### Query Limits | Type | Count | |------|-------| | Auto-injected | 2 (runSql + describeTables) | | Schema-defined (getSchema is optional) | Max 7 | | **Total** | **Max 9** | ### Summary Table | Query | in-memory | file-based | |-------|----------|-----------| | `getSchema` | OPTIONAL (rarely needed) | OPTIONAL (only for CLI DB bootstrap) | | `runSql` | Auto-injected (SELECT only) | Auto-injected (all statements) | | `describeTables` | Auto-injected (structured discovery) | Auto-injected (structured discovery) | | 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 `runSql` 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 ``` --- ## HTTP Resources (v4.2.0) HTTP Resources allow schemas to reference remote files (typically SQLite databases) that are fetched via HTTPS and cached locally. They combine the rich query interface of SQLite resources with the distribution flexibility of remote hosting. ### Use Cases | Scenario | Example | |----------|---------| | Large reference databases too big for the repo | OFAC SDN sanctions list (SQLite, ~40 MB) | | Frequently updated datasets | Daily-updated government open data | | Externally maintained datasets | Third-party data providers | ### HTTP Resource Example ```javascript resources: { ofacList: { source: 'http', url: 'https://cdn.example.com/ofac-sdn.sqlite', cacheTtl: 86400, description: 'OFAC SDN List — updated daily', 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' } ] }, searchByName: { sql: 'SELECT * FROM sdn_list WHERE name LIKE ? LIMIT 20', description: 'Search the SDN list by name (partial match)', parameters: [ { position: { key: 'name', value: '{{USER_PARAM}}' }, z: { primitive: 'string()', options: [ 'min(3)' ] } } ], output: { mimeType: 'application/json', schema: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', description: 'Entity name' }, type: { type: 'string', description: 'SDN type' }, programs: { type: 'string', description: 'Sanctioned programs' } } } } }, tests: [ { _description: 'Search for common name', name: '%Khan%' }, { _description: 'Search for organization', name: '%Corp%' }, { _description: 'Search partial match', name: '%Ltd%' } ] } } } } ``` ### HTTP Resource Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `source` | `'http'` | Yes | Resource type. Must be `'http'`. | | `url` | `string` | Yes | HTTPS URL of the remote file. Must use HTTPS (no HTTP). | | `cacheTtl` | `number` | Yes | Cache duration in seconds. The downloaded file is reused until TTL expires. | | `description` | `string` | Yes | What this resource provides. Appears in resource discovery. | | `queries` | `object` | Yes | Query definitions. Same structure as SQLite `in-memory` queries. | ### Caching Behavior | Aspect | Behavior | |--------|----------| | **Cache location** | `.flowmcp/cache/resources/{hash}.db` | | **Cache key** | SHA-256 of the URL | | **TTL check** | On every schema load | | **Expired cache** | Re-fetch from URL | | **Fetch failure** | Use stale cache if available, error if no cache | | **SQL access** | `better-sqlite3` with `readonly: true` (same as `in-memory`) | ### Validation Rule **RES024:** `source: 'http'` requires a `url` field. The URL MUST use HTTPS (`https://`). HTTP URLs are rejected to prevent insecure data transfer. --- ## 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 runSql | 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 | OPTIONAL for file-based — required only when CLI MUST bootstrap the DB on first run (derives CREATE TABLE) | | 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 `runSql`. 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 | getSchema is optional. Plus 2 auto-injected (runSql + describeTables) = 9 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. | | `runSql` 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/` | ### Reserved Route Name — `about` The Resource route name `about` is reserved as a namespace-level convention by the FlowMCP Grading Specification ([`flowmcp-grading` — `spec/1.0.0/11-about-convention.md`](https://github.com/FlowMCP/flowmcp-grading/blob/main/spec/1.0.0/11-about-convention.md)). A namespace MAY expose a `Resource.About` route; when it does, the resource is expected to follow the content contract defined in the Grading-Spec. This is a **forward-looking convention**, not a v4.1 validation rule. The Schemas-Spec does NOT enforce the reservation; conformant schemas MUST NOT use the route name `about` for a resource that is not an About Resource per the Grading-Spec contract. The binding content contract lives in the Grading-Spec. --- ## Validation Rules The following rules are enforced when validating resource definitions: Resource validation codes are shared with [09-validation-rules.md](./09-validation-rules.md), which is the canonical RES code catalog. Codes `RES001`–`RES024` carry the same meaning in both chapters. Codes `RES025`+ are SQLite/markdown/sqlite-gtfs-specific rules defined locally here. `RES001` and `RES036` are enforced by core (`ResourceDatabaseManager`); all other RES codes are pipeline-level validation checks. | Code | Severity | Rule | |------|----------|------| | RES001 | error | `source` must be `'sqlite'`, `'markdown'`, or `'http'`. | | RES002 | error | `description` must be a non-empty string. | | RES005 | error | Maximum 2 resources per schema. | | 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. | | 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 | `source: 'http'` requires a `url` field. The URL MUST use HTTPS. (added in v4.2.0) | | RES025 | error | `mode` is required for `source: 'sqlite'` and MUST be `'in-memory'` or `'file-based'`. | | RES026 | error | `origin` is required and MUST be `'global'`, `'project'`, or `'inline'`. | | RES027 | error | `name` is required, must be a non-empty string with the correct extension (`.db` for sqlite, `.md` for markdown). | | RES028 | error | Maximum 7 schema-defined queries per SQLite resource (9 total with auto-injected runSql + describeTables). | | RES029 | error | For `mode: 'in-memory'`, schema-defined SQL MUST begin with `SELECT` or `WITH` (CTE). | | RES030 | error | `source: 'sqlite-gtfs'` requires `mode: 'file-based'`. `in-memory` is not allowed. | | RES031 | error | `source: 'sqlite-gtfs'` requires the `addon` field (add-on name). | | RES032 | error | Database at `path` does not contain `meta.qualitySeal === 'sqlite-gtfs'`. Schema rejected. | | RES033 | error | Database at `path` cannot be opened (file missing or corrupt). | | RES034 | warning | Database `meta.specRevision` is outside the expected range. | | RES035 | error | Path variable in `path` (e.g. `${FLOWMCP_RESOURCES}`) cannot be resolved (environment variable not set AND no default available). | | RES036 | error | `source: 'http'` requires a `path` field (local cache file). Enforced by core (`ResourceDatabaseManager`). (added in v4.2.0) | | RES037 | error | `mode: 'file-based'` requires `origin: 'project'`. | | RES038 | error | `source: 'markdown'` MUST NOT have a `mode` field. | | RES039 | error | `source: 'markdown'` MUST NOT have a `queries` field. | | RES040 | warning | `source: 'sqlite'` with `origin: 'inline'` is not recommended (data privacy). | | RES041 | error | All resource fields are required. No field MAY be omitted. | | RES042 | info | SQLite resources MAY include a `getSchema` query for CLI bootstrap (file-based) or downstream tooling. Not required. | --- ## 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 v4.2.0 — Skills | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [12-prompt-architecture.md](./12-prompt-architecture.md), [18-prefill.md](./18-prefill.md), [16-id-schema.md](./16-id-schema.md), [06-agents.md](./06-agents.md), [17-selections.md](./17-selections.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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/4.0.0', type: 'namespace', 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/4.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. | | `whenToUse` | `string` | Must not be empty | **New in v4.** When an AI agent SHOULD activate this skill. One sentence describing the trigger scenario. | | `type` | `string` | One of: `'namespace'`, `'selection'`, `'agent'` | **New in v4.** Skill classification. See [Skill Types](#skill-types). | ### 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. | | `prefill` | `array` | `[]` | **New in v4.** Array of tool call specifications to execute before delivering the Skill. See `18-prefill.md`. | ### Skill Types (v4.2.0) The `type` field classifies the skill's intended scope. Values are bare strings without `-skill` suffix. | Type | Description | Location (file path) | |------|-------------|----------------------| | `'namespace'` | General-purpose skill for any user of this namespace | `providers/{namespace}/skills/{name}.mjs` | | `'selection'` | Skill bound to a specific Selection (curated tool subset) | `selections/{selectionName}/skills/{name}.mjs` | | `'agent'` | Agent-specific skill, tested with a specific LLM | `agents/{agentName}/skills/{name}.mjs` | Skills are registered into their parent scope via the parent's manifest (`selection.skills`, `agent.skills`) or by being placed in the namespace's `skills/` directory. **There is no `main.skills` field on schemas in v4.2.0** — that pattern is forbidden (see VAL016). ### Field Details #### `name` The skill name is the primary identifier. It is used in the MCP prompt registration, in `{{skill:name}}` placeholder references, and as the key under which the skill is registered in `selection.skills` or `agent.skills` (or as the file basename in `providers/{ns}/skills/`). 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 v4, the valid value is `'flowmcp/4.0.0'`. The prefix `flowmcp/` aligns with the agent manifest versioning. ```javascript // Valid version: 'flowmcp/4.0.0' // Deprecated (v3 format — accepted with warning during migration) version: 'flowmcp-skill/1.0.0' // Invalid version: '1.0.0' // missing prefix version: '4.0.0' // must include flowmcp/ prefix ``` Unified versioning in v4.2.0: all FlowMCP primitives (Schema, Selection, Agent, Skill, Prompt) carry the same `flowmcp/X.Y.Z` version string. When the FlowMCP specification changes in a breaking way, the version increments across all primitives in lockstep (e.g. `flowmcp/5.0.0`). #### `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 registered in the current scope (`selection.skills`, `agent.skills`, or the active namespace's `providers/{ns}/skills/` directory). `main.skills` is forbidden in v4.0.0. | `{{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 registered skill in the current scope (`selection.skills`, `agent.skills`, or the active namespace's `providers/{ns}/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. --- ## Skill Registration (v4.2.0) Skills in v4.2.0 are **top-level entities** that live outside the schema's `main` block. Each skill `.mjs` file is registered into one of three scopes via the parent's manifest or by directory placement. **The `main.skills` field is forbidden** in v4.2.0 (see VAL016). ### Three Registration Scopes | Scope | Registered Via | File Location | |-------|----------------|---------------| | Namespace | Directory placement (no explicit registration) | `providers/{namespace}/skills/{skill-name}.mjs` | | Selection | `selection.skills` in `selections/{name}/selection.mjs` | `selections/{name}/skills/{skill-name}.mjs` | | Agent | `agent.skills` in `agents/{name}/agent.mjs` | `agents/{name}/skills/{skill-name}.mjs` | ### Selection-Scoped Skill Registration ```javascript // selections/evm-contract-research/selection.mjs export const selection = { name: 'evm-contract-research', version: '4.0.0', // ... skills: { 'contract-deep-dive': { file: './skills/contract-deep-dive.mjs' } } } ``` ### Agent-Scoped Skill Registration ```javascript // agents/crypto-auditor/agent.mjs export const agent = { name: 'crypto-auditor', version: 'flowmcp/4.0.0', // ... skills: { 'audit-report': { file: './skills/audit-report.mjs' } } } ``` ### Namespace-Scoped Skill Discovery Namespace-scoped skills are discovered by directory scan — no explicit registration object on the schema: ``` providers/etherscan/ ├── tools/ ├── resources/ └── skills/ ├── full-contract-audit.mjs └── quick-summary.mjs ``` ### Registration Entry Fields (Selection / Agent) | 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. Must NOT contain a `/` (see VAL110 Slash-Rule). | | `file` | `string` | Yes | Relative path from the parent manifest (selection.mjs or agent.mjs) to the skill `.mjs` file. Must end with `.mjs`. | If the key and the skill file's `name` field differ, the validator raises SKL003. ### MCP Registration Each skill is registered as an MCP prompt with the server. The fully qualified prompt name follows the scope-aware pattern: ``` etherscan/skill/full-contract-audit (namespace-scoped) evm-contract-research/skill/contract-deep-dive (selection-scoped) crypto-auditor/skill/audit-report (agent-scoped) ``` 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/4.0.0 ``` The version string has two parts separated by `/`: | Part | Value | Description | |------|-------|-------------| | Prefix | `flowmcp` | Unified FlowMCP specification identifier (shared across Schema, Selection, Agent, Skill, Prompt) | | Version | `4.0.0` | Semver version of the FlowMCP specification | ### Current Version The only valid version in this release is `flowmcp/4.0.0`. The legacy `flowmcp-skill/1.0.0` string from v3 is accepted with a deprecation warning during migration; new skills MUST use `flowmcp/4.0.0`. ### Unified Versioning Rationale All FlowMCP primitives carry the same `flowmcp/X.Y.Z` version string: 1. **Single source of truth.** Schemas, Selections, Agents, Skills, and Prompts evolve as one specification. Mixing primitive versions inside one catalog is not supported. 2. **Validation targeting.** The validator uses the version string to apply the correct rule set. A primitive at `flowmcp/4.0.0` is validated against this specification revision. A future `flowmcp/5.0.0` may add new required fields or change semantics; the version increment signals that consuming tools SHOULD apply the new rules. 3. **Upgrade paths.** A coordinated version bump signals that existing catalog content needs review and migration in lockstep. --- ## 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 under which the skill is registered (`selection.skills`, `agent.skills`) or the file basename without `.mjs` for namespace-scoped skills. | | SKL004 | error | `skill.version` is required and MUST be `'flowmcp/4.0.0'`. `'flowmcp-skill/1.0.0'` is accepted with a deprecation warning during migration. | | 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 | Skill registration entries (`selection.skills`, `agent.skills`): `file` must end with `.mjs` | | SKL017 | error | Skill registration entries (`selection.skills`, `agent.skills`): referenced file MUST exist | | SKL018 | error | Maximum 4 skills per registration scope (selection or agent) | ### 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 registered in the current scope (`selection.skills`, `agent.skills`, or the active namespace). | | 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 | --- ## One-Shot Design Principle (v4.2.0) A Skill MUST be written so that an AI agent can execute the complete workflow in a single pass — without requiring additional ToolSearch roundtrips for disambiguation or parameter discovery. ### Empirical Basis Testing conducted in March 2026 (FlowMCP Harness CLI): | Skill Version | Tool Descriptions | Parameter Tables | Example Calls | 5-Run Success Rate | |---------------|------------------|------------------|---------------|-------------------| | Enriched | Embedded | Embedded | Embedded | **5/5 (100%)** | | Bare | Tool name only | Missing | Missing | **0/5 (0%)** | The difference is unambiguous: when a Skill embeds all the information the agent needs, it succeeds every time. When it relies on the agent discovering parameters at runtime, it fails every time. ### What "One-Shot" Means An agent executes a Skill in one shot when: 1. The agent reads the Skill's `content` field 2. The agent can identify which tools to call 3. The agent can determine the correct parameters for each tool call 4. The agent executes the workflow and produces the expected output 5. No additional calls to ToolSearch or ToolDescription are required A Skill that causes the agent to call ToolSearch to discover a tool's parameters has **failed the one-shot constraint**. ### Required Content Elements Every Skill `content` field MUST embed: | Element | Why Required | Format | |---------|-------------|--------| | Tool call description | Agent MUST know what tool to call | `## Step N: Call {{tool:toolName}}` | | Parameter table per tool | Agent MUST know which parameters exist | Markdown table with name, type, description, required | | Enum values | Agent MUST know valid values | Listed inline in the parameter table | | Example call | Agent MUST understand the pattern | Code block with placeholder values | | Expected output | Agent MUST know what to do with the result | One sentence per tool | ### Non-Compliant Skill (Fails One-Shot) ```javascript const content = ` ## Step 1: Get Token Price Call {{tool:getSimplePrice}} for the token {{input:tokenId}}. ` // FAILS: No parameter table, no enum values, no example call // Agent MUST call ToolSearch to discover parameters → not one-shot ``` ### Compliant Skill (Passes One-Shot) ```javascript const content = ` ## Step 1: Get Token Price Call {{tool:getSimplePrice}} to retrieve the current price. **Parameters:** | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | ids | string | Yes | Token ID (e.g. \`bitcoin\`, \`ethereum\`, \`solana\`) | | vs_currencies | string | Yes | Currency code (e.g. \`usd\`, \`eur\`, \`gbp\`) | **Example:** \`\`\` ids: {{input:tokenId}} vs_currencies: usd \`\`\` **Output:** JSON object with token ID as key and price as value. ` // PASSES: Parameter table embedded, example included → agent can proceed without ToolSearch ``` ### Validation The one-shot constraint is **not statically verifiable** by `flowmcp validate` — it depends on LLM behavior. However, skill authors MUST follow the content embedding requirements above. Reviewers MAY flag skills that lack embedded parameter tables as failing the one-shot design principle. See `10-tests.md` (One-Shot Test for Skills) for the test protocol. --- ## 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 registration scope | 4 | Keeps each Selection or Agent focused. Skills beyond 4 in one scope indicate the scope SHOULD be split. Namespace-scoped skills are bounded by directory contents, not by an explicit limit. | | 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 based on their registration scope. The pipeline collects skills from three sources: namespace `skills/` directories, `selection.skills` entries, and `agent.skills` entries. Each source uses the same per-file validation pipeline. ### Loading Sequence ```mermaid flowchart TD A[Resolve active scope] --> B{Scope type?} B -->|Namespace| C1[Scan providers/{ns}/skills/] B -->|Selection| C2[Read selection.skills entries] B -->|Agent| C3[Read agent.skills entries] C1 --> D[Read each skill file as string] C2 --> D C3 --> D 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 catalog] I --> J[Validate requires.resources against catalog] J --> K[Validate placeholders in content] K --> L[Check skill-to-skill depth limit] L --> M[Register as MCP prompts] ``` The diagram shows how skill loading collects from three scope sources and runs the same validation pipeline on each. There is no `main.skills` step in v4.2.0. ### Step-by-Step 1. **Resolve scope** — determine whether the active context is a namespace, a Selection, or an Agent. 2. **Collect skill file paths** — from the namespace `skills/` directory, or from the parent manifest's `skills` field (Selection or Agent). 3. **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/4.0.0', type: 'namespace', 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 v4.2.0 — Catalog | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [03-shared-lists.md](./03-shared-lists.md), [06-agents.md](./06-agents.md), [16-id-schema.md](./16-id-schema.md), [21-schema-lifecycle.md](./21-schema-lifecycle.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). 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) │ └── ... ├── selections/ <- Curated tool subsets (v4.2.0) │ ├── evm-research/ │ │ └── selection.mjs │ └── defi-monitor/ │ └── selection.mjs └── 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 | | `selections/` | Root | Curated tool subsets for agent context loading (v4.2.0). See `17-selections.md`. | | `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": "4.0.0", "description": "Official FlowMCP community catalog", "schemaSpec": "4.0.0", "contentHash": "sha256:a1b2c3d4e5f6789abcdef01234567890abcdef01234567890abcdef01234567890", "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" } ], "selections": [ { "name": "evm-research", "description": "Tools for EVM contract and token research", "file": "selections/evm-research/selection.mjs" }, { "name": "defi-monitor", "description": "Tools for DeFi protocol monitoring", "file": "selections/defi-monitor/selection.mjs" } ] } ``` ### `contentHash` Field The `contentHash` field contains the **SHA-256 hash of the canonically serialized registry contents**. It provides integrity verification for the catalog. **Hash input:** The hash is computed over a canonical JSON serialization of all referenced Primitives (schemas, shared lists, agents, selections). The algorithm: 1. Collect all file paths referenced in the registry (`shared[].file`, `schemas[].file`, `agents[].manifest`, `selections[].file`) 2. For each file, read the raw content and sort by file path (alphabetical) 3. Concatenate: `{filePath}:{fileContent}\n` for each file 4. Compute SHA-256 of the concatenated string This ensures that any change to any referenced file invalidates the hash, signaling that the catalog needs re-validation. **Note:** The `contentHash` field is optional. When absent, the runtime skips integrity verification for the catalog. 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 v4.2.0 — ID Schema | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [18-prefill.md](./18-prefill.md), [12-prompt-architecture.md](./12-prompt-architecture.md), [14-skills.md](./14-skills.md), [17-selections.md](./17-selections.md), [15-catalog.md](./15-catalog.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). A unified ID system for referencing all FlowMCP primitives. IDs MUST be unambiguous, human-readable, and resolvable. This document defines the ID format, component rules, Schema-File-ID, CLI-Adapter mapping, the No Short Form rule, 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 ``` ### Complete ID Types | Type | Format | Slashes | Example | |------|--------|---------|---------| | **Schema-File** | `namespace/schema-name` | **1** | `etherscan-io/contracts` | | Tool | `namespace/tool/name` | 2 | `etherscan-io/tool/getAbi` | | Resource | `namespace/resource/name` | 2 | `etherscan-io/resource/chainDb` | | Prompt | `namespace/prompt/name` | 2 | `etherscan-io/prompt/intro` | | Skill | `namespace/skill/name` | 2 | `etherscan-io/skill/audit` | | Selection | `namespace/selection/name` | 2 | `evm-research/selection/contract` | | Agent | `namespace/agent/name` | 2 | `crypto/agent/researcher` | **Distinguishing rule:** 1 Slash = Schema-File-ID (Container). 2 Slashes = Primitive-ID (Content). ### 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`, `skill`, `selection`, `agent` | Required | Type discriminator. Always required — Short Form is not supported in v4. | | 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 seven kinds of addressable primitives in v4.2.0: | Type | Maps To | Defined In | |------|---------|-----------| | `tool` | MCP `server.tool` | `main.tools` | | `resource` | MCP `server.resource` | `main.resources` | | `prompt` | MCP `server.prompt` | `main.prompts` | | `skill` | MCP `server.prompt` (skill variant) | `providers/{ns}/skills/`, `selections/{name}/skills/`, or `agents/{name}/skills/` — never `main.skills` (forbidden) | | `list` | Shared list | `list.meta.name` | | `selection` | Selection | `selections/{name}/selection.mjs` | | `agent` | Agent | `agents/{name}/agent.mjs` | #### 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` | --- ## Schema-File-ID A schema is a `.mjs` file containing 1–8 Primitives. The Schema-File-ID identifies this file as a whole. **Format:** `namespace/schema-name` (1 slash) ``` Schema-File-ID: etherscan-io/contracts └── namespace: etherscan-io └── schema-name: contracts (equals filename without .mjs) Contains Primitive-IDs: etherscan-io/tool/getAbi etherscan-io/tool/getContractCreation etherscan-io/resource/abiCache ``` ### Naming Rules for schema-name - Kebab-case, only lowercase letters and hyphens - Thematic, not technical (e.g., `contracts`, `nft`, `defi` — not `schema1`, `tools-v2`) - For providers with multiple schemas: topic prefix optional (`moralis-nft`, `moralis-defi`) - Matches exactly the filename without `.mjs` ### Directory Mapping ``` schemas/v4.1.0/providers/etherscan-io/contracts.mjs └── namespace └── schema-name.mjs → Schema-File-ID: etherscan-io/contracts ``` --- ## CLI-Adapter The MCP protocol does not allow slashes in tool names. The CLI maps Spec-IDs to internal MCP tool names: | External Spec-ID | Internal MCP Tool Name | |------------------|------------------------| | `etherscan-io/tool/getAbi` | `getAbi_etherscan-io` | | `moralis/tool/getTokenBalance` | `getTokenBalance_moralis` | **Mapping Rule:** `routeName_namespace` (underscore separator, namespace at end). Implemented in `#buildToolName()` in the CLI. This mapping is internal. Users and agents always use full Spec-IDs. --- ## No Short Form Short Form is not supported in FlowMCP v4. `flowmcp add getTokenBalance` (without namespace/type) is not allowed. **Reason:** Ambiguity and hidden data provenance. `moralis/tool/getTokenBalance` is explicit — the namespace immediately shows the data source. For LLMs especially, full Spec-IDs are semantically unambiguous. All CLI commands use full Spec-IDs. --- ## 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 or less| F["Error: ID001 — full form required"] 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. Three segments required: namespace, type, name. Any other count: validation error ID001 (Short Form is not supported). 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`). --- ## 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 registered in the current scope (`selection.skills`, `agent.skills`, or the active namespace's `providers/{ns}/skills/`). `main.skills` is forbidden in v4.0.0. | | `{{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 MUST be one of: `tool`, `resource`, `prompt`, `list`, `skill`, `selection`, `agent` | | ID004 | error | Name MUST NOT be empty | | ID005 | error | Short Form is not supported — full form (`namespace/type/name`) is always required | | ID006 | error | Full form is required everywhere — no context-based inference | ### 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`. --- # FlowMCP Specification v4.2.0 — Selections | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md), [16-id-schema.md](./16-id-schema.md) | | Related | [06-agents.md](./06-agents.md), [18-prefill.md](./18-prefill.md), [14-skills.md](./14-skills.md), [12-prompt-architecture.md](./12-prompt-architecture.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). **Primitive:** Selection (5th primitive) --- ## Overview A **Selection** is a named collection of Primitives (Tools, Resources, Prompts, Skills) that belong together thematically. Selections enable agents to activate a coherent set of capabilities with a single operation. --- ## Export Format ```javascript export const selection = { namespace: 'evm-research', name: 'contract-analysis', version: 'flowmcp/4.0.0', description: 'Tools and Skills for Smart Contract analysis on EVM chains', whenToUse: 'Activate this Selection when the user wants to analyze, debug, or inspect a smart contract.', tools: [ 'etherscan-io/tool/getSmartContractAbi', 'etherscan-io/tool/getContractCreation' ], skills: [ 'etherscan-io/skill/contract-deep-dive' ], resources: [], prompts: [] } ``` ## Required Fields | Field | Type | Description | |-------|------|-------------| | `namespace` | string | Namespace owning this selection | | `name` | string | Selection name (kebab-case) | | `version` | string | Must be `'flowmcp/4.0.0'` | | `description` | string | What this selection provides | | `whenToUse` | string | When an agent SHOULD activate this selection (SEL001) | | `tools` | string[] | Tool Primitive-IDs included | | `skills` | string[] | Skill Primitive-IDs included | | `resources` | string[] | Resource Primitive-IDs included | | `prompts` | string[] | Prompt Primitive-IDs included | At least one array MUST be non-empty (SEL002). ## ID Format Selection ID: `namespace/selection/name` (2 slashes) Example: `evm-research/selection/contract-analysis` ## File Location ``` schemas/v4.1.0/selections/ evm-research/ contract-analysis.mjs ``` Directory `selections/` is at root level, alongside `providers/` and `agents/`. ## Validation Rules | Code | Severity | Rule | |------|----------|------| | SEL001 | error | `whenToUse` is required and MUST NOT be empty | | SEL002 | error | At least one array (tools/skills/resources/prompts) must be non-empty | | SEL003 | error | All referenced Primitive-IDs MUST be resolvable | | SEL004 | info | If a Selection includes inline-skill objects, SkillValidator runs on each. Recorded in the validation report. Optional — present only when inline skills exist. | ## Selection as Test-Trigger A Selection-File can be used as a transitive test trigger via: ``` flowmcp dev test single ``` This: 1. Loads the Selection. 2. Resolves every member ID via the IdResolver (transitive). 3. Recursively gathers tests from each member schema (Tools, Resources, Skills, Prompts). 4. Executes all member tests and aggregates per-primitive PASS/FAIL counts. 5. Reports an aggregate status: `M/N Members PASS`. **Important**: The Selection itself has no execution tests — it is a *grouping*. The test-trigger model uses Selections as **batch loaders** for transitive testing of their members. ### Inline Skills When a Selection defines inline skills (`selection.skills[]` entries that are full Skill objects rather than ID references), each inline-skill is treated as a Skill-Test target: - Structural validation runs (placeholders resolvable, prefills declared) - The inline skill is then bound to the Selection's namespace for context ### Output Format Example ``` ┌─ Selection: defi-pools-toolkit │ ├─ SEL003: all member IDs resolvable ✓ │ │ │ ├─ Tools (1) │ │ └─ dexscreener.searchPair 3/3 PASS │ │ │ ├─ Resources (2) │ │ ├─ pools.searchPoolsByToken 3/3 PASS │ │ └─ tokens.getTokenBySymbol 3/3 PASS │ │ │ └─ Skills (1) │ └─ analyze-token-pools │ ├─ Structural (Placeholder-Resolution) ✓ │ └─ Prefill executed (2 Resources) ✓ │ └─ Selection Aggregat: 4/4 Members PASS ``` If a Selection includes inline-skill objects, the SelectionValidator additionally runs SkillValidator on each. This is recorded as SEL004 in the validation report. Optional — present only when inline skills exist. ## Runtime Behavior - If a referenced Primitive-ID is unresolvable, the Selection fails to load with a clear error message. - Example: `"Selection evm-research/selection/contract-analysis: Reference 'etherscan-io/tool/getSmartContractAbi' not found"` - AGT030: Agent startup fails if a referenced Selection cannot be loaded. --- # FlowMCP Specification v4.2.0 — Prefill and Placeholders | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [16-id-schema.md](./16-id-schema.md), [14-skills.md](./14-skills.md) | | Related | [12-prompt-architecture.md](./12-prompt-architecture.md), [17-selections.md](./17-selections.md), [02-parameters.md](./02-parameters.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). --- ## Overview **Placeholders** are template tokens embedded in Skill content that are resolved at runtime. **Prefill** is a mechanism to pre-execute a tool and embed its result into the Skill before delivery. --- ## Placeholder Syntax All placeholders use double-brace syntax: `{{type:reference}}` or `{{type:reference:field}}`. ## Complete Placeholder Table | Syntax | Resolves To | |--------|------------| | `{{tool:ns/name}}` | Complete tool block (description + parameters) | | `{{tool:ns/name:description}}` | Tool description only | | `{{tool:ns/name:parameters}}` | Parameter table only | | `{{tool:ns/name:test}}` | First test case as example call | | `{{tool:ns/name:meta}}` | Meta flags (isReadOnly, isDestructive, etc.) | | `{{tool:ns/name:call}}` | Ready-to-use call command | | `{{resource:ns/name}}` | Resource reference | | `{{prompt:ns/name}}` | Prompt reference | | `{{skill:name}}` | Skill reference (1 level, no nesting) | | `{{input:key}}` | User input parameter | | `{{prefill:ns/tool/name}}` | Pre-executed tool result | | `{{listName:alias}}` | Shared List value via alias | ## Prefill Declaration In a Skill, prefill is declared in the `prefill` array: ```javascript export const skill = { name: 'contract-analysis', prefill: [ { tool: 'etherscan-io/tool/getNetworkStatus', params: { network: '{{input:chain}}' } } ], content: ` Network status: {{prefill:etherscan-io/tool/getNetworkStatus}} Now analyze contract at {{input:address}}... ` } ``` ## Resolution Flow (8 Steps) 1. Skill is requested (with `input` parameters) 2. `prefill[]` entries are executed in order 3. Results are stored temporarily 4. `{{input:key}}` tokens are replaced with user parameters 5. `{{prefill:ns/tool/name}}` tokens are replaced with pre-executed results 6. `{{tool:...}}`, `{{resource:...}}`, `{{prompt:...}}` tokens are resolved from catalog 7. `{{listName:alias}}` tokens are resolved from Shared Lists 8. Fully resolved Skill content is delivered to the agent ## Error Handling **Principle:** Always deliver the Skill. Errors are visible in the placeholder, not silent. If a placeholder cannot be resolved, the error message replaces the placeholder: ``` {{tool:unknown/ns/missingTool}} → [ERROR: Tool 'unknown/ns/missingTool' not found] {{prefill:ns/tool/name}} → [ERROR: Prefill execution failed: HTTP 503] ``` This ensures the Skill is always delivered — even if some references are broken. --- # FlowMCP Specification v4.2.0 — MCP Server Integration | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) | | Related | [09-validation-rules.md](./09-validation-rules.md), [13-resources.md](./13-resources.md), [14-skills.md](./14-skills.md), [04-output-schema.md](./04-output-schema.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). --- ## Overview When FlowMCP is used as an MCP Server, each Tool is registered with MCP-specific metadata. The `meta` block in every Tool definition provides this metadata. --- ## Meta Block (Required per Tool) Every Tool in v4.2.0 MUST have a `meta` block: ```javascript export const schema = { main: { /* ... */ }, tools: { getSmartContractAbi: { description: 'Get the ABI for a verified smart contract', parameters: { /* ... */ }, meta: { isReadOnly: true, isConcurrencySafe: true, isDestructive: false, searchHint: 'contract ABI ethereum smart contract', aliases: [ 'getAbi' ], alwaysLoad: false } } } } ``` ## Meta Fields | Field | Type | Required | Description | |-------|------|----------|-------------| | `isReadOnly` | boolean | Yes (VAL101) | Tool does not modify any state | | `isConcurrencySafe` | boolean | Yes (VAL102) | Safe to call concurrently | | `isDestructive` | boolean | Yes (VAL103) | Tool can cause irreversible changes | | `searchHint` | string | Yes (VAL104) | Keywords for ToolSearch (not empty) | | `aliases` | string[] | Yes (VAL105) | Alternative names for ToolSearch | | `alwaysLoad` | boolean | Yes (VAL106) | Always register with MCP (bypass lazy loading) | ## MCP Translation When a Tool is registered with an MCP Server, `meta` fields are translated to MCP annotations: | FlowMCP Field | MCP Annotation | |---------------|----------------| | `meta.alwaysLoad` | `_meta['anthropic/alwaysLoad']` | | `meta.searchHint` | `_meta['anthropic/searchHint']` | This translation happens at registration time in the FlowMCP CLI/Core. ## alwaysLoad Policy `alwaysLoad: true` should be used sparingly. Guidelines: - **true**: Tool is almost always needed in any session (e.g., a core utility tool) - **false** (default): Tool is loaded on demand via ToolSearch Excessive `alwaysLoad: true` pollutes the agent's active tool list and degrades performance. ## aliases Field `aliases` enables ToolSearch to find a Tool by alternative names: If an agent searches for `getAbi`, ToolSearch finds `getSmartContractAbi` because `getAbi` is in its `aliases` array. Empty array `[]` is valid — means no aliases. ## Validation Rules | Code | Severity | Rule | |------|----------|------| | VAL100 | error | Every Tool MUST have a `meta` block | | VAL101 | error | `meta.isReadOnly` required (boolean) | | VAL102 | error | `meta.isConcurrencySafe` required (boolean) | | VAL103 | error | `meta.isDestructive` required (boolean) | | VAL104 | error | `meta.searchHint` required (string, not empty) | | VAL105 | error | `meta.aliases` required (string[]) | | VAL106 | error | `meta.alwaysLoad` required (boolean) | --- # FlowMCP Specification v4.2.0 — Validation Strategy | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [09-validation-rules.md](./09-validation-rules.md) | | Related | [22-scoring-protocol.md](./22-scoring-protocol.md), [21-schema-lifecycle.md](./21-schema-lifecycle.md), [10-tests.md](./10-tests.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). --- ## Overview FlowMCP v4.2.0 introduces a two-layer validation strategy: **deterministic** (structural) and **probabilistic** (LLM-based). Together they produce a **Grade Report** with a letter grade (A–F). --- ## See also — Grading-Spec in `flowmcp-grading` A separate **Grading-Spec** (`gradingSpec/1.0.0`) covers Single-Schema and Selection grading in the [flowmcp-grading](https://github.com/FlowMCP/flowmcp-grading) repo. The Validation Strategy in this file remains the deterministic baseline; the Grading System defined in the Grading-Spec extends (and partly replaces) the simple A–F Grade System described below. The Schemas-Spec v4.2.0 remains the highest instance. Entry point: [flowmcp-grading/spec/1.0.0/00-overview.md](https://github.com/FlowMCP/flowmcp-grading/blob/main/spec/1.0.0/00-overview.md). --- ## Grade System | Grade | Condition | Meaning | |-------|-----------|---------| | A | PASS + Score ≥ 4.0 | Production-ready | | B | PASS + Score ≥ 3.0 | Good, minor gaps | | C | PASS + Score ≥ 2.0 | Acceptable | | D | PASS + Score < 2.0 | Weak | | F | FAIL | Not loadable | **PASS** means the deterministic validator found no errors (warnings are allowed). ## Grade Report Format ```json { "schemaId": "etherscan-io/contracts", "validatorVersion": "validation/4.0", "timestamp": "2026-05-11T09:00:00Z", "deterministic": { "status": "PASS", "errors": [], "warnings": [] }, "probabilistic": { "score": 4.2 }, "primitives": [ "etherscan-io/tool/getAbi", "etherscan-io/tool/getContractCreation" ], "grade": "A" } ``` ### Field Reference | Field | Type | Description | |-------|------|-------------| | `schemaId` | string | Schema-File-ID (`namespace/schema-name`, 1 slash) — references the physical `.mjs` file | | `validatorVersion` | string | Validator version used (internal, not stored in schemas) | | `timestamp` | string | ISO 8601 when the report was generated | | `deterministic.status` | string | `PASS` or `FAIL` | | `deterministic.errors` | array | Validation errors (VAL codes) | | `deterministic.warnings` | array | Validation warnings | | `probabilistic.score` | number | LLM evaluation score (0.0–5.0) | | `primitives` | string[] | All Primitive-IDs active in this schema at validation time | | `grade` | string | Final grade: A, B, C, D, or F | ## schemaId is Schema-File-ID `schemaId` MUST be the Schema-File-ID (`namespace/schema-name`, 1 slash) — not a Primitive-ID (2 slashes). **Correct:** `"schemaId": "etherscan-io/contracts"` **Wrong:** `"schemaId": "etherscan-io/tool/getAbi"` ← This is a Primitive-ID The Schema-File-ID connects the report to the physical file on disk. ## Validator Versioning Validator versions are internal and are NOT stored in schema files. They appear only in the Grade Report: - `validation/4.0` — initial v4 validator - `validation/4.1` — incremental update (backward compatible) This avoids `validationVersion` fields in schema files (anti-pattern — the Grade Report is the record). ## Deterministic Validation Validates structural correctness: - Schema structure (`main`, `tools`, `resources`, etc.) - Required fields per primitive type - Validation rules VAL001–VAL107 - SEL, AGT, RES rule sets ## Probabilistic Validation LLM-based quality evaluation: - Description quality (clear, accurate, useful) - Parameter documentation completeness - Test case coverage - One-Shot completeness for Skills Score range: 0.0 (unusable) to 5.0 (excellent). ## Partial Schema Policy Before Production deploy, all failing Primitives MUST be removed from the schema file. A Language Model calling `flowmcp add etherscan-io/contracts` receives the tool list and assumes all tools work. A failing tool in Production causes unpredictable errors. **Rule:** 1 failing primitive gets removed — regardless of how many others pass. --- # FlowMCP Specification v4.2.0 — Schema Lifecycle | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md) | | Related | [20-validation-strategy.md](./20-validation-strategy.md), [22-scoring-protocol.md](./22-scoring-protocol.md), [10-tests.md](./10-tests.md), [09-validation-rules.md](./09-validation-rules.md), [15-catalog.md](./15-catalog.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). --- ## Overview Every FlowMCP schema passes through a defined lifecycle from initial research to production deployment. This document defines the six stages, special rules for static schemas and migrated schemas, and the policy for handling partially passing schemas. --- ## Lifecycle Stages | Stage | Label | Entry Condition | Exit Condition | |-------|-------|-----------------|----------------| | 1 | `stage:research` | API endpoint discovered | API reachable, schema creation is feasible | | 2 | `stage:creation` | Research complete | Schema file created in `tests/new-schemas/` | | 3 | `stage:api-test` | Schema created | `flowmcp test single` → min. 1 PASS (see Special Rule) | | 4 | `stage:validation` | API test passed | `flowmcp validate` → 0 errors | | 5 | `stage:grade` | Validation passed | Grade B or better | | 6 | `stage:production` | Grade B+ confirmed | Deployed to `providers/` in production catalog | ### Stage Details **`stage:research`** — The API has been identified as a candidate. The developer verifies that the endpoint is reachable and that the schema design is feasible (authentication model, response format, parameter structure). **`stage:creation`** — The schema file is written and placed in `tests/new-schemas/{provider}/`. This stage covers the authoring process — defining tools, parameters, shared list references, handlers, and test cases. **`stage:api-test`** — The schema is tested against the live API using `flowmcp test single `. At least one tool must return a PASS result. See the [API-Test Special Rule](#api-test-special-rule-for-static-schemas) below for schemas with no HTTP tools. **`stage:validation`** — The schema passes structural validation: `flowmcp validate ` returns 0 errors. All validation rules from `09-validation-rules.md` must be satisfied. **`stage:grade`** — The schema receives a quality grade. Grade B or better is required for production deployment. See the [Grading-Spec 2.0.0](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/2.0.0/00-overview.md) for the grading criteria — FlowMCP delegates the grading model to this standard. **`stage:production`** — The schema is moved from `tests/new-schemas/` to `providers/{namespace}/` in the production catalog and registered in `registry.json`. --- ## API-Test Special Rule for Static Schemas Not all schemas make HTTP calls. A schema is considered **static** when all of its primitives are non-HTTP: | Schema Type | Has HTTP? | API-Test Required? | |-------------|-----------|-------------------| | At least 1 Tool or HTTP-Resource | Yes | **Yes** — min. 1 PASS required | | Exclusively Prompts / static Skills | No | **Auto-PASS** — stage skipped | **Static schemas** — schemas that contain only Prompts and/or static Skills (no Tools, no HTTP-Resource) — receive an automatic PASS for `stage:api-test`. There is no live API to test against. The stage is considered complete and the schema proceeds directly to `stage:validation`. **Note:** In practice, static schemas are rare. The primary use case is future Prompt-only schemas or documentation-only namespaces. Most schemas in the community catalog contain at least one Tool. **Migration schemas** — schemas migrated from v3 that contain only static primitives also receive Auto-PASS. See [Migration Special Rule](#migration-special-rule). --- ## Migration Special Rule Schemas migrated from v3.0.0 to v4.0.0 do not need to repeat the full lifecycle from scratch: - Migrated schemas start at **`stage:api-test`** (not `stage:research`) - The research and creation stages are considered complete by virtue of the existing v3 schema - All subsequent stages (`stage:api-test` through `stage:production`) apply normally This shortens the migration path: a previously passing v3 schema that has been updated to v4 syntax needs only an API test, validation pass, and grade check before it can re-enter production. --- ## Partial Schema Policy ### Core Rule All failing primitives **MUST** be removed before a schema is deployed to production. A schema with failing tools, resources, or skills cannot enter `stage:production`. **Rationale:** An LLM working with a schema assumes that every registered primitive is functional. If `getTokenPrice` is listed but always errors, the agent has no way to know — it will attempt the call, fail, and potentially produce incorrect results. Removing failing primitives eliminates silent failures at the cost of reduced coverage. ### Example: Before and After ```javascript // BEFORE — etherscan-io/contracts.mjs with 3 tools, 1 failing tools: { getContractAbi: { /* ... */ }, // PASS — keep getSourceCode: { /* ... */ }, // PASS — keep getCreationCode: { /* ... */ } // FAIL — remove before production } // AFTER — ready for production tools: { getContractAbi: { /* ... */ }, // PASS getSourceCode: { /* ... */ } // PASS } // getCreationCode removed — tracked as separate sub-issue ``` ### Threshold There is no percentage threshold. **Each failing primitive is evaluated individually:** - 1 of 8 tools failing → remove the 1 failing tool, deploy the 7 passing tools - 3 of 5 tools failing → remove 3 failing tools, deploy 2 passing tools - All tools failing → schema does not deploy (no primitives remain) The threshold-free policy prevents edge cases where a "60% pass rate" is considered acceptable. Either a primitive works or it does not. ### What Happens to Removed Primitives Removed primitives are not abandoned — they are tracked for future resolution: 1. **Sub-Issue created** — A GitHub issue is opened for each removed primitive. The issue is linked to the parent schema issue. 2. **Backlog stage** — The removed primitive starts at `stage:research` (with the API reachability already known). 3. **Resolution path** — When the underlying issue is fixed (changed API, missing auth, updated handler), the primitive is re-added to the schema and goes through `stage:api-test` → `stage:validation` → `stage:grade`. 4. **Re-integration** — The fixed primitive is merged back into the production schema. A new grade check may be required if the primitive significantly changes the schema's scope. ### What Counts as Failing A primitive fails the API test when: - The HTTP response is a non-2xx status code (authentication error, rate limit, deprecated endpoint) - The response does not match the declared `output.schema` - The handler throws an uncaught exception - The tool times out consistently (> 30 seconds) A primitive passes when at least one of its test cases returns a 2xx response with a parseable body that matches the declared output shape. --- # Scoring Protocol v1 | Field | Value | |-------|-------| | Depends on | [00-overview.md](./00-overview.md), [20-validation-strategy.md](./20-validation-strategy.md) | | Related | [21-schema-lifecycle.md](./21-schema-lifecycle.md), [10-tests.md](./10-tests.md), [04-output-schema.md](./04-output-schema.md), [09-validation-rules.md](./09-validation-rules.md) | > Normative language (MUST/SHOULD/MAY) follows the conventions defined in [00-overview.md](./00-overview.md) (Conformance Language). Specification for grading FlowMCP v4 schemas via LLM evaluation. Documents the data formats exchanged between the CLI and an external Grader (e.g. Claude Code harness, third-party implementation). --- ## Purpose The scoring protocol decouples three concerns: 1. **Prompt generation** — done deterministically by `flowmcp-core/v4/GradeReporter.buildEvalPrompts` 2. **Score production** — done by an external Grader (LLM-based) 3. **Grade calculation** — done deterministically by `flowmcp-core/v4/GradeReporter.grade` CLI is responsible for 1 and 3. Grader is responsible for 2. Communication via JSON files (`prompts.json`, `scores.json`). --- ## Delegation — the grading model lives in the Grading-Spec FlowMCP is the highest instance and **delegates the grading model** to a separate, independently versioned standard: the **Grading-Spec** (`gradingSpec/2.0.0`), published as a dedicated docs area. The Grading-Spec owns the grading model — score-to-grade thresholds, the extended dimension set, Scoring System, Grading System, Categorical-Veto, Tier-trim, and skill families. This file owns the **upstream scoring transport** only: the deterministic `prompts.json` / `scores.json` artefact pair exchanged between the CLI and an external Grader. The Grading-Spec **sub-consumes** this transport and treats it as the highest instance for the artefact pair — it does not redefine it. Entry point: [Grading-Spec 2.0.0 — Overview](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/2.0.0/00-overview.md). --- ## Protocol Version `scoringProtocol: "v1"` Versioning allows future changes without breaking existing graders. Implementations MUST check the version and fail gracefully on unknown values. --- ## Dimensions Each schema is evaluated on 2 dimensions: | Dimension | What is rated | |-----------|---------------| | `whenToUse` | Clarity and specificity of the schema description (does an LLM know when to use this schema?) | | `parameters` | How well the parameter descriptions enable correct tool invocation | Each dimension yields a score on a 1.0-5.0 scale (floating point). These two are the **base transport dimensions**; the Grading-Spec ([`08-grading-model.md` §Dimension Enum](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/2.0.0/08-grading-model.md)) extends this set with the additional grading dimensions it owns. --- ## Grade Thresholds — delegated to the Grading-Spec The score-to-grade banding, the production gate, the tier-trim rule and the Categorical-Veto list are **owned by the Grading-Spec** (`gradingSystem/1.0.0`), not by this transport protocol. See [Grading-Spec 2.0.0 — §4.1 Score-to-Grade Thresholds](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/2.0.0/07-scoring-vs-grading.md). Changing any threshold or the numeric mapping bumps the `gradingSystem` version independently of `scoringProtocol`. --- ## File Formats ### `prompts.json` — Emitted by CLI Written by `flowmcp dev grade --emit-prompts`. Read by Grader. ```json { "schemaId": "namespace/file-base", "schemaIdSlug": "namespace_file-base", "schemaPath": "/abs/path/to/schema.mjs", "scoringProtocol": "v1", "scoringInstructions": "Du bist ein Schema-Bewerter. Bewerte unabhaengig basierend nur auf den unten gelieferten Informationen. Antworte als JSON-Array: [ { \"dimension\": \"...\", \"score\": <1.0-5.0>, \"reasoning\": \"...\" }, ... ]. Keine externen Annahmen, keine Kontext-Vermutungen.", "prompts": [ { "dimension": "whenToUse", "prompt": "Rate the clarity and specificity of the following Schema description on a scale 1.0-5.0. Schema description: \"...\"" }, { "dimension": "parameters", "prompt": "Rate how well the parameter descriptions in the following schema enable an LLM to call the tools correctly on a scale 1.0-5.0. Schema: {...}" } ] } ``` ### `scores.json` — Written by Grader Read by `flowmcp dev grade --consume-scores `. ```json { "schemaIdSlug": "namespace_file-base", "scoringProtocol": "v1", "creator": { "skill": "grade-score-single", "skillVersion": "1.0.0", "session": "claude-code-2026-05-18-03-15" }, "harness": { "name": "claude-code", "version": "1.0.0", "model": "claude-opus-4-7", "modelContext": "1M" }, "timestamp": "2026-05-18T03:15:00Z", "scores": [ { "dimension": "whenToUse", "score": 4.0, "reasoning": "Description is clear and specific..." }, { "dimension": "parameters", "score": 3.5, "reasoning": "Tools have descriptive names..." } ] } ``` ### Grade Report — Written by CLI `flowmcp dev grade --consume-scores ` writes the final report: ```json { "schemaId": "namespace/file-base", "schemaIdSlug": "namespace_file-base", "schemaPath": "/abs/path/to/schema.mjs", "schemaHash": "sha256:abc...", "date": "2026-05-18", "grade": "B", "score": 3.75, "scoringProtocol": "v1", "creator": { ... }, "harness": { ... }, "timestamps": { "startedAt": "2026-05-18T03:00:00Z", "scoredAt": "2026-05-18T03:15:00Z", "gradedAt": "2026-05-18T03:16:00Z", "reportedAt": "2026-05-18T03:16:00Z" }, "dimensions": [ ... ], "validationPassed": true, "validationErrors": [] } ``` --- ## Reproducibility Guarantees The protocol provides **statistical reproducibility** (not bitwise): | Guarantee | How | |-----------|-----| | Same schema | `schemaHash` (sha256) in report | | Same prompt template | `scoringProtocol: "v1"` in scores + report | | Same LLM | `harness.model` in scores + report | | Same scoring context | Grader MUST use isolated context (no session-state) | | Same output format | JSON-Schema for scores | Expectation: same inputs → same score within ±0.2 on the 1.0-5.0 scale. --- ## Schema ID Slug Convention Schema IDs follow the `/` pattern (e.g. `mudab/marine-data`). For use in file paths, the `/` is replaced with `_`: ``` mudab/marine-data → mudab_marine-data coingecko-com/search-ohlc → coingecko-com_search-ohlc ``` Reference implementation: `flowmcp-core/v4/GradeReporter._formatSuggestedFileName`. --- ## Grader Implementation Requirements A Grader MUST: 1. Read `prompts.json`, parse `scoringProtocol` field 2. Reject unknown protocol versions 3. Score each prompt independently in an isolated context (no shared state between prompts of the same schema) 4. Return scores in the same order as input prompts (or include `dimension` field for matching) 5. Set `creator`, `harness`, `timestamp` metadata 6. Write `scores.json` atomically (write-temp + rename) A Grader MUST NOT: - Use external context beyond what's in `prompts.json` - Use tools or external lookups - Embed scores from previous sessions - Modify the `scoringProtocol` value --- ## Reference Implementations | Implementation | Type | Location | |----------------|------|----------| | `grade-score-single` | Workbench Skill | `cli/memo-toolkit/skills/grade/grade-score-single/SKILL.md` | | `grade-score-batch` | Workbench Skill | `cli/memo-toolkit/skills/grade/grade-score-batch/SKILL.md` | | `flowmcp dev grade` | CLI Producer/Consumer | `flowmcp-cli/src/task/FlowMcpCli.mjs:grade()` | | `GradeReporter` | Core Module | `flowmcp-core/src/v4/task/GradeReporter.mjs` | --- ## References - [`20-validation-strategy.md`](./20-validation-strategy.md) — overall validation strategy - [`21-schema-lifecycle.md`](./21-schema-lifecycle.md) — when grading happens in the lifecycle ---