agentsclimarketplace

Mcp bau

Skill andy-builds-ai/claude-code-skills/mcp-bau

A small collection of Claude Code skills for Python development.

Install
npx -y skills add andy-builds-ai/claude-code-skills --skill mcp-bau

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

When building or extending an MCP server. Build one tool at a time, and prove every tool on the test bench (plain call, then Inspector) before it enters a client config.

SKILL.md

11.3 KB, as published. Nobody here has run it

mcp-bau

When this skill triggers

When an MCP server is being built or extended — a new server from scratch, a new tool on an existing server, or a change to a tool that's already wired into a client. The trigger is MCP-specific work: FastMCP, @mcp.tool(), a claude_desktop_config.json entry, an Inspector session.

Not active for general Python building — that's bau-begleiter (its rules still apply underneath, this skill adds the MCP layer on top). Not active for reading tracebacks — that's error-explainer.

The core rule

No tool enters a client config before it has passed the test bench.

The client (Claude Desktop, Claude Code) is the worst place to find a bug. Every debugging round through the client costs a config edit, a client restart, and a prompt — minutes per attempt, and the error surfaces as a vague "tool call failed" instead of a traceback. The test bench turns the same round into seconds. The client is where a finished tool gets connected, not where a raw one gets debugged.

Bau — how a server grows

Start from the smallest running server

One file, one tool, runnable. Not the full tool list sketched out first.

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("vault")

@mcp.tool()
def search_notes(query: str) -> str:
    """Search the vault for notes whose text contains the query.
    Returns the matching note titles, one per line."""
    ...

if __name__ == "__main__":
    mcp.run()

This runs, it's testable, and everything after it is repetition of the same loop: one tool → test bench → next tool. Five untested tools are five unknowns at once; when the client then fails, it's unclear which one broke.

The environment is a plain venv: py -m venv .venv (on Windows, python may hit the Store stub — use the py launcher), then pip install "mcp[cli]". The official SDK is the package mcp; the similarly named community package "fastmcp" is a different project.

One tool, one job

The bau-begleiter rule, applied to tools: if the tool's description needs an "and", it's two tools. search_and_summarize_notes is a search tool and a summarize tool glued together — the model can compose two small tools itself, but it can't take a fat one apart. Small tools also fail small: when something breaks, the bench call that reproduces it is one line.

The docstring is the interface

A human reads your code — the model reads only the tool name, the docstring, and the parameter schema. That's everything it gets when deciding whether and how to call the tool. So the docstring is written for the model, not as a comment:

  • First sentence: what the tool does. One verb, concrete. "Search the vault for notes whose text contains the query."
  • What comes back. "Returns the matching note titles, one per line." Without this, the model guesses at the output format and mis-parses it.
  • When to use it — if it's not obvious. Two similar tools need a distinguishing sentence each, otherwise the model picks one at random.

The parameter schema comes from the type hints. query: str becomes a string parameter; a missing hint becomes a vague schema the model fills with junk. Every parameter gets a precise type hint, always.

Config and secrets live next to the script, not in it

Two rules, both learned the hard way:

  • Load .env relative to the script file, never the working directory. The client starts the server with a foreign cwd — a cwd-relative .env works in your terminal and dies in the client:

    from pathlib import Path
    from dotenv import load_dotenv
    
    load_dotenv(Path(__file__).parent / ".env")
    
  • Secrets stay in .env, and out of every output channel. Whatever is secret (an IP, a key, a token) appears in .env (gitignored) and nowhere else — not in code, not in commits, not in tool responses, and not in logs. Watch the last one: HTTP libraries log full request URLs at INFO level, secret included. Silence them deliberately:

    import logging
    logging.getLogger("httpx").setLevel(logging.WARNING)
    

stdout is the wire

An MCP server on stdio transport speaks JSON-RPC over stdout. Any print() corrupts the protocol — the client reads your debug line, fails to parse it as JSON, and the connection dies with an unhelpful error. Debug output goes to stderr:

import logging
logging.basicConfig(level=logging.INFO)  # logging writes to stderr by default
log = logging.getLogger("vault")

This is the single most common way a server that "worked on the bench" dies in the client — a leftover print() on a code path the bench didn't hit.

Prüfstand — the two bench stages

Stage 1: the plain call

The tool function is an ordinary Python function — @mcp.tool() doesn't take that away. Before the protocol enters the picture, call it directly with real values, exactly like bau-begleiter says:

>>> search_notes("bitcoin")
'Bitcoin Guardian\nBitcoin Guardian Agent'
>>> search_notes("")
ValueError: query is empty
>>> search_notes("xyzzy-no-match")
''

One normal value, one edge value, one miss. If the logic is wrong, it shows up here as a readable traceback — not later as a failed tool call. Only a function that survives stage 1 moves to stage 2.

