Skip to main content

Agent-Managed flAPI: Dynamic Endpoint Creation

The most powerful thing you can do with flAPI's MCP support is to let an AI agent reconfigure flAPI itself. Instead of editing YAML by hand, a developer asks Claude (or any MCP-aware agent) "expose orders.unfulfilled as a REST endpoint with caching" — and the agent uses flAPI's built-in flapi_* admin tools to inspect the schema, write the SQL, register the endpoint, validate it, hot-reload it, and prime the cache. All over a single MCP session, no flapi restart required.

This recipe walks through the full sequence, payload by payload.

What You'll Build

A flAPI server started with --config-service, plus a worked example of an agent driving it through a 7-step create-an-endpoint workflow:

  1. Discover available tables (flapi_get_schema)
  2. Confirm the target path is free (flapi_list_endpoints)
  3. Register the endpoint config (flapi_create_endpoint)
  4. Write the SQL template (flapi_update_template)
  5. Validate the template syntax (flapi_test_template)
  6. Hot-reload into the running server (flapi_reload_endpoint)
  7. Prime the cache (flapi_refresh_cache)

The end state: curl http://localhost:8080/orders/unfulfilled returns data, and the developer never edited a YAML file directly.

Prerequisites

  • flAPI installed (Quickstart)
  • An MCP-compatible agent (Claude Desktop, Claude Code, or a custom client)
  • A connection already declared in flapi.yaml — the agent can create endpoints, but not new connections

Activating the Config Service

The 19 flapi_* admin tools live behind a feature flag. flAPI's standard binary does not load them; you must start the server with --config-service.

# Minimal (no auth — local dev only!)
$ ./flapi -c flapi.yaml --config-service

# Recommended: protect mutating tools with a bearer token
$ export FLAPI_CONFIG_SERVICE_TOKEN="$(openssl rand -hex 32)"
$ ./flapi -c flapi.yaml --config-service --config-service-token "$FLAPI_CONFIG_SERVICE_TOKEN"

Without the flag, every flapi_* call returns:

{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32603,
"message": "Tool execution failed: Config tools not available"
}
}

See MCP Config Tools for the full tool catalogue and authentication rules.

Token hygiene

Never paste the config-service token directly into a chat with the agent. Keep it in an environment variable (FLAPI_CONFIG_SERVICE_TOKEN) and have your MCP bridge inject it into the Authorization: Bearer … header on every request. Tokens that land in chat transcripts end up in shared logs, training data exports, and screenshot bug reports. Treat them like database passwords.

Project Layout

orders-api/
├── flapi.yaml # already configured; declares the connection
├── data/
│ └── orders.parquet
└── sqls/
└── (the agent will populate these files)

flapi.yaml (pre-existing):

project-name: orders-api
project-description: Orders REST API, agent-managed

template:
path: './sqls'

connections:
orders-parquet:
properties:
path: './data/orders.parquet'

duckdb:
access_mode: READ_WRITE

mcp:
enabled: true

Note that data/orders.parquet is loaded as a single DuckDB-readable file; the agent will treat it as table orders.

The Agent's Workflow

Every call below is a POST /mcp/jsonrpc request. Sessions, headers, and error codes are documented in MCP Protocol Reference. Only the JSON body is shown.

Mutating tools (flapi_create_endpoint, flapi_update_template, flapi_reload_endpoint, flapi_refresh_cache) additionally require:

Authorization: Bearer $FLAPI_CONFIG_SERVICE_TOKEN

Step 1 — Discover the schema

The agent has no idea what tables are available. The first call is always flapi_get_schema.

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "flapi_get_schema",
"arguments": {}
}
}

Response (abridged):

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"tables\":[{\"name\":\"orders\",\"schema\":\"main\",\"columns\":[{\"name\":\"order_id\",\"type\":\"INTEGER\",\"nullable\":false},{\"name\":\"customer_id\",\"type\":\"INTEGER\",\"nullable\":false},{\"name\":\"status\",\"type\":\"VARCHAR\",\"nullable\":false},{\"name\":\"order_date\",\"type\":\"DATE\",\"nullable\":false},{\"name\":\"total_amount\",\"type\":\"DOUBLE\",\"nullable\":true}]}]}"
}
]
}
}

The agent parses the inner JSON and now knows the table is called orders, has a status column, and that filtering by status = 'UNFULFILLED' is a viable strategy.

Step 2 — Confirm the path is free

Before creating an endpoint, the agent checks that orders/unfulfilled is not already taken.

{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "flapi_list_endpoints",
"arguments": {}
}
}

Response:

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{
"type": "text",
"text": "{\"count\": 0, \"endpoints\": []}"
}
]
}
}

Zero endpoints — clear to create.

Step 3 — Register the endpoint

flapi_create_endpoint writes a new YAML file under sqls/. The agent supplies the URL path, HTTP method, and the filename of the SQL template (which doesn't exist yet — the next step fills it in).

{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "flapi_create_endpoint",
"arguments": {
"path": "orders/unfulfilled",
"method": "GET",
"template_source": "orders-unfulfilled.sql"
}
}
}

Response:

{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"status\":\"success\",\"path\":\"orders/unfulfilled\",\"method\":\"GET\",\"template_source\":\"orders-unfulfilled.sql\",\"message\":\"Endpoint created successfully\"}"
}
]
}
}

flAPI has now written sqls/orders-unfulfilled.yaml containing the basic scaffold (url-path, method, template-source, connection). The endpoint is registered but not yet live — the SQL template doesn't exist.

request fields

flapi_create_endpoint only accepts path, method, and template_source. To add request: parameters, validators, caching, or auth, the agent edits the generated YAML using flapi_update_endpoint (for top-level switches) or by writing the YAML directly through the Config Service REST API. For this recipe we keep the endpoint parameter-free and apply caching inline in the YAML once flAPI bootstraps it.

Step 4 — Write the SQL

Now the agent writes the actual SQL template. The Mustache placeholders are unused here (no parameters), but the template still needs to be valid Mustache.

{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "flapi_update_template",
"arguments": {
"endpoint": "orders/unfulfilled",
"content": "-- Unfulfilled orders, newest first\nSELECT\n order_id,\n customer_id,\n status,\n order_date,\n total_amount\nFROM orders\nWHERE status = 'UNFULFILLED'\nORDER BY order_date DESC\nLIMIT 500"
}
}
}

Response:

{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "{\"endpoint\":\"orders/unfulfilled\",\"message\":\"Template updated successfully\",\"content_length\":162}"
}
]
}
}

The SQL is written to sqls/orders-unfulfilled.sql on disk.

Step 5 — Validate the template

There is no flapi_validate_endpoint in flAPI's tool catalogue — validation is split across two read-only tools:

  • flapi_test_template — checks Mustache syntax (does every section have a closing tag, etc.)
  • flapi_expand_template — renders the Mustache against sample parameters so the agent can preview the SQL before it ever hits DuckDB.

The agent calls both. First, syntax validation:

{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "flapi_test_template",
"arguments": { "endpoint": "orders/unfulfilled" }
}
}

Response:

{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "{\"endpoint\":\"orders/unfulfilled\",\"valid\":true,\"message\":\"Template syntax is valid\"}"
}
]
}
}

Then optionally an expansion to see the resolved SQL:

{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "flapi_expand_template",
"arguments": {
"endpoint": "orders/unfulfilled",
"params": {}
}
}
}

Response:

{
"jsonrpc": "2.0",
"id": 6,
"result": {
"content": [
{
"type": "text",
"text": "{\"endpoint\":\"orders/unfulfilled\",\"expanded_sql\":\"-- Unfulfilled orders, newest first\\nSELECT\\n order_id,\\n customer_id,\\n status,\\n order_date,\\n total_amount\\nFROM orders\\nWHERE status = 'UNFULFILLED'\\nORDER BY order_date DESC\\nLIMIT 500\",\"status\":\"Template expanded successfully\"}"
}
]
}
}

If flapi_test_template had returned valid: false, the response would have included a reason field — the agent can read it and call flapi_update_template again with corrected content.

Step 6 — Hot-reload

The endpoint config and template are on disk, but the running flAPI process hasn't picked them up yet. The agent issues flapi_reload_endpoint to trigger an in-place reload — no restart, no downtime for other endpoints.

{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "flapi_reload_endpoint",
"arguments": { "path": "orders/unfulfilled" }
}
}

Response:

