Skip to content

Resources

Normative language (MUST/SHOULD/MAY) follows the conventions defined in Conformance Language.

Resources provide local data access via SQLite databases and Markdown documents. They map to the MCP server.resource primitive and are defined in main.resources alongside main.tools. This document defines the resource format, two SQLite modes (in-memory and file-based), the origin-based storage system, Markdown resources, query definitions, parameter binding, handler integration, and validation rules.


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:

sqlite

markdown

in-memory

file-based

Schema Definition

Resource Declaration

source?

mode?

Markdown File

better-sqlite3

readonly: true

better-sqlite3

WAL mode

Query Results

Text Content

MCP Resource Response

The diagram shows the data flow from the schema’s resource declaration through either SQLite (two modes) or Markdown into MCP resource responses.

Use CaseMechanismExample
Live API dataToolCurrent token price from CoinGecko
Static reference dataResource (SQLite in-memory)Token metadata by symbol or contract address
Agent-generated dataResource (SQLite file-based)PageSpeed analysis results, collected metrics
API documentationResource (Markdown)DuneSQL syntax reference, API field descriptions

FlowMCP supports two resource types, identified by the source field:

main.resources

source: 'sqlite'

source: 'markdown'

source: 'http'

mode: 'in-memory'

mode: 'file-based'

Buffer copy in RAM

SELECT only

File unchanged

Direct file operations

All SQL statements

Changes persistent

Markdown file

Text loaded as string

Parameter-based access

Remote file via HTTPS

Cached locally

Same query interface as sqlite

SourceModeDescription
sqlitein-memoryDB opened with readonly: true. SELECT only. File remains unchanged.
sqlitefile-basedDB opened with WAL mode. All SQL statements. Changes persistent on disk.
markdownMarkdown file loaded as string. Parameter-based access (section, lines, search).
httpRemote file fetched via HTTPS and cached locally. Same SQLite query interface as sqlite source.

FieldTypeRequiredDescription
source'sqlite'YesResource type. Must be 'sqlite' for SQLite resources.
mode'in-memory' or 'file-based'YesAccess mode. Determines readonly vs. writable.
origin'global', 'project', or 'inline'YesStorage location. See Origin System.
namestringYesFilename with extension. Must end with .db. Convention: {namespace}-{descriptive-name}.db.
descriptionstringYesWhat this resource provides. Appears in resource discovery.
queriesobjectYesQuery 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.

The database is opened with better-sqlite3 using readonly: true. Only SELECT statements are allowed. The file on disk is never modified.

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
}
}
AspectValue
Runtimebetter-sqlite3 with readonly: true
Allowed statementsSELECT only
File changesNever — readonly flag on DB level
Allowed originsglobal (recommended) or project
Use caseReference data, lookups, open data
getSchemaOPTIONAL — prefer auto-injected describeTables
runSqlAuto-injected by runtime (SELECT only)
describeTablesAuto-injected by runtime (AI-friendly schema discovery)

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.

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
}
}
AspectValue
Runtimebetter-sqlite3 with WAL mode
Allowed statementsAll — SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, DROP
File changesYes — persistent on disk
Only allowed originproject
Use caseAnalysis results, agent memory, data collection
getSchemaOPTIONAL — define only if CLI MUST bootstrap DB via CREATE TABLE
runSqlAuto-injected by runtime (all statements)
describeTablesAuto-injected by runtime (AI-friendly schema discovery)
DB does not existCLI creates DB based on getSchema if defined, otherwise reports missing-DB warning
BackupAutomatic .bak copy before first write per session
Concurrent writesSupported via WAL mode
Aspectin-memoryfile-based
Runtime flagreadonly: trueWAL mode
SELECTYesYes
INSERT/UPDATE/DELETENoYes
CREATE/DROP TABLENoYes
Changes persistentNoYes
Allowed originsglobal, projectOnly project
DB MUST existYes (warning if missing)No (CLI creates via getSchema)
getSchemaOPTIONAL (rarely needed)OPTIONAL (only for CLI DB bootstrap)
runSqlAuto-injected (SELECT only)Auto-injected (all statements)
describeTablesAuto-injected (structured discovery)Auto-injected (structured discovery)
BackupNot needed.bak before first write
Concurrent accessYes (readonly)Yes (WAL mode)

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

