Skip to main content

flAPI speaks MCP 2026-07-28 — and your slow queries finally stopped timing out

· 9 min read
DataZoo Team
flAPI Development Team

flAPI turns a long-running query across BigQuery, SAP, Iceberg, Postgres and S3 into a task that returns instantly

There's a moment every team hits when they wire an AI agent up to their real data warehouse. The demo works. The "show me last quarter's revenue by region" query works. And then someone asks for something that scans a few hundred million rows across BigQuery and SAP, the query takes ninety seconds, and the whole thing falls over — not because the query failed, but because something in the middle gave up waiting. A reverse proxy. A load balancer. The client's own timeout. The query was fine. The connection wasn't.

flAPI v26.08.31 is largely about that ninety-second gap — and about a bigger shift underneath it: MCP, the protocol AI clients use to call your tools, quietly stopped being a session protocol. Both of those changes landed in this release, and neither one breaks a single existing client.

If you're new here: flAPI turns SQL templates and a little YAML into REST APIs and MCP tools at the same time. You write a query; flAPI gives you an endpoint your services can call and a tool your AI agents can call, from the same file. It's a single C++ binary with DuckDB inside, so those tools reach BigQuery, Postgres, Iceberg, S3, SAP and 50-odd other sources.

Let's start with the protocol shift, because everything else follows from it.

MCP grew up: from sessions to stateless, without leaving anyone behind

The 2026-07-28 revision of MCP made a decision that sounds academic and turns out to be liberating: it dropped sessions. No more initialize handshake, no more Mcp-Session-Id to carry around, no server-side state pinned to a connection. Every request now stands on its own, carrying its protocol version and capabilities in a small _meta block, and discovery moves to a single server/discover call.

For a lot of MCP servers, that's a painful migration — they built real machinery on sessions. For flAPI it was almost free, and the reason is a nice illustration of a good architectural accident: flAPI never really trusted the session anyway. Every tools/call already re-authenticated from the HTTP request and re-derived the caller's roles for RBAC, audit, and rate-limiting. The session was a formality. Deleting it lost nothing — and it means flAPI is now genuinely stateless, so any request can hit any replica behind a load balancer. Horizontal scaling stopped being a story about the protocol and became a story about the (already solved) DuckDB cache.

The important part for you: flAPI is dual-era. It serves the modern stateless path and the legacy initialize/session path from the same endpoint, and it decides per request based on whether the client sent the modern _meta. Your existing Claude Desktop, VS Code, or Goose setup keeps working exactly as before. A newer client gets the modern path automatically. You don't choose; the request does.

A modern client's discovery call looks like this:

POST /mcp/jsonrpc
{
"jsonrpc": "2.0", "id": 1, "method": "server/discover",
"params": { "_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientCapabilities": { "extensions": {} }
}}
}

and comes back with the supported versions, honest capabilities, and cache hints (ttlMs/cacheScope) so clients can cache your tool list — which, for flAPI, only changes when you reload config, so it caches beautifully.

The unlock hiding in the release: clients can finally authenticate

Here's a smaller change with an outsized effect. flAPI now implements OAuth 2.0 Protected Resource Metadata (RFC 9728). When you configure OIDC, flAPI serves /.well-known/oauth-protected-resource and returns proper 401/403 challenges with a WWW-Authenticate header pointing at your authorization server.

Why care? Because before this, standing flAPI up as a real MCP server for a third-party client meant handing over a bearer token out of band and hoping. Now the standard browser OAuth flow that Claude, VS Code, and Goose already implement can actually start on its own. This was the single biggest blocker to "flAPI as a production MCP server," and it's gone:

mcp:
auth:
type: oidc
oidc:
issuer-url: https://accounts.example.com
scopes-supported: [mcp.read, mcp.write]

The Tasks extension: your ninety-second query, solved properly

