Building AI Agents with Open Source Models
Track 2: Tool Use OS Track · Module 7
⏱ 65 min 📊 Intermediate
🦎
Open Source Track — M07 MCP is provider-neutral. The same server you build here would plug into Claude Desktop or any MCP host — but our host is a local Ollama + Mistral-7B agent driven by the openai client. You will build the bridge that turns MCP tools into the OpenAI tool_calls your model already speaks. Everything runs offline.

M07: MCP & Rich Output

In M05 and M06 you hand-wrote tool definitions and dispatch logic inside your agent. That works — until you want the same tools in three agents, or someone else's tools in yours. The Model Context Protocol (MCP) is the open standard that turns tools, data, and prompts into reusable servers any agent can plug into. This module builds an MCP server, bridges it into a local Mistral agent, and uses it to produce rich output — including generated charts, not just text.

Learning Objectives

  • Explain the N×M integration problem and how MCP collapses it to N+M
  • Describe MCP's client–server architecture and the three things a server exposes: Resources, Tools, and Prompts
  • Choose between the stdio and HTTP+SSE transports, and explain the JSON-RPC messages underneath
  • Build a working MCP server with FastMCP (Python) exposing a tool, a resource, and a web-search capability
  • Write the bridge that maps MCP tools into OpenAI tool_calls so a local Mistral agent can use them
  • Produce rich, visual output — have a tool render a chart and return it as an image artifact
  • Connect a filesystem + chart MCP server to a Mistral agent end to end

The N×M Problem: Why MCP Exists

Before the Pain

BEFORE: Think about electronics before USB. A camera had one proprietary cable, your phone another, the printer a third, each laptop a different port. Every device maker had to support every computer maker, and every computer maker had to support every device. A drawer full of incompatible cables was the physical proof of the mess.

PAIN: Agent tooling started in exactly that drawer. You have N agents (a triage bot, a coding agent, a research assistant) and M capabilities (a database, GitHub, a filesystem, a search API). With hand-wired integrations, connecting them is N×M bespoke adapters — and worse, each is written in that agent's private dialect. Add one new capability and you re-integrate it into every agent; add one new agent and you re-wire it to every capability. The work grows like a multiplication table, and none of it is reusable.

MAPPING: USB-C fixed devices by agreeing on one connector: any device with the port works with any computer with the port. MCP is USB-C for agents. A capability is wrapped once as an MCP server that speaks the standard protocol; any agent that speaks MCP (the client) plugs into it with no custom code. Now adding a capability is +1 server that every agent can use, and adding an agent is +1 client that reaches every server. N×M bespoke adapters collapse into N+M standard plugs.

The animation makes the arithmetic visceral. On the left, every agent wires directly to every capability — the links multiply. On the right, each side connects once to the MCP standard, and the tangle becomes a hub.

N×M Tangle vs N+M Through MCP
❌ BEFORE — direct wiring
Agent A
Agent B
Agent C
DB
GitHub
Files
3 × 3 = 9 custom adapters
✅ AFTER — through MCP
Agent A
Agent B
Agent C
MCP
standard
DB
GitHub
Files
3 + 3 = 6 standard plugs
Left: 3 agents each wired directly to 3 capabilities = 9 bespoke adapters that grow as N×M. Right: each agent and each capability connects once to the MCP standard hub = 3 + 3 = 6 standard plugs that grow as N+M.
Why It Matters: The Ecosystem Effect

The N+M math is not just tidier — it unlocks a marketplace. Because an MCP server is agent-agnostic, the GitHub team can publish one server and every MCP host on earth gains GitHub access for free. Today there are hundreds of community MCP servers (Postgres, Slack, Puppeteer, filesystem, web search) you can plug into your Mistral agent without writing a line of integration code. With hand-wired tools, you would re-implement each one against your specific agent. MCP turns "integrate this capability" from a coding project into a config line.

USB-C only works because both ends agree on the shape of the plug. For MCP, that shared shape is a small client–server protocol with exactly three kinds of thing a server can offer. Understanding those three is the whole architecture.

MCP Architecture: Client, Server, and Three Primitives

Definition: MCP Host, Client, and Server

An MCP server wraps a capability (a database, a folder, an API) and exposes it over the protocol. An MCP client is the connector inside your application that talks to one server. The host is the application that owns the agent and spins up clients — in this course, your Python/Node program driving Mistral. One host can run many clients, each connected to a different server, so a single agent can reach a filesystem server, a database server, and a web-search server at once.

A server exposes its capability through three distinct primitives. Knowing which to use is most of designing a good server:

PrimitiveWhat it isWho controls itExample
ResourceRead-only data the agent can load into context, addressed by a URIApp / agent decides when to readfile:///ucc/filing-123.txt
ToolAn action the model can invoke, with typed arguments and a resultThe model decides when to callsearch_filings(query)
PromptA reusable, parameterized prompt template the server offersUser picks it (e.g. a slash command)/summarize-filing
Resource vs Tool: the Distinction That Trips Everyone

The line is data vs action. A resource is something you read — it has no side effects and the application decides when to pull it into context (like opening a file). A tool is something the model does — it may have side effects, takes arguments, and the model chooses to call it during reasoning (like running a query). "Get the contents of filing 123" is a resource; "search all filings for the ones mentioning bankruptcy" is a tool. When in doubt: if the model needs to decide to invoke it with arguments, it is a tool.

🦎 What This Means for an Open-Source Agent

Open models like Mistral support tool calling natively (you used it in M05). Resources and prompts, though, are MCP-host features — there is no "resources" field in the OpenAI chat API. So your bridge will map MCP tools directly onto tool_calls, and surface resources by reading them and injecting their text into the conversation yourself. That asymmetry is the main thing that makes an open-source MCP host different from Claude Desktop, and you will see it concretely in the bridge code.

Architecture tells you what flows between client and server. The transport tells you how those messages physically travel — and MCP gives you two options for two very different situations.

Transports & JSON-RPC

Under the hood, every MCP message is JSON-RPC 2.0A lightweight standard for remote procedure calls encoded as JSON. Each request has a method (e.g. "tools/call"), params, and an id; the response carries a matching id and either a result or an error. MCP uses it for every client–server exchange. — a tiny, well-understood request/response format. What changes is the pipe those JSON messages flow through. MCP defines two transports:

TransportHow it worksUse when
stdioThe host launches the server as a subprocess; messages flow over its stdin/stdoutThe server runs locally — a filesystem, a local DB, a script. Default for development.
HTTP + SSEThe server is a web service; requests go over HTTP, streamed responses come back via Server-Sent EventsThe server is remote or shared — a hosted API, a multi-user service, a different machine.

The animation traces a single tool call over stdio: the client asks for the tool list, the server answers, the client calls a tool, and the result comes back — four JSON-RPC messages over the subprocess pipe.

An MCP Exchange Over stdio (JSON-RPC)
🤖
Client: list tools
tools/list
📡
Server: here they are
[search, read]
Client: call tool
tools/call search
📦
Server: result
{matches: [...]}
Client sends tools/list; server returns the available tools; client sends tools/call for "search" with arguments; server runs it and returns the result. All four are JSON-RPC 2.0 messages flowing over the server subprocess's stdin/stdout.
What Just Happened?

You saw the entire MCP interaction model in four messages: discover what a server offers (tools/list), then invoke one (tools/call). The transport (stdio here) is just the pipe; the conversation is always JSON-RPC. The good news for you as a builder: a framework like FastMCP hides every byte of this. You write a normal Python function, decorate it, and the protocol marshalling is handled. Let's build one.

Build an MCP Server