{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "{\"status\":\"success\",\"path\":\"orders/unfulfilled\",\"message\":\"Endpoint configuration reloaded from disk\",\"original_method\":\"GET\",\"original_template\":\"orders-unfulfilled.sql\"}"
}
]
}
}

At this moment GET /orders/unfulfilled becomes a live REST route. flAPI also emits a notifications/tools/list_changed notification so other MCP clients re-fetch their tool list (see MCP Protocol Reference — Notifications).

Step 7 — Prime the cache

If the endpoint config enabled DuckLake caching (the agent would have added a cache: block to the YAML before reload), the first user request would otherwise pay the full SQL latency. The agent triggers a manual refresh so the cache is warm before anyone calls the endpoint.

{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "flapi_refresh_cache",
"arguments": { "path": "orders/unfulfilled" }
}
}

Response:

{
"jsonrpc": "2.0",
"id": 8,
"result": {
"content": [
{
"type": "text",
"text": "{\"path\":\"orders/unfulfilled\",\"status\":\"Cache refresh triggered\",\"cache_table\":\"orders_unfulfilled_cache\",\"timestamp\":\"1731585600\",\"message\":\"Cache refresh has been scheduled\"}"
}
]
}
}

If caching wasn't enabled, this returns Cache not enabled for this endpoint with a hint to add a cache: block — at which point the agent can either skip the step or update the YAML.

End State

The developer's terminal now shows:

$ curl http://localhost:8080/orders/unfulfilled | jq '.data[0]'
{
"order_id": 4117,
"customer_id": 712,
"status": "UNFULFILLED",
"order_date": "2024-11-12",
"total_amount": 1834.50
}

A complete REST endpoint, sourced from a Parquet file, validated, hot-reloaded, and cache-primed — created by an agent in eight JSON-RPC calls. No ./flapi restart, no manual YAML editing, no SSH.

Audit Trail

For each mutation the agent makes, flAPI logs the call (and the bearer token's identity if one was supplied). The agent can request the audit log for cache operations:

{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "flapi_get_cache_audit",
"arguments": { "path": "orders/unfulfilled" }
}
}

Response:

{
"jsonrpc": "2.0",
"id": 9,
"result": {
"content": [
{
"type": "text",
"text": "{\"path\":\"orders/unfulfilled\",\"cache_table\":\"orders_unfulfilled_cache\",\"audit_log\":[{\"timestamp\":\"1731585600\",\"event\":\"cache_refreshed\",\"status\":\"success\"}]}"
}
]
}
}

For project- and template-level changes, inspect the underlying YAML files in sqls/ — they are still the source of truth, and they live in your VCS history.

Safety Rails for Production

Letting an agent mutate the live server is powerful and dangerous. The standard safety practices:

  1. Stage agent mutations in a dev project, then promote the resulting YAML via your normal git review. The agent edits files; treat those edits like any other PR.
  2. Use a different config-service token per environment. Dev gets a permissive token; staging and production gate the token behind your secret manager.
  3. Never expose --config-service to the public internet. Bind it to localhost or behind a VPN. The mutating tools have no per-action ACL beyond the bearer token.
  4. Audit flapi_* calls in your reverse proxy. Log every mutating method name and the agent that made it. If something breaks at 3 a.m., you want to know who created orders/unfulfilled and when.
  5. Disable the flag entirely in production for static deployments. If your endpoints are stable, ship the YAML in a Docker image and start flAPI without --config-service. The flapi_* tools disappear from tools/list and there is no agent-write surface to defend.

The Full Tool Family

This recipe used 7 of the 19 flapi_* tools. The rest cover related operations:

CategoryTools used hereOther tools available
Discovery (5)flapi_get_schema, flapi_list_endpointsflapi_get_project_config, flapi_get_environment, flapi_get_filesystem, flapi_refresh_schema
Template (4)flapi_update_template, flapi_test_template, flapi_expand_templateflapi_get_template
Endpoint (6)flapi_create_endpoint, flapi_reload_endpointflapi_get_endpoint, flapi_update_endpoint, flapi_delete_endpoint
Cache (4)flapi_refresh_cache, flapi_get_cache_auditflapi_get_cache_status, flapi_run_cache_gc

See MCP Config Tools for the complete catalogue with arguments, auth requirements, and example payloads.

See Also

🍪 Cookie Settings