Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 19 additions & 20 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1357,18 +1357,22 @@ The binary is statically linked with all dependencies and can be deployed to any

### PyPI Wheels

Both `flapi` (server) and `flapii` (CLI) are distributed as platform-specific Python wheels, installable via pip:
Both the `flapi` server and the `flapii` CLI ship inside a **single** PyPI
package, `flapi-io`. Installing it provides both console commands:

```bash
pip install flapi-io # SQL-to-API server
pip install flapii # CLI client
pip install flapi-io # installs both `flapi` (server) and `flapii` (CLI)
```

Wheels are built automatically during release using [bin-to-wheel](https://github.com/DataZooDE/bin-to-wheel).
The `flapi_io` wheel bundles both binaries (`flapi_io/bin/flapi` and
`flapi_io/bin/flapii`) and declares console-script entry points for each
(`flapi = flapi_io:main_flapi`, `flapii = flapi_io:main_flapii`). Wheels are
built automatically during release using
[bin-to-wheel](https://github.com/DataZooDE/bin-to-wheel).

**Package names on PyPI:**
- `flapi-io` β€” the server ("flapi" was taken on PyPI)
- `flapii` β€” the TypeScript CLI client
**Package name on PyPI:**
- `flapi-io` β€” the only published package ("flapi" was taken on PyPI); carries
both the server and the CLI. There is **no** separate `flapii` PyPI package.

## Release Process

Expand All @@ -1391,38 +1395,33 @@ Versioning: `v*` tags (e.g., `v0.5.0`). Tag push triggers the full release pipel

1. Ensure all CI builds pass on `main` (`gh run list`)
2. Tag and push: `git tag v{version} && git push origin v{version}`
3. `.github/workflows/release.yaml` triggers on `v*` tag push:
- Downloads build artifacts (4 flapi + 4 flapii binaries)
3. `.github/workflows/build.yaml` triggers on `v*` tag push (there is no separate `release.yaml`):
- Downloads build artifacts (4 flapi server + 4 flapii CLI binaries)
- Creates archive assets (`.tar.gz` for Unix, `.zip` for Windows)
- Builds 8 Python wheels via `bin-to-wheel` (4 flapi-io + 4 flapii)
- Creates GitHub Release with all archives and wheels
- Publishes wheels to PyPI via trusted publishing (OIDC)
- Builds **4** `flapi_io` Python wheels via `bin-to-wheel`, each bundling both the flapi and flapii binaries for that platform
- Creates the GitHub Release with the archives and wheels
- Publishes the `flapi-io` wheels to PyPI via trusted publishing (OIDC)

### PyPI Trusted Publishing Setup

Uses OIDC trusted publishing β€” no API tokens needed. Configuration:

| Package | GitHub Environment | PyPI Pending Publisher |
|---------|-------------------|----------------------|
| `flapi-io` | `pypi-flapi-io` | DataZooDE/flapi, release.yaml, pypi-flapi-io |
| `flapii` | `pypi-flapii` | DataZooDE/flapi, release.yaml, pypi-flapii |
| `flapi-io` | `pypi-flapi-io` | DataZooDE/flapi, build.yaml, pypi-flapi-io |

Each package needs a separate GitHub environment because PyPI requires unique `(owner, repo, workflow, environment)` tuples for trusted publishers.
Only `flapi-io` is published (it carries both the server and the CLI), so a single trusted-publisher environment is needed.

### Wheel Output

Each release produces 8 wheels:
Each release produces **4** `flapi_io` wheels (one per platform), each bundling both the `flapi` and `flapii` binaries:

| Package | Platform | Wheel platform tag |
|---------|----------|--------------------|
| `flapi-io` | Linux x86_64 | `manylinux_2_17_x86_64` |
| `flapi-io` | Linux ARM64 | `manylinux_2_17_aarch64` |
| `flapi-io` | macOS ARM64 | `macosx_11_0_arm64` |
| `flapi-io` | Windows x64 | `win_amd64` |
| `flapii` | Linux x86_64 | `manylinux_2_17_x86_64` |
| `flapii` | Linux ARM64 | `manylinux_2_17_aarch64` |
| `flapii` | macOS ARM64 | `macosx_11_0_arm64` |
| `flapii` | Windows x64 | `win_amd64` |

### flapii Build Process

Expand Down
13 changes: 8 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ flAPI is a powerful service that automatically generates read-only APIs for data
## ⚑ Features

- **Automatic API Generation**: Create APIs for your datasets without coding
- **MCP (Model Context Protocol) Support**: Declarative creation of AI tools alongside REST endpoints β€” with **per-tool RBAC** (`allowed-roles`), **shadow/dry-run** (`_dryRun: true`), **response shaping** (`max-rows` / `redact-columns` / `sample`), **per-tool rate limit**, and a **tool-description hygiene scanner** for prompt-injection attempts
- **MCP (Model Context Protocol) Support**: Declarative AI tools alongside REST endpoints, speaking the latest **MCP `2026-07-28`** revision (dual-era: modern *and* legacy clients) β€” with the **Tasks extension** for long-running queries, **typed schemas + structured results**, **OAuth discovery**, **per-tool RBAC**, **shadow/dry-run**, **response shaping**, **rate limiting**, and a **prompt-injection hygiene scanner**
- **Multiple Data Sources**: Connect to [BigQuery](https://github.com/hafenkran/duckdb-bigquery), SAP ERP & BW (via [ERPL](https://github.com/datazoode/erpl)), Parquet, [Iceberg](https://github.com/duckdb/duckdb_iceberg), [Postgres](https://github.com/duckdb/postgres_scanner), [MySQL](https://github.com/duckdb/duckdb_mysql), and more
- **SQL Templates**: Mustache-like syntax. Typed `{{ params.X }}` references on `int`/`double`/`boolean`/`date`/`time`/`uuid`/`enum`/`email`/`string` fields are bound as **DuckDB prepared statements** β€” SQL injection is structurally impossible for those sites
- **Caching**: DuckLake-backed cache with full refresh and incremental sync
Expand Down Expand Up @@ -133,13 +133,16 @@ flAPI now supports the **Model Context Protocol (MCP)** in a **unified configura

### Key Features

- **MCP `2026-07-28` (dual-era)**: serves the latest stateless MCP revision (`server/discover`, per-request metadata, cacheable results, OAuth discovery via RFC 9728) **alongside** the legacy `initialize`/session protocol β€” existing clients keep working unchanged
- **Long-running tools (Tasks extension)**: mark a tool `async` and slow queries return a task handle immediately instead of blocking the connection; the durable task store survives a restart, with `tasks/get` / `tasks/cancel` and per-caller isolation
- **Typed, structured tool contracts**: tool parameters advertise real types and constraints (int ranges, dates, uuid, enum, …), results carry machine-readable `structuredContent`, an `outputSchema` is learned after first use, and failures return actionable `isError` results the model can self-correct from
- **Unified Configuration**: Single YAML files can define REST endpoints, MCP tools, and MCP resources
- **Automatic Detection**: Configuration type is determined by presence of `url-path` (REST), `mcp-tool` (MCP tool), or `mcp-resource` (MCP resource)
- **Shared Components**: MCP tools and resources use the same SQL templates, parameter validation, authentication, and caching as REST endpoints
- **Concurrent Servers**: REST API (port 8080) and MCP server (port 8081) run simultaneously
- **Declarative Definition**: Define everything using YAML configuration with SQL templatestocol
- **Tool Discovery**: Automatic tool discovery and schema generation
- **Security Integration**: Reuse existing authentication, rate limiting, and caching features
- **Security Integration**: method authorization enforced on every request, per-tool/resource/prompt RBAC (`allowed-roles`), shadow/dry-run (`_dryRun`), response shaping, per-tool rate limiting, and a tool-description hygiene scanner
- **Tool Discovery**: automatic tool discovery, pagination, resource templates (`flapi://customers/{id}`), and `x-mcp-header` for per-tenant edge routing

See [docs/MCP_REFERENCE.md](docs/MCP_REFERENCE.md) β€” the dual-era model and all new capabilities are documented in Β§11.

### MCP Endpoints

Expand Down
118 changes: 118 additions & 0 deletions docs/blog/2026-08-31-mcp-2026-and-tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
---
title: "flAPI speaks MCP 2026-07-28 β€” and your slow queries finally stopped timing out"
description: "flAPI v26.08.31 adds dual-era MCP 2026-07-28 support and a Tasks extension that turns multi-minute analytical queries into durable, pollable tasks β€” plus typed tool schemas and OAuth discovery. Here's what changed and why it matters."
date: 2026-08-31
tags: [mcp, duckdb, ai-tools, release]
---

# flAPI speaks MCP 2026-07-28 β€” and your slow queries finally stopped timing out

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](https://modelcontextprotocol.io) 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:

```json
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](https://datatracker.ietf.org/doc/html/rfc9728))**. 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:

```yaml
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:

```yaml
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:

```json
{ "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:

```yaml
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](https://github.com/DataZooDE/flapi/blob/main/docs/MCP_REFERENCE.md) 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`:

```bash
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.
Loading