CLAUDE LABJP
MCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructureEXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioningADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applicationsQUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the windowPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days outFIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attributionMCP — Support for the 2026-07-28 spec is rolling out across Claude. The protocol moves from bidirectional and stateful to request/response, so MCP servers can now live on serverless and edge infrastructureEXTENSIONS — Three official extensions have landed: MCP Apps for server-rendered UI, Tasks for async and long-running work, and Enterprise Managed Auth for IdP-based org-wide provisioningADOPTION — MCP passed 400 million monthly SDK downloads, roughly 4x growth this year, settling into its role as the standard way to connect agents to applicationsQUOTA — Today, August 19, is the last day of the 50 percent weekly usage boost for Claude Code subscribers. If you have long agent runs queued, this is the windowPRICING — Claude Sonnet 5's introductory rate of $2 per million input tokens and $10 output ends August 31; standard pricing of $3 and $15 takes over on September 1, twelve days outFIX — A bug where MCP v2 connections endlessly reopened subscriptions against servers with fixed timeouts is resolved, and a forward_user_identity setting was added for user attribution
Articles/Claude Code
Claude Code/2026-07-29Advanced

The MCP Server Connects Fine and Still Hands Back Zero Tools — Catching Silent Capability Drift with a Tool Manifest Diff

A missing credential does not break the MCP handshake. initialize succeeds, tools/list succeeds, and the array comes back empty. Here is the measured behaviour, and a preflight that locks the expected tool surface and fails closed before the agent ever starts.

MCP51Claude Code225CI/CD19Unattended automationOperations16

Premium Article

I was reading through an unattended run log when something stopped me.

Exit code 0. The agent had produced a coherent summary. But the figures it was supposed to have pulled from our internal reporting server were nowhere in the output.

Not a single error line anywhere.

The cause was a missing credential. The MCP server had started, the handshake had completed, and it had offered exactly zero tools. The agent, seeing no tools available, answered from what it already knew. Nothing crashed, which is precisely why it took so long to notice.

Once you understand the protocol, this behaviour is unsurprising. It still feels like a betrayal when you are the one who handed the work over. As an indie developer running jobs overnight, nobody is watching until morning.

Connectivity and capability live on different layers

MCP startup has two meaningful steps. initialize negotiates protocol version and capabilities. tools/list then reports what can actually be called.

Credentials usually get consulted at or after that second step. What a server does when authentication is absent is left to whoever wrote it:

  • return an error and refuse the connection
  • keep the connection and return an empty tool list
  • advertise the tools and fail only when one is invoked

The second option is the dangerous one. Nothing about it violates the protocol. The JSON-RPC response carries result, not error. Your exception handler has nothing to catch.

And Claude Code does not halt when the available tool set shrinks. That leniency is a kindness in an interactive session. Unattended, it inverts.

Changing only the credential, and measuring

Rather than reason about it, I built the smallest server that could reproduce it — about eighty lines of dependency-free Python speaking JSON-RPC over stdio.

#!/usr/bin/env python3
"""Minimal stdio MCP server. Exposes tools only when REPORT_API_TOKEN is present."""
import json, os, sys
 
TOKEN = os.environ.get("REPORT_API_TOKEN")
 
TOOLS = [
    {"name": "fetch_daily_report", "description": "Fetch the daily sales report",
     "inputSchema": {"type": "object", "properties": {"date": {"type": "string"}}, "required": ["date"]}},
    {"name": "list_report_dates", "description": "List available report dates",
     "inputSchema": {"type": "object", "properties": {}}},
]
 
def respond(rid, result):
    sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": rid, "result": result}) + "\n")
    sys.stdout.flush()
 
def main():
    for line in sys.stdin:
        line = line.strip()
        if not line:
            continue
        req = json.loads(line)
        method, rid = req.get("method"), req.get("id")
        if method == "initialize":
            # The handshake succeeds regardless of credential state.
            respond(rid, {
                "protocolVersion": "2026-07-28",
                "capabilities": {"tools": {}},
                "serverInfo": {"name": "report-server", "version": "1.0.0"},
            })
        elif method == "notifications/initialized":
            continue
        elif method == "tools/list":
            # Unauthenticated returns an EMPTY list, not an error.
            respond(rid, {"tools": TOOLS if TOKEN else []})
        elif method == "shutdown":
            respond(rid, {})
            return
        elif rid is not None:
            respond(rid, {})
 
