I opened Antigravity first thing in the morning and picked up where I had left off the day before. The summary that came back read well, and nothing in it made me pause.
What stopped me was scrolling back through the log a while later. There was no call to the MCP server I had connected — not a single one. Initialization had failed at startup, and I had been working for a good stretch without noticing.
The same state shows up on the forum. In a report of MCP servers failing to initialize on every launch, the connection is only created after a manual refresh, and the server itself runs fine when started on its own.
The hard part isn't the fix. It's noticing — and noticing early — that the session began without the tools you assumed were there.
The agent will not tell you something is missing
When MCP tools fail to load, nothing halts. The agent shifts to whatever else it has — web search, a file scan, general knowledge it already carries — and hands you something plausible.
The output looks fine, so you don't question it. For a while I treated "an answer came back" as proof that the connection was live. That didn't serve me well. I later found that half a day of work had been built on sources I had never actually read.
A gap is visible only when a tool fails. When the agent quietly substitutes something else, no failure event ever occurs. The absence of errors in the log didn't mean things were healthy; it meant nothing had been attempted.
Opening the config file at this point usually turns up nothing. The syntax is valid and the server starts by hand. A static check of the config takes about forty lines before startup, but this symptom lives one step further along — in whether the handshake actually completed.
Ask for tools/list yourself and read the names
There is one reliable way to check: don't trust the editor's display. Connect to the MCP server yourself and read the tool names that come back.
MCP is JSON-RPC over stdio, so you send initialize, reply with notifications/initialized, then send tools/list — and the tools that are genuinely visible come back as an array of names. The script below does this for every server in your config file and exits with code 1 if a required tool is absent.
// mcp-readiness.mjs — connect to the configured servers and print the tools they actually expose
// Usage: node mcp-readiness.mjs <mcp_config.json> [required tool names ...]
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
const [configPath, ...required] = process.argv.slice(2);
const config = JSON.parse(readFileSync(configPath, 'utf8'));
const servers = config.mcpServers ?? config.servers ?? {};
const PROTOCOL_VERSION = '2025-06-18';
const TIMEOUT_MS = 8000;
function listTools(name, spec) {
return new Promise((resolve) => {
const child = spawn(spec.command, spec.args ?? [], {
env: { ...process.env, ...(spec.env ?? {}) },
stdio: ['pipe', 'pipe', 'pipe'],
});
let buffer = '';
let settled = false;
const done = (result) => {
if (settled) return;
settled = true;
child.kill();
resolve({ name, ...result });
};
const timer = setTimeout(
() => done({ ok: false, reason: `no response within ${TIMEOUT_MS}ms` }),
TIMEOUT_MS,
);
child.on('error', (e) => { clearTimeout(timer); done({ ok: false, reason: e.message }); });
const send = (msg) => child.stdin.write(JSON.stringify(msg) + '\n');
child.stdout.on('data', (chunk) => {
buffer += chunk;
let index;
while ((index = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, index).trim();
buffer = buffer.slice(index + 1);
if (!line) continue;
let msg;
try { msg = JSON.parse(line); } catch { continue; } // some servers interleave plain log lines
if (msg.id === 1) {
send({ jsonrpc: '2.0', method: 'notifications/initialized' });
send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
}
if (msg.id === 2) {
clearTimeout(timer);
done({ ok: true, tools: (msg.result?.tools ?? []).map((t) => t.name) });
}
}
});
send({
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: PROTOCOL_VERSION,
capabilities: {},
clientInfo: { name: 'mcp-readiness', version: '1.0.0' },
},
});
});
}
const results = await Promise.all(
Object.entries(servers).map(([name, spec]) => listTools(name, spec)),
);
const seen = new Set();
for (const r of results) {
if (r.ok) {
r.tools.forEach((t) => seen.add(t));
console.log(`OK ${r.name} ${r.tools.length} tools: ${r.tools.join(', ')}`);
} else {
console.log(`NG ${r.name} ${r.reason}`);
}
}
const missing = required.filter((t) => !seen.has(t));
if (missing.length > 0) {
console.log(`\nMissing required tools: ${missing.join(', ')}`);
process.exit(1);
}
console.log('\nAll required tools are present');A few of those choices came from being bitten. Printing names rather than a count matters because a server can be up while exposing fewer tools than it did on an earlier version. "Three are visible" isn't enough; you need to know whether the one you depend on is among those three.
The catch { continue; } around JSON parsing is there for the same practical reason. Some servers interleave human-readable log lines on startup, and if you can't skip a line, the very first one ends the check.
I settled on an eight second timeout because three seconds wasn't enough for servers that fetch dependencies on first run, while thirty seconds meant I learned far too late that something wasn't coming up at all.
Three branches, decided in advance
Having a way to check doesn't help much if you reason it out from scratch each time. I decided the branches ahead of time.
| Situation | What I do | What it costs |
|---|---|---|
| A manual reload brings the names back | Carry on — after confirming by name a second time, not by the fact that a reload happened | A few dozen seconds |
| One server still won't come up after a reload | Stop phrasing requests around that one tool; fetch what I need myself and paste it in | More manual work, but a source I can point at |
| A write-capable or permissioned server won't come up | I don't start that work in this session. I fix the configuration first | One item slides to another day |
The axis I split on is whether the tool fetches or writes.
Against my expectations, the fetching side turned out to be the dangerous one. A missing write tool announces itself: nothing changes, and you notice immediately. A missing read tool gets replaced by something plausible, and that substitute flows straight into the deliverable. Tracing provenance backwards afterwards cost far more than the work itself.
So my line now is that if even one read-side tool is absent, I stop that task. It isn't a judgment about the agent's ability. I simply have no grounds to trust the result.
Put the stop signal on your side of the process
What I settled on after that morning was this: don't delegate the check to the agent. Asking "can you see the MCP tools?" returns an answer bounded by what it can see. What it can't see stays invisible.
Whether the tools are there is a question for my own process, not for the agent. That is the one line I try not to cross even on a rushed day.
The practice itself is plain. I run the Lab sites on a daily automated update, and the rule there has the same shape — if the data a step depends on comes back empty, it writes one line to the log and stops. Continuing in silence always finds its way back to me later. MCP is no different: deciding in advance that a non-zero exit from mcp-readiness.mjs means "don't rely on that server today" removed the hesitation entirely.
Narrowing what you hand the agent in the first place also shortens what you have to verify. Paired with scoping the MCP tools an agent receives, the morning check takes about thirty seconds.
What to do tomorrow morning
Run the script once against your own config file. You don't need to pass any required tool names yet — just read the list that comes back.
If the names you expected are there, that's your grounds for trusting the session. If they aren't, today was a day to postpone that particular task — and knowing that alone protects the next several hours.
Thank you for reading. If it makes someone else's first thirty seconds of the morning a little lighter, I'm glad.