The fastest path in Python is FastMCPThe high-level server API in the official `mcp` Python package. You decorate plain functions with @mcp.tool() or @mcp.resource(); FastMCP infers the JSON schema from type hints and handles all JSON-RPC plumbing., the high-level API in the official mcp package. You write ordinary functions; decorators turn them into protocol-correct tools and resources, inferring the argument schema from your type hints. Here is a server exposing a search tool, a web-search tool, and a file resource.

WHAT: An MCP server exposing two tools (filing search, web search) and one file resource
WHY: Decorators + type hints become the JSON-RPC tool schema — you write functions, not protocol
GOTCHA: Tool docstrings and type hints ARE the model's documentation — vague hints produce bad tool calls
# ucc_server.py — an MCP server.  Install:  pip install "mcp[cli]" httpx
# WHAT: Exposes tools (search_filings, web_search) and a resource (filing files)
# WHY:  FastMCP turns typed functions into protocol-correct MCP primitives
# GOTCHA: Run over stdio for local dev; the host launches this file as a subprocess
import json, pathlib, httpx
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("ucc-tools")                       # the server's name, shown to clients
FILINGS = pathlib.Path("filings")                # a folder of mock UCC filing .txt files

@mcp.tool()
def search_filings(query: str, limit: int = 5) -> list[dict]:
    """Search local UCC filings for a keyword. Returns matching filings with a snippet.

    Args:
        query: keyword to search for (case-insensitive)
        limit: max number of results to return
    """
    results = []
    for path in sorted(FILINGS.glob("*.txt")):
        text = path.read_text(encoding="utf-8")
        if query.lower() in text.lower():
            idx = text.lower().index(query.lower())
            results.append({"file": path.name, "snippet": text[max(0, idx-40):idx+60]})
        if len(results) >= limit:
            break
    return results

@mcp.tool()
def web_search(query: str, k: int = 3) -> list[dict]:
    """Search the public web for up-to-date information the model can't know.

    Args:
        query: the search query
        k: number of results to return
    """
    # AI-native web search returns clean, agent-ready snippets — not a page of HTML.
    # Swap this for any provider (Brave, Tavily, SearXNG self-hosted, etc.).
    try:
        resp = httpx.get("https://api.search.example/q",
                         params={"q": query, "k": k}, timeout=10)
        resp.raise_for_status()
        return resp.json()["results"]            # [{title, url, snippet}, ...]
    except httpx.HTTPError as e:
        return [{"error": f"web_search unavailable: {e}"}]

@mcp.resource("filing://{name}")
def read_filing(name: str) -> str:
    """Return the full text of one filing by file name (a READ-ONLY resource)."""
    path = FILINGS / name
    if not path.is_file():
        raise FileNotFoundError(f"no filing named {name!r}")
    return path.read_text(encoding="utf-8")

if __name__ == "__main__":
    mcp.run()                                    # default transport: stdio
// ucc_server.js — an MCP server.  Install:  npm i @modelcontextprotocol/sdk zod
// WHAT: Exposes a search_filings tool and a filing resource over stdio
// WHY:  The official SDK gives the same primitives as FastMCP, with zod schemas
// GOTCHA: Schemas are explicit here (zod) rather than inferred from type hints
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "node:fs";
import path from "node:path";

const server = new McpServer({ name: "ucc-tools", version: "1.0.0" });
const FILINGS = "filings";

server.tool(
  "search_filings",
  "Search local UCC filings for a keyword; returns matches with a snippet.",
  { query: z.string(), limit: z.number().default(5) },
  async ({ query, limit }) => {
    const results = [];
    for (const name of fs.readdirSync(FILINGS).sort()) {
      const text = fs.readFileSync(path.join(FILINGS, name), "utf8");
      const i = text.toLowerCase().indexOf(query.toLowerCase());
      if (i !== -1) results.push({ file: name, snippet: text.slice(Math.max(0, i - 40), i + 60) });
      if (results.length >= limit) break;
    }
    return { content: [{ type: "text", text: JSON.stringify(results) }] };
  }
);

server.resource(
  "filing", "filing://{name}",
  async (uri, { name }) => {
    const text = fs.readFileSync(path.join(FILINGS, name), "utf8");
    return { contents: [{ uri: uri.href, text }] };
  }
);

await server.connect(new StdioServerTransport());   // serve over stdio

Read the Python server in three moves — this is the whole pattern:

  1. One object, three decorators. FastMCP("ucc-tools") is the server. @mcp.tool() turns a function into a callable tool; @mcp.resource("filing://{name}") turns one into a URI-addressed resource. You never touch JSON-RPC.
  2. Type hints become the schema. query: str, limit: int = 5 is automatically published as the tool's argument schema, and the docstring becomes its description. This is not boilerplate — it is the only documentation the model gets, so precise hints and docstrings directly determine whether the model calls the tool correctly.
  3. mcp.run() speaks stdio. With no arguments it serves over stdin/stdout, ready for a host to launch it as a subprocess. The same file can serve HTTP by changing the transport — the tool code does not change.
🦎 #12 in the wild: AI-Native Web Search

Notice web_search is just another MCP tool. That is the open-source answer to "how does my offline Mistral agent reach today's web?": you do not need a model with a built-in browser — you expose search as a tool. An AI-native search API returns clean snippets (title, url, summary) instead of a wall of HTML, so the model gets agent-ready context. Self-host SearXNG for a fully offline setup, or point at Brave/Tavily. Either way the agent's interface is identical: call a tool, get back facts.

The server is running, but Mistral cannot talk to it yet — the OpenAI chat API has no idea what MCP is. The bridge is the adapter that makes them speak: it discovers MCP tools, hands them to the model as tool_calls, and routes the model's calls back to the server.

Connect It to a Local Agent: the MCP→Ollama Bridge

This is the part Claude Desktop hides and you get to build. The bridge does three jobs: (1) connect to the MCP server and list its tools, (2) translate each MCP tool schema into the OpenAI tool format Mistral expects, and (3) run the normal tool-calling loop — but dispatch each tool call to the MCP server instead of a local function.

WHAT: A bridge that lists MCP tools, exposes them to Mistral as OpenAI tools, and routes calls back to MCP
WHY: This adapter is what lets any open model use any MCP server — the glue Claude Desktop hides
GOTCHA: MCP tool schemas map to OpenAI's {"type":"function","function":{...}} shape; the reply role must be "tool"
# bridge.py — let a local Mistral agent use MCP tools.  pip install "mcp[cli]" openai
# WHAT: Discover MCP tools, expose them to Mistral, dispatch tool_calls back to MCP
# WHY:  Open models speak OpenAI tool_calls, not MCP — this is the translation layer
# GOTCHA: Tool results go back as role:"tool" messages keyed by tool_call_id
import asyncio, json
from openai import OpenAI
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

llm = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

def mcp_tool_to_openai(t) -> dict:
    # Map an MCP tool's schema onto the OpenAI tool-calling format Mistral expects
    return {"type": "function", "function": {
        "name": t.name,
        "description": t.description or "",
        "parameters": t.inputSchema,            # MCP already gives JSON Schema
    }}

