When Gemini Says Nothing: Two Silent Failure Modes in MCP + LibreChat
April 2026 — field notes from wiring a Kubernetes SRE agent to Gemini 2.5 Flash
I spent the better part of two days debugging an AI agent that would reliably respond with… nothing. No error. No explanation. Just a blank chat bubble where a tool call should have been.
This is the story of two bugs that look identical, live in completely different layers of the stack, and are nearly impossible to find without intercepting raw HTTP traffic.
What I was building
The goal was an autonomous SRE agent: a Go MCP server called cluster-shepherd that exposes Kubernetes and Flux operations as tools, connected to LibreChat as the UI, with Gemini 2.5 Flash as the reasoning model.
The tool list included things like get_flux_status, get_pod_logs, list_workloads, assess_risk, restart_workload — 13 tools in total. LibreChat connects to the MCP server over SSE, fetches the tool definitions, and hands them to Gemini when the agent runs.
Clean on paper. In practice, step 4 kept producing a blank response. No tool call. No text. No error anywhere in the logs.
The symptom
Both bugs produce exactly the same output:
finishReason: STOP
promptTokenCount: 3421
candidatesTokenCount: 0
Gemini acknowledges it received the input. It processes it. Then it generates nothing and exits cleanly. LibreChat shows a blank message. No LangChain exception. No HTTP error code. Nothing to grep for.
This is what makes both failures genuinely nasty — the signal you’d normally look for (an error) is completely absent.
How to see what’s actually happening
The only reliable diagnostic is intercepting the raw Gemini streaming response from inside the LibreChat pod. Drop this into a .cjs script and run it with kubectl exec:
// /tmp/diag.cjs
const origFetch = globalThis.fetch;
globalThis.fetch = async function(url, opts) {
if (String(url).includes('generativelanguage')) {
const resp = await origFetch.call(this, url, opts);
const body = await resp.clone().text();
const tokens = body.match(/candidatesTokenCount[\":]+(\d+)/)?.[1];
const finish = body.match(/finishReason[\":]+(\w+)/)?.[1];
console.log('Gemini:', finish, 'outputTokens:', tokens || 0);
// Also log the tool names being sent
const parsed = JSON.parse(opts.body);
const decls = parsed.tools?.[0]?.functionDeclarations || [];
console.log('Tool names sent:', decls.map(d => d.name));
return resp;
}
return origFetch.call(this, url, opts);
};
With this in place you can run your agent test and immediately see whether Gemini is receiving the tools and what finish reason it returns. outputTokens: 0 with finish: STOP is the fingerprint for both failures below.
Failure mode 1: hyphens in MCP server names
The setup
In librechat.yaml, MCP servers are declared with a logical name:
mcpServers:
cluster-shepherd:
type: sse
url: "http://cluster-shepherd.cluster-shepherd.svc.cluster.local:8080/sse"
LibreChat composes tool names as {tool_name}_mcp_{server_name}. With the config above, my tools arrive at Gemini named like this:
get_flux_status_mcp_cluster-shepherd
get_pod_logs_mcp_cluster-shepherd
assess_risk_mcp_cluster-shepherd
Why this fails
The Gemini API requires all function names to match [a-zA-Z_][a-zA-Z0-9_]*. A hyphen is not a valid character. When Gemini receives a function declaration with an invalid name, it does not return an error — it silently drops the entire tool list and returns an empty STOP response as if no tools were available.
The tool names section in the fetch interceptor output makes this obvious immediately:
Tool names sent: ['get_flux_status_mcp_cluster-shepherd', ...]
Gemini: STOP outputTokens: 0
The fix
Change the logical name in librechat.yaml to use underscores:
mcpServers:
cluster_shepherd: # was: cluster-shepherd
type: sse
url: "http://cluster-shepherd.cluster-shepherd.svc.cluster.local:8080/sse"
The Kubernetes service URL is unaffected. Only the key in mcpServers: changes — but that key becomes part of every single tool name, so it matters.
Note: any LibreChat agents you created while the hyphenated name was active will have stale tool references. You need to recreate them after the rename.
Failure mode 2: tool descriptions that trip the safety filter
After fixing failure mode 1…
Same symptom. finishReason: STOP, zero output tokens. But now the tool names are valid. Something else is wrong.
The weird part: the failure was intermittent. If I tested with a subset of tools — say, 7 — it worked fine. All 13 tools: blank response. That suggests a token budget or complexity threshold, but that’s a red herring.
Binary search
The debugging method that actually works here is binary search on the tool set. Comment out half the tools, test, find which half contains the problem, repeat. In code:
// Inside the LibreChat pod — test with N tools
const { tools } = await client.listTools();
const subset = tools.slice(0, N); // adjust N until you find the threshold
// ... run the agent with subset
After a few rounds, I isolated the problem to a single tool: run_node_cmd. Its description was:
Execute an allowlisted command on a Linux node via SSH.
MUST call assess_risk before any write/mutating command (systemctl restart/stop).
Read commands (df, journalctl, top, etc.) can run directly.
Remove that one tool — everything works. Add it back — blank response.
Why this fails
Gemini 2.5 Flash applies safety filtering to the entire function-calling context, including tool descriptions. The trigger is combinatorial: when the full set of 13 tools is present in a single request, the combined signal from descriptions that contain:
- execution verbs with remote-access framing: “Execute”, “run … via SSH”
- mutation keywords: “restart”, “stop”,
systemctl restart/stop - authority language: “MUST call X first”
…can push the request over Gemini’s safety threshold. The result is an empty STOP response. No safety block reason is surfaced. It’s indistinguishable from the function-name failure above without the fetch interceptor.
The fix
Rephrase descriptions using observability/diagnostic framing. Same meaning, different vocabulary:
// Before — triggers safety filter in combination
mcp.WithDescription(
"Execute an allowlisted command on a Linux node via SSH. " +
"MUST call assess_risk before any write/mutating command (systemctl restart/stop). " +
"Read commands (df, journalctl, top, etc.) can run directly.")
// After — safe framing, functionally identical
mcp.WithDescription(
"Run a diagnostic or observability command on a cluster node. " +
"Read-only diagnostics (df, journalctl, top, etc.) can run directly. " +
"Calls that modify node state require assess_risk first.")
Key substitutions:
- “Execute … via SSH” → “Run a diagnostic or observability command”
- “MUST call … before any mutating command (systemctl restart/stop)” → “Calls that modify node state require assess_risk first”
The agent still understands the guardrail. Gemini no longer refuses to engage.
After both fixes: it works
The full pipeline — LibreChat → Gemini → MCP SSE → real Flux API → back to Gemini → human response — verified working end-to-end inside the production pod.
What should be fixed upstream
Both failures are genuinely fixable at a lower level:
LibreChat could sanitise MCP server names when composing tool names — replace any non-[a-zA-Z0-9_] character with _ in toolname_mcp_servername, and maintain a reverse mapping for execution. The relevant code is loadToolDefinitions() in packages/api/src/server/services/ToolService.js.
LangChain google-genai could validate function names against [a-zA-Z_][a-zA-Z0-9_]* in ChatGoogleGenerativeAI.bindTools() and throw a descriptive error instead of silently sending invalid names to the API.
The Gemini API itself could return a 400 Bad Request with the invalid name rather than silently dropping the entire tool list.
None of these are fixed yet. Until they are: underscores only in MCP server names, and test your full tool set descriptions before shipping.
TL;DR
If your LibreChat + Gemini agent produces blank responses with no error:
- Check tool names: if your MCP server name in
librechat.yamlcontains a hyphen, every tool name is invalid for Gemini. Use underscores. - Check tool descriptions: if the issue is intermittent or goes away when you remove specific tools, you’re hitting the safety filter. Rephrase descriptions that combine execution verbs, SSH/remote framing, and mutation keywords.
- Fetch interceptor: the only way to distinguish these two failures (and everything else) is to inspect the raw Gemini streaming response from inside the LibreChat pod.
outputTokens: 0+STOPmeans one of these two things.
The debugging session ran on Gemini 2.5 Flash via LibreChat v0.8.4 with 13 MCP tools. Results may vary across Gemini versions — the safety filter threshold in particular is model-version-specific and will likely shift across releases.


