ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-08-04Advanced

Startup Dropped to 3ms, and the First Turn Saw Zero Tools

Non-blocking MCP loading cut startup from 2,907ms to 3ms. The wait did not disappear — it moved into the first turn. Here is where it went, measured, plus a readiness gate that waits only for what a turn actually needs.

MCP22Antigravity CLI21startup timereadinessarchitecture20

Premium Article

The Antigravity CLI 1.1.9 notes carry a single line about MCP servers now loading without blocking interactive startup.

Faster startup. That seemed like the whole story.

I upgraded, restarted, and the difference was obvious right away. So far, so expected.

What caught me came a second later. I typed my usual opening — "search this directory for the config loader" — and the agent answered from its own guess instead of calling the search tool.

I typed the identical thing again. This time it used the tool.

Same prompt, same config, different behavior. The only variable was how quickly I started typing.

Did the wait vanish, or just move?

Non-blocking loading pulls server connection out of the startup path and runs it behind the scenes. The visible wait shrinks.

But nothing about that makes a handshake cheaper. The real cost of connecting and initializing is untouched.

So where does that time go?

That question is too vague to answer by feel. I turned it into something measurable.

MCP servers speak JSON-RPC over stdio. So a minimal server with configurable startup and initialize costs, plus a harness that swaps only the connection strategy, is enough to watch the wait move.

Building the harness

The mock server first. Process startup cost (imports, runtime boot) and initialize response latency are separate parameters, because in production they behave differently — one is fixed per binary, the other scales with what the server does at init.

#!/usr/bin/env python3
"""Minimal MCP-ish stdio server: newline-delimited JSON-RPC.
Simulates a real server's cold start: boot cost -> initialize cost.
usage: mock_server.py <name> <boot_ms> <init_ms> <tool_ms>
"""
import sys, json, time
 
name, boot_ms, init_ms, tool_ms = sys.argv[1], int(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
time.sleep(boot_ms / 1000.0)  # process boot / module import cost
 
for line in sys.stdin:
    line = line.strip()
    if not line:
        continue
    req = json.loads(line)
    m = req.get("method")
    if m == "initialize":
        time.sleep(init_ms / 1000.0)
        res = {"protocolVersion": "2025-06-18", "serverInfo": {"name": name}}
    elif m == "tools/list":
        res = {"tools": [{"name": f"{name}_query"}]}
    elif m == "tools/call":
        time.sleep(tool_ms / 1000.0)
        res = {"content": [{"type": "text", "text": "ok"}]}
    else:
        res = {}
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": req.get("id"), "result": res}) + "\n")
    sys.stdout.flush()

Then the harness. Three landmarks, kept deliberately distinct — collapse them and the relocation becomes invisible.

  • t_prompt — when the user can submit a first turn
  • t_ready — when every server has finished initialize and tools/list
  • t_first — when the first tool call returns

The server lineup mirrors a realistic mcp_config.json: several cheap ones, one or two expensive ones.

#!/usr/bin/env python3
import json, subprocess, sys, threading, time
 
SERVERS = [
    # (name, boot_ms, init_ms, tool_ms)
    ("fs",       120,  40,  15),
    ("github",   180, 320,  60),
    ("db",       150, 210,  35),
    ("search",   140, 890,  45),   # the expensive one
    ("chrome",   260, 480, 110),
]
 
def spawn(spec):
    n, b, i, t = spec
    return subprocess.Popen(
        [sys.executable, "-u", "mock_server.py", n, str(b), str(i), str(t)],
        stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, bufsize=1)
 
def rpc(p, method, _id):
    p.stdin.write(json.dumps({"jsonrpc": "2.0", "id": _id, "method": method}) + "\n")
    p.stdin.flush()
    return json.loads(p.stdout.readline())
 
def handshake(spec, procs, done, lock):
    p = spawn(spec)
    rpc(p, "initialize", 1)
    rpc(p, "tools/list", 2)
    with lock:
        procs[spec[0]] = p
        done.append((spec[0], time.perf_counter()))
 
def run(strategy):
    t0 = time.perf_counter()
    procs, done, lock = {}, [], threading.Lock()
 
    if strategy == "blocking":
        for s in SERVERS:
            handshake(s, procs, done, lock)
        t_prompt = time.perf_counter() - t0
    else:
        ths = [threading.Thread(target=handshake, args=(s, procs, done, lock)) for s in SERVERS]
        for t in ths: t.start()
        t_prompt = time.perf_counter() - t0     # the input box is usable immediately
        for t in ths: t.join()
 
    t_ready = time.perf_counter() - t0
 
    # aim the first tool call at the slowest server -- the realistic worst case
    t_call0 = time.perf_counter()
    rpc(procs["search"], "tools/call", 3)
    t_first = time.perf_counter() - t0
    call_wall = time.perf_counter() - t_call0
 
    for p in procs.values():
        p.stdin.close(); p.terminate()
    return dict(strategy=strategy, t_prompt=t_prompt, t_ready=t_ready,
                t_first=t_first, call_wall=call_wall)
 
if __name__ == "__main__":
    for s in ("blocking", "nonblocking"):
        rows = [run(s) for _ in range(3)]
        med = lambda k: sorted(r[k] for r in rows)[1]
        print(f"{s:12s} t_prompt={med('t_prompt')*1000:7.1f}ms  "
              f"t_ready={med('t_ready')*1000:7.1f}ms  "
              f"t_first={med('t_first')*1000:7.1f}ms  "
              f"call_wall={med('call_wall')*1000:6.1f}ms")

Three runs, median reported. A single run is dominated by process-spawn jitter.

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
Blocking vs non-blocking measured at three separate landmarks, showing that the wait relocates rather than disappears
The inverted relationship nobody expects: the faster startup gets, the emptier the tool catalogue is when the first turn lands
A complete readiness gate that blocks only on the servers a turn needs, with per-server deadlines and an explicit degraded result
Secure payment via Stripe · Cancel anytime

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

Related Articles

Integrations2026-07-19
When an Unresponsive MCP Server Freezes Your Agent: Separate Timeouts for Connect, List, and Call
Antigravity CLI 1.1.3 closed the case where an unresponsive MCP server stalls an agent forever, by adding timeouts to connect, list-tools, and call-tool. This walks through why the three boundaries fail differently, and builds a defensive wrapper with a circuit breaker and failure-only notifications, backed by working code and a week of overnight runs.
Integrations2026-08-03
I Measured Before Writing a Number: MCP Connect and Tool Calls Differed by 486x
Antigravity 2.4.3 lets you set a timeout per MCP server. To find a defensible number I built a stdio server, measured each boundary separately, and found why a single value cannot cover both.
Integrations2026-07-28
Three Tools Named read_file: Catching MCP Name Collisions Before Startup
Bundle enough MCP servers and tool names collide quietly. Here is what a real 5-server, 21-tool setup measured at 43% collision, plus a Python preflight that catches them before startup and assigns deterministic aliases.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →