◉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.

MCP29Antigravity CLI42startup timereadinessarchitecture20

✦ Premium Article

The Antigravity CLI 1.1.9 notes carry a single line about MCP servers now loading without blocking interactive startup. I'm an indie developer, and every server on my machine is one I added myself when a project needed it, so I thought I knew exactly what was connected and what was slow.

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-10-22
The Project-Local mcp_config.json Is Read, but No MCP Server Starts — Checking With a One-File Probe
Antigravity CLI logs that it is using a project-local config, yet the MCP servers in it never start. Based on reports in the official repository, here is what that log line may and may not promise, plus a tiny probe server you can use to check your own setup.
⬡ Integrations2026-10-19
Connecting GitHub MCP to Antigravity CLI: The Fix Was One Word, url to serverUrl
To use GitHub MCP from Antigravity CLI, the url key in your config has to become serverUrl. Based on the official MCP docs, here are the remote and Docker setups, a small script that checks the file before you load it, where to keep the token, and the order to debug in.
⬡ Integrations2026-10-10
Figma MCP or Stitch MCP: Choosing by Whether the Screen Already Exists
When you turn a screen into code, which MCP should Antigravity talk to: Figma or Stitch? One question settles it: does the correct screen already exist? Here is the rule I use, a switching routine, and a small script for the days I can't decide.