The source: 'sqlite-gtfs' resource type is a top-level sub-type of sqlite (Design C — top-level with inheritance). It denotes a SQLite database that has been produced by a FlowMCP add-on (such as gtfs-sqlite-toolkit) and carries a verified quality seal in its meta table. The seal guarantees that the database structure, validation status, and discoverable capabilities are conformant with a known add-on contract — which allows FlowMCP-CLI to automatically inject standard tools without the schema author writing any SQL by hand.

sqlite-gtfs is not a fallback for sqlite. A database without a valid seal is rejected (see Validation RulesRES032). Users who want full manual control over a SQLite database continue to use source: 'sqlite'.

FieldTypeRequiredDescription
source'sqlite-gtfs'YesResource type. Must be 'sqlite-gtfs'.
mode'file-based'YesAccess mode. Only 'file-based' is allowed. 'in-memory' is rejected (RES030) because the seal verification depends on the persisted meta table.
pathstringYesAbsolute path to the converted SQLite database. May contain the path variable ${FLOWMCP_RESOURCES} (see Path Variable Support).
addonstringYesName of the FlowMCP add-on responsible for producing and interpreting the database (e.g. gtfs-sqlite-toolkit). The add-on is the authority over which standard tools are auto-injected.
FieldTypeRequiredDescription
addonVersionstringNoSemVer 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.
addonSourcestringNoAdd-on source reference. Default: github:FlowMCP/<addon>. NPM is not supported — only github: paths are valid. FlowMCP is not published to the NPM registry.

Every sqlite-gtfs resource MUST point to a database whose meta table contains the entry qualitySeal === 'sqlite-gtfs'. This is a hard requirement enforced by the validator (RES032). A database that does not carry the seal — whether because it was produced by an unverified converter, manually edited, or partially converted — is rejected at schema activation time.

The seal is written by the converter (e.g. MetaWriter in gtfs-sqlite-toolkit) only when the conversion completes successfully and all pre-validation checks pass strictly. A failed or partial conversion produces qualitySeal: null and the database is not usable as a sqlite-gtfs resource.

The meta table of a sealed database MUST contain the following ten keys. They support reproducibility, capability detection, and audit logging:

KeyPurpose
qualitySealAdd-on identifier (here: 'sqlite-gtfs'). The seal value.
specRevisionFlowMCP spec revision the converter targeted (e.g. '2026-04-27'). Used for spec-drift warnings (RES034).
specUrlURL of the spec revision document.
converterVersionSemVer of the add-on that produced the database (e.g. gtfs-sqlite-toolkit@0.1.0).
capabilitiesJSON-encoded map of capability booleans (e.g. basicLookup, routing, shapesVisualization, flexService). Drives auto-injection.
validationReportJSON-encoded summary of pre-validation results (counts of errors / warnings / info).
buildDateISO-8601 timestamp of the conversion run.
rowCountsJSON-encoded per-table row counts.
sourceUrlURL of the upstream data source the converter consumed (provider feed URL).
sourceHashSHA-256 hash of the source archive — supports content-addressed deduplication and replay.

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.

VariableResolutionDefault 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.resourcesFLOWMCP_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.

A minimal POC schema demonstrating sqlite-gtfs:

export const schema = {
namespace: 'gtfsde',
name: 'gtfsde-transit-v2',
version: '4.2.0',
main: {
resources: [
{
source: 'sqlite-gtfs', // Design C
mode: 'file-based',
path: '${FLOWMCP_RESOURCES}/gtfs-de.db', // User-configurable
addon: 'gtfs-sqlite-toolkit', // Authority over default methods
addonVersion: '>=0.1.0', // optional
addonSource: 'github:FlowMCP/gtfs-sqlite-toolkit' // NPM is not supported
}
],
tools: [
// OPTIONAL: schema-specific tools only.
// Standard GTFS tools are auto-injected by the add-on.
]
}
}

When the FlowMCP-CLI activates this schema via flowmcp add, it (1) resolves the path variable, (2) verifies the seal, (3) reads capabilities from the meta table, (4) loads the add-on declared in addon, and (5) injects the standard toolset that the add-on derives from the active capabilities.

