LIVE
Anthropic says Claude helped trace the origin of NIST's mysterious ECDSA seeds16/10/26 · Anthropic|Reflection AI unveils Beam, a 501-billion-parameter open model16/10/26 · Reflection AI|Meta releases Muse Spark Open, a mixture-of-experts model built for local Mac Studio inference15/10/26 · Meta|Meta releases Muse Spark Open, an open-weight multimodal model15/10/26 · Meta|Google DeepMind outlines safety measures behind Gemini 4 Argon13/10/26 · Google DeepMind|Tev-Gym: A Training Recipe to Fix Sparse Rewards in Long-Horizon Agents13/10/26|Google DeepMind references Gemini 4, code-named Argon13/10/26 · Google DeepMind|Google DeepMind unveils Gemini 4 Argon, a robotics-focused variant13/10/26 · Google DeepMind|Character.AI files for Chapter 11 bankruptcy, sells assets to Disney13/10/26 · Character.AI|Study Shows AI Models Can Provably Conceal Arbitrary Capabilities13/10/26|Websites are increasingly shutting out AI agents18/10/26|NVIDIA RTX Spark laptops bring local AI to premium Windows PCs17/10/26 · NVIDIA|Anthropic says Claude helped trace the origin of NIST's mysterious ECDSA seeds16/10/26 · Anthropic|Reflection AI unveils Beam, a 501-billion-parameter open model16/10/26 · Reflection AI|Meta releases Muse Spark Open, a mixture-of-experts model built for local Mac Studio inference15/10/26 · Meta|Meta releases Muse Spark Open, an open-weight multimodal model15/10/26 · Meta|Google DeepMind outlines safety measures behind Gemini 4 Argon13/10/26 · Google DeepMind|Tev-Gym: A Training Recipe to Fix Sparse Rewards in Long-Horizon Agents13/10/26|Google DeepMind references Gemini 4, code-named Argon13/10/26 · Google DeepMind|Google DeepMind unveils Gemini 4 Argon, a robotics-focused variant13/10/26 · Google DeepMind|Character.AI files for Chapter 11 bankruptcy, sells assets to Disney13/10/26 · Character.AI|Study Shows AI Models Can Provably Conceal Arbitrary Capabilities13/10/26|Websites are increasingly shutting out AI agents18/10/26|NVIDIA RTX Spark laptops bring local AI to premium Windows PCs17/10/26 · NVIDIA|
Advanced⚙️

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· Updated October 6, 2026
📖 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 #008Agentic pentesting: same model, opposite results.3 MIN

Read next