if __name__ == "__main__":
    main()

The probe collects handshake outcome, tools/list outcome, and the resulting tool names in a single launch.

#!/usr/bin/env python3
"""Probe an stdio MCP server: measure handshake outcome and the resulting tool surface."""
import json, os, subprocess, sys, time
 
def probe(cmd, env):
    proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE, text=True, env=env, bufsize=1)
    def call(method, params=None, rid=None):
        msg = {"jsonrpc": "2.0", "method": method}
        if params is not None:
            msg["params"] = params
        if rid is not None:
            msg["id"] = rid
        proc.stdin.write(json.dumps(msg) + "\n"); proc.stdin.flush()
        if rid is None:
            return None
        return json.loads(proc.stdout.readline())
 
    t0 = time.perf_counter()
    init = call("initialize", {"protocolVersion": "2026-07-28", "capabilities": {},
                               "clientInfo": {"name": "preflight-probe", "version": "0.1"}}, rid=1)
    call("notifications/initialized")
    listed = call("tools/list", {}, rid=2)
    elapsed_ms = (time.perf_counter() - t0) * 1000
    call("shutdown", None, rid=3)
    proc.wait(timeout=5)
 
    tools = listed.get("result", {}).get("tools", [])
    return {
        "handshake_ok": "result" in init,
        "server": init.get("result", {}).get("serverInfo", {}).get("name"),
        "tools_list_ok": "result" in listed,
        "tool_count": len(tools),
        "tool_names": sorted(t["name"] for t in tools),
        "elapsed_ms": round(elapsed_ms, 1),
    }
 
if __name__ == "__main__":
    server = [sys.executable, os.path.join(os.path.dirname(__file__), "report_server.py")]
    for label, extra in (("credential present", {"REPORT_API_TOKEN": "test-token"}),
                         ("credential missing", {})):
        env = {k: v for k, v in os.environ.items() if k != "REPORT_API_TOKEN"}
        env.update(extra)
        print(f"--- {label} ---")
        print(json.dumps(probe(server, env), ensure_ascii=False))

Output from my Linux box, Python 3.10:

--- credential present ---
{"handshake_ok": true, "server": "report-server", "tools_list_ok": true, "tool_count": 2, "tool_names": ["fetch_daily_report", "list_report_dates"], "elapsed_ms": 19.8}
--- credential missing ---
{"handshake_ok": true, "server": "report-server", "tools_list_ok": true, "tool_count": 0, "tool_names": [], "elapsed_ms": 18.6}

This is where my expectation turned out to be backwards.

I had assumed a failed credential would surface as an anomaly at handshake time. Instead both handshake_ok and tools_list_ok stayed true. The only difference was tool_count: 2 against 0. Even the timing was flat — 19.8 ms versus 18.6 ms, no meaningful gap.

The failure does not arrive as slowness, and it does not arrive as an exception. It arrives as a quietly smaller number. If your monitoring watches connectivity, this difference is recorded nowhere at all.

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
Measured proof that a missing credential still passes initialize and tools/list — on stdio and on remote HTTP alike, tool count drops 2 to 0 with no error and no timing signal
A preflight that probes both transports behind one interface, enforces a deadline, and exits 78 when the observed tool surface is short
Three defects found in the verification tool itself — the position of Popen, a readline no timeout could reach, and lock generation that recorded a failed probe as the expectation
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

Claude 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

Claude Code2026-07-16
Your Overnight Session Wakes Up at 3GB — Four Places Memory Piles Up, and How to Tell Them Apart
The Claude Code process I left running overnight had grown to 3.4GB of resident memory by morning. Here are the four accumulation sources closed in 2.1.209, how to separate what's left in your own setup by sampling RSS slope, and a watchdog pattern that folds a session before it hurts.
Claude Code2026-08-19
Fixing the Code Doesn't Evict a Broken Page From the Edge Cache
I shipped a fix and the broken page kept serving. The problem was not the code but the edge cache, which had stored a broken HTML response because the store decision looked only at the status code. Here is the integrity guard I now run before every cache write, and the thresholds I had to tune in production.
Claude Code2026-08-03
Existence Checks Pass, Writes Fail — Probing Capabilities Before an Unattended Run
A directory existing and a directory being writable are two different facts. Measured results from five broken-environment cases, and a capability-probe preflight for unattended Claude Code 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
See all →