The sqlite-gtfs resource type delegates the definition of standard tools to the add-on. The spec describes the resource contract; the add-on (e.g. gtfs-sqlite-toolkit) owns the catalog of default methods — searchStops, searchRoutes, getDepartures, getShapeForRoute, getFlexBookingRules, and so on — and decides which ones become available based on the capabilities map.

This split keeps the spec stable while letting add-ons evolve independently. New add-ons (for example a future sqlite-netex or sqlite-trias) follow the same Design C pattern: top-level source value, seal requirement, add-on reference, capability-driven auto-injection.

Provider GTFS data MUST NOT be committed to the spec repository, the add-on repository, or any other public FlowMCP repository. GTFS feeds carry third-party licensing terms that typically prohibit redistribution. Users provide their own GTFS data locally — either by downloading from the provider directly or by using the add-on’s converter against a local source archive. The converted database lives under the user-controlled ${FLOWMCP_RESOURCES} directory and is never published.

The synthetic Mini-GTFS fixture shipped with gtfs-sqlite-toolkit (under a CC0 license) is the only GTFS-shaped data permitted in any public FlowMCP repository. It exists exclusively for testing and CI.


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.

origin: 'global'

In user home

~/.{base}/resources/

tranco-ranking.db

origin: 'project'

In workspace

.{base}/resources/

tranco-ranking.db

origin: 'inline'

In schema directory

providers/{namespace}/resources/

tranco-ranking.db

OriginResolved PathWho CreatesDescription
inline{schema-dir}/resources/{name}Schema authorShips with the schema. Committed to repo.
project.{base}/resources/{name}User or CLIProject-local. Not committed.
global~/.{base}/resources/{name}User (download)System-level. Shared across projects.
AspectValue
Defaultflowmcp
ConfigurableYes, via CLI flag
Affectsproject and global origins
Example default~/.flowmcp/resources/, .flowmcp/resources/
Example custom~/.myagent/resources/, .myagent/resources/
OriginSQLite in-memorySQLite file-basedMarkdown
inlineNot recommended (data privacy)Not allowedYes (recommended)
projectYesYes (only allowed origin)Yes
globalYes (recommended)Not allowedYes

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.

The name field contains the complete filename including extension:

// SQLite
name: 'tranco-ranking.db'
name: 'ofacsdn-sanctions.db'
name: 'pagespeed-results.db'
// Markdown
name: 'duneanalytics-sql-reference.md'
PartRuleExample
PrefixNamespace of the schematranco
SeparatorHyphen-
DescriptionKebab-caseranking
Extension.db or .md.db
Completetranco-ranking.db

The folder is always named resources/ (not data/).


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.

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

