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. The server advertises the listChanged capability in the initialize response but does not push notifications/tools/list_changed today, so other MCP clients have to re-poll tools/list to pick up the new tool (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_schemaflapi_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_list_endpoints, 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