This document describes the end-to-end flow of requests through flAPI for both REST and MCP protocols.
sequenceDiagram
participant Client
participant Crow as APIServer (Crow)
participant CORS as CORSHandler
participant RL as RateLimitMiddleware
participant Auth as AuthMiddleware
participant RH as RequestHandler
participant RV as RequestValidator
participant CM as ConfigManager
participant STP as SQLTemplateProcessor
participant QE as QueryExecutor
participant DM as DatabaseManager
participant Cache as CacheManager
participant DuckDB
Client->>Crow: HTTP GET /customers?id=123
Crow->>CORS: before_handle()
CORS->>RL: before_handle()
RL-->>RL: Check rate limit
alt Rate limit exceeded
RL-->>Client: 429 Too Many Requests
end
RL->>Auth: before_handle()
Auth-->>Auth: Validate JWT/Basic/OIDC
alt Auth failed
Auth-->>Client: 401 Unauthorized
end
Auth->>RH: handle_request()
RH->>CM: getEndpointForPathAndMethod("/customers", "GET")
CM-->>RH: EndpointConfig
RH->>RH: Extract params from query/path/body/header
RH->>RV: validateParams(params, endpoint.request_fields)
alt Validation failed
RV-->>Client: 400 Bad Request
end
RH->>Cache: isCacheEnabled(endpoint) && getCachedResult()
alt Cache hit
Cache-->>Client: 200 OK (cached)
end
RH->>STP: processTemplate(endpoint, params)
STP-->>RH: rendered SQL
RH->>QE: executeQuery(sql, params)
QE->>DM: getConnection()
DM-->>QE: duckdb_connection
QE->>DuckDB: Execute SQL
DuckDB-->>QE: Result set
QE-->>RH: QueryResult
RH->>Cache: updateCache(endpoint, result)
RH->>RH: Format JSON response
RH-->>Client: 200 OK (JSON)
The Crow HTTP server receives the request and routes it through the middleware chain.
// Route registration
CROW_ROUTE(app, "/api/<path>")
.methods("GET"_method, "POST"_method, ...)
([this](const crow::request& req, const std::string& path) {
return handleRequest(req, path);
});CORS Handler (built-in Crow middleware):
- Adds CORS headers for cross-origin requests
- Handles preflight OPTIONS requests
Rate Limit Middleware (src/rate_limit_middleware.cpp):
- Tracks request counts per client IP
- Returns 429 if rate limit exceeded
- Configurable via
rate_limitinflapi.yaml
Auth Middleware (src/auth_middleware.cpp):
- Extracts credentials from
Authorizationheader - Validates JWT tokens, Basic auth, or OIDC tokens
- Sets
auth_contextfor downstream handlers - Returns 401 if authentication fails
Parameters are extracted based on field-in configuration:
field-in |
Source | Example |
|---|---|---|
query |
URL query string | ?id=123 |
path |
URL path segments | /customers/123 |
body |
Request body (JSON) | {"id": 123} |
header |
HTTP headers | X-Custom-Id: 123 |
std::map<std::string, std::string> params;
for (const auto& field : endpoint.request_fields) {
if (field.fieldIn == "query") {
params[field.fieldName] = req.url_params.get(field.fieldName);
} else if (field.fieldIn == "path") {
params[field.fieldName] = extractPathParam(req, field.fieldName);
}
// ...
}Each parameter is validated against its configured validators:
validators:
- type: int
min: 1
max: 999999
- type: string
pattern: "^[a-zA-Z0-9]+$"Validation failure returns 400 with details:
{
"error": "Validation failed",
"field": "id",
"message": "Value must be a positive integer"
}If caching is enabled for the endpoint:
- Check if cached result exists and is valid (within TTL)
- Return cached result if available
- Continue to query execution if cache miss
Mustache template is expanded with parameters:
Input template:
SELECT * FROM customers
WHERE 1=1
{{#params.id}}
AND customer_id = {{{ params.id }}}
{{/params.id}}With params {id: "123"}:
SELECT * FROM customers
WHERE 1=1
AND customer_id = '123'- Get connection from DatabaseManager pool
- Execute SQL on DuckDB
- Convert result to QueryResult struct
- Return connection to pool
Results are serialized to JSON and returned:
{
"data": [
{"customer_id": 123, "name": "Alice", ...}
],
"meta": {
"total": 1,
"cached": false
}
}sequenceDiagram
participant Client as MCP Client
participant MRH as MCPRouteHandlers
participant Auth as MCPAuthHandler
participant SM as MCPSessionManager
participant MTH as MCPToolHandler
participant CM as ConfigManager
participant STP as SQLTemplateProcessor
participant QE as QueryExecutor
participant DM as DatabaseManager
participant DuckDB
Client->>MRH: POST /mcp (JSON-RPC)
Note over Client,MRH: {"jsonrpc":"2.0","method":"tools/call","params":{...},"id":1}
MRH->>MRH: parseMCPRequest()
MRH->>Auth: validateAuth(request)
alt Auth failed
Auth-->>Client: JSON-RPC error (-32001)
end
MRH->>SM: getOrCreateSession()
SM-->>MRH: session_id
MRH->>MRH: dispatchMCPRequest(request)
Note over MRH: Route by method: tools/call
MRH->>MTH: handleToolCall(tool_name, arguments)
MTH->>CM: getEndpointByMCPToolName(tool_name)
CM-->>MTH: EndpointConfig
MTH->>MTH: mapArgumentsToParams(arguments)
MTH->>STP: processTemplate(endpoint, params)
STP-->>MTH: rendered SQL
MTH->>QE: executeQuery(sql, params)
QE->>DM: getConnection()
DM-->>QE: duckdb_connection
QE->>DuckDB: Execute SQL
DuckDB-->>QE: Result set
QE-->>MTH: QueryResult
MTH->>MTH: formatMCPResponse(result)
MTH-->>MRH: MCPResponse
MRH->>MRH: createJsonRpcResponse()
MRH-->>Client: JSON-RPC response
Note over Client,MRH: {"jsonrpc":"2.0","result":{...},"id":1}
| Method | Handler | Description |
|---|---|---|
initialize |
handleInitializeRequest |
Protocol handshake, capability negotiation |
tools/list |
handleToolsListRequest |
List available tools |
tools/call |
handleToolsCallRequest |
Execute a tool |
resources/list |
handleResourcesListRequest |
List available resources |
resources/read |
handleResourcesReadRequest |
Read a resource |
prompts/list |
handlePromptsListRequest |
List available prompts |
prompts/get |
handlePromptsGetRequest |
Get a prompt template |
ping |
handlePingRequest |
Health check |
std::optional<MCPRequest> parseMCPRequest(const crow::request& req) {
auto json = crow::json::load(req.body);
MCPRequest request;
request.jsonrpc = json["jsonrpc"].s();
request.method = json["method"].s();
request.params = json["params"];
request.id = json["id"].s();
return request;
}- Extract session ID from
Mcp-Session-Idheader - Create new session if not exists
- Store client capabilities for the session
Request is routed to appropriate handler based on method:
MCPResponse dispatchMCPRequest(const MCPRequest& request) {
if (request.method == "initialize") {
return handleInitializeRequest(request);
} else if (request.method == "tools/list") {
return handleToolsListRequest(request);
} else if (request.method == "tools/call") {
return handleToolsCallRequest(request);
}
// ...
}For tools/call:
- Look up endpoint by MCP tool name
- Map MCP arguments to endpoint parameters
- Process template and execute query
- Format result in MCP content format
// Argument mapping
std::map<std::string, std::string> params;
for (const auto& field : endpoint.request_fields) {
if (arguments.has(field.fieldName)) {
params[field.fieldName] = arguments[field.fieldName].s();
}
}MCP responses follow JSON-RPC 2.0 format:
Success:
{
"jsonrpc": "2.0",
"result": {
"content": [
{
"type": "text",
"text": "{\"data\": [...]}"
}
]
},
"id": 1
}Error:
{
"jsonrpc": "2.0",
"error": {
"code": -32602,
"message": "Invalid params"
},
"id": 1
}Write operations (POST, PUT, DELETE with operation.type: write) follow a similar flow with additional considerations:
sequenceDiagram
participant Client
participant RH as RequestHandler
participant DM as DatabaseManager
participant Cache as CacheManager
participant DuckDB
Client->>RH: POST /customers (JSON body)
RH->>RH: Validate params (stricter for writes)
RH->>DM: executeWriteInTransaction()
DM->>DuckDB: BEGIN TRANSACTION
DM->>DuckDB: Execute INSERT/UPDATE/DELETE
alt Success
DuckDB-->>DM: rows_affected
DM->>DuckDB: COMMIT
alt Cache invalidation enabled
RH->>Cache: invalidateCache(endpoint)
end
alt Cache refresh enabled
RH->>Cache: refreshCache(endpoint)
end
RH-->>Client: 200 OK
else Failure
DuckDB-->>DM: Error
DM->>DuckDB: ROLLBACK
RH-->>Client: 500 Error
end
operation:
type: write # Marks as write operation
transaction: true # Wrap in transaction (default)
returns_data: true # Use RETURNING clause
validate_before_write: true # Stricter validation
cache:
invalidate_on_write: true # Clear cache after write
refresh_on_write: false # Optionally refresh immediately| Step | REST | MCP |
|---|---|---|
| Entry point | src/api_server.cpp |
src/mcp_route_handlers.cpp |
| Auth | src/auth_middleware.cpp |
src/mcp_auth_handler.cpp |
| Config lookup | ConfigManager::getEndpointForPathAndMethod |
ConfigManager::getEndpointForPath |
| Validation | src/request_validator.cpp |
src/mcp_tool_handler.cpp |
| Template | src/sql_template_processor.cpp |
src/sql_template_processor.cpp |
| Execution | src/query_executor.cpp |
src/query_executor.cpp |
| Cache | src/cache_manager.cpp |
src/cache_manager.cpp |
| Code | Cause |
|---|---|
| 400 | Validation failure, bad request format |
| 401 | Authentication required/failed |
| 403 | Authorization denied (insufficient roles) |
| 404 | Endpoint not found |
| 429 | Rate limit exceeded |
| 500 | Query execution error, server error |
| Code | Meaning |
|---|---|
| -32700 | Parse error |
| -32600 | Invalid request |
| -32601 | Method not found |
| -32602 | Invalid params |
| -32603 | Internal error |
| -32001 | Authentication error |
| -32002 | Tool not found |
- ARCHITECTURE.md - Component overview
- components/config-system.md - Configuration loading
- components/query-execution.md - SQL execution details
- components/security.md - Auth and validation