Aspectin-memoryfile-based
SELECTYesYes
INSERTNoYes
UPDATENoYes
DELETENoYes
CREATE TABLENoYes
DROP TABLENoYes
LIMIT default100 (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

Section titled “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:

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:

[
{ 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' }
]
Aspectin-memoryfile-based
Auto-injectedYesYes
Underlying SQLsqlite_master JOIN pragma_table_infoidentical
Return formatRows (table_name, column, type)identical
Use caseAI-friendly schema discoveryAI-friendly schema discovery
Read-only safetyYes (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:

AspectdescribeTables (auto)getSchema (optional, author-defined)
FormatStructured rowsCREATE TABLE text
Constraints (PRIMARY KEY, NOT NULL, FK)Not includedIncluded
AI-friendlinessHigh (immediate consumption)Medium (regex parsing required)
Use caseDefault AI discoveryAuthor 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.

TypeCount
Auto-injected2 (runSql + describeTables)
Schema-defined (getSchema is optional)Max 7
TotalMax 9
Queryin-memoryfile-based
getSchemaOPTIONAL (rarely needed)OPTIONAL (only for CLI DB bootstrap)
runSqlAuto-injected (SELECT only)Auto-injected (all statements)
describeTablesAuto-injected (structured discovery)Auto-injected (structured discovery)
Domain queriesSchema-definedSchema-defined

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.

resources: {
sqlReference: {
source: 'markdown',
origin: 'inline',
name: 'duneanalytics-sql-reference.md',
description: 'Complete DuneSQL syntax reference — functions, datatypes, table catalog'
}
}
FieldTypeRequiredDescription
source'markdown'YesResource type. Must be 'markdown'.
origin'global', 'project', or 'inline'YesStorage location. inline recommended.
namestringYesFilename with .md extension.
descriptionstringYesWhat this document contains.

All fields are required. There is no mode field (Markdown is always read-only) and no queries field.

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.

Small (< 100 KB)

Large (> 100 KB)

Markdown Resource

Size?

Load full document

(default behavior)

Use access parameters

section: '## Functions'

lines: '11-33'

search: 'CREATE TABLE'

ParameterTypeDescription
(none)Full document as string (default)
sectionstringMarkdown heading name (e.g. '## Functions')
linesstringLine range as 'from-to' (e.g. '11-33')
searchstringText search, returns matches with context lines

These access parameters are auto-injected by the runtime. Schema authors do not define them.

AspectValue
File formatOnly .md (Markdown)
Max size~2 MB (recommendation)
BehaviorFile is read, content returned as string
Recommended origininline (committed with schema)
No queries fieldText is loaded directly
Referenceable{{resource:namespace/name}} from prompts and skills
  • 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?)
{schema-dir}/resources/duneanalytics-sql-reference.md

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.

ScenarioExample
Large reference databases too big for the repoOFAC SDN sanctions list (SQLite, ~40 MB)
Frequently updated datasetsDaily-updated government open data
Externally maintained datasetsThird-party data providers
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%' }
]
}
}
}
}
FieldTypeRequiredDescription
source'http'YesResource type. Must be 'http'.
urlstringYesHTTPS URL of the remote file. Must use HTTPS (no HTTP).
cacheTtlnumberYesCache duration in seconds. The downloaded file is reused until TTL expires.
descriptionstringYesWhat this resource provides. Appears in resource discovery.
queriesobjectYesQuery definitions. Same structure as SQLite in-memory queries.
AspectBehavior
Cache location.flowmcp/cache/resources/{hash}.db
Cache keySHA-256 of the URL
TTL checkOn every schema load
Expired cacheRe-fetch from URL
Fetch failureUse stale cache if available, error if no cache
SQL accessbetter-sqlite3 with readonly: true (same as in-memory)

RES024: source: 'http' requires a url field. The URL MUST use HTTPS (https://). HTTP URLs are rejected to prevent insecure data transfer.


LayerWhat Happens
DB opened with readonly: truebetter-sqlite3 enforces read-only at DB level
Only SELECT in runSqlRuntime enforces SELECT-only
File unchangedNo 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.

LayerWhat Happens
Only project-levelNo access to global or inline DBs
All statements allowedUser has consciously chosen file-based
CLI asks on creationUser MUST confirm
Backup before first write.bak copy as safety net
WAL modeConcurrent access safely possible
Changes persistentIntended — 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.

Protection

in-memory: DB-level readonly

file-based: project-only + backup

file-based

better-sqlite3

WAL mode

All statements

Changes persistent

in-memory

better-sqlite3

readonly: true

SELECT only

File unchanged


For file-based databases at project level, the DB MUST exist before the agent can write. But who creates it?

Yes

No

Yes

User: Yes

User: No

No

Agent wants to write to DB

DB exists?

Write

Error returned to agent

Agent calls CLI

Resource defined?

source: sqlite

mode: file-based

CLI asks user:

Create database {name}?

Create DB + tables

derived from getSchema

Abort

Error: No writable resource defined

RuleValue
CLI creates DBOnly when source: 'sqlite' + mode: 'file-based' + DB missing
User confirmationRequired — CLI always asks
PathAlways project: .{base}/resources/{name}
getSchemaOPTIONAL 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)

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
AspectValue
WhenBefore the first write statement per session
WhereSame directory, .bak extension
OverwriteYes — always the latest backup
DeletableYes — .bak file can be deleted anytime
RecoveryRename .bak to .db

better-sqlite3 is the unified runtime for all SQLite resources, replacing sql.js:

