LIVE
Breaking Claude Code Opus 5 Auto Mode31/08/26 · Anthropic|A milestone in expanding access to AI31/08/26 · OpenAI|Claude Session URL appended to commit messages and PR descriptions by default30/08/26 · Anthropic|Vuk97/forward-implementation-first: Stop your coding agent from stalling real work on self-invented bookkeeping - receipts, hashes, locks, certification rituals. Ship first, then verify. Skill for Claude Code, Codex, and other agents.30/08/26 · Anthropic|useagenthq/useagent: Hand off the work. Get back the result. The open-source AI coworker for your team: agents with their own cloud computer, your tools and context, handing back finished work - websites, decks, spreadsheets, reports, PRs. Runs Claude Code, Codex, OpenCode on your subscription.29/08/26 · Anthropic|Good Culture Is the Biggest Productivity Hack, Not AI29/08/26|Debian votes to allow "responsible use of generative AI"29/08/26|Breaking Claude Code Opus 5 Auto Mode31/08/26 · Anthropic|A milestone in expanding access to AI31/08/26 · OpenAI|Claude Session URL appended to commit messages and PR descriptions by default30/08/26 · Anthropic|Vuk97/forward-implementation-first: Stop your coding agent from stalling real work on self-invented bookkeeping - receipts, hashes, locks, certification rituals. Ship first, then verify. Skill for Claude Code, Codex, and other agents.30/08/26 · Anthropic|useagenthq/useagent: Hand off the work. Get back the result. The open-source AI coworker for your team: agents with their own cloud computer, your tools and context, handing back finished work - websites, decks, spreadsheets, reports, PRs. Runs Claude Code, Codex, OpenCode on your subscription.29/08/26 · Anthropic|Good Culture Is the Biggest Productivity Hack, Not AI29/08/26|Debian votes to allow "responsible use of generative AI"29/08/26|
AdvancedNew⚙️

Build your minimal MCP server: the loop closes

Step 7/9 of the Skills & MCP learning path. The two tools of our connecting thread in line-by-line annotated Python: server structure, trigger docstrings, write guardrails, errors designed for the model — and the skill + MCP loop finally complete.

16 min readPublished August 31, 2026 · today
📖 SKILLS the expertise manual the skill from step 3 says HOW ✓ acquired 🧠 MODEL decides and orchestrates reads the manual, works the hands, writes the summary context window 🤚 MCP the hands your server from step 7 supplies WHAT and DOES ◉ you are here — building reads calls result 🗺️ The path map — step 7/9: the loop closes For the first time, everything is lit: the manual, the brain, the hands — and you built all of it.

What we are building — and why it is short

The requirements fit in two lines, inherited from our thread: give the model the means to fetch the raw notes of a meeting, and to distribute the produced summary. Two tools, not one more — you know the rule since step 6: every tool is a privilege, and privileges must be justified.

⚙️ The minimal server's architecture 🧠 Host model + summary skill launches the server as a child process 🤚 notes_server.py 🔧 get_meeting_notes 🔧 send_summary 🗄️ Notes the note-taking tool 📮 Delivery the team's messaging stdio read write ⚠️ One tool that looks, one tool that acts — and all the care goes to the second.

The language: Python, with the protocol's official SDK. Why it, for learning: its way of declaring tools is the most readable in the ecosystem — a function, a decorator, a docstring, and the SDK builds all the rest (the JSON-RPC from step 5, discovery, schemas). The concept being identical in TypeScript and elsewhere, what you learn here transposes as is.

The skeleton: ten lines that make a server

"""The thread's MCP server: meeting notes + delivery."""
import os
from mcp.server.fastmcp import FastMCP

# The name the host and the logs will see (step 6: name clearly)
mcp = FastMCP("meeting-notes")

# Secrets come from the environment — never from the code.
# This is the "env" of the configuration seen at step 6.
NOTES_TOKEN = os.environ["NOTES_API_TOKEN"]

if __name__ == "__main__":
    mcp.run()   # stdio transport by default: the host runs us as a child

Three observations before going further:

That really is all. Create the server object, run it. Session negotiation, answering tools/list, JSON-RPC validation: the SDK handles it. Your work concentrates where your value is — the tools.

The secret is already in the right place. os.environ["NOTES_API_TOKEN"] materializes what we have repeated since step 2: the server holds the credentials, the code does not contain them, the model will never see them. And accessing via environ[...] (rather than a silent .get(...)) makes the server crash at startup if the token is missing — a server that refuses to start misconfigured beats a server that will fail mid-task.

