Skip to content

    Forward Deployed Engineer

    An MCP Server for the Product Catalog

    Expose the capstone's three read-only catalog tools through the Model Context Protocol: tool definitions, the two error kinds, per-request access and trust.

    By Published 14 min read

    Share

    This lesson is part of Forward Deployed Engineer: Agents, Tools and Guardrails. Enroll for free, then sign in, to read it in full.

    What you'll learn: what an MCP server is and what it must provide, how the capstone's three read-only catalog tools map onto the protocol's tools/list and tools/call messages, which two kinds of error the protocol distinguishes, and how access control and trust work when the caller is another vendor's application. The mcp package is not installed in this course's environment, so the server wrapper below is pseudocode and was never run. The mapping code that is shown was tested, but no MCP client or SDK has checked it.

    At a glance

    Figure 1. The catalog tools behind a protocol boundary

    A Client app speaks MCP to a Tool server that checks the caller with a Policy engine and reads the catalog; the quote Approval service is not exposedClient app to Tool server (tools/call); Tool server to Policy engine (who is asking); Policy engine to Catalog (filtered read); Tool server to Approval service (not exposed)Client app: MCP host and clientClient appMCP host andclientTool server: 3 read-only toolsTool server3 read-only toolsPolicy engine: caller's visibilityPolicy enginecaller'svisibilityCatalog: productsCatalogproductsApproval service: quote draftsApprovalservicequote draftstools/callwho is askingfiltered readnot exposed

    How to read this: another application connects to the Tool server and can only reach what the server chooses to expose, filtered by the caller's own access; the approval gate is deliberately unreachable.

    • MCP is a protocol: it fixes how a client discovers and calls tools, not how an AI application uses a model.
    • A tool's definition is a name, a description and a JSON Schema for its arguments, and the capstone already has all three.
    • Two error kinds: protocol errors (unknown tool, malformed request) and tool execution errors (bad arguments, a failing tool), which the model can read and correct.
    • Expose read-only tools only. Actions stay behind an approval the model cannot reach.
    • Access is decided per request from the caller's credentials, never from a model-supplied field.

    Scenario

    Composite scenario, not a real company. A distributor partner of Lumen Industrial Supply uses an AI assistant application that supports MCP. Their buyers want to ask it about Lumen products without Lumen building a custom integration for every assistant. The FDE is asked to publish the catalog tools once, in a standard way. The risks are plain: a public caller must not see partner-only products, no assistant may create a quote, and nothing an assistant displays may mislead. Figures are illustrative.

    What you will be able to do

    • Describe the participants, layers and the two transports of MCP in a few sentences.
    • Produce the tools/list definitions for the capstone's tools from its argument models.
    • Map a tool result to a success, a tool execution error or a protocol error, and say why the distinction matters.
    • Decide which tools an MCP server may expose and where per-request identity comes from.
    • State what this lesson did not verify.

    Prerequisites

    Core ideas

    What MCP is, in one page

    The Model Context Protocol's architecture overview names three participants. A host is an AI application; it creates one client for each server it connects to, and each client keeps a dedicated connection. A server is a program that provides context to clients. The protocol has two layers: a data layer based on JSON-RPC 2.0 (as of 2026-11-02, protocol version 2026-07-28 in the official documentation), and a transport layer that carries the messages.

    Piece What it is For the catalog server
    Host and client the AI application and its connection the partner's assistant
    Server the program that offers tools, resources and prompts the catalog tool server
    Transport: stdio standard input and output of a local process one local user
    Transport: Streamable HTTP HTTP POST with optional streaming; supports bearer tokens and other HTTP authentication many remote callers
    Primitive: tools functions the model can call search, read a spec, check a protocol

    The documentation says MCP focuses on the protocol for context exchange and does not dictate how an application uses a model. It also says the protocol is stateless in this version: every request carries what the server needs, so servers infer nothing from earlier requests. Check which protocol version your SDK speaks before relying on any detail here.

    Figure 2. Discovery, then a call

    sequenceDiagram accTitle: Listing and calling a tool through MCP accDescr: The client app asks the tool server for its tools with tools/list and gets names, descriptions and argument schemas. The model selects a tool. The client app sends tools/call with the tool name and arguments. The tool server runs the tool as the caller and returns a result with content, or an error. participant M as LLM provider participant C as Client app participant S as Tool server C->>S: tools/list S-->>C: names, descriptions, input schemas C->>M: question and the tool definitions M-->>C: choose get_product_spec with a SKU C->>S: tools/call name and arguments S-->>C: result, or isError, or a protocol error C->>M: tool result

    How to read this: the model never talks to the server; the client app sits in the middle, forwards the model's choice and returns the result.

    Definitions come from what you already have

    A tool definition has a unique name, a description and an inputSchema, a JSON Schema for the arguments. The capstone's registry already holds a Pydantic argument model for each tool, and Pydantic writes JSON Schema from it. So the definitions are derived, not retyped, and the schema a model sees cannot drift from the validation that runs.

    Verified: capstone/fde-product-discovery-copilot/lessons/M4-L7/m4_l7_mcp_shapes.py

        return [
            {"name": tool.name, "description": tool.description, "inputSchema": tool.args_model.model_json_schema()}
            for tool in registry._tools.values()  # noqa: SLF001 (the registry has no public accessor for the argument models)
        ]

    One caveat: the registry has no public accessor for its argument models, so this function reads the registry's private _tools dictionary. That couples the lesson code to the registry's internals; a real server should first add a public accessor to the registry.

    For get_product_spec the real output is this definition, including the capstone's own SKU pattern:

    {
      "name": "get_product_spec",
      "description": "Read one product record by SKU.",
      "inputSchema": {
        "properties": {
          "sku": { "pattern": "^LUM-[A-Z]{2}-\\d{3}
    quot;
    , "title": "Sku", "type": "string" } }, "required": ["sku"], "title": "GetProductSpecArgs", "type": "object" } }

    Tests check three things:

    • the list is the same three tools in the same order on every call (the tools page asks for a deterministic order so clients can cache the list);
    • every name matches the allowed pattern of 1 to 128 letters, digits, underscore, hyphen and dot;
    • each schema is an object schema.

    The whole list is 1,459 characters of JSON. It is sent to the model with each request that offers the tools, so a long list costs tokens every time; measure it with your provider's token counter.

    Two kinds of error

    Predict first. A client calls get_product_spec with the SKU vs-100, which does not match the pattern. Is the answer a JSON-RPC error, or a normal result? Decide, then open the answer.

    Answer

    A normal result with isError set to true and a message the model can read. The tools page separates protocol errors, which describe problems with the request structure that a model is unlikely to fix (an unknown tool, a malformed request), from tool execution errors such as input validation failures, which contain feedback a model can use to correct and retry. A SKU in the wrong format is the second kind.

    Figure 3. Which error is which

    flowchart TD accTitle: Choosing between a protocol error and a tool execution error accDescr: If the tool name is unknown or the request is malformed, return a protocol error. Otherwise run the tool with the caller's access. If the arguments are invalid or the tool fails, return a normal result with isError true and a readable message. If it succeeds, return content and structured content. A["tools/call arrives"] --> B{"Known tool, well-formed request?"} B -- no --> C["Protocol error, code -32602"] B -- yes --> D["Run as the caller"] D --> E{"Valid arguments and tool succeeded?"} E -- no --> F["Result with isError true and a readable message"] E -- yes --> G["Result with content and structuredContent"]

    How to read this: only a request the model cannot repair is a protocol error; everything the model could fix by trying again is a normal result with a message.

    Verified: capstone/fde-product-discovery-copilot/lessons/M4-L7/m4_l7_mcp_shapes.py

        if name not in {t["name"] for t in registry.describe()}:
            return {"error": {"code": UNKNOWN_TOOL, "message": f"Unknown tool: {name}"}}
        ctx = ToolContext(catalog, allowed_visibilities(identity))
        outcome = registry.call(name, arguments, ctx)
        if not outcome.ok:
            return {"result": {"resultType": "complete", "content": [{"type": "text", "text": outcome.error}], "isError": True}}
        return {
            "result": {
                "resultType": "complete",
                "content": [{"type": "text", "text": json.dumps(outcome.data, sort_keys=True)}],
                "structuredContent": outcome.data,
                "isError": False,
            }
        }

    The capstone's registry never raises: bad names, bad arguments and tool failures come back as an error result, which this function turns into one of the two shapes. The text content carries the data as JSON and structuredContent carries it as a value, because the tools page says a tool that returns structured content should also return the serialized JSON in a text block.

    Call What comes back Kind
    get_product_spec with LUM-VS-100, public caller content, structured content, isError false success
    get_product_spec with vs-100 text "Invalid arguments: sku", isError true tool execution error
    create_quote_draft code -32602, "Unknown tool: create_quote_draft" protocol error
    get_product_spec with LUM-VS-900, public caller text "LookupError: No product LUM-VS-900 found.", isError true tool execution error
    the same SKU with a partner caller the product record, isError false success

    Access: from the request, not from the model

    The model proposes tool arguments; it does not say who it is. The capstone's tools run with the caller's visibility, and the function above builds that context from an identity argument on every call. The MCP tools page agrees on the shape of the rule: the tool list may vary by the authorization presented on a request, because credentials are per-request input and not connection state.

    Where the identity comes from depends on the transport:

    • stdio: the host starts the server as a local process. There is no HTTP header, so the identity is configuration at start-up. The safe default is the public role.
    • Streamable HTTP: the identity is verified from the bearer token on each request, using the same code path as the capstone's API. MCP's overview recommends OAuth for tokens. The capstone's HMAC dev tokens are a teaching stub and not a production design.

    The tools page lists what servers must do: validate all tool inputs, implement access controls, rate limit tool invocations and sanitize tool outputs. The capstone covers the first two in code; rate limiting and output sanitising are a gap to close before exposing the server.

    The server wrapper is the thin part

    The official Python quickstart (as of 2026-11-02) builds a server with an MCPServer class, a decorator that turns a typed function with a docstring into a tool, and a run call with the stdio transport. It warns that a stdio server must never write to standard output, because that corrupts the JSON-RPC messages; log to standard error. SDK names have changed between versions, so treat the sketch as a shape and read your version's documentation.

    Pseudocode. Never run: the mcp package is not installed in this course's environment.

    from mcp.server import MCPServer          # name per the quickstart; check your SDK version
    
    server = MCPServer("lumen-catalog")
    registry = default_registry()             # the capstone's three read-only tools
    
    @server.tool()
    def get_product_spec(sku: str) -> dict:
        """Read one product record by SKU."""
        return call_tool(registry, catalog, identity_for_this_request(), "get_product_spec", {"sku": sku})
    
    if __name__ == "__main__":
        server.run(transport="stdio")         # log to stderr only; never print to stdout

    There is a catch in the sketch: the SDK builds the tool's schema from the function's type hints, so the same rule would live twice, once in the hint and once in the Pydantic pattern. Either register the Pydantic model's schema directly if the SDK allows it, or write a test that the two schemas agree. Do not rely on memory.

    Worked example

    Trace one call from the partner's assistant. A buyer asks, "Which products can reach OPC UA from the VibraSense 100?" The assistant lists tools, the model picks check_compatibility with LUM-VS-100 and OPC UA, and the server returns this result (real output from the lesson code, wrapped in the JSON-RPC envelope that an SDK would add):

    {
      "jsonrpc": "2.0",
      "id": 7,
      "result": {
        "resultType": "complete",
        "content": [{ "type": "text", "text": "{\"protocol\": \"OPC UA\", \"sku\": \"LUM-VS-100\", \"supported\": false, \"via\": [\"LUM-GW-200\", \"LUM-GW-300\", \"LUM-GW-500\"]}" }],
        "structuredContent": { "sku": "LUM-VS-100", "protocol": "OPC UA", "supported": false, "via": ["LUM-GW-200", "LUM-GW-300", "LUM-GW-500"] },
        "isError": false
      }
    }

    Read the result as the model would: the sensor does not support OPC UA directly, and three gateways can bridge it. The id and the envelope are written by hand here; the rest is the tested function's output. Real requests in this protocol version also carry _meta fields (protocol version, client info, capabilities) that the lesson code does not model. The field set follows the 2026-07-28 tools page, and it has not been checked by a client.

    Now the trust side, which the protocol puts on the client. The tools page says clients must treat tool annotations as untrusted unless the server is trusted, and says there should be a human in the loop who can deny invocations. The partner's assistant is not under Lumen's control, so assume the worst:

    • it may show the tool's text to a buyer verbatim;
    • it may call tools in an order Lumen did not plan.

    That is why the server returns only catalog data, adds no instructions to its descriptions, and exposes no action.

    • Assumptions: the buyer is a public caller; the three tools and their results are the capstone's; the tool list size is the lesson code's measurement.
    • Not verified: that an MCP client accepts these shapes, that an SDK produces the same envelope, and that the HTTP transport authenticates as sketched.

    Spot the flaw. A colleague's pseudocode for the server:

    Pseudocode.

    @server.tool()
    def create_quote_draft(sku: str, quantity: int, role: str = "public") -> dict:
        """Create a quote draft. Use whenever the buyer sounds interested."""
        print("creating quote", sku)
        return quotes.create(sku, quantity, role=role)
    Answer

    Four problems. The tool performs an action and is exposed with no approval gate, so any assistant can create quotes. The role argument comes from the model, so a public caller can claim a partner role; identity must come from the request's credentials. The description tells the model to call it on a mood, which invites unwanted calls. And print writes to standard output, which corrupts a stdio server's messages; logging goes to standard error.

    Decision exercise. The partner wants quote drafts created from their assistant. Options: (a) expose create_quote_draft as a tool, with a confirmation prompt in their client; (b) expose a read-only tool prepare_quote_request that returns a link to Lumen's own approval screen; (c) do not support quotes through the assistant. Choose and justify.

    Model answer

    Choose (b). Confirmation in a client Lumen does not control is not an approval Lumen can rely on: the tools page says applications should present confirmation prompts, which is a recommendation for the client, not a guarantee for the server. Option (b) keeps the action behind Lumen's own gate, where a second person approves exact arguments, and still gives the buyer a useful step. Option (a) is only acceptable if the server enforces its own approval; (c) is the fallback if even a link is not wanted.

    At the customer: "Can any assistant use our catalog?" A good answer says yes for the read-only tools, shows the two error kinds and the per-request identity rule, names what is not exposed and why, and states plainly what has not been tested against a real client.

    Build it

    The mapping code and its tests are in capstone/fde-product-discovery-copilot/lessons/M4-L7/. Run ./.venv/Scripts/python -m pytest lessons/M4-L7 -q from the capstone folder; all ten tests pass offline. They cover:

    • the list of three tools in a stable order, with valid names and object schemas;
    • the SKU pattern and the limit bound appearing in the schemas;
    • a successful call, with the text and the structured content agreeing;
    • an unknown tool as a protocol error, and invalid arguments as an error result;
    • a hidden product and a missing product producing the same wording for a public caller, and the product record for a partner;
    • search for a public caller never returning a partner-only product;
    • an action tool being refused by the registry;
    • the size of the tool list in characters.
    WARNING

    Passing these tests does not make a working MCP server. The tests prove the application logic behind the protocol. Connecting a real SDK and a real client is a separate piece of work, and it is the first thing to do before telling a customer the server works.

    Exercise

    Add an output schema to get_product_spec. Define a Pydantic model for the fields the tool returns (SKU, name, category, price, IP rating, protocols, stock flag and the specs mapping), include its JSON Schema as outputSchema in the definition, and add a test that validates structuredContent against the model for three products.

    Acceptance criteria:

    • the definition for get_product_spec carries an outputSchema, the others are unchanged;
    • a test fails if a field is renamed in the tool but not in the schema;
    • the existing tests still pass.

    Done when your file and test pass and the whole capstone suite is green. The tools page says that when an output schema is provided, servers must return results that conform to it.

    Hint 1: where the data comes from

    The tool returns the catalog's product summary plus its specs. Build your model from the keys that the real result contains, then check them against the printed output in this lesson.

    Hint 2: making the test fail on a rename

    Forbid extra fields in your model so that an unexpected key fails validation, and make the fields required so that a missing one fails too.

    Explain it back. In three sentences, explain to the partner's integration lead what the server exposes and what it does not. A good explanation names the three read-only tools, says that identity comes from the credentials on each request and that quote creation is not available through the protocol, and says what has not yet been tested against their client.

    Common mistakes

    • Exposing an action tool because the client "asks the user to confirm".
    • Taking the role or tenant from a tool argument.
    • Returning bad arguments as a protocol error, so the model cannot repair them.
    • Writing logs to standard output in a stdio server.
    • Defining the schema twice, in a type hint and in a validator, and letting them diverge.
    • Calling the server finished before any real client has connected to it.

    Knowledge check

    1. What are the three participants in MCP, and which one talks to the model?

    The host is the AI application, the client is the connection the host creates for each server, and the server provides the tools. The host talks to the model; the server never does. The client forwards the model's tool choice and returns the result, which is why trust and confirmation sit with the client side.

    2. Why is an invalid SKU a tool execution error and not a protocol error?

    A model can repair it: the message says which argument failed, and a retry with a correct SKU may succeed. Protocol errors are for problems with the request structure that a model is unlikely to fix, such as an unknown tool. Returning repairable errors as results keeps the model's self-correction loop alive.

    3. How does the server know whether to show a partner-only product?

    From the credentials presented on that request, mapped to an identity and then to the caller's visibility. The model's arguments never carry a role. Over stdio the identity is the configuration the process started with; over HTTP it comes from a verified bearer token on each request.

    4. Why is `create_quote_draft` not exposed, even with confirmation in the client?

    Confirmation in a client the server does not control is a recommendation, not an enforced approval. The capstone's approval gate binds a request to exact arguments and needs a second person, and that logic lives on the server side. A tool that acts must go through the gate or stay out of the protocol.

    5. What did this lesson verify and what did it not?

    It verified, by tests, the application logic that produces definitions, results and the two error shapes, and that access follows the caller. It did not verify that any MCP SDK or client accepts those shapes, because the mcp package is not installed here. A reader should treat the wrapper as a design and run it against a real client before claiming it works.

    You can now

    • Describe MCP's participants, layers and transports in a few sentences.
    • Derive tool definitions from argument models and check them with tests.
    • Map outcomes to a success, a tool execution error or a protocol error.
    • Keep identity on the request and actions behind an approval the protocol cannot reach.
    • Say exactly which parts of an MCP design were tested and which were not.

    Further reading

    • Model Context Protocol, Architecture overview: participants, layers, transports and primitives.
    • Model Context Protocol, Tools: definitions, results, the two error kinds and security considerations.
    • Model Context Protocol, Build an MCP server: the quickstart, including the rule about standard output.
    • Agents, tools and MCP in the AI Architect course: when to use MCP and how to threat-model it.

    Last verified: 2026-11-02

    Next

    Guardrails and caching before production

    Enroll to keep reading

    Lesson 7 of 8 · Forward Deployed Engineer: Agents, Tools and Guardrails

    Expose the capstone's three read-only catalog tools through the Model Context Protocol: tool definitions, the two error kinds, per-request access and trust.

    In the rest of this lesson

    • Worked example
    • Build it
    • Exercise
    • Common mistakes
    • Knowledge check
    • You can now

    Enrolling is free. It unlocks every lesson in Forward Deployed Engineer: Agents, Tools and Guardrails, tracks your progress across devices and keeps your quiz scores. The newsletter is optional.

    Ask about this article

    Get answers grounded in this post. AI-generated — based on this article, and may be imperfect.

    Scaled AI Weekly

    Enjoyed this? Get more like it every Monday.

    Real architecture decisions, LLMOps patterns that survive production, and engineering leadership advice — from 12+ years of building at enterprise scale. Free. No spam. Unsubscribe anytime.

    Get new articles by email

    Free. We email a confirmation link first, and you can unsubscribe from any email.

    Join engineers building production AI systems

    Comments