Aspectsql.js (v3.0.0)better-sqlite3 (v3.1.0)
In-memoryreadFileSync -> Buffer -> new SQL.Database(buffer)new Database(path, { readonly: true })
File-basedNot possible (RAM only)new Database(path) + pragma journal_mode = WAL
ReadonlyNo native supportreadonly: true flag
WAL modeNot supportedNatively supported
Concurrent accessNoYes (with WAL mode)
PerformanceSlower (WebAssembly)Faster (native C binding)
InstallationPure JS (no build needed)Requires native build (node-gyp)
  • No feature split — no “this only works in this mode because different library” problems
  • WAL mode — concurrent writes only possible with better-sqlite3
  • Real readonlyreadonly: true at DB level instead of pattern matching on SQL strings
  • Performance — native C binding instead of WebAssembly

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).

// 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 )

Each query defines a SQL prepared statement, its parameters, output schema, and tests.

FieldTypeRequiredDescription
sqlstringYesSQL prepared statement with ? placeholders for parameter binding.
descriptionstringYesWhat this query does. Appears in the MCP resource description.
parametersarrayYesParameter definitions using the position + z system. Can be empty [] for no-parameter queries.
outputobjectYesOutput schema declaring expected result shape. Uses the same format as tool output schemas (see 04-output-schema).
testsarrayYesExecutable test cases. At least 1 per query.

A SQL prepared statement using ? as the placeholder for bound parameters. Parameters are bound in the order they appear in the parameters array.

// 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.

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.

  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.

Resource parameters use the same position + z system as tool parameters (see 02-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.

{
position: { key: 'symbol', value: '{{USER_PARAM}}' },
z: { primitive: 'string()', options: [ 'min(1)' ] }
}
FieldTypeRequiredDescription
position.keystringYesParameter name exposed to the AI client.
position.valuestringYesMust be '{{USER_PARAM}}' for user-provided values, or a fixed string.
z.primitivestringYesZod-based type declaration. Same primitives as tool parameters.
z.optionsstring[]YesValidation constraints. Same options as tool parameters.

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.

// 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.

Resource parameters support scalar Zod primitives only:

PrimitiveDescriptionExample
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.


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:

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

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)
HandlerWhenInputMust Return
postRequestAfter 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.

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

Resource queries use the same test format as tool tests (see 10-tests). Each test provides parameter values for a query execution against the database.

tests: [
{ _description: 'Well-known stablecoin (USDC)', symbol: 'USDC' },
{ _description: 'Major L1 token (ETH)', symbol: 'ETH' },
{ _description: 'Case-insensitive match (lowercase)', symbol: 'wbtc' }
]
FieldTypeRequiredDescription
_descriptionstringYesWhat this test demonstrates
{paramKey}matches parameter typeYes (per required param)Value for each {{USER_PARAM}} parameter
ScenarioMinimumRecommended
Query with no parameters11
Query with 1-2 parameters12-3
Query with enum parameters12-3 (different enum values)

Minimum: 1 test per query is required. A query without tests is a validation error.


sqlite

markdown

in-memory

file-based

Yes

No

Yes

No

Yes

No

Schema Load

source?

mode?

Read file as string

Open DB with readonly: true

DB exists?

Open DB with WAL mode

CLI creates DB via getSchema

Receive query request

DYNAMIC_SQL?

Runtime security check

Execute prepared statement

Execute user SQL with LIMIT

Return result rows

Handler exists?

postRequest transforms rows

Return rows directly

Wrap in response envelope

Return text content


A schema can define both tools and resources in the same main export:

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: { /* ... */ }
}
}
}
}
  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.

ConstraintValueRationale
Max resources per schema2Keeps schemas focused. Resources SHOULD be tightly scoped to one data domain.
Max schema-defined queries per resource7getSchema 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.dbStandardized file extension for SQLite databases.
Markdown file extension.mdStandardized file extension for Markdown documents.
Resource folder nameresources/Standardized folder name (not data/).
source value'sqlite' or 'markdown'Only these two types are supported.
mode value (sqlite only)'in-memory' or 'file-based'Two explicit modes.
origin value'global', 'project', or 'inline'Three storage locations.
runSql LIMITDefault 100, max 1000Prevents unbounded result sets.
All fieldsRequiredNo defaults, no optional fields.

Resource definitions participate in schema hash calculation with specific inclusion and exclusion rules:

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)
ExcludedReason
Database file contentsData updates SHOULD NOT invalidate the schema hash.
Markdown file contentsSame reasoning as database contents.
Handler codeConsistent with tool handler exclusion. Handler functions are in the handlers export, not in main.