The transport is step 6's. mcp.run() speaks stdio: the host launches us as a child process — exactly the "local server" case of the local/remote trade-off.

Tool #1 — get_meeting_notes: the hand that looks

@mcp.tool()
def get_meeting_notes(date: str) -> str:
    """Fetches the raw notes of the meetings on a given date.

    Use it to obtain the raw material for a meeting summary,
    minutes or a meeting recap.

    Args:
        date: the meetings' date, in YYYY-MM-DD format.
    """
    notes = notes_api.fetch(date=date, token=NOTES_TOKEN)
    if not notes:
        return (f"No notes found for {date}. "
                "Check the date, or ask the user which "
                "meeting they mean.")
    return format_notes(notes)

Let's dissect — every line applies a principle already met:

The decorator builds the bridge. @mcp.tool() turns an ordinary Python function into a tool the host discovers. The function name becomes the tool name; the type annotations (date: str) become the input schema the protocol will validate at moment ② of step 5's sequence. You write Python; the SDK publishes MCP.

The docstring is the description — hence the trigger. Reread it: it says what the tool does, when to use it, in the trade's words ("meeting summary", "minutes", "recap"). It is exactly the step-② method from step 3, applied to tools: the model picks its tools by reading their descriptions, just as it picks its skills. A vague docstring produces a tool never called — or called at the wrong time. The gate test applies here too, word for word.

The empty case is an answer, not an error. "No notes for this date" is not a failure: it is information, phrased so the model knows what to do next (check the date, ask the user). Remember the move — it becomes central two sections down.

Tool #2 — send_summary: the hand that acts

ALLOWED_RECIPIENTS = ("team@example.com", "management@example.com")

@mcp.tool()
def send_summary(recipient: str, summary: str) -> str:
    """Delivers a finalized summary to one of the team's lists.

    Use only when the summary is complete and the user has
    asked for it to be sent.

    Args:
        recipient: the mailing list (among the team's lists).
        summary: the full summary, in the house format.
    """
    if recipient not in ALLOWED_RECIPIENTS:
        return (f"Recipient refused: {recipient}. "
                f"Allowed lists: {', '.join(ALLOWED_RECIPIENTS)}. "
                "Nothing was sent.")
    if len(summary) < 200:
        return ("Suspiciously short summary: send refused. "
                "Check that the full summary was produced "
                "before requesting delivery.")
    mailer.send(to=recipient, body=summary, token=MAIL_TOKEN)
    return f"Summary delivered to {recipient}."

Here is where the difference between a demo server and a server you dare to plug in is made. Two guardrails, two philosophies:

The recipient allowlist. The tool structurally refuses to send outside the planned lists — even if the model asked, even if something read in the notes pushed it there (you remember moment ④ from step 5: what enters the context can influence the model). It is least privilege applied in the code, where no instruction, no injection, no reasoning can bypass it. A skill saying "only send to the team" is a wish; an allowlist is a control — step 4's distinction, made executable.

The plausibility check. A summary under 200 characters is probably not a summary: the tool says so and does not send. Every write tool deserves its question: "what does a call I should refuse look like?" — then the refusal gets coded. It is your last net before the irreversible.

And note the docstring: "only when the summary is complete and the user has asked for it to be sent". The when is bounded in the description itself — the trigger is a guardrail too.

Errors: what the model sees when things break

The most underrated topic in MCP development. When a tool fails, its error message enters the context window — and the model reasons on it to decide what comes next. Your error message is therefore not a developer log: it is a recovery instruction for the model. Compare:

# ❌ What the model cannot exploit:
#    "KeyError: 'items' at notes_api.py line 42" — noise, plus the
#    risk of exposing paths, versions, even configuration fragments
#    into the context.

# ✅ What the model can exploit:
return ("The notes service is temporarily unreachable. "
        "Retry shortly; if it persists, tell the user — "
        "and do not invent content.")

The three rules of the well-mannered error message: it says what happened (in task language, not implementation language), it says what to do next (retry, ask the user, abort cleanly), it forbids invention ("do not invent content" — the line that prevents a summary fabricated out of thin air on an outage day). And symmetrically: never a raw stack trace, never infrastructure details — everything you put in an error ends up in the context, hence potentially in a conversation.

Plugging in your own server

Your server plugs in exactly like the ones from step 6 — you have simply moved to the other side of the counter:

{
  "mcpServers": {
    "meeting-notes": {
      "command": "python",
      "args": ["/path/to/notes_server.py"],
      "env": {
        "NOTES_API_TOKEN": "${NOTES_API_TOKEN}",
        "MAIL_TOKEN": "${MAIL_TOKEN}"
      }
    }
  }
}

And step 6's ritual applies to your own production: on host restart, check that discovery shows your two tools and nothing else; first try in read mode (get_meeting_notes on a known date); and keep the send confirmation active on send_summary — your own code deserves the same observation period as a third-party server.

The loop closes

Scroll back to the map at the top of this step: for the first time in the path, everything is lit. Let's run the thread one last time, end to end, with your pieces:

  1. "Write up this morning's meeting summary and send it to the team"
  2. The skill from step 3 loads — its description intercepted "meeting summary"
  3. The model calls your get_meeting_notes — its docstring did the rest
  4. The notes flow into the context; the model applies the house format, the edge cases, the one-page limit
  5. The model calls your send_summary — the allowlist stands guard, the delivery goes out

The manual said how (steps 2-4), the hands supplied what and did (steps 5-7), the brain orchestrated — and every link, you wrote, tested, governed. What remains is armoring the whole (step 8) and knowing where the ecosystem can spare you writing the next piece (step 9).

💡
THE concept of this step: in an MCP server, everything the model reads is design. The docstring is the trigger (same method as skill descriptions), the error message is a recovery instruction (never a stack trace), and the guardrails live in the code (allowlist, validation) — where no injection and no reasoning can bypass them. The protocol itself is the SDK's business.
📚Going deeper

For the geeks: stdio has one golden rule — stdout belongs to the protocol. With the stdio transport, the process's standard output is the JSON-RPC channel: the slightest debug print() injects text between the frames and corrupts the session — the most classic first-server bug, and the most disorienting ("it worked, I added a print, everything died"). Logs therefore go to stderr or a file: logging.basicConfig(stream=sys.stderr) from line one, and a print ban in code review. Second subtlety of the same barrel: the libraries you import may themselves write to stdout — a chatty SDK, a progress bar — and break the session without a single line of your code at fault; at the first erratic behavior, audit what your dependencies print. Third reflex: the input schemas generated from your type annotations are your first validation, not your last — the type says "it is a string", your code must still say "it is a plausible date". Type-check at the edge, business logic at the center.

📚Going deeper

For the geeks: testing a server a model consumes. Three storeys, fastest to fullest. Storey 1 — the bare functions: your tools are Python functions; test them as such (pytest, nominal case, empty case, guardrail-refused case) without starting any server — this is where the allowlist is verified, in three assertions. Storey 2 — the server in isolation: the protocol's official inspection tool (the Inspector) connects to your server and shows you exactly what a host would see — discovered tools, their schemas, their descriptions — and lets you call each tool by hand; it is MCP's counterpart of curl against an API, and the right place to reread your docstrings with the model's eyes. Storey 3 — triggering in real conditions: step 3's grid applies to tools — phrasings that must trigger the call, neighboring phrasings that must not, and the collision case if several servers expose similar tools. A server that passes all three storeys plugs in serenely; a server tested only "by hand in the conversation" will be debugged in production.

The four pitfalls of the first server: 1. ❌ The print() on stdio — stdout belongs to the protocol: a single debug print corrupts the session; logs go to stderr 2. ❌ The secret in the code — a hardcoded token will end up in Git: the environment, always (and the server must refuse to start without it) 3. ❌ The developer docstring — "Fetches notes via the API" triggers nothing: the docstring is written for the model, what + when, trade words 4. ❌ The guardrail in the skill — "only send to the team" as an instruction is a wish; the allowlist in the code is a control — the irreversible is protected server-side

📍 Skills & MCP path — step 7/9

  1. 🗺️ The map before the territory
  2. 🔬 Anatomy of a skill
  3. 🛠️ Create your first skill
  4. 🏛️ Skills in the enterprise: governance
  5. 🔌 MCP: the protocol explained
  6. Use an existing MCP server
  7. ⚙️ Build your minimal MCP server ← you are here
  8. 🛡️ Secure your MCP servers
  9. 📡 The ecosystem: where to find, where it moves

Next step → Secure your MCP servers: the full attack surface — indirect injection, confused deputy, exfiltration by chaining — and the countermeasures, from least privilege to supervision. The step your CISOs will read first.

Your server is running? The AI agent security sheet gives you a 90-second foretaste of step 8.

Tags
skillsmcpagentsparcours-skills-mcppythondeveloppement
⚡ FICHE #005Skills & MCP: the fiche that maps the whole path2 MIN

Read next