Back to that gap. flAPI federates DuckDB across BigQuery, SAP (via ERPL), Postgres, Iceberg and S3. Multi-minute analytical queries aren't an edge case — they're Tuesday. And until now the honest options were bad: block the HTTP response until something killed it, or simply don't expose the slow endpoint as a tool. That second option quietly shrinks what your agents can do.

The MCP Tasks extension (io.modelcontextprotocol/tasks) turns a blocking call into a durable handle. And in flAPI, opting in is one line:

mcp-tool:
name: revenue_by_region
description: Quarterly revenue across all regions
async: true # return a task immediately
# async-after: 5000 # or: stay synchronous, become a task only if it outruns 5s

Now the call returns immediately with a task, not a timeout:

{ "result": {
"resultType": "task",
"task": { "taskId": "task_7c...", "status": "working",
"pollIntervalMs": 1000, "ttlMs": 3600000 }
}}

The client polls tasks/get until the status is completed and the rows are right there under result. There's a tasks/cancel, per-caller isolation (a taskId is a name, not a capability — ownership is re-checked on every poll), and a nice default in async-after: fast calls stay synchronous and simple, and only the genuinely slow ones degrade into tasks. Simple-first, still.

The part we're quietly proud of: the task store is durable. Tasks are persisted to a flapi_mcp_tasks table in DuckDB and recovered on startup, so a task survives a restart (when duckdb.db_path is file-backed). A task that was mid-flight when the process died comes back as failed — honestly, because its query didn't survive — rather than lying that it's still working. Your agent restarts, polls the same taskId, and gets a real answer instead of a ghost.

The quiet wins: tools your model can actually use correctly

The headline features are the protocol and Tasks. But the changes that will most improve your day-to-day are smaller and land on every tool call.

Typed input schemas. flAPI already knew your parameters were integers, dates, UUIDs, emails, or enums — your validators said so, and those validators drive prepared-statement binding. It just wasn't telling the model. Every tool parameter used to be advertised as a plain string. Now the schema carries the real types and constraints:

request:
- field-name: customer_id
field-in: query
validators: [{ type: int, min: 1, max: 999999 }]
- field-name: status
field-in: query
validators: [{ type: enum, allowedValues: [active, inactive, pending] }]

becomes an inputSchema a model reads correctly on the first try — customer_id is an integer in a range, status is an enum. Fewer wrong calls, less flailing.

Structured results and a learned output schema. flAPI is a data API — it returns rows. It used to stringify those rows into a text blob and throw the shape away. Now results carry machine-readable structuredContent alongside the text, and flAPI learns each tool's outputSchema from the first real result and advertises it on tools/list. Your agent (and anything downstream, like a chart renderer) gets JSON it can trust.

Errors a model can recover from. A bad date literal used to come back as an opaque JSON-RPC protocol error — the kind of thing a model treats as "this is broken, give up." The spec deliberately separates protocol errors from tool-execution errors so the model sees the latter and self-corrects. flAPI now returns tool failures as isError results carrying the actual validation message: "customer_id: Integer is less than the minimum allowed value." The model reads that, fixes the argument, and tries again — which is exactly what you want.

There's more in the box — cursor pagination, parameterised resource templates (flapi://customers/{id}), x-mcp-header for per-tenant edge routing, and a security fix that closed a gap where method authorization could be skipped by omitting the session header. The MCP reference documents the whole dual-era model in §11.

Try it

Everything above ships in one package — the server and the CLI both live in flapi-io:

pip install flapi-io      # gives you both `flapi` (server) and `flapii` (CLI)

Point it at a flapi.yaml, add mcp.enabled: true, and you have a REST API and an MCP server that speaks both the latest protocol and the one your current clients already know — over the same SQL you'd have written anyway.

The oldest promise of flAPI was that your API should emerge from a query instead of a backend. v26.08.31 extends that promise to your AI tools: write the SQL, and get a tool that modern agents can discover, authenticate to, call with the right types, and — finally — wait for.

🍪 Cookie Settings