ElementConventionPatternExample
Resource namecamelCase^[a-z][a-zA-Z0-9]*$tokenLookup, chainConfig
Query namecamelCase^[a-z][a-zA-Z0-9]*$bySymbol, byAddress, getSchema
Parameter keycamelCase^[a-z][a-zA-Z0-9]*$symbol, chainId
Database filenamekebab-case with namespace prefix^[a-z][a-z0-9-]*\.db$tranco-ranking.db, ofacsdn-sanctions.db
Markdown filenamekebab-case with namespace prefix^[a-z][a-z0-9-]*\.md$duneanalytics-sql-reference.md
Resource folderresources/FixedNot data/

The Resource route name about is reserved as a namespace-level convention by the FlowMCP Grading Specification (flowmcp-gradingspec/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.


The following rules are enforced when validating resource definitions:

Resource validation codes are shared with 09-validation-rules.md, which is the canonical RES code catalog. Codes RES001RES024 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.

CodeSeverityRule
RES001errorsource must be 'sqlite', 'markdown', or 'http'.
RES002errordescription must be a non-empty string.
RES005errorMaximum 2 resources per schema.
RES007errorEach query MUST have a sql field of type string.
RES008errorEach query MUST have a description field of type string.
RES009errorEach query MUST have a parameters array.
RES010errorEach query MUST have an output object with mimeType and schema.
RES011errorEach query MUST have at least 1 test.
RES014errorNumber of parameters MUST match number of ? placeholders in the SQL statement.
RES015errorResource parameters MUST NOT have a location field in position.
RES016errorResource parameters MUST NOT use {{SERVER_PARAM:...}} values.
RES017errorResource name MUST match ^[a-z][a-zA-Z0-9]*$ (camelCase).
RES018errorQuery name MUST match ^[a-z][a-zA-Z0-9]*$ (camelCase).
RES019errorResource parameter primitives MUST be scalar: string(), number(), boolean(), or enum().
RES020warningDatabase file SHOULD exist at validation time. Missing file produces a warning.
RES021erroroutput.schema.type must be 'array' for resource queries.
RES022errorTest parameter values MUST pass the corresponding z validation.
RES023errorTest objects MUST be JSON-serializable.
RES024errorsource: 'http' requires a url field. The URL MUST use HTTPS. (added in v4.2.0)
RES025errormode is required for source: 'sqlite' and MUST be 'in-memory' or 'file-based'.
RES026errororigin is required and MUST be 'global', 'project', or 'inline'.
RES027errorname is required, must be a non-empty string with the correct extension (.db for sqlite, .md for markdown).
RES028errorMaximum 7 schema-defined queries per SQLite resource (9 total with auto-injected runSql + describeTables).
RES029errorFor mode: 'in-memory', schema-defined SQL MUST begin with SELECT or WITH (CTE).
RES030errorsource: 'sqlite-gtfs' requires mode: 'file-based'. in-memory is not allowed.
RES031errorsource: 'sqlite-gtfs' requires the addon field (add-on name).
RES032errorDatabase at path does not contain meta.qualitySeal === 'sqlite-gtfs'. Schema rejected.
RES033errorDatabase at path cannot be opened (file missing or corrupt).
RES034warningDatabase meta.specRevision is outside the expected range.
RES035errorPath variable in path (e.g. ${FLOWMCP_RESOURCES}) cannot be resolved (environment variable not set AND no default available).
RES036errorsource: 'http' requires a path field (local cache file). Enforced by core (ResourceDatabaseManager). (added in v4.2.0)
RES037errormode: 'file-based' requires origin: 'project'.
RES038errorsource: 'markdown' MUST NOT have a mode field.
RES039errorsource: 'markdown' MUST NOT have a queries field.
RES040warningsource: 'sqlite' with origin: 'inline' is not recommended (data privacy).
RES041errorAll resource fields are required. No field MAY be omitted.
RES042infoSQLite resources MAY include a getSchema query for CLI bootstrap (file-based) or downstream tooling. Not required.

A full schema combining SQLite in-memory, SQLite file-based, and Markdown resources with tools and prompts:

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