Stage 2: the Inspector

Stage 1 proved the logic; stage 2 proves the protocol side — what a client will actually see. The MCP Inspector runs the server and gives a browser UI to call it (from the venv; it needs Node.js for the UI):

mcp dev server.py

Two checks per tool:

  1. The listing. Open the tool list and read name, description, and schema as the model will read them. Is the description still accurate? Does the schema show the right parameters with the right types? This is the only place to review the model's view of your tool before a model uses it.
  2. The call. Invoke the tool through the Inspector with the same three values as stage 1. Same results? Then serialization, schema, and transport are fine. A tool that passes stage 1 but fails stage 2 has a protocol problem (usually a return type that doesn't serialize, or a print() on the wire).

Only then: the client

The tool passed both stages — now it earns its config entry. Three things break here that the bench can't catch, so they get checked deliberately:

  • Quit the client completely before editing its config. A running Claude Desktop holds claude_desktop_config.json in memory and writes it back whenever one of its own settings changes — silently overwriting your edit. Quit it from the tray (not just the window), then edit, then start it again. Written is not loaded: the entry takes effect only on the next full start.
  • Absolute paths, explicit interpreter. The client starts the server from its own working directory with its own environment. "command": "python" finds the wrong Python; a relative path to server.py finds nothing. Full path to the venv's interpreter, full path to the script, backslashes escaped.
  • One smoke prompt per tool. After the client restart, one prompt that forces the tool: "Search my vault for bitcoin." If the model calls the tool and uses the result, the chain is closed. That's a smoke test, not a debug session — anything deeper goes back to the bench.

Gotchas

Building the whole tool list before the first connection. Five tools feel like progress; five untested tools are five unknowns. When the client then shows "tool call failed", the search space is the whole server. How to avoid: the loop is one tool → stage 1 → stage 2 → next tool. The client config comes after the first tool, not after the last — the earlier the chain is proven end to end, the cheaper every later tool is.

Debugging through the client. The error appeared in Claude Desktop, so the fix gets tested in Claude Desktop. What happens: every attempt costs edit + restart + prompt, and the client hides the traceback. How to avoid: reproduce the failing call on the bench first — stage 1 if the logic is suspect, stage 2 if the protocol is. Fix it there, then one smoke prompt in the client to confirm.

Editing the client config while the client runs. The entry is written, the file looks right — and minutes later it's gone, because the running client flushed its in-memory copy over your edit. How to avoid: quit the client completely first; verify the entry is still there after the next start.

A print() left on an untested code path. The bench calls went through the happy path; the error branch still has a debug print(). In the client, the first error kills the whole connection. How to avoid: no print() in a stdio server, period — set up logging in the first five lines and use it everywhere, including quick debug output.

Docstrings written for humans. "Helper for note access" reads fine in code review and tells the model nothing. What happens: the model skips the tool, picks the wrong one, or passes junk arguments. How to avoid: read your tool listing in the Inspector (stage 2, check 1) and ask — could a stranger decide from this text alone when to call this and what they'll get back?

"Works on the bench" treated as "works in the client". The bench runs in your shell — your directory, your venv, your PATH. The client starts the server with none of that. What happens: ModuleNotFoundError or "server disconnected" only in the client. How to avoid: treat the config entry as its own step with its own checks (quit first, absolute paths, explicit interpreter, smoke prompt) — it's the third stage, not a formality.

Example run

The user starts the Obsidian-vault MCP server and writes the first tool, search_notes.

Bau. Smallest running server (the snippet above), one tool, docstring with all three parts: what it does, what it returns, query: str typed. The vault path comes from a .env loaded relative to the script. A second idea comes up while writing — "it could also return the note contents" — and gets parked as a separate future tool read_note(title) instead of growing this one.

Stage 1. Three plain calls in the console:

>>> search_notes("bitcoin")
'Bitcoin Guardian\nBitcoin Guardian Agent'
>>> search_notes("")
ValueError: query is empty
>>> search_notes("xyzzy")
''

Expected, expected, expected. On to the protocol.

Stage 2. mcp dev server.py, Inspector opens. The listing shows search_notes with the docstring and a required string parameter query — the model's view checks out. The same three calls through the Inspector UI return the same results. One thing surfaces: a leftover print(f"searching {query}") from stage 1 garbles the connection on the first call. It becomes log.info(...), restart, green.

Client. Claude Desktop is quit completely, then the config entry: absolute path to the venv's Python and to server.py. Start Desktop, one smoke prompt: "Search my vault for bitcoin." The model calls search_notes, gets two titles back, answers with them. Chain closed.

End result: one tool, proven at three levels — logic, protocol, client. The next tool (read_note) now runs through the same loop in a fraction of the time, because the chain around it is already known-good.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.