MCP Client (Agentic Loop)
Overview
Section titled “Overview”Floopy can act as an MCP client: it connects to external MCP servers on behalf of your agents and injects their tools into the conversation. When the LLM decides to call a tool, Floopy executes it, appends the result to the conversation, and loops back to the model — all transparently.
This is the agentic loop: the model reasons, calls tools, observes results, and reasons again until it reaches a final answer.
The Agent Loop
Section titled “The Agent Loop”flowchart TD
A[User message] --> B[LLM call]
B --> C{Tool call requested?}
C -- No --> D[Return final response]
C -- Yes --> E[Execute tools via MCP server]
E --> F[Tool result post-processing]
F --> G[Append tool results to messages]
G --> H{Round < max_rounds?}
H -- Yes --> B
H -- No --> I[Return last model response]The loop is bounded by max_rounds to prevent infinite execution. When the limit is reached, Floopy returns the last model response. A global timeout (default 120s) also applies to the entire loop.
Plugin YAML Schema
Section titled “Plugin YAML Schema”Configure the agentic loop with a plugin YAML attached to your routing rule or sent as a request header (floopy-mcp-plugin).
Full Example
Section titled “Full Example”version: "1"
mcp_servers: - id: web_search url: "https://mcp.example.com/search" auth: type: bearer secret_ref: "secret.mcp_search_api_key" # resolved from Floopy Vault tools: - search_web - fetch_page forward_headers: ["x-tenant-id", "x-trace-id"] timeout_ms: 5000 max_retries: 2
- id: code_interpreter url: "https://mcp.example.com/code" auth: type: api_key header: "X-Api-Key" secret_ref: "secret.mcp_code_api_key" tools: "*" # expose all tools from this server timeout_ms: 15000
agent: max_rounds: 10 stream_mode: final_only # final_only | disabled tool_call_parallel: true # execute independent tool calls in parallel tool_cache_ttl_seconds: 300 # cache tool results (0 = disabled) # prompt_guard_on_tool_output was the pre-migration ONNX-based scan # of tool results — currently a no-op while the firewall sync→async # interface is reworked. Field accepted for backwards compatibility.Field Reference
Section titled “Field Reference”mcp_servers[]
Section titled “mcp_servers[]”| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique identifier for this server within the plugin |
url | string | yes | HTTP(S) endpoint of the MCP server (must pass SSRF validator) |
auth | object | no | Authentication to use when calling the server |
tools | string[] or "*" | no | Tools to expose. Defaults to "*" (all) |
timeout_ms | integer | no | Per-request timeout. Default: 5000 |
max_retries | integer | no | Retry attempts on transient errors. Default: 1 |
forward_headers | string[] | no | Names of inbound request headers to forward to this server on every tool call (case-insensitive match). Reserved headers (authorization, cookie, host, and framing headers) are never forwarded. Default: []. |
Use forward_headers to pass selected headers from the original client request through to a specific MCP server — for example a tenant id, trace id, or a per-end-user token your tool needs. The headers are resolved fresh on every turn and sent only to the server they are configured on, never to the other servers in the plugin. For safety, the gateway never forwards authorization, cookie, host, or framing headers, so your Floopy API key is never leaked upstream.
| Auth type | Fields | Description |
|---|---|---|
bearer | secret_ref | Sends Authorization: Bearer <secret> |
api_key | header, secret_ref | Sends the secret in a custom header |
oauth | token_url, client_id, secret_ref, scopes | OAuth 2.1 client credentials grant — Floopy mints an access token and sends it as Authorization: Bearer <token> |
hmac | secret_ref, algorithm | Signs the request body (SHA-256 default) |
none | — | No authentication |
OAuth 2.1 (client credentials)
Section titled “OAuth 2.1 (client credentials)”For machine-to-machine MCP servers that issue short-lived tokens, use type: oauth. Floopy performs the OAuth 2.1 client_credentials grant against the server’s token endpoint, caches the resulting access token in-memory until shortly before it expires, and injects it as a bearer token on every tool call. There is no user redirect, PKCE, or refresh-token flow — this is a server-to-server credential exchange.
mcp_servers: - id: billing_tools url: "https://mcp.example.com/billing" auth: type: oauth token_url: "https://idp.example.com/oauth/token" # must be HTTPS client_id: "floopy-agent" secret_ref: "secret.mcp_oauth_client_secret" # the OAuth client secret scopes: ["billing.read", "billing.write"] # optional tools: "*"| Field | Type | Required | Description |
|---|---|---|---|
token_url | string | yes | OAuth token endpoint. Must be HTTPS and pass the SSRF validator. |
client_id | string | yes | OAuth client identifier issued by the provider. |
secret_ref | string | yes | Floopy Vault reference to the OAuth client secret. |
scopes | string[] | no | Scopes requested in the token exchange (space-joined on the wire). |
The token is cached per (organization, server) and refreshed automatically ~60s before expires_in. A failed token exchange (non-2xx, timeout, or oversized body) aborts the tool call rather than falling back to an unauthenticated request.
In the dashboard, select OAuth 2.1 client credentials as the auth type under MCP → Servers and fill in the Token URL, Client ID, Scopes, and a Client secret vault entry.
| Field | Type | Default | Description |
|---|---|---|---|
max_rounds | integer | 5 | Maximum tool-call iterations before returning |
stream_mode | enum | final_only | When to stream: final_only or disabled |
tool_call_parallel | boolean | true | Execute non-dependent tool calls in parallel |
tool_cache_ttl_seconds | integer | 0 | Cache identical tool calls (by args hash) |
prompt_guard_on_tool_output | boolean | false | Pre-migration ONNX scan of tool outputs. Currently a no-op while the sync Validator interface is reworked. Field still accepted for backwards compatibility. |
Secret Management
Section titled “Secret Management”Never put API keys directly in the YAML. Store them in Floopy Vault and reference them by name.
Storing a Secret
Section titled “Storing a Secret”- Go to Settings > Secrets in the dashboard
- Click Add Secret
- Enter the name (e.g.,
mcp_search_api_key) and value - Click Save
The secret is encrypted at rest (AES-256) and injected at runtime — it is never logged or returned in API responses.
Referencing a Secret
Section titled “Referencing a Secret”Use the secret. prefix followed by the name you stored:
auth: type: bearer secret_ref: "secret.mcp_search_api_key"The format is always secret.<name> where <name> matches exactly what you stored in the dashboard. Only alphanumeric characters, hyphens, and underscores are allowed (max 64 characters). Characters like :, /, and . (beyond the prefix) are rejected for security reasons.
Internally, each secret is isolated per organization — your secrets are never accessible by other tenants.
Streaming Modes
Section titled “Streaming Modes”| Mode | Behavior |
|---|---|
final_only | Streams the final LLM response after all tool calls complete. Intermediate tool calls are not streamed. |
disabled | Returns the complete response as a single JSON object when the loop finishes. |
Note: intermediate tool call steps are always available in the request log under Observability, regardless of streaming mode.
Loop Limits and Timeouts
Section titled “Loop Limits and Timeouts”Set max_rounds to a value appropriate for your use case:
| Use case | Recommended max_rounds |
|---|---|
| Single-tool lookup | 2–3 |
| Multi-step research | 5–8 |
| Complex autonomous agent | 10–15 |
Each round adds LLM latency plus tool execution time. Keep timeout_ms per server low to avoid stalling the loop.
A hard gateway timeout of 120 seconds applies to the entire agentic loop. Requests exceeding this limit are terminated and the partial response is returned with a timeout finish reason.
Sending the Plugin via Header
Section titled “Sending the Plugin via Header”Instead of attaching the plugin to a routing rule, you can send it inline per-request using the floopy-mcp-plugin header with a base64-encoded YAML value:
import { OpenAI } from "openai";import { Buffer } from "buffer";
const plugin = `version: "1"mcp_servers: - id: search url: "https://mcp.example.com/search" auth: type: bearer secret_ref: "secret.mcp_search_api_key"agent: max_rounds: 5`;
const client = new OpenAI({ baseURL: "https://api.floopy.ai/v1", apiKey: process.env.FLOOPY_API_KEY, defaultHeaders: { "floopy-mcp-plugin": Buffer.from(plugin).toString("base64"), },});
const response = await client.chat.completions.create({ model: "gpt-4o", messages: [{ role: "user", content: "What is the current price of BTC?" }],});import base64, osfrom openai import OpenAI
plugin = """version: "1"mcp_servers: - id: search url: "https://mcp.example.com/search" auth: type: bearer secret_ref: "secret.mcp_search_api_key"agent: max_rounds: 5"""
client = OpenAI( base_url="https://api.floopy.ai/v1", api_key=os.environ["FLOOPY_API_KEY"], default_headers={ "floopy-mcp-plugin": base64.b64encode(plugin.encode()).decode(), },)
response = client.chat.completions.create( model="gpt-4o", messages=[{"role": "user", "content": "What is the current price of BTC?"}],)End-to-End Example
Section titled “End-to-End Example”The following example wires a web search MCP server to a GPT-4o agent that answers research questions.
Plugin YAML (attached to routing rule “Research Agent”):
version: "1"
mcp_servers: - id: brave_search url: "https://mcp.brave.com/search" auth: type: bearer secret_ref: "secret.brave_api_key" tools: - web_search timeout_ms: 8000
agent: max_rounds: 6 stream_mode: final_only tool_call_parallel: false # prompt_guard_on_tool_output is currently a no-op (see Field Reference table)Request:
const response = await client.chat.completions.create({ model: "gpt-4o", messages: [ { role: "user", content: "What are the three most cited papers on transformer attention published in 2024?", }, ],});
console.log(response.choices[0].message.content);// The model searched the web, read results, and synthesized a final answer.What happened internally:
- GPT-4o called
web_search("transformer attention papers 2024") - Floopy executed the tool via the Brave MCP server
- Results were appended to the conversation
- GPT-4o called
web_search("citation counts transformer 2024")for follow-up - Floopy returned the synthesized final answer after round 2
Observability
Section titled “Observability”Every agentic loop execution is logged in full:
- Tool calls made (name, arguments, duration)
- Tool results (sanitized — secrets redacted)
- Number of rounds completed
- Total tokens consumed across all rounds
- Whether the loop hit
max_rounds
View logs under Observability > Requests in the dashboard. Filter by has_tool_calls: true to isolate agentic sessions.