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.

MCP25Antigravity CLI34startup 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 $15 for lifetime access
View Membership →

Related Articles

Integrations2026-08-22
Adding MCP Servers Without Opening the Config File: The agy mcp Subcommand
Antigravity CLI 1.1.16, released on August 20, adds an mcp subcommand so you can register and toggle MCP servers without hand-editing JSON. Here is how to add stdio and HTTP servers, isolate a misbehaving one, and script your whole setup so it travels to a new machine.
Integrations2026-08-20
Find the one broken MCP entry before Antigravity starts, with 40 lines of Node
A single typo in your MCP settings can take every server down with it. Here is a small preflight script that catches the mistakes statically, the real output from a deliberately broken config, and the duplicate-key trap that JSON hides from you.
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.
📚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