async def run(user_msg: str) -> str:
    # Launch the MCP server as a subprocess and open a session over stdio
    params = StdioServerParameters(command="python", args=["ucc_server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()                       # MCP handshake
            tools = (await session.list_tools()).tools       # tools/list
            openai_tools = [mcp_tool_to_openai(t) for t in tools]

            messages = [{"role": "user", "content": user_msg}]
            for _ in range(6):                               # bounded agent loop
                resp = llm.chat.completions.create(
                    model="mistral", messages=messages, tools=openai_tools)
                msg = resp.choices[0].message
                if not msg.tool_calls:                       # model is done
                    return msg.content
                messages.append(msg)
                for call in msg.tool_calls:                  # dispatch each call to MCP
                    args = json.loads(call.function.arguments or "{}")
                    result = await session.call_tool(call.function.name, args)  # tools/call
                    text = "".join(c.text for c in result.content if c.type == "text")
                    messages.append({"role": "tool", "tool_call_id": call.id,
                                     "content": text})        # reply keyed by id
            return "stopped: hit the tool-call iteration cap"

if __name__ == "__main__":
    print(asyncio.run(run("Find filings that mention bankruptcy and summarize them.")))
// Node.js: the bridge is structurally identical, using the same two SDKs:
//   import { Client } from "@modelcontextprotocol/sdk/client/index.js";
//   import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
//   import OpenAI from "openai";
//
// 1) const transport = new StdioClientTransport({ command: "node", args: ["ucc_server.js"] });
//    const mcp = new Client({ name: "bridge", version: "1.0.0" });
//    await mcp.connect(transport);
//
// 2) const { tools } = await mcp.listTools();
//    const openaiTools = tools.map(t => ({ type: "function", function: {
//        name: t.name, description: t.description ?? "", parameters: t.inputSchema } }));
//
// 3) Run the same bounded loop you used in M06: on each msg.tool_calls,
//    const res = await mcp.callTool({ name: call.function.name,
//                                     arguments: JSON.parse(call.function.arguments) });
//    then push { role: "tool", tool_call_id: call.id,
//               content: res.content.map(c => c.text).join("") }.
//
// The async MCP plumbing is heavier in Node; this module ships the full runnable
// path in Python and the structural map here. The OpenAI tool_calls loop itself is
// exactly the one from M06 — only the dispatch target (MCP, not a local fn) changes.

The bridge is short because it reuses the loop you already know. Three things are worth pinning down:

  1. Schema translation is one function. mcp_tool_to_openai wraps the MCP tool in OpenAI's {"type":"function", ...} envelope. The crucial detail: t.inputSchema is already JSON Schema, so it drops straight into parameters. No hand-writing argument definitions — the server's type hints flow all the way to the model.
  2. The loop is the M06 loop. Call the model; if it wants no tools, return; otherwise execute the calls and append the results. The only change from M06 is the dispatch line: instead of calling a local Python function, you call session.call_tool(name, args) — a JSON-RPC round trip to the server.
  3. The reply contract is exact. Each tool result is appended as a role:"tool" message carrying the matching tool_call_id. Get that id wrong and the model loses track of which result answers which call — the most common bridge bug.
What Just Happened?

Your offline Mistral agent just used a tool it never knew about at code-time. You did not hand-write a tool definition or a dispatcher — you pointed the bridge at a server and the tools appeared. Swap in the official GitHub MCP server and the same agent gains GitHub powers with zero code changes. That is the payoff of the standard: capabilities become plug-ins. Now let's make a tool return something richer than text.

Every tool result so far has been text or JSON. But agents are not limited to words — a tool can hand back an image, and "show me the trend" becomes a chart the agent produced itself. That is rich, visual output.

Rich & Visual Output

Definition: Visual Output Generation

Visual output generation is an agent producing a visual artifact — a chart, a diagram, a rendered image — as the result of its work, not just describing one in prose. The model itself does not draw pixels; instead it calls a tool that renders the visual (here, matplotlib making a PNG) and returns it. MCP supports this directly: a tool result can be an image content block, not only text. So "plot last quarter's filings by month" ends with an actual chart, generated deterministically by code the model orchestrated.

This matters because a number in a sentence and a trend line tell a human very different stories. An agent that can answer "are UCC filings rising?" with a rendered line chart is far more useful than one that can only say "yes, somewhat." The key insight: keep the rendering in deterministic code and let the model decide when and with what data to call it. Here is a chart tool added to our server.

WHAT: An MCP tool that renders a bar chart with matplotlib and returns it as an image artifact
WHY: The model orchestrates when to chart and with what data; deterministic code draws the pixels
GOTCHA: Return an MCP Image (base64 PNG), not a file path — the host may not share your filesystem
# add to ucc_server.py — a tool that returns a CHART, not text.  pip install matplotlib
# WHAT: Render a bar chart from labelled values and return it as an MCP image
# WHY:  Lets the agent answer "show the trend" with an actual visual artifact
# GOTCHA: Use the non-interactive 'Agg' backend — there is no display on a server
import io, base64
import matplotlib
matplotlib.use("Agg")                            # headless: render to a buffer, no GUI
import matplotlib.pyplot as plt
from mcp.server.fastmcp import Image

@mcp.tool()
def make_bar_chart(labels: list[str], values: list[float], title: str = "") -> Image:
    """Render a bar chart and return it as a PNG image.

    Args:
        labels: x-axis category labels (e.g. months)
        values: the bar heights, one per label
        title: chart title
    """
    if len(labels) != len(values):
        raise ValueError("labels and values must be the same length")
    fig, ax = plt.subplots(figsize=(6, 3.5))
    ax.bar(labels, values, color="#F97316")
    ax.set_title(title)
    fig.tight_layout()

    buf = io.BytesIO()
    fig.savefig(buf, format="png", dpi=120)      # render to memory, not disk
    plt.close(fig)                               # always free the figure
    # MCP Image carries the bytes; the host renders it inline. No file path leaks.
    return Image(data=buf.getvalue(), format="png")
// Node.js: render with a headless canvas, return an MCP image content block.
//   npm i @modelcontextprotocol/sdk chartjs-node-canvas chart.js
//
//   import { ChartJSNodeCanvas } from "chartjs-node-canvas";
//   const canvas = new ChartJSNodeCanvas({ width: 600, height: 350 });
//
//   server.tool("make_bar_chart", "Render a bar chart, return a PNG image.",
//     { labels: z.array(z.string()), values: z.array(z.number()), title: z.string().default("") },
//     async ({ labels, values, title }) => {
//       const png = await canvas.renderToBuffer({
//         type: "bar",
//         data: { labels, datasets: [{ data: values, backgroundColor: "#F97316" }] },
//         options: { plugins: { title: { display: !!title, text: title } } },
//       });
//       return { content: [{ type: "image",
//                            data: png.toString("base64"), mimeType: "image/png" }] };
//     });
//
// Same idea as Python: deterministic rendering code, an image content block back.
// The model only decides WHEN to call it and with WHAT data.

The animation shows the full path: the user asks a question, the Mistral agent reasons that it needs data and a picture, calls the search tool, then the chart tool, and returns a rendered image as its answer.

Agent → MCP Tools → a Rendered Chart
User asks
"plot filings"
🤖
Mistral agent
reason → act
🔍
search_filings
MCP tool
📊
make_bar_chart
MCP tool
📈
Chart image
PNG artifact
The user asks the agent to plot filings. The Mistral agent reasons it needs data and a visual, calls the search_filings MCP tool to get counts, then the make_bar_chart MCP tool to render them, and returns a PNG chart image as its answer.
Why It Matters: Orchestrate, Don't Hallucinate

It is tempting to ask a model to "draw" things by emitting SVG or ASCII art directly. Don't — a 7B model will produce wonky, unreliable visuals and waste tokens doing it. The robust pattern is exactly the one here: the model supplies structured data and intent, and battle-tested rendering code (matplotlib, Chart.js) produces a correct, consistent image every time. The agent's job is to decide what to chart; the tool's job is to draw it right. This separation is why a small local model can still ship polished visual output — the pixels never depend on the model's drawing ability.

You have a server with data tools, web search, and a chart renderer, plus a bridge that lets Mistral drive all of them. The lab wires the whole thing into one agent you can talk to.

Lab: A Filesystem + Chart MCP Server With a Mistral Agent

What you'll build: an MCP server exposing filing search and a chart tool, plus a bridge that lets a local Mistral agent answer "search the filings about X and chart the results." Time: ~45 min. Prereqs: Ollama with mistral pulled; Python 3.10+.

mkdir mcp-lab && cd mcp-lab
pip install "mcp[cli]" openai matplotlib httpx
ollama pull mistral
mkdir filings
printf 'Debtor: Acme LLC\nSecured party: First Bank\nNote: bankruptcy risk flagged Q2.\n' > filings/f-101.txt
printf 'Debtor: Globex Inc\nSecured party: Credit Union\nNote: clean standing.\n'           > filings/f-102.txt
printf 'Debtor: Initech\nSecured party: First Bank\nNote: prior bankruptcy on record.\n'    > filings/f-103.txt
Build the server

Create ucc_server.py with search_filings, the read_filing resource, and make_bar_chart from this module. Sanity-check it standalone: mcp dev ucc_server.py opens the MCP Inspector so you can call each tool by hand before any agent is involved.

Build the bridge

Create bridge.py from the bridge section. It launches ucc_server.py as a subprocess, lists its tools, translates them to OpenAI format, and runs the bounded Mistral tool-calling loop.

Ask a text question

Run python bridge.py with the prompt "Which filings mention bankruptcy?" Confirm the agent calls search_filings("bankruptcy") via MCP and answers with f-101 and f-103 — tools it never saw at code-time.

Ask for a chart

Change the prompt to "Count filings per secured party and chart it." The agent should call search_filings to gather data, then make_bar_chart(labels=["First Bank","Credit Union"], values=[2,1]), and return a PNG. Save the image bytes and open it — visual output from a local model.

Add a second server (stretch)

Install the community filesystem MCP server and connect it alongside yours in the bridge (two sessions, merged tool lists). Confirm the same agent now reaches both servers — the N+M ecosystem effect, demonstrated.

Definition of Done

Your Mistral agent answers a text question by calling an MCP tool it discovered at runtime; it answers a "show me" question with a rendered PNG chart it orchestrated but did not draw; and you can articulate why this is N+M, not N×M — the server would plug into Claude Desktop or any other host unchanged. That is a real, standard, reusable capability layer for an open-source agent.

Knowledge Check

Test your understanding of the N×M problem, MCP architecture, transports, the bridge, and rich output.

1. What core problem does MCP solve?

A
It makes models run faster on local hardware
B
The N×M integration explosion: wrapping each capability once as a standard server turns N×M bespoke adapters into N+M standard plugs
C
It compresses prompts to save tokens
D
It replaces the need for tool calling entirely

2. What protocol do MCP client and server use to talk?

A
GraphQL
B
SOAP/XML
C
JSON-RPC 2.0 (over stdio or HTTP+SSE)
D
Raw TCP with a custom binary format

3. When should you use the stdio transport rather than HTTP+SSE?

A
When the server runs locally and the host can launch it as a subprocess (the common dev case)
B
Only when the server is on a different continent
C
Whenever the server must serve many remote users at once
D
stdio is only for binary file transfer, never tools

4. What is the difference between an MCP Resource and a Tool?

A
Resources are written in Python, tools in JavaScript
B
Tools are read-only; resources perform actions
C
They are identical; the names are interchangeable
D
A resource is read-only data the app loads into context; a tool is an action the model chooses to invoke with arguments

5. In the open-source bridge, how do MCP tools reach the Mistral model?

A
Mistral speaks MCP natively, so no translation is needed
B
The bridge maps each MCP tool's JSON Schema into the OpenAI tool_calls format, then dispatches the model's calls back via session.call_tool
C
The tools are pasted into the system prompt as plain text
D
The model downloads and executes the server code directly

6. What is the right way to give an agent visual output like a chart?

A
Ask the model to emit SVG or ASCII art directly
B
Fine-tune the model to draw pixels
C
Have the model supply structured data and intent, and call a deterministic rendering tool (matplotlib/Chart.js) that returns an image artifact
D
Charts can't be produced by a local model at all
0/6