# FlowMCP Specification v4.3.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 26 specification documents into a single file for LLM consumption.
> Source: https://github.com/FlowMCP/flowmcp-spec
> version: 4.3.0
> sha: 934e619
> generated_at: 2026-07-16T12:43:24.686Z
Source: specification@4.3.0:934e619
## 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 and Placeholders
20. MCP Server Integration
21. Validation Strategy
22. Schema Lifecycle
23. Scoring Protocol
24. License & Terms of Services
25. Philosophy
26. Agent CLI Usage Contract
---
# 00. Overview
| Field | Value |
|---|---|
| Status | Informative |
| 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) |
> **Informative.**
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.3.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.3.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. The provider directory name MUST equal `main.namespace` of every schema it contains — the folder↔namespace invariant `VAL019` (see `09-validation-rules.md` and the fallback/rename rules in `16-id-schema.md`). 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. |
| **Agent-Skill** | A `SKILL.md` file — a Claude Code skill that runs in the IDE harness (e.g. `flowmcp-schema-create`, `flowmcp-schema-diagnose`, indexed by the `flowmcp-schema` entry point). These are **not** part of the FlowMCP schema format. They live in `flowmcp-spec/skills/` or a project `.claude/skills/` directory and are invoked by the developer's AI coding assistant. Do not confuse with Schema-Skills. |
| **Schema-Skill** | A `.mjs` file with `export const skill` — a FlowMCP first-class primitive (see `14-skills.md`) that maps to the MCP `server.prompt` interface. Schema-Skills live inside the schema catalog (`providers/{ns}/skills/`, etc.) and are invoked via `flowmcp call` or by an MCP client. Do not confuse with Agent-Skills. |
| **lens** | Overloaded term with two distinct meanings depending on context. (1) **Grading context:** a Persona sub-facet forming the second part of the `--` Persona slug (e.g. `mira-tanaka--domain`). It is a structural parameter that selects which Grading persona variant to use. (2) **Spec-Documentation context:** one of the per-persona review questions defined in `personas/persona-lens.md` — a review-checklist item, not a structural field. |
---
## 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; folder↔namespace invariant `VAL019` |
| `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; dev-track grade F vs. monitoring-track `blocked` record |
| `21-schema-lifecycle.md` | Schema Lifecycle | Six lifecycle stages, API-test special rule for static schemas, partial schema policy; two-track split (dev lifecycle here, grading-monitoring in the Grading-Spec) |
---
## 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 SHOULD contain zero import statements — this is the normative target the security scanner enforces on the private ad-hoc load path (see [Private ad-hoc calls](#private-ad-hoc-calls) and [05-security.md](./05-security.md)). A small number of trusted, registered add-on schemas still carry top-level imports of Node built-ins; they load scan-free over the trusted path and are the known gap the zero-import target closes.
### 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 declare all dependencies in `requiredLibraries` and receive them 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 — this gate is **enforced on the private ad-hoc load path**, where an unregistered schema is loaded by path. Trusted schemas registered in `schemaFolders[]` are curated at registration time and load scan-free; the scanner is the gate for schemas that were **not** curated that way. See [Private ad-hoc calls](#private-ad-hoc-calls) and [05-security.md](./05-security.md).
---
## Private ad-hoc calls
FlowMCP separates two load paths, and they have different trust properties:
| Path | How a schema is loaded | Security scan | Discoverability |
|------|------------------------|---------------|-----------------|
| **Trusted path** | Registered in `schemaFolders[]`, merged into the catalog | **Off** (curated at registration time) | Visible in `search` / `list` / `serve` |
| **Private ad-hoc path** | Loaded by exact file path, one call at a time | **On** (scan runs before `import()`) | Never registered, never merged — structurally invisible to `search` / `list` / `serve` |
A private schema is invoked by its path: it is **never** written into `schemaFolders[]` and **never** merged into the shared catalog, so it cannot appear in `search`, `list`, or an MCP `serve` session — invisibility is structural, not a filter. Because such a schema was never curated, the private path runs the **security scan before any `import()`**: a file that matches a forbidden pattern is rejected without ever executing. This is the "scan on private path" rule — the scan protects exactly the uncurated load, while trusted, already-reviewed schemas keep loading scan-free.
---
## 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.3.0
The v4.3.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`.
- **`meta` block relaxed to optional** — A Tool's `meta` block is now optional; VAL100 no longer fires when it is absent. When present, it must still be complete (VAL101–VAL106). See `19-mcp-integration.md`.
### v4.0.0
The v4.0.0 release establishes the major-4 baseline with a mandatory MCP integration layer:
- **`meta` block per Tool** — v4.0.0 introduced a `meta` block declaring `isReadOnly`, `isConcurrencySafe`, `isDestructive`, `searchHint`, `aliases`, and `alwaysLoad` (VAL100–VAL106). It was mandatory in v4.0.0 and relaxed to optional in v4.3.0 (see above); when present it must be complete.
- **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`.
## Related
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./15-catalog.md](./15-catalog.md) — see chapter 15.
- [./17-selections.md](./17-selections.md) — see chapter 17.
---
# 01. Schema Format
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
A FlowMCP schema is a single `.mjs` module that describes how to reach an API and turn its endpoints into tools. It splits into two named exports: a static, JSON-serializable `main` block that holds all declarative configuration, and an optional `handlers` factory that transforms requests and responses. The sections below walk through both exports, the tool and field definitions inside `main`, the naming conventions, and the constraints the runtime enforces at load time.
---
## Schema File Structure
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` | Schema-Content-Version, must match pattern `\d+\.\d+\.\d+` (semver, free per schema). Bump rules defined in the grading specification. Initial value for migrated schemas: `1.0.0`. |
| `root` | `string` | Base URL for all tools. Must start with `https://` (no trailing slash). Not required for resource-only schemas. |
| `tools` | `object` | Tool definitions. Keys are tool names in camelCase. Maximum 8 tools. May be empty `{}` if the schema defines resources or skills. |
| `meta` | `object` | Optional MCP integration metadata block per tool. When present it must be complete (all 6 fields). See `19-mcp-integration.md`. |
#### Two Version Axes
| 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:
```javascript
export const main = {
namespace: 'etherscan',
name: 'GetContractEthereum',
description: 'Fetch verified contract source by address',
version: '4.0.0',
schemaVersion: '1.0.0',
root: 'https://api.etherscan.io',
tools: { /* ... */ }
}
```
> **Note:** `schemaHash` is a **derived-only** field. It is computed by the grading toolchain (`HashGenerator`) from the canonical-JSON-serialised schema **excluding** the `schemaHash` field itself, and stored only in grading entries, `SourceSnapshot`s, and `index.json`. It MUST NOT appear as a source field in `.mjs` schema files. The `FolderScanner` flags an in-source `schemaHash` as defect `SCN-012`.
### Mandatory Informational Fields
These fields are required (`MUST` be present). They carry **no functional effect** on the runtime — the runtime does not validate URLs, interpret ToS, or classify licenses. Their purpose is to document provenance for schema authors and the grading pipeline.
> **Enforcement is phased:** validation currently emits a **Warning** (VAL027, VAL028) for missing/invalid values; this will escalate to **Error** in a future minor release once existing schemas have been migrated. See `09-validation-rules.md`.
| Field | Type | Description |
|-------|------|-------------|
| `docs` | `string[]` | Non-empty array of documentation URLs for the API provider. Every API has documentation — this MUST be filled. |
| `termsOfService` | `string` | URL to the API provider's Terms of Service — **OR** the sentinel value `"no-tos-found"` when the provider publishes no ToS. The field MUST be present; omitting it is a validation warning (escalates to error). **FlowMCP does not classify or interpret ToS.** See `23-license-and-tos.md`. |
**Sentinel contract for `termsOfService`:** Use `"no-tos-found"` (exact string) when the provider has no published ToS. This signals a conscious author decision rather than an accidentally omitted field.
### Optional Fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `termsOfServiceCheckedAt` | `string \| null` | `null` | ISO-Date (YYYY-MM-DD) when `termsOfService` URL was last verified by FlowMCP authors. |
| `termsOfServiceLanguage` | `string \| null` | `null` | Two-letter language code of the ToS document (`'en'`, `'de'`, `'multi'`). |
| `dataLicense` | `string \| null` | `null` | URL to the data license of API responses (e.g. CC-BY, Public Domain), if separately published by provider. |
| `dataLicenseName` | `string \| null` | `null` | Human-readable name of the data license (e.g. `'CC-BY-SA-4.0'`, `'Public Domain'`). |
| `tags` | `string[]` | `[]` | Categorization tags for tool discovery (e.g. `['defi', 'tvl']`). |
| `requiredServerParams` | `string[]` | `[]` | Environment variable names needed at runtime (e.g. `['ETHERSCAN_API_KEY']`). |
| `requiredLibraries` | `string[]` | `[]` | npm packages needed by handlers. Must be on the runtime allowlist. See `05-security.md`. |
| `headers` | `object` | `{}` | Default HTTP headers applied to all routes. Route-level headers override these. |
| `sharedLists` | `object[]` | `[]` | Shared list references. See `03-shared-lists.md` for format. |
| `resources` | `object` | `{}` | Resource definitions. Keys are resource names in camelCase. Maximum 2 resources. See `13-resources.md`. |
### Deprecated Fields
| 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.
The namespace is **also the provider folder name** (`providers//`), and the two MUST stay in sync — the folder↔namespace invariant `VAL019` (see `09-validation-rules.md`); the all-unparseable fallback and rename-on-parse rules are in `16-id-schema.md`.
```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. Each tool MUST have at least 3 tests. 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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./19-mcp-integration.md](./19-mcp-integration.md) — see chapter 19.
---
# 02. Parameters
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
A parameter declares where a value is placed in the outgoing API request and how it is validated before the call is made. Each tool parameter pairs a `position` block (placement) with a `z` block (validation); resource parameters reuse the same pairing without a `location` field, and skill input uses a simpler shape.
---
## 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 schema-check 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 schema-check 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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./03-shared-lists.md](./03-shared-lists.md) — see chapter 03.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./18-prefill.md](./18-prefill.md) — see chapter 18.
---
# 03. Shared Lists
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
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 that is versioned, validated, and centrally maintained. The sections below cover the list file format, the meta and field definitions, the dependency model between lists, how schemas reference and filter lists, how the runtime injects resolved data into handlers, and the validation rules that keep the registry consistent.
---
## 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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./15-catalog.md](./15-catalog.md) — see chapter 15.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
---
# 04. Output Schema
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Output schemas make tool responses predictable. By declaring the expected shape of a response at the route level, an AI client knows in advance what fields it will receive and can reason about them without parsing guesswork. The sections below cover the output declaration format, the supported types and MIME-types, the standard response envelope, how output interacts with `postRequest` handlers, and the validation rules applied to every declaration.
---
## 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 being migrated from an earlier major version
- 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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./10-tests.md](./10-tests.md) — see chapter 10.
- [./19-mcp-integration.md](./19-mcp-integration.md) — see chapter 19.
- [./22-scoring-protocol.md](./22-scoring-protocol.md) — see chapter 22.
---
# 05. Security Model
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
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.
---
## Where the Scan Runs — Trusted vs. Private Load Paths
The static scan is the gate for **uncurated** schemas. FlowMCP loads schemas through two paths with different trust properties, and the scan protects exactly the one that is not curated:
| Load path | How a schema enters the runtime | Static scan | Discoverable |
|-----------|---------------------------------|-------------|--------------|
| **Trusted path** | Registered in `schemaFolders[]` and merged into the catalog | **Skipped** | Yes — `search` / `list` / `serve` |
| **Private ad-hoc path** | Loaded by exact file path for a single call | **Active** — runs before `import()` | No — never registered, never merged |
**Trusted path.** Schemas placed in a configured `schemaFolders[]` source are reviewed once, at registration time, by the operator who adds the folder. Loading them does not re-run the static scan. This is a deliberate design choice: a universal scan on every load would reject the small number of curated add-on schemas that legitimately import Node built-ins (for example a geo add-on that opens a TLS connection or reads a bundled SQLite file), and most substring matches on real schema files are false positives. The trust for these schemas comes from the registration step, not from the scanner.
**Private ad-hoc path.** A private schema is invoked by its path — `private call '{json}'` — and is **never** written into `schemaFolders[]` nor merged into the shared catalog. Two consequences follow:
1. **Structural invisibility.** Because the schema never enters the registration/merge machinery, it cannot appear in `search`, `list`, or an MCP `serve` session. This is invisibility by construction, not a visibility filter.
2. **Scan on load.** Because the schema was never curated, the private path runs the **security scan before any `import()`**. A file that matches a forbidden pattern is rejected and never executed; only a clean file is dynamically imported and run.
This is the "scan on the private path" rule: the scan defends the uncurated ad-hoc load, while trusted, already-reviewed schemas keep loading scan-free.
### Known gap
The zero-import target ([`00-overview.md`](./00-overview.md), Principle 3) is a `SHOULD`, and a small number of registered add-on schemas still carry top-level imports of Node built-ins. They load scan-free over the trusted path, so those imports are not blocked today. Closing the gap fully would require every registered schema to reach zero-import conformance. A private ad-hoc call of the **same** file is still rejected by the scan — the private path holds the line regardless of the trusted-path state. A universal scan across all load paths remains a possible future step, contingent on that conformance and on further scanner hardening.
---
## 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 |
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./23-license-and-tos.md](./23-license-and-tos.md) — see chapter 23.
---
# 06. Agents
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
An Agent is a complete, purpose-driven definition that bundles tools from multiple providers for a specific task. Unlike a plain tool list, an Agent is a full composition: it cherry-picks tools across providers, binds them to a specific LLM, defines a system prompt and behavioral guidelines, and ships with its own tests, explanatory prompts, instructional skills, and optional resources. The sections below cover the agent manifest format, how tool references are resolved, model binding, the system prompt, integrity verification through hashes, and the validation rules applied at activation.
---
## 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
#### The `name` field
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)
```
#### The `version` field
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 (accepted with warning during migration)
4.0.0 <- INVALID (missing flowmcp/ prefix)
flowmcp/2.0.0 <- INVALID (agents did not exist in that earlier spec line)
```
#### The `model` field
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)
```
#### The `systemPrompt` field
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.'
}
```
#### The `tools` field
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.
#### The `maxRounds` field
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.
#### The `maxTokens` field
The maximum number of tokens the LLM MAY generate per response. Default is `4096`. This controls response length, not total context.
#### The `prompts` field
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.
#### The `skills` field
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`.
#### The three content layers: 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.
#### The `resources` field
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}} || '%'"
}
}
}
}
}
```
#### The `sharedLists` field
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.
#### The `inputSchema` field
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 the older Group concept. The migration is conceptual — agents are not backward-compatible with groups because they serve a fundamentally different purpose.
### What Changed
| Aspect | Group (legacy) | Agent (current) |
|--------|-----------|------------|
| 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 Group-to-Agent 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.
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...'
}
```
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./10-tests.md](./10-tests.md) — see chapter 10.
- [./12-prompt-architecture.md](./12-prompt-architecture.md) — see chapter 12.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./17-selections.md](./17-selections.md) — see chapter 17.
---
# 07. MCP Tasks
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
MCP Tasks describe long-running asynchronous operations — a query that takes thirty seconds to finish, a job that is submitted now and collected later. The underlying MCP protocol gives such operations a lifecycle of creation, polling, completion, and cancellation. FlowMCP does not yet model this lifecycle: task support is a reserved, forward-looking area of the specification, and this page documents what is held back and why.
---
## Why It Is Held Back
A schema describes how to talk to an **external API's** asynchronous pattern — submitting a job, polling its status, retrieving the result once it is ready. The MCP Tasks protocol describes something different: how the **MCP server itself** surfaces its own asynchronous operations to AI clients. These are two distinct layers, and binding them together cleanly is what task support depends on. A future revision will define:
- Schema-level fields for declaring asynchronous tools
- A mapping between an external API's own status values and MCP Task states
- Integration with the MCP Tasks protocol operations (`tasks/get`, `tasks/result`, `tasks/cancel`)
---
## Reserved Fields
Schema authors MAY include an `async` field in tool definitions for forward compatibility. The runtime currently **ignores** this field but preserves it untouched, so schemas written today remain valid once task support lands.
---
## Reference
- [MCP Tasks Specification](https://modelcontextprotocol.io/specification/basic/utilities/tasks)
- [SEP-1686: Tasks — protocol discussion](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686)
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./10-tests.md](./10-tests.md) — see chapter 10.
---
# 08. Migration Guide
| Field | Value |
|---|---|
| Status | Informative |
| 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) |
> **Informative.**
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 schema-check --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 schema-check
```
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 schema-check
```
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 schema-check`)
- [ ] Tests pass (`flowmcp grading deterministic`)
- [ ] (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 removes `main.skills` and may optionally add a `meta` block to Tools (the `meta` block is optional as of v4.3.0). 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 (optional) | When adding one, 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 schema-check` → PASS |
| 6 | API-Test | `flowmcp grading deterministic` → 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 optional (as of v4.3.0); when present, all 6 fields are required (VAL101–VAL106). Missing `meta` is valid.
### Why No Automatic Migration
The v3→v4 migration is more complex than v2→v3. Adding `meta` blocks (now optional) 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
}
}
}
}
```
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./21-schema-lifecycle.md](./21-schema-lifecycle.md) — see chapter 21.
---
# 09. Validation Rules
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Every validation rule that `flowmcp schema-check` enforces carries a code, a severity, and a one-line description. Rather than collect those rules into a single distant list, the specification keeps each family of rules next to the feature it governs — resource rules live with the resource spec, skill rules with the skill spec, and so on. This page is the wayfinder for that arrangement: it names each rule family, explains its code prefix, and links straight to the section where the rules are defined and explained.
A rule is defined in exactly one place. The page below it in the table is the authoritative home for that code prefix; nothing on this page duplicates those tables. What remains here is the cross-cutting material that belongs to the validation system as a whole — the meaning of each severity level and the shape of the CLI output.
---
## Rule Families
Each rule family is owned by one topic page. Follow the link to read the rules, their severities, and the rationale behind them.
| Rule family | Code prefix | Defined in |
|-------------|-------------|------------|
| Schema structure, `main` block, tools, parameters, output | `VAL` | [Schema Format → Constraints](./01-schema-format.md#constraints) |
| Parameters, `z` validation, placeholders | `VAL`, `PH` | [Parameters → Z Block (Validation)](./02-parameters.md#z-block-validation) |
| Output schema | `VAL` | [Output Schema → Validation Rules](./04-output-schema.md#validation-rules) |
| Static security scan, library allowlist | `SEC` | [Security → Static Security Scan](./05-security.md#static-security-scan) |
| Agents | `AGT` | [Agents → Validation Rules](./06-agents.md#validation-rules) |
| Tests | `TST` | [Tests → Validation Rules](./10-tests.md#validation-rules) |
| Caching / preload | `VAL` | [Preload & Caching → Validation Rules](./11-preload.md#validation-rules) |
| Prompts | `PRM` | [Prompt Architecture → Validation Rules](./12-prompt-architecture.md#validation-rules) |
| Resources | `RES` | [Resources → Validation Rules](./13-resources.md#validation-rules) |
| Skills, dependency cross-checks | `SKL`, `DEP` | [Skills → Validation Rules](./14-skills.md#validation-rules) |
| Catalog / registry | `CAT` | [Catalog → Validation Rules](./15-catalog.md#validation-rules) |
| IDs and namespaces | `ID` | [ID Schema → Validation Rules](./16-id-schema.md#validation-rules) |
| Selections | `SEL` | [Selections → Validation Rules](./17-selections.md#validation-rules) |
| Shared lists | `LST` | [Validation Strategy](./20-validation-strategy.md) |
---
## CLI Runtime Error Codes
The families above are **schema-check** rules — emitted when `flowmcp schema-check` validates a schema file. The `flowmcp` CLI *also* emits `PREFIX-NNN` codes at **runtime**, when a command (`call`, `serve`, `list`, `grading`, …) hits a caught failure. These share the same format (3–4 uppercase letters, `-`, three digits; regex `^([A-Z]{3,4}-\d{3})`) and the same three severities, but their authoritative home is the CLI implementation, not a schema-spec page: every runtime code is defined at its `try`/`catch` site in `flowmcp-cli` (`src/task/FlowMcpCli.mjs`). `flowmcp doctor` reads them back structurally.
| Namespace | Code prefix | Concern |
|-----------|-------------|---------|
| Shared-list resolution (runtime) | `LST`, `HND` | a declared `sharedLists` ref that cannot be located/resolved fails loud (never a silent empty list) |
| Config / single-source | `CFG` | reading/validating `~/.flowmcp/config.json` and `schemaFolders[]` |
| Schema load / resolve | `SCH` | loading a schema module or resolving its on-disk path |
| Tool call / dispatch | `CAL`, `CLI` | `call`/argument parsing, tool matching, generic CLI failures |
| Libraries | `LIB` | resolving `requiredLibraries` from the CLI base |
| SQLite / resource add-ons | `SQL` | sqlite-gtfs pipeline, resource create/migrate |
| Cache / seal | `CCH` | read/write of the response and seal caches (a missing cache is a normal empty state, not surfaced) |
| Grading | `GRD` | grading module load, gradings dirs |
| Health / doctor | `HLT` | health-check probes and `doctor` |
| Selections | `SEL` | selection list/show/validate at runtime |
| Skills | `SKL` | skills-block discovery and count |
| Env / addons / import / misc | `ENV`, `ADN`, `IMP`, `GRP`, `NET`, `HSH` | env prompts, add-on load, import, group append, fetch, hashing |
`LST` and `SEC` are shared with the schema-check namespace by design — the same subject (shared lists, security) keeps one prefix across both static validation and runtime. `SEC200`–`SEC299` is reserved for runtime security 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 |
---
## Validation Output Format
The CLI displays results grouped by severity with the rule code, severity, location, and description:
```
flowmcp schema-check 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 schema-check etherscan/contracts.mjs
0 errors, 0 warnings
Schema is valid
```
With security flag:
```
flowmcp schema-check --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)
```
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./05-security.md](./05-security.md) — see chapter 05.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./17-selections.md](./17-selections.md) — see chapter 17.
- [./20-validation-strategy.md](./20-validation-strategy.md) — see chapter 20.
---
# 10. Tests
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Tests are executable examples embedded in tool and resource query definitions. For agents, tests are prompts paired 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 become the basis for generating accurate output schemas. The sections below cover the test format for tools, resources, and agents, the design principles that keep tests useful, and the response-capture lifecycle that turns a test into an output schema.
---
## 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 schema-check` 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.3.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 schema-check`.
### 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 schema-check 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 |
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./22-scoring-protocol.md](./22-scoring-protocol.md) — see chapter 22.
---
# 11. Preload & Caching
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Some tools return data that barely moves — a full list of hospitals in a country, every memorial stone in a city, a reference table that is refreshed once a week. Re-fetching such a dataset on every call costs bandwidth and time for no benefit. The optional `preload` field lets a schema author declare that a tool's response is safe to cache and for how long, so a runtime can serve a stored copy instead of hitting the network each time. Caching always stays optional on the runtime's side: `preload` expresses intent, and the runtime decides whether to honour it. The sections below specify the field, its validation rules, the cache key derivation, the runtime cache flow, and the author guidelines for choosing a sensible time-to-live.
---
## 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 validation rule set indexed in [09-validation-rules.md](./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
Tool tests (see [10-tests.md](./10-tests.md)) always bypass the cache to ensure they exercise the live API. The `--no-cache` flag is implied during test execution.
### Output Schema
The output schema (see [04-output-schema.md](./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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./10-tests.md](./10-tests.md) — see chapter 10.
- [./13-resources.md](./13-resources.md) — see chapter 13.
---
# 12. Prompt Architecture
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
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 schema-check providers/coingecko/prompts/price-comparison.mjs
0 errors, 0 warnings
Prompt is valid
```
```
flowmcp schema-check 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 schema-check 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)
```
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./18-prefill.md](./18-prefill.md) — see chapter 18.
---
# 13. Resources
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Resources give a schema local, deterministic data access alongside its network tools. They map to the MCP `server.resource` primitive and live in `main.resources` next to `main.tools`. A resource is backed by a SQLite database (read-only in-memory or writable file-based), a remote SQLite file fetched over HTTPS, or a Markdown document — and the runtime exposes each through a small set of typed queries, with the auto-injected `runSql` and `describeTables` rounding out the query surface. Where a tool reaches out to a third-party API, a resource serves data that should always be available: reference tables, lookups, agent-collected results, and inline documentation.
---
## Why Resources Exist
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 (the `geo-gtfs-toolkit` package, cloned from `github:FlowMCP/geo-gtfs-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. `geo-gtfs-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 `github:FlowMCP/geo-gtfs-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. `github:FlowMCP/geo-gtfs-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: 'geo-gtfs-toolkit', // Authority over default methods
addonVersion: '>=0.1.0', // optional
addonSource: 'github:FlowMCP/geo-gtfs-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 initializes this resource schema (on first use — there is no separate activation command), 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 (the `geo-gtfs-toolkit` package, cloned from `github:FlowMCP/geo-gtfs-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 `github:FlowMCP/geo-gtfs-toolkit` (under a CC0 license) is the only GTFS-shaped data permitted in any public FlowMCP repository. It exists exclusively for testing and CI.
---
## Add-on URL Mode (`mode: 'url'`)
Add-on resources (`source: 'geo-geojson'`, `source: 'geo-csv'`) support a second access mode, `mode: 'url'`, in addition to the file-based mode used by `sqlite-gtfs`. In URL mode the schema carries only a thin reference — `source`, `mode`, `url`, `addon` (and, for CSV, a mandatory `parseConfig`). At schema activation the add-on fetches the **complete** dataset in a **single** request, parses it, validates it on load, and holds it **in memory** for the lifetime of the process. There is no SQLite file, no on-disk artifact, and no quality seal.
### How it Differs from `source: 'http'`
`source: 'http'` (see below) downloads a **SQLite file**, caches it on disk, and exposes it through `runSql`/`queries`. `mode: 'url'` on an add-on source downloads a **complete GeoJSON or CSV document**, holds it **in memory**, and exposes it through the add-on's central default methods (`featuresInBBox`, `nearPoint`, `byType`) — not through hand-written SQL. The two are deliberately distinct: file-cache + `runSql` versus in-memory + add-on methods.
### Required Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `source` | `'geo-geojson'` \| `'geo-csv'` | Yes | Add-on resource type. `sqlite-gtfs` does NOT support URL mode (`RES043`). |
| `mode` | `'url'` | Yes | Access mode. Explicit — there is no default mode (`RES044`). |
| `url` | `string` | Yes | HTTPS URL of the complete dataset. MUST use HTTPS (`RES044`). |
| `addon` | `string` | Yes | Add-on package that owns the parser and default methods (e.g. `geo-geojson-toolkit`). |
### Conditional Fields
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `parseConfig` | `object` | For `geo-csv`: Yes | Parse configuration (delimiter, header, column types, lat/lon columns). For CSV this is **mandatory** — there is no silent default (`RES045`). For GeoJSON, omit (structure is self-describing). |
### Lifecycle
1. **Init (on first use):** the CLI loads the add-on declared in `addon`, calls its consumer API to fetch the full file from `url` in one request, parse it, and **validate it on load** (valid GeoJSON / required CSV columns) — replacing the file-based seal check. The parsed records are held in memory, keyed by URL.
2. **Runtime:** queries are served from the in-memory store via the add-on's default methods. Spatial queries (`nearPoint`, `featuresInBBox`) use a pure-`Math` Haversine implementation; no external module is required.
3. **Central maintenance:** the add-on owns the methods. A fix in the add-on propagates to every schema that references it.
### Scope
URL mode is for **complete datasets downloadable in a single step** — small static GeoJSON files and tabular CSV (Memo-090 class K3). Paginated or windowed sources (e.g. WFS) are out of scope and require a dedicated adapter.
### Schema Example
A minimal `mode: 'url'` GeoJSON schema:
```javascript
resources: {
berlinPois: {
source: 'geo-geojson',
mode: 'url',
url: 'https://example.org/berlin-pois.geojson', // HTTPS only
addon: 'geo-geojson-toolkit', // owns nearPoint / featuresInBBox / byType
description: 'Berlin points of interest — fetched and held in memory'
}
}
```
For CSV the `parseConfig` field is mandatory:
```javascript
resources: {
stations: {
source: 'geo-csv',
mode: 'url',
url: 'https://example.org/stations.csv',
addon: 'geo-csv-tsv-toolkit',
parseConfig: { // mandatory — no silent default
delimiter: ',',
header: true,
columns: { name: 'string', lat: 'number', lon: 'number' },
latColumn: 'lat',
lonColumn: 'lon'
},
description: 'Charging stations — in-memory, spatial queries via lat/lon'
}
}
```
---
## 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.3.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'`, `'markdown'`, or `'http'` | Three supported resource sources (see `RES001`). |
| `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.
---
## MCP Exposure of Resource Queries
Resources map to the MCP `server.resource` primitive (see the introduction), but every **query** a resource declares is *also* exposed as a callable MCP tool, so an agent invokes it exactly the way it invokes any other tool. The exposure is deterministic and identical across `search`, `call`, and `serve`:
| Concern | Contract |
|---------|----------|
| **Wire name** | Each query is exposed under `${queryName}_${namespace}` — the same `name_namespace` convention that tools use (see [16-id-schema.md](./16-id-schema.md#cli-adapter)). The **query** name, not the resource name, is the wire name. |
| **Auto-injected queries** | `runSql` and `describeTables` are exposed the same way (`runSql_${namespace}`, `describeTables_${namespace}`). |
| **`search` == `call` == `serve`** | The name advertised by `search`, the name resolved by `call`, and the name registered by `serve` MUST be the identical `${queryName}_${namespace}` string. A schema author never declares a `tools` entry to make a resource query callable. |
| **`about` reads** | An About resource (reserved route `about`, above) is a Markdown resource with no queries; it is read through the MCP `server.resource` primitive, not exposed as a query tool. Surfaces that enumerate callable queries (e.g. `search`) skip it. |
| **Max query surface** | At most 7 schema-defined queries per resource, plus the 2 auto-injected (`runSql`, `describeTables`) = 9 callable names per resource (`RES028`). |
| **`http` source** | `source: 'http'` is a first-class resource source alongside `sqlite` and `markdown`; its queries are exposed under the same `${queryName}_${namespace}` contract (`RES001`, `RES024`). |
This is the single canonical resource-call convention: one query, one wire name, consistent across every surface.
---
## 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.3.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.3.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. |
| RES043 | error | `mode: 'url'` is only valid for `source: 'geo-geojson'` or `'geo-csv'`. `sqlite-gtfs` does not support URL mode. (added in v4.3.0) |
| RES044 | error | `mode: 'url'` requires `url` (HTTPS) and `addon`. The URL MUST use HTTPS. `mode` MUST be explicit (no default). (added in v4.3.0) |
| RES045 | error | `source: 'geo-csv'` with `mode: 'url'` requires a `parseConfig` object. No silent default. (added in v4.3.0) |
---
## 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`.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./05-security.md](./05-security.md) — see chapter 05.
- [./11-preload.md](./11-preload.md) — see chapter 11.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./19-mcp-integration.md](./19-mcp-integration.md) — see chapter 19.
---
# 14. Skills
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Skills are reusable, self-contained instruction sets that an AI agent can load and follow. They map to the MCP `server.prompt` primitive and live in `.mjs` files, each with a structured `export const skill` object that pairs Markdown instructions with typed metadata. A skill declares the tools and resources it needs, the input it expects, and the output it produces, and its instructions reference those primitives through `{{tool:...}}`, `{{resource:...}}`, and `{{input:...}}` placeholders resolved at load time.
---
## Where Skills Fit
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.3.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.3.0** — that pattern is forbidden (see VAL016).
### Field Details
#### The `name` Field
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
```
#### The `version` Field
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.3.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`).
#### The `description` Field
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)
```
#### The `requires` Object
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.
#### The `input` Array
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.
#### The `output` Field
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.'
```
#### The `content` Field
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.3.0)
Skills in v4.3.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.3.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 schema-check`.
| 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.3.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 schema-check` — 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.3.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
```
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./12-prompt-architecture.md](./12-prompt-architecture.md) — see chapter 12.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./17-selections.md](./17-selections.md) — see chapter 17.
- [./18-prefill.md](./18-prefill.md) — see chapter 18.
---
# 15. Catalog
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
A Catalog is the top-level organizational unit in FlowMCP v4. 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 load boundary** — a runtime that consumes the directory needs knowledge of its 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 the CLI uses it as the entry point when the directory is registered in `schemaFolders[]`. Schemas load directly from the configured folder — there is no separate remote-import or download step.
```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/v4.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.3.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.3.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`
- A provider namespace directory name MUST equal `main.namespace` of every schema it contains (folder↔namespace invariant, `VAL019` in `09-validation-rules.md`). For an all-unparseable folder the directory name is the fallback namespace and is renamed to match once a schema parses (rename-on-parse, see `16-id-schema.md`).
- 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/v4.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 configuration** — each catalog is referenced independently in the CLI's `schemaFolders[]`. Adding 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. `"4.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. |
---
## Catalog Resolution Flow
This describes how a catalog's contents are resolved once its directory is referenced in the CLI's `schemaFolders[]`. Agent manifests are resolved alongside provider schemas and shared lists.
```mermaid
flowchart LR
A["catalog dir in schemaFolders[]"] --> B[Read registry.json]
B --> C[Load shared lists]
B --> D[Load provider schemas]
B --> E[Load agent manifests]
E --> F[Agents available locally]
F --> G[agent tools available]
```
The diagram shows how a catalog referenced in `schemaFolders[]` is resolved: registry.json is read, then shared lists, provider schemas, and agent manifests are loaded, making the catalog's tools and agents available locally.
### Phase 1: Catalog Read
1. **Read `registry.json`** from the referenced catalog directory.
2. **Validate manifest** — check required fields, verify `schemaSpec` compatibility.
3. **Load shared lists** — resolve each `shared[].file` path and load the `.mjs` files.
4. **Load provider schemas** — resolve each `schemas[].file` path and load the `.mjs` files.
5. **Load agent manifests** — resolve each `agents[].manifest` path and load the `agent.mjs` files (and associated prompt, skill, and resource files).
6. **Index locally** — register all loaded entries from the catalog directory.
### Phase 2: Agent Resolution
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. **Make tools available** — the agent's required tools become callable from the resolved catalog.
4. **Register prompts** — make the agent's prompts available as MCP 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 loaded or published.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./03-shared-lists.md](./03-shared-lists.md) — see chapter 03.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./21-schema-lifecycle.md](./21-schema-lifecycle.md) — see chapter 21.
---
# 16. ID Schema
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
A FlowMCP catalog exposes three MCP primitives — Tools, Resources, and Skills (prompts) — across potentially hundreds of schemas from dozens of providers, and references to those primitives turn up everywhere: group definitions, skill placeholders, registry entries, CLI commands, and cross-schema dependencies. Without a unified scheme, a bare name like `lookupItem` is ambiguous — is it a tool, a resource, or a prompt, and which provider owns it? The ID schema removes that ambiguity by giving every tool, resource, prompt, and shared list exactly one canonical, human-readable, resolvable identifier built from a namespace, a type discriminator, and a name.
```mermaid
flowchart LR
A[Namespace] --> B["/"]
B --> C[Resource Type]
C --> D["/"]
D --> E[Name]
E --> F["acme/tool/lookupItem"]
```
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** | `directory-io/records` |
| Tool | `namespace/tool/name` | 2 | `directory-io/tool/getEntry` |
| Resource | `namespace/resource/name` | 2 | `directory-io/resource/entryDb` |
| Prompt | `namespace/prompt/name` | 2 | `directory-io/prompt/intro` |
| Skill | `namespace/skill/name` | 2 | `directory-io/skill/audit` |
| Selection | `namespace/selection/name` | 2 | `record-research/selection/lookup` |
| Agent | `namespace/agent/name` | 2 | `research/agent/researcher` |
**Distinguishing rule:** 1 Slash = Schema-File-ID (Container). 2 Slashes = Primitive-ID (Content).
### Full Form Examples
| ID | Description |
|----|-------------|
| `weather/tool/getForecast` | Tool from the weather provider |
| `weather/resource/supported-cities` | Resource from the weather provider |
| `weather/prompt/forecast-summary` | Prompt from the weather provider |
| `data-research/prompt/record-deep-dive` | Agent prompt |
| `shared/list/regionCodes` | 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 unique within its own `schemaFolder` — each folder is its own registry (see [Namespace Rules](#namespace-rules)). The CLI aggregates multiple folders into one catalog, so the same namespace MAY appear in more than one aggregated folder; cross-folder collisions are disambiguated by the optional source coordinate `:` (see [Source Coordinate](#source-coordinate)).
```
weather ← provider namespace
directory ← provider namespace
catalog ← provider namespace
data-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.3.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 | `getForecast`, `getEntry` |
| Resource | camelCase | `recordLookup`, `regionConfig` |
| Prompt | kebab-case | `forecast-summary`, `record-deep-dive` |
| Shared List | camelCase | `regionCodes`, `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: directory-io/records
└── namespace: directory-io
└── schema-name: records (equals filename without .mjs)
Contains Primitive-IDs:
directory-io/tool/getEntry
directory-io/tool/getEntryHistory
directory-io/resource/entryCache
```
### Naming Rules for schema-name
- Kebab-case, only lowercase letters and hyphens
- Thematic, not technical (e.g., `records`, `events`, `reports` — not `schema1`, `tools-v2`)
- For providers with multiple schemas: topic prefix optional (`registry-records`, `registry-events`)
- Matches exactly the filename without `.mjs`
### Directory Mapping
```
schemas/v4.1.0/providers/directory-io/records.mjs
└── namespace └── schema-name.mjs
→ Schema-File-ID: directory-io/records
```
The path segment labelled "namespace" above **MUST equal** `main.namespace` of every schema in the directory — it is a binding equality, not merely a label or a derivation. This is the folder↔namespace invariant `VAL019` (see `09-validation-rules.md`). The grading-monitoring track and the namespace-resolution fallback below consume this invariant.
### Namespace Resolution / Fallback
The namespace of a provider folder is resolved as follows:
1. **Normal case.** The namespace is `main.namespace`, declared in the schema. The folder name MUST equal it (`VAL019`).
2. **All-unparseable fallback.** When **all** schemas in a folder are unparseable (no readable `main.namespace`), the **folder name** is the fallback namespace identifier. The fallback name MUST itself be a valid namespace (`^[a-z][a-z0-9-]*$`); a folder name that is not a valid namespace is an error, never silently normalised.
3. **Rename-on-parse.** Once a schema parses and exposes `main.namespace`, that field is **authoritative** and the folder is renamed to match it. A rename is an identity transition, not a delete.
The "all-unparseable → folder name" behaviour is **pipeline** behaviour, owned by the grading track (see the Grading-Spec [`19-folder-layout.md`](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/3.0.0/19-folder-layout.md) and [`22-workbench-island.md`](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/3.0.0/22-workbench-island.md)). The Schemas-Spec's job here is only to (a) name the fallback source (the folder name) and (b) assert the post-parse equality invariant (`VAL019`).
---
## 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 |
|------------------|------------------------|
| `directory-io/tool/getEntry` | `getEntry_directory-io` |
| `registry/tool/getRecord` | `getRecord_registry` |
**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.
### Resource Queries in the MCP Tool Name
A resource does not map to a single tool — each **query** it declares is exposed as its own callable MCP tool. The wire name is built from the **query** name and the namespace, using the same `name_namespace` rule:
| External Spec-ID (resource query) | Internal MCP Tool Name |
|-----------------------------------|------------------------|
| `mailarchive/resource/mail`, query `searchMail` | `searchMail_mailarchive` |
| auto-injected `runSql` on namespace `tokens` | `runSql_tokens` |
**Mapping Rule (resource query):** `${queryName}_${namespace}` — the **query** name, not the resource name, is the wire name. The auto-injected `runSql` and `describeTables` follow the same rule. This is the single convention shared by `search`, `call`, and `serve` (see [13-resources.md](./13-resources.md#mcp-exposure-of-resource-queries)).
### Source Coordinate in the MCP Tool Name
When the CLI aggregates several `schemaFolders[]` and two folders expose the same namespace, the bare `routeName_namespace` tool name would collide at the MCP layer (the MCP protocol requires unique tool names). To let both folders coexist, the source coordinate (see [Source Coordinate](#source-coordinate)) is carried through `#buildToolName()` and appended to the internal MCP tool name on collision, so each tool stays addressable:
| External Spec-ID | Internal MCP Tool Name |
|------------------|------------------------|
| `folder-a:weather/tool/getForecast` | `getForecast_weather` (uncontested source kept bare) |
| `folder-b:weather/tool/getForecast` | `getForecast_weather_folder-b` (source appended to break the collision) |
Without this propagation, two equally-named folders cannot both be served — the MCP layer aborts on the duplicate tool name (the `serve` dedup/rename/error path). The CLI applies a deterministic dedup-or-rename so the source disambiguation reaches all the way into the served tool name; a genuine duplicate that cannot be disambiguated is reported, never silently dropped.
---
## No Short Form
Short Form is not supported in FlowMCP v4. `flowmcp call getRecord` (without namespace/type) is not allowed.
**Reason:** Ambiguity and hidden data provenance. `registry/tool/getRecord` 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 catalog. The catalog is the aggregation of the per-folder registries from `schemaFolders[]`; each folder maps its namespaces to schema file locations. When more than one aggregated folder owns the namespace, a qualified reference (`:/...`, see [Source Coordinate](#source-coordinate)) selects the exact folder, while an unqualified reference resolves first-wins (first folder in `schemaFolders[]` order) plus a visible collision warning.
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.getForecast`).
---
## 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:getEntry}}` | Tool reference — resolved to a tool in the same schema's `main.tools` |
| `{{resource:verifiedEntries}}` | 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:recordId}}` | 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 folder — each `schemaFolder` is its own registry. The CLI aggregates multiple folders into one catalog; across aggregated folders the same namespace is allowed and is disambiguated by the optional source coordinate (see [Source Coordinate](#source-coordinate)). Namespaces follow strict governance rules.
### One Folder, One Registry
A `schemaFolder` is a self-contained registry: namespace uniqueness is required **within** a folder, not across all folders the CLI knows about. The folder↔namespace invariant `VAL019` (see [Directory Mapping](#directory-mapping)) already operates per folder — the folder name MUST equal `main.namespace` of every schema in that folder.
The CLI aggregates the folders listed in `schemaFolders[]` into a single catalog at load time. Two different folders MAY each carry a schema with the same namespace; this is not an error. References are resolved as follows:
- **Qualified** — a reference prefixed with the source coordinate (`:/...`, see [Source Coordinate](#source-coordinate)) selects exactly one folder's namespace.
- **Unqualified** — a reference without a source coordinate resolves first-wins across the aggregated folders (the first folder in `schemaFolders[]` order that owns the namespace), and the CLI emits a visible collision warning so the ambiguity is never silent.
### Source Coordinate
When the CLI aggregates several `schemaFolders[]`, two folders may expose the same namespace. The **source coordinate** is an optional prefix that qualifies a reference to exactly one folder:
```
:[//]
```
```
folder-a:weather/tool/getForecast
folder-b:weather/tool/getForecast
```
- `` identifies the originating `schemaFolder` (the source key the CLI assigns to that folder).
- The separator is a **colon** (`:`), chosen so it does not disturb the slash grammar of the ID: the namespace/type/name segments are still split on `/` exactly as before, and the three-segment count is unchanged. The colon prefix is parsed off before the slash resolution runs.
- The coordinate applies to all referenceable primitives — `tool`, `resource`, `prompt`, `skill`, `list`, `selection`, and `agent`.
- **Unqualified** references (no `:` prefix) resolve first-wins across the aggregated folders plus a visible collision warning; the source coordinate is the way to pin the exact folder.
### Namespace Assignment
| Source | Namespace Derivation | Example |
|--------|---------------------|---------|
| API Provider | Domain-derived name | `weather`, `directory`, `catalog` |
| Agent | Agent name | `data-research`, `status-monitor` |
| Shared resources | Reserved `shared` | `shared/list/regionCodes` |
### 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.weather.com → weather
directory.io → directory
catalog.com → catalog
pro-api.marketplace.com → marketplace
```
### 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-]*$`).
```
data-research ← agent that performs record research
status-monitor ← agent that monitors service status
```
### 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 schema-check --id "weather/tool/getForecast"
0 errors, 0 warnings
ID is valid
```
```
flowmcp schema-check --id "WEATHER/tool/getForecast"
ID002 error Namespace "WEATHER" must match ^[a-z][a-z0-9-]*$
1 error, 0 warnings
ID is invalid
```
```
flowmcp schema-check --id "getForecast"
ID001 error ID MUST contain at least one "/" separator
1 error, 0 warnings
ID is invalid
```
---
## Examples
### Tool Reference
```
weather/tool/getForecast
```
- **Namespace**: `weather` — the weather provider
- **Type**: `tool` — an MCP tool (API endpoint)
- **Name**: `getForecast` — the specific tool name (camelCase)
### Resource Reference
```
weather/resource/supported-cities
```
- **Namespace**: `weather` — the weather provider
- **Type**: `resource` — an MCP resource (SQLite data)
- **Name**: `supported-cities` — the specific resource
### Prompt Reference
```
data-research/prompt/record-deep-dive
```
- **Namespace**: `data-research` — an agent namespace
- **Type**: `prompt` — an MCP prompt (skill)
- **Name**: `record-deep-dive` — the specific prompt (kebab-case)
### Shared List Reference
```
shared/list/regionCodes
```
- **Namespace**: `shared` — reserved namespace
- **Type**: `list` — a shared list
- **Name**: `regionCodes` — 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`.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./12-prompt-architecture.md](./12-prompt-architecture.md) — see chapter 12.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./15-catalog.md](./15-catalog.md) — see chapter 15.
- [./17-selections.md](./17-selections.md) — see chapter 17.
- [./18-prefill.md](./18-prefill.md) — see chapter 18.
---
# 17. Selections
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
A **Selection** is a named, thematically-coherent collection of Primitives (Tools, Resources, Prompts, Skills) that an agent can activate together in 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 grading deterministic
```
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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./06-agents.md](./06-agents.md) — see chapter 06.
- [./12-prompt-architecture.md](./12-prompt-architecture.md) — see chapter 12.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./18-prefill.md](./18-prefill.md) — see chapter 18.
---
# 18. Prefill and Placeholders
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Skill content carries two complementary template mechanisms that the runtime processes before handing the Skill to an agent. *Placeholders* are tokens embedded directly in the content — `{{type:reference}}` — that are substituted with tool descriptions, resource references, user input, or shared-list values at resolution time. *Prefill* goes one step further: a Skill can declare tool calls in a `prefill` array that the runtime executes up front, then embeds the live results into the content via `{{prefill:...}}` tokens, so the agent receives a Skill already populated with current data.
---
## 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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./02-parameters.md](./02-parameters.md) — see chapter 02.
- [./12-prompt-architecture.md](./12-prompt-architecture.md) — see chapter 12.
- [./14-skills.md](./14-skills.md) — see chapter 14.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./17-selections.md](./17-selections.md) — see chapter 17.
---
# 19. MCP Server Integration
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
When FlowMCP runs as an MCP Server, each Tool may be exposed to the agent with MCP-specific metadata that an MCP host can read before it decides whether and how to invoke the Tool. That metadata is declared optionally, per Tool, in a `meta` block, and the CLI/Core translates the relevant fields into MCP annotations at registration time. This page describes the `meta` block, how its fields map to MCP, and the behaviour of the search-related and loading-related fields.
---
## Meta Block (Optional per Tool)
A Tool MAY declare a `meta` block. It is optional — a Tool without a `meta` block is valid. When a `meta` block IS present, it MUST be complete: all fields below are required (VAL101–VAL106).
```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
The `meta` block is optional, but when present every field below is required:
| 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, the loading- and search-related `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. Schema authors set the FlowMCP-side fields; the annotation shape is produced by the registration step.
## alwaysLoad Policy
`alwaysLoad: true` should be used sparingly:
- **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, so the default of lazy loading is the right choice for most Tools.
## aliases Field
`aliases` lets ToolSearch find a Tool by alternative names. If an agent searches for `getAbi`, ToolSearch finds `getSmartContractAbi` because `getAbi` is in its `aliases` array. An empty array `[]` is valid and simply means the Tool declares no aliases.
## Validation Rules
The structural rules for the `meta` block are defined alongside the other schema rules in [09-validation-rules.md](./09-validation-rules.md); they are listed here for reference at the point of use:
| Code | Severity | Rule |
|------|----------|------|
| VAL100 | error | `meta` block is optional; when present it MUST be a plain object (absent `meta` is allowed) |
| 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) |
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./14-skills.md](./14-skills.md) — see chapter 14.
---
# 20. Validation Strategy
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
Validation in FlowMCP works in two layers. A **deterministic** layer checks structural correctness — that the schema is well-formed and every primitive declares the fields it must. A **probabilistic** layer adds an LLM-based quality judgement on top of a structurally clean schema. Together the two layers produce a **Grade Report** that assigns a single letter grade (A–F), which is the record this page defines.
A separate **Grading-Spec** in the `flowmcp-grading` repository covers Single-Schema and Selection grading in more depth. The strategy described here remains the deterministic baseline; the Grading System defined by the Grading-Spec extends, and in places replaces, the simple A–F Grade System below. The Schemas-Spec remains the highest instance.
---
## 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).
### Two tracks: dev-track grade F vs. monitoring-track `blocked` record
A validation FAIL is recorded differently depending on the track:
- **Development track** (this strategy, the Grade Report). A validation FAIL yields the terminal grade **F = "Not loadable"**. The development gate is unchanged: validate-clean is still required before `stage:production` (see [21-schema-lifecycle.md](./21-schema-lifecycle.md)). Grade F continues to represent a not-loadable schema in the dev/grade-report sense.
- **Monitoring / grading track.** The same FAIL produces a **`blocked` record with a repairable reason** (e.g. `validation-failed`), **not** the terminal grade F. This is the emit-on-failure behaviour of the Grading-Spec: the import gate emits a `blocked` node and continues rather than aborting. The pinned `blocked` reason set and the status-record semantics live in the Grading-Spec (status + reason, no grade).
## 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 (an 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 tools from `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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./10-tests.md](./10-tests.md) — see chapter 10.
- [./21-schema-lifecycle.md](./21-schema-lifecycle.md) — see chapter 21.
- [./22-scoring-protocol.md](./22-scoring-protocol.md) — see chapter 22.
---
# 21. Schema Lifecycle
| Field | Value |
|---|---|
| Status | Informative |
| 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) |
> **Informative.**
Every FlowMCP schema travels a defined path from an initial idea to a production deployment, and this page is the canonical description of that path: the six lifecycle stages, the special rules for static and migrated schemas, and the policy for handling schemas where only some primitives pass. It is the recommended way to develop a schema, and where other lifecycle descriptions in the project (README files, runbooks, skill documentation) disagree, this page is authoritative.
A namespace candidate enters the picture even before the first stage. Before `stage:research`, a candidate lives as a **Prospect** on the Kanban board — identified as interesting but not yet confirmed as reachable or feasible. A Prospect sits upstream of the six stages and is not tracked within them; the act of verifying reachability and feasibility is what promotes it into `stage:research`.
---
## 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 grading deterministic` → min. 1 PASS (see Special Rule) |
| 4 | `stage:validation` | API test passed | `flowmcp schema-check` → 0 errors |
| 5 | `stage:grade` | Validation passed | `namespaceAggregate` grade B or better |
| 6 | `stage:production` | `namespaceAggregate` grade B+ confirmed | Deployed to `providers/` in production catalog |
> **Two tracks.** The six stages above are the **development lifecycle** and live here. Monitoring, issue tracking, and the grade rollup live in the **Grading-Spec**, not on this page. These two are no longer the same single sequential gate — see [Validate-before-grade ordering](#validate-before-grade-ordering-two-tracks) below.
### 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 grading deterministic `. 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 schema-check ` returns 0 errors. All validation rules from `09-validation-rules.md` must be satisfied.
**`stage:grade`** — The schema receives a quality grade. The gate references the **`namespaceAggregate`** grade — the provider-level rollup — not an implied per-schema grade computed inside this lifecycle. A per-schema grade rolls up into the namespace aggregate, and the aggregate is what the gate checks: `namespaceAggregate` grade B or better is required for production deployment. The grade computation — including the rollup and the aggregate — is owned entirely by the Grading-Spec; the lifecycle only consumes the resulting `namespaceAggregate` and delegates the grading model to that standard.
**`stage:production`** — The schema is moved from `tests/new-schemas/` to `providers/{namespace}/` in the production catalog and registered in `registry.json`.
### Validate-before-grade ordering (two tracks)
Up to 4.2 this lifecycle implied a single strict sequence: validation passes, *then* a grade is produced. From 4.3 that sequencing is relaxed for the **monitoring/grading track**:
- A **monitoring/grading record MAY exist in a `blocked` state for a schema that has NOT cleared `stage:validation`** (emit-on-failure — the Grading-Spec import gate emits a `blocked` node with `reason: validation-failed` instead of aborting).
- This `blocked` monitoring record does **NOT** advance the schema toward `stage:production`. The **development gate is unchanged**: validate-clean (`flowmcp schema-check` → 0 errors) is still required before `stage:production`.
In other words: the *development gate* (validate before production) stays; the *monitoring record* (emitted regardless of validation outcome) is the grading track's concern. The two are no longer the same single sequential gate. The emit-on-failure contract and the pinned `blocked` reason set are defined by the Grading-Spec.
---
## 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
A schema brought forward from an older FlowMCP version does not need to repeat the lifecycle from scratch. It enters at **`stage:api-test`** rather than `stage:research`, because the research and creation stages are already complete by virtue of the existing schema; every subsequent stage (`stage:api-test` through `stage:production`) then applies normally. The full migration procedure — what changes between versions and how to update each part of a schema — lives in the [Migration Guide](./08-migration.md).
---
## Partial Schema Policy
> **Two tracks.** This policy is part of the development lifecycle (the six stages above). Monitoring, issue tracking, and the grade rollup live in the Grading-Spec. A removed primitive becomes a **blocker on the one grading-issue per namespace**, not a standalone issue.
### 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 a blocker on the namespace grading-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. **Blocker on the namespace grading-issue** — A removed primitive becomes a `blocked` node / `blockers[]` entry under the **one grading-issue per namespace** (defined by the Grading-Spec monitoring track). No separate per-primitive issue is opened, and there is no parent-schema-issue link — the two tracks are not coupled.
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.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./10-tests.md](./10-tests.md) — see chapter 10.
- [./15-catalog.md](./15-catalog.md) — see chapter 15.
- [./20-validation-strategy.md](./20-validation-strategy.md) — see chapter 20.
- [./22-scoring-protocol.md](./22-scoring-protocol.md) — see chapter 22.
---
# 22. Scoring Protocol
| Field | Value |
|---|---|
| Status | Normative |
| 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) |
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/3.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 3.0.0 — Overview](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/3.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/3.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 3.0.0 — §4.1 Score-to-Grade Thresholds](https://github.com/FlowMCP/flowmcp-spec/blob/main/grading/3.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 grading non-deterministic --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 grading non-deterministic --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 grading non-deterministic --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 | `projects/memo-init/repos/core/skills/grade/grade-score-single/SKILL.md` |
| `grade-score-batch` | Workbench Skill | `projects/memo-init/repos/core/skills/grade/grade-score-batch/SKILL.md` |
| `flowmcp grading non-deterministic` | CLI Producer/Consumer | `flowmcp-cli/src/task/FlowMcpCli.mjs` — `gradingDeterministic()` / `gradingNonDeterministic()` |
| `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
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./04-output-schema.md](./04-output-schema.md) — see chapter 04.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./10-tests.md](./10-tests.md) — see chapter 10.
- [./20-validation-strategy.md](./20-validation-strategy.md) — see chapter 20.
- [./21-schema-lifecycle.md](./21-schema-lifecycle.md) — see chapter 21.
---
# 23. License & Terms of Services
| Field | Value |
|---|---|
| Status | Normative |
| Depends on | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md) |
| Related | [19-mcp-integration.md](./19-mcp-integration.md), [05-security.md](./05-security.md), [21-schema-lifecycle.md](./21-schema-lifecycle.md) |
FlowMCP sits between its own code, the third-party APIs a schema calls, and the data those APIs return — and each of those carries its own legal terms. The purpose of this page is to draw a clear line around what FlowMCP takes responsibility for and what it deliberately leaves to the user: FlowMCP records and links the relevant Terms of Services and data licenses, but it never interprets or certifies them. The page describes that three-layer model, the optional schema fields that carry the links, and where responsibility for compliance ultimately rests.
## Three-Layer License Model
FlowMCP operates in a three-layer license model. **Schema authors and users MUST understand all three layers.**
| Layer | What | Who Decides | What FlowMCP Does |
|-------|------|-------------|---------------------|
| **1. FlowMCP Schema Code** | The schema definition (`.mjs` files), core library, CLI | FlowMCP (we) | MIT-licensed |
| **2. API Provider ToS** | Terms of Services for using the third-party API | API Provider | **Link only — no classification** |
| **3. Data License** | License of returned data (e.g. CC-BY, Public Domain) | Data publisher | **Link only — no classification** |
## What FlowMCP Does NOT Do
We **do not** classify or interpret Terms of Services. This is intentional:
- ToS are living documents — they change without notice
- ToS interpretation requires legal expertise
- We have no jurisdiction to make compliance statements
- Users are solely responsible for compliance
## Schema-Level Optional Fields
Schemas MAY include optional ToS-related fields in the `main` block:
```javascript
export const schema = {
main: {
namespace: 'coingecko',
version: '4.0.0',
// ... required fields ...
// Optional ToS Fields (informational only)
docs: ['https://docs.coingecko.com'],
termsOfService: 'https://www.coingecko.com/en/terms',
termsOfServiceCheckedAt: '2026-05-18',
termsOfServiceLanguage: 'en',
dataLicense: null,
dataLicenseName: null
},
tools: { ... }
}
```
### Field Semantics
| Field | Set when | Null when |
|-------|----------|-----------|
| `termsOfService` | ToS URL is known and verified | No ToS found, or not yet researched |
| `termsOfServiceCheckedAt` | URL was verified to be live and applicable to the API | URL not yet checked |
| `termsOfServiceLanguage` | Primary language of ToS document detected | URL not yet checked |
| `dataLicense` | Provider explicitly publishes a separate data license URL | No explicit data license |
| `dataLicenseName` | Common name of the data license (e.g. CC-BY) | No explicit data license |
## Implementation in flowmcp-cli
Schema authors are encouraged to use the `tos-research` skill to populate these fields semi-automatically.
The CLI exposes an opt-in disclaimer flag:
```bash
# In flowmcp.config.json
{
"licenseDisclaimer": true
}
# Then:
flowmcp call coingecko_market_chart
# [License Info] Provider: coingecko
# [License Info] ToS: https://www.coingecko.com/en/terms (last checked: 2026-05-18)
# [License Info] We do not interpret ToS. Please review before commercial use.
```
Default: `licenseDisclaimer: false` (off).
## User Responsibility
Users are solely responsible for:
- Reviewing each API provider's ToS before use
- Complying with rate limits, attribution, and re-distribution rules
- Determining commercial vs non-commercial fit
- Adhering to LLM-training restrictions where present
FlowMCP makes **no warranty** about ToS compliance, data licensing, or fitness for any purpose.
## Update Strategy
ToS change. FlowMCP recommends:
- **6-month re-check cadence** for all schemas with `termsOfService`
- Reactive update when a known provider changes its ToS
- Audit script `audit-tos-freshness.mjs` flags schemas with stale `termsOfServiceCheckedAt`
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./05-security.md](./05-security.md) — see chapter 05.
- [./19-mcp-integration.md](./19-mcp-integration.md) — see chapter 19.
- [./21-schema-lifecycle.md](./21-schema-lifecycle.md) — see chapter 21.
---
# 24. Philosophy
| Field | Value |
|---|---|
| Status | Informative |
| Depends on | [00-overview.md](./00-overview.md) |
| Related | [00-overview.md](./00-overview.md), [01-schema-format.md](./01-schema-format.md), [05-security.md](./05-security.md), [09-validation-rules.md](./09-validation-rules.md), [19-mcp-integration.md](./19-mcp-integration.md) |
> **Informative.**
> **Informative.** This chapter describes the worldview behind FlowMCP — why a schema exists, what a graded tool is worth, and how an agent is meant to find and call it. It is written in prose; the normative rules that operationalize these convictions live in the chapters it points to.
FlowMCP rests on a small set of convictions about how an autonomous agent and an unfamiliar API should meet: that a schema is a contract rather than a cage, that a tool is graded before it is trusted, that nothing fails silently, and that everything in the configured folders is discoverable without a hidden ceremony. None of these convictions is decorative — each one exists to prevent a concrete failure mode, and each later chapter turns the conviction into a rule. This chapter records the worldview so that it stays explicit as the specification grows.
## The Guardrail (Highway) Analogy
A schema is a guardrail, not a cage. FlowMCP does not try to drive the agent's car for it; it lines the highway so that the car can move fast on a road it has never seen. The whole point of the project is to let an agent drive an unfamiliar API at speed — without reading the provider's documentation, without guessing the authentication flow, without learning the parameter names by trial and error — and still not go off the cliff.
A guardrail is a constraint that is cheap to set up and expensive to omit. The schema names the parameters, fixes their types, encodes the route, and labels the credentials a call needs. Inside that frame the agent is free; outside it, it drifts into invented endpoints, malformed requests, and silent wrong answers. The schema is never there to slow the agent down. It is there so that going fast is the same thing as staying on the road.
## Schema as Contract — the Wall Socket
Push the analogy one step further and the schema becomes a standardized socket. The agent plugs in without knowing the wiring behind the wall. It does not need to know whether the data comes from REST or GraphQL, whether the provider paginates by cursor or by offset, whether the timestamps are Unix seconds or ISO strings — the socket presents one shape, and the agent calls it the same way it calls every other tool in the catalog.
This is why interoperability is the foreground concern, not an afterthought. A socket is only worth standardizing if a great many things can plug into it. FlowMCP's value grows with the number of other tools, agents, and MCP clients it can connect — through the CLI and through the MCP/A2A server alike (see [19-mcp-integration.md](./19-mcp-integration.md)). A schema that only one caller can use is a custom wire; a schema that any conformant agent can call is a socket. The specification optimizes for the socket.
## Grade Before Trust
A schema that parses is not yet a tool worth trusting. Before a tool earns a place in the trusted catalog it is graded — first by a deterministic data pretest, then by non-deterministic scoring. The deterministic pretest is blunt and unforgiving on purpose: it actually calls the live endpoint and checks two things together, that the response is HTTP 200 **and** that the data is non-empty.
The second clause matters because a tool can answer HTTP 200 and still hand back nothing — an empty array, a null payload, a polite shell with no content. A pretest that checked only the status code would wave such a tool through as healthy, and an agent would then build on a contract that returns air. Treating "200 but empty" as a failure, caught at grading time, means a tool in the trusted catalog is one that has been observed to return real data, not merely a successful status line. The grading workflow and its scoring areas are described in [09-validation-rules.md](./09-validation-rules.md).
## No Silent Defaults — Fail Loudly
FlowMCP never invents a value to paper over one that is missing. When a required parameter is absent, a credential is unset, or a configuration field is ambiguous, the system fails loudly rather than quietly substituting a plausible default and continuing. A silent default is a guess wearing the costume of a fact, and a guess that nobody was told about is the hardest kind of failure to find.
The rule is strictest where it is most dangerous. Anything that would write or assume on the user's behalf — a credential, an environment file, a configuration value — checks first and announces what it is about to do; it never overwrites an existing value or invents a missing one in silence. Overwriting a real credential with a default, for instance, is the kind of quiet action that destroys data while reporting success. Loud failure is recoverable; silent success on wrong data is not. The boundaries that enforce this around credentials and configuration are set out in [05-security.md](./05-security.md).
## Search, Then Call
There is no hidden activation step. Every tool in the configured schema folders is immediately discoverable through search and list, and immediately callable through call. An agent does not register a tool, enable it, install it into a session, or perform any ceremony between finding a tool and using it — it searches the catalog, reads the required parameters and example from the search result, and calls. The interaction model is exactly two moves:
| Step | Move | What it returns |
|------|------|-----------------|
| Discover | `search` / `list` | The matching tools, with their required parameters and a callable example |
| Invoke | `call` | The live result of the tool, called directly |
The simplification is deliberate: there is no intermediate state in which a tool is found but not yet callable. A gap between discovery and use — a tool that is visible but needs a separate activation step before it can run — is a confusing state for a person and an outright trap for an autonomous agent. The catalog an agent can see is exactly the catalog it can use.
## Disabled, Not Broken
The "search, then call" promise raises an obvious question: what about a tool whose credentials are missing? FlowMCP answers it by degrading gracefully. A tool that needs a key the environment does not have is not hidden and is not broken — it appears in the catalog clearly labeled, as `[disabled: missing KEY]`, naming the exact credential it is waiting for.
This matters because the two failure modes it avoids are both worse. A hidden tool leaves the agent unable to discover a capability that exists; a tool that looks callable but throws an opaque error at call time leaves the agent debugging the framework instead of its task. A disabled-but-visible tool tells the truth: the capability exists, here is precisely what it needs to come online, and nothing about it is mysterious. Key-gating is honesty about credentials, not concealment of capability.
## The Catalog Is Indexed, Not Re-Derived
At scale, you read the index; you do not rebuild the world. FlowMCP can hold a large catalog of schemas, and the cost of treating that catalog naively grows with its size. A namespace index already records, ahead of time, which schema owns which tool. The correct move when resolving a single tool is to consult that index and load the one schema it points to.
The principle is simple: precomputed structure exists to be read, not re-derived on every request. Resolving one tool by loading the entire catalog produces the right answer at the wrong cost — and an operation whose cost scales with the size of the whole catalog when it should scale with a single entry is a defect, however correct its output happens to be. Reading the index keeps a single-tool call cheap no matter how large the catalog grows.
## A Home for Project Thinking
These convictions are easy to hold while a project is small and easy to lose while it grows. A guardrail that nobody can name becomes bureaucracy; a rule whose purpose has been forgotten becomes a rule people quietly route around. This chapter exists so that does not happen — it is the place where FlowMCP's guiding ideas are written down, each one paired with the failure mode it prevents, so the worldview stays explicit even as the normative chapters multiply. When a new rule is proposed, it can be measured against the philosophy recorded here. The specification's letter lives in the chapters this page points to. Its spirit lives here.
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./01-schema-format.md](./01-schema-format.md) — see chapter 01.
- [./05-security.md](./05-security.md) — see chapter 05.
- [./09-validation-rules.md](./09-validation-rules.md) — see chapter 09.
- [./19-mcp-integration.md](./19-mcp-integration.md) — see chapter 19.
---
# 26. Agent CLI Usage Contract
| Field | Value |
|---|---|
| Status | Normative |
| Depends on | [00-overview.md](./00-overview.md), [16-id-schema.md](./16-id-schema.md) |
| Related | [05-security.md](./05-security.md), [13-resources.md](./13-resources.md), [15-catalog.md](./15-catalog.md), [24-philosophy.md](./24-philosophy.md) |
This chapter is the normative contract for how an AI client or an agent uses the FlowMCP CLI to discover and invoke tools. Where [24-philosophy.md](./24-philosophy.md) explains *why* the interaction model is shaped the way it is (search-then-call, disabled-not-broken, fail loudly), this chapter states the binding rules an agent MUST follow. It is the specification home for the FlowMCP usage and SOP skills.
The contract has one governing idea: **the catalog an agent can see is exactly the catalog it can use, and every callable name is the same across `search`, `call`, and `serve`.** There is no activation step, no hidden registry, and no per-surface name drift.
---
## The Two-Move Workflow
An agent interacts with the trusted catalog through exactly two moves. There is no third, intermediate move.
| Step | Move | Command | Returns |
|------|------|---------|---------|
| Discover | `search` / `list` | `flowmcp search ` · `flowmcp list` | Matching tools, each with its `requiredParams` and a callable `example` |
| Invoke | `call` | `flowmcp call '{json}'` | The live result of the tool, called directly |
Rules:
1. An agent MUST discover a tool through `search` or `list` before calling it, and MUST read the `requiredParams` and `example` from the discovery result to build the call — it MUST NOT construct tool names or parameter shapes from memory.
2. An agent MUST NOT perform any activation, registration, installation, or enable step between discovery and invocation. No such step exists — a tool that appears in `search`/`list` is immediately callable via `call`.
3. Every tool in the folders configured in `schemaFolders[]` (see [15-catalog.md](./15-catalog.md)) is part of the trusted catalog and is discoverable and callable without further ceremony.
4. `list` MAY be used instead of `search` to enumerate the whole catalog; `flowmcp call list-tools` enumerates the callable wire names (tools **and** resource queries, see [Call-ID Grammar](#call-id-grammar)).
This two-move model is normative precisely because a gap between discovery and use — a tool that is visible but needs a separate step before it runs — is a trap for an autonomous agent.
---
## Private Ad-Hoc Calls
A schema that is **not** registered in `schemaFolders[]` is never part of the trusted catalog: it does not appear in `search`, `list`, or an MCP `serve` session (invisibility is structural, see [05-security.md](./05-security.md)). Such a schema is invoked by its exact file path through a distinct move:
```
flowmcp private call '{json}'
```
Rules:
1. `private call` takes an explicit filesystem path to the schema file, a tool (or resource-query) name, and a JSON argument object.
2. The runtime MUST run the security scan **before** any `import()` of the private schema file. A file matching a forbidden pattern is rejected and never executed. Only a clean file is imported and run. See [05-security.md](./05-security.md) (the "scan on the private path" rule).
3. A private schema MUST NOT be written into `schemaFolders[]` and MUST NOT be merged into the shared catalog. `private call` is the single supported way to invoke it — there is no registered "private folder".
4. `private call` resolves resource queries the same way `call` does: a resource query is addressed by its wire name `${queryName}_${namespace}` (see [Call-ID Grammar](#call-id-grammar)).
---
## Parameter Types
The JSON argument object passed to `call` / `private call` is built from the parameter declarations carried in the discovery result. An agent MUST supply parameters that conform to the declared type.
| Declared type | Meaning | Example declaration |
|---------------|---------|---------------------|
| `string` | Text value | `"address": { "type": "string", "required": true }` |
| `number` | Numeric value | `"start": { "type": "number", "required": false }` |
| `enum` | One value from a fixed list | `"interval": { "type": "enum", "values": ["h1", "d1"] }` |
| `array` | List of values | `"ids": { "type": "array", "required": true }` |
Modifiers:
- `"required": true` — the parameter MUST be present in the call. A missing required parameter MUST fail loudly (no silent default, see [24-philosophy.md](./24-philosophy.md)); it MUST NOT be substituted with an invented value.
- `"required": false` — the parameter MAY be omitted.
- `"default": ` — when the parameter is omitted, the runtime applies the declared default. Defaults are always declared in the schema; the runtime MUST NOT invent an undeclared default.
For an `enum` parameter, the supplied value MUST be one of the declared `values`. The full parameter model is specified in [02-parameters.md](./02-parameters.md).
---
## Disabled Tools — Missing Keys
A tool whose required credential (a `requiredServerParams` key, see [15-catalog.md](./15-catalog.md)) is not present in the environment is **disabled, not hidden and not removed**.
Rules:
1. A key-gated tool with a missing key MUST still appear in `search` / `list`, clearly labeled `[disabled: missing KEY]`, naming the exact credential it requires.
2. Calling a disabled tool MUST return a clear, actionable message that names the missing key and states that the other tools remain callable — it MUST NOT throw an opaque framework error.
3. A missing key MUST NOT hide the capability and MUST NOT silently return empty data disguised as success.
This "disabled, not broken" behavior is honesty about credentials: the capability exists, the message names precisely what it needs to come online, and every other tool stays usable.
---
## Call-ID Grammar
Every reference an agent uses resolves through the ID schema in [16-id-schema.md](./16-id-schema.md). The forms an agent encounters are:
| Form | Slashes | Meaning | Example |
|------|---------|---------|---------|
| `namespace` | 0 | A whole namespace / provider | `weather` |
| `namespace/schema-name` | 1 | A schema file (container) | `directory-io/records` |
| `namespace/tool/name` | 2 | A tool primitive | `weather/tool/getForecast` |
| `namespace/resource/name` | 2 | A resource primitive | `weather/resource/supportedCities` |
| `namespace/prompt/name` | 2 | A prompt primitive | `weather/prompt/forecast-summary` |
| `namespace/selection/name` | 2 | A selection | `record-research/selection/lookup` |
Any form MAY carry an optional source coordinate prefix `:` to pin one folder when the CLI aggregates several `schemaFolders[]` that expose the same namespace — e.g. `folder-b:weather/tool/getForecast` (see [16-id-schema.md](./16-id-schema.md#source-coordinate)). Short form (a bare name without namespace/type) is not supported.
### MCP Wire Names
The MCP protocol forbids `/` in tool names, so the CLI maps a Spec-ID to an internal wire name with the `name_namespace` rule. Two cases matter to an agent that reads a served tool list:
| Primitive | Wire name | Example |
|-----------|-----------|---------|
| Tool | `${routeName}_${namespace}` | `getForecast_weather` |
| Resource query | `${queryName}_${namespace}` | `searchMail_mailarchive` |
A resource does not map to a single tool: **each query it declares is exposed as its own callable wire name** `${queryName}_${namespace}` — the query name, not the resource name, is the wire name. The auto-injected `runSql` and `describeTables` follow the same rule (`runSql_${namespace}`, `describeTables_${namespace}`). This wire name MUST be identical across `search` (advertised), `call` (resolved), and `serve` (registered) — a single canonical convention with no per-surface drift. See [13-resources.md](./13-resources.md#mcp-exposure-of-resource-queries).
---
## Forbidden Patterns
An agent MUST NOT:
1. Call `flowmcp schemas` directly — the output is unbounded.
2. Pipe CLI output through `node -e`, `sh -c`, or similar shells to post-process it.
3. Guess flags. Flags that are not declared in a command's help do not exist and MUST NOT be invented.
4. Construct tool wire names, Spec-IDs, or schema paths by hand — these come from the `search` / `list` result.
5. Use an activation, registration, or install step (`add`, `group`, `import`, `update`) — none exist; every catalog tool is immediately callable.
6. Treat an HTTP 4xx/5xx or an empty result as success — a call that does not return real data is a failure, not a pass.
Structural validation and grading are separate developer-track commands (`flowmcp schema-check`, `flowmcp grading …`, `flowmcp doctor`, `flowmcp --version`) and are not part of the agent invocation path.
---
## Related
- [./00-overview.md](./00-overview.md) — see chapter 00.
- [./05-security.md](./05-security.md) — see chapter 05.
- [./13-resources.md](./13-resources.md) — see chapter 13.
- [./15-catalog.md](./15-catalog.md) — see chapter 15.
- [./16-id-schema.md](./16-id-schema.md) — see chapter 16.
- [./24-philosophy.md](./24-philosophy.md) — see chapter 24.
---