{"slug": "zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client", "title": "Zero Dependencies, 250KB, 486 Tests: What I Learned Building an MCP Client", "summary": "A developer built mcptoon, a zero-dependency CLI tool that reduces AI agent context window usage by keeping MCP tool schemas out of context. The project, which uses only Python's standard library, was motivated by the uv security incident and resulted in hand-rolled HTTP, CLI, validation, and terminal formatting code, adding significant development cost but providing deeper protocol understanding.", "body_md": "This is not a product pitch. It's an engineering diary. If you want the pitch,\n\n[the README is here]. This is about the cost of zero.\n\nSix weeks ago I started building [mcptoon](https://github.com/activeing123/mcptoon) — a CLI tool that sits between AI agents (Claude Code, Cursor, Codex) and MCP servers. The problem it solves: MCP tool schemas get injected into your context window as JSON. 255 tools = ~91K tokens of JSON braces, brackets, quotes, and commas — before any actual work happens.\n\nmcptoon keeps schemas out of context. The agent runs shell commands. Only the compact result enters context.\n\nBut none of that is what I want to talk about.\n\nI want to talk about the decision that shaped everything: **zero dependencies**.\n\n```\n# pyproject.toml\ndependencies = []\n```\n\nNot \"minimal dependencies.\" Not \"few dependencies.\" **Zero.**\n\nThe trigger was the [uv security incident](https://github.com/astral-sh/uv/issues/9423). A transitive dependency in a popular Python tool had a supply chain vulnerability. Thousands of projects were affected. Not because they did anything wrong — because someone upstream did something wrong.\n\nI looked at my own `pip install`\n\nhistory. How many packages had I installed in the last year? Hundreds. Each one pulling in its own dependency tree. How many of those dependencies had I audited? Zero.\n\nSo when I started mcptoon, I made a rule: **no third-party imports. Python standard library only.**\n\nThis sounded reasonable in theory. In practice, it meant I was about to hand-roll a lot of things.\n\n`requests`\n\n→ hand-write an HTTP client\nThe standard library has `http.client`\n\nand `urllib`\n\n. They work. But they're verbose. Here's what a POST request looks like with `urllib`\n\n:\n\n``` python\nimport json, urllib.request\n\ndef http_post(url, data, headers=None):\n    body = json.dumps(data).encode(\"utf-8\")\n    req = urllib.request.Request(\n        url, data=body,\n        headers={\"Content-Type\": \"application/json\", **(headers or {})}\n    )\n    with urllib.request.urlopen(req, timeout=30) as resp:\n        return json.loads(resp.read().decode(\"utf-8\"))\n```\n\nThat's 8 lines. With `requests`\n\n, it would be 1:\n\n``` python\nimport requests\nresp = requests.post(url, json=data, headers=headers, timeout=30)\n```\n\n**Cost: ~200 lines of HTTP plumbing** (streaming SSE, error handling, retry logic, auth). With `requests`\n\n, maybe 30 lines.\n\n**Was it worth it?** For SSE (Server-Sent Events) parsing — yes, I learned how the protocol actually works. For basic HTTP — no, it was just plumbing.\n\n`click`\n\nor `argparse`\n\nextensions → hand-write CLI parsing\nPython's stdlib `argparse`\n\nis... fine. But `click`\n\nis so much nicer. Decorators, subcommands, context, help text generation. With `argparse`\n\n, I ended up with a 400-line CLI dispatch function:\n\n``` python\ndef main():\n    parser = argparse.ArgumentParser(prog=\"mcptoon\")\n    sub = parser.add_subparsers(dest=\"command\")\n\n    # ... 15 subcommands, each with its own args ...\n\n    add_cmd = sub.add_parser(\"add\")\n    add_cmd.add_argument(\"name\")\n    add_cmd.add_argument(\"--stdio\", nargs=\"+\")\n    add_cmd.add_argument(\"--url\")\n    # ... etc for every command\n```\n\n**Cost: ~400 lines of argument parsing.** With `click`\n\n, maybe 150 lines.\n\n`pydantic`\n\n→ hand-write validation\nMCP servers return JSON. Without `pydantic`\n\n, every response is a `dict`\n\nand you validate by hand:\n\n``` python\ndef validate_tool_result(result):\n    if not isinstance(result, dict):\n        raise ValueError(\"Expected dict\")\n    if \"content\" not in result:\n        raise ValueError(\"Missing 'content'\")\n    for item in result[\"content\"]:\n        if \"type\" not in item:\n            raise ValueError(\"Each content item needs 'type'\")\n        if item[\"type\"] == \"text\" and \"text\" not in item:\n            raise ValueError(\"text content missing 'text' field\")\n```\n\n**Cost: ~300 lines of validation across the codebase.** With `pydantic`\n\n, models would self-validate.\n\n`rich`\n\n→ hand-write terminal formatting\nThis one actually surprised me. I didn't need `rich`\n\n. ANSI escape codes work fine:\n\n``` python\ndef bold(text): return f\"\\033[1m{text}\\033[0m\"\ndef green(text): return f\"\\033[32m{text}\\033[0m\"\ndef dim(text): return f\"\\033[2m{text}\\033[0m\"\n```\n\n**Cost: ~50 lines.** Not bad.\n\n`pytest`\n\nplugins → plain `unittest`\n\n-style tests\nActually, I do use `pytest`\n\nas a dev dependency (in `[project.optional-dependencies]`\n\n). But no `pytest-mock`\n\n, no `pytest-cov`\n\n, no `responses`\n\n, no `httpx`\n\nfor mocking. Just `unittest.mock`\n\n:\n\n``` python\nfrom unittest.mock import patch, MagicMock\n\n@patch(\"mcptoon.client.MCPClient._stdio_request\")\ndef test_call_tool(mock_request):\n    mock_request.return_value = {\"result\": {\"content\": [{\"type\": \"text\", \"text\": \"hello\"}]}}\n    client = MCPClient(stdio=[\"echo\", \"test\"])\n    result = client.call_tool(\"search\", {\"q\": \"test\"})\n    assert result[\"content\"][0][\"text\"] == \"hello\"\n```\n\n**Cost: More verbose test setup.** But 486 tests still run in 0.5 seconds because there are no heavy fixtures.\n\n``` bash\n$ pip install mcptoon\n# Downloaded 250KB. Installed in 0.3s.\n```\n\nFor comparison, a typical MCP client with `requests`\n\n, `pydantic`\n\n, `click`\n\n, `rich`\n\n:\n\n`requests`\n\n+ its deps: ~5MB`pydantic`\n\n+ its deps: ~15MB`click`\n\n: ~200KB`rich`\n\n: ~5MBmcptoon is **1% of that**.\n\n``` bash\n$ pip audit mcptoon\n# No vulnerabilities found.\n# (Because there's nothing to audit beyond stdlib.)\n```\n\nWhen the next supply chain attack hits npm or PyPI, mcptoon users are unaffected. Not because I was clever — because there's nothing to attack.\n\nMost Python CLI tools are developed on macOS/Linux and \"should work on Windows.\" With zero dependencies, there are no platform-specific binary wheels to worry about. No `uvloop`\n\nthat doesn't support Windows. No `uvicorn`\n\nworker model differences. Just `sys.platform`\n\nchecks for `.cmd`\n\nvs binary names:\n\n``` python\ndef _resolve_cmd(cmd):\n    if sys.platform == \"win32\" and not cmd[0].endswith(\".cmd\"):\n        if shutil.which(cmd[0] + \".cmd\"):\n            cmd = [cmd[0] + \".cmd\"] + cmd[1:]\n    return cmd\n```\n\nmcptoon works on Windows, macOS, and Linux. Not \"should work\" — \"tested on all three.\"\n\n``` bash\n$ time pip install mcptoon\n# real    0m0.3s\n\n$ time pip install <competitor-with-20-deps>\n# real    0m12.4s\n```\n\nWhen your CI runs 1000 times a day, 12 seconds per install adds up.\n\nWhen someone reads your source and sees `import json, subprocess, urllib.request, argparse`\n\n— they understand it. There's no `import magical_toolkit`\n\nthat does something opaque. The entire codebase is readable by anyone who knows Python.\n\nThis matters for adoption. Developers who care about security (and MCP users tend to) can audit your code in an afternoon. They don't need to audit 30 transitive dependencies.\n\nI'm not going to pretend zero dependencies is always the right choice. Here's when it hurts:\n\n**When you're building a web app.** You need a router, a template engine, a database ORM, session management. Hand-writing all of these is insane. Use Django, FastAPI, Flask.\n\n**When the problem is already solved well.** `json`\n\nparsing? Use stdlib. HTTP/2? Use `httpx`\n\nor `h2`\n\n— the protocol is complex enough that a hand-rolled implementation will have bugs.\n\n**When your team is larger than one.** Zero dependencies means everyone needs to understand the entire stack. With libraries, you can treat them as black boxes. That scales better with team size.\n\n**When you need to move fast.** Zero dependencies means writing more code. More code means more bugs. If you're racing to market, use libraries.\n\nFor mcptoon, it was the right choice because:\n\nThis is going to sound like a motivational poster. Bear with me.\n\nWhen you use `requests.post()`\n\n, you don't think about:\n\nWhen you hand-write HTTP, you have to understand all of it.\n\nWhen you use `pydantic`\n\n, you don't think about:\n\n`None`\n\nvs missingWhen you hand-write validation, you own all of it.\n\nWhen you use `click`\n\n, you don't think about:\n\n`sys.argv`\n\nWhen you hand-write CLI parsing, you understand your own interface.\n\nI'm not saying you should never use libraries. I'm saying: **if you've never built something with zero dependencies, you should try it at least once.** The things you learn about the tools you use every day are worth the extra code.\n\nAfter six weeks of zero-dependency development:\n\n| Metric | Value |\n|---|---|\n| Source size | ~250KB |\n| Lines of code | ~6,400 |\n| Tests | 486 |\n| Test runtime | 0.5s |\n| Dependencies | 0 |\n| Install time | 0.3s |\n| GitHub stars | 177 |\n| PyPI versions | 8 (v0.1.0 → v0.5.1) |\n| Security vulnerabilities | 0 |\n\nThe most surprising number is the test runtime. 486 tests in 0.5 seconds. No fixtures to load, no mocking frameworks to initialize, no database to set up. Just pure Python functions. I can run the entire test suite before my terminal even finishes rendering the prompt.\n\nmcptoon v0.5.1 just shipped with `mcptoon serve`\n\n(stdio bridge mode) and `mcptoon demo`\n\n(zero-config one-command experience). The project is at 177 stars and growing.\n\nThe zero-dependency rule stays. It's not just an engineering decision — it's a promise to users: **when you install this tool, you get exactly what you see. No hidden code. No transitive surprises. No supply chain.**\n\nIf that resonates with you:\n\n```\npip install mcptoon\n```\n\nOr [read the source](https://github.com/activeing123/mcptoon). It's 250KB. You can audit it in an afternoon.\n\n*This is an independent project. Not affiliated with Anthropic. Apache 2.0 licensed. If you found it useful, a GitHub star helps others find it.*", "url": "https://wpnews.pro/news/zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client", "canonical_source": "https://dev.to/mcptokensaver/zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client-439i", "published_at": "2026-08-20 02:34:23+00:00", "updated_at": "2026-08-20 02:42:59.298239+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-infrastructure"], "entities": ["mcptoon", "Claude Code", "Cursor", "Codex", "MCP", "uv", "astral-sh"], "alternates": {"html": "https://wpnews.pro/news/zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client", "markdown": "https://wpnews.pro/news/zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client.md", "text": "https://wpnews.pro/news/zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client.txt", "jsonld": "https://wpnews.pro/news/zero-dependencies-250kb-486-tests-what-i-learned-building-an-mcp-client.jsonld"}}