{
  "module": "DD-22",
  "title": "MCP: Building Production Tool Servers",
  "course": "Harness Engineering Master Course",
  "version": "1.0.0",
  "duration_minutes": 30,
  "total_questions": 15,
  "bloom_distribution": {
    "target": "20% recall / 40% application / 40% analysis",
    "actual": { "recall": 3, "application": 6, "analysis": 6 }
  },
  "passing_score_percent": 70,
  "questions": [
    {
      "id": "Q01",
      "bloom": "recall",
      "type": "multiple_choice",
      "prompt": "What are the three primitives MCP defines?",
      "options": [
        "Actions, queries, and subscriptions",
        "Tools, resources, and prompts",
        "Functions, data sources, and templates",
        "Methods, events, and handlers"
      ],
      "answer_index": 1,
      "rationale": "MCP defines three primitives: tools (model-invoked functions), resources (server-exposed passive data), and prompts (server-authored templates). Each maps to a distinct trust boundary."
    },
    {
      "id": "Q02",
      "bloom": "recall",
      "type": "multiple_choice",
      "prompt": "Which message envelope does MCP use for all communication?",
      "options": [
        "gRPC with protobuf",
        "REST over HTTPS",
        "JSON-RPC 2.0",
        "WebSocket with MessagePack"
      ],
      "answer_index": 2,
      "rationale": "MCP uses JSON-RPC 2.0 — the same substrate as the Language Server Protocol. Three message types: request (id, method, params), response (id, result | error), notification (no id)."
    },
    {
      "id": "Q03",
      "bloom": "recall",
      "type": "multiple_choice",
      "prompt": "What are the two MCP transports?",
      "options": [
        "gRPC and WebSocket",
        "stdio and HTTP+SSE",
        "TCP and UDP",
        "File-based and network-based"
      ],
      "answer_index": 1,
      "rationale": "stdio (local — agent spawns server as subprocess, communicates over stdin/stdout) and HTTP+SSE (remote — agent connects over HTTP, receives updates via Server-Sent Events). Same protocol, different pipes."
    },
    {
      "id": "Q04",
      "bloom": "application",
      "type": "multiple_choice",
      "prompt": "A client sends tools/call immediately after the initialize response, without sending the initialized notification first. What happens?",
      "options": [
        "The server executes the tool call normally",
        "The server rejects it — the contract is not yet in force until the initialized notification confirms the handshake",
        "The server buffers the call until initialized arrives",
        "The server sends an error notification but executes anyway"
      ],
      "answer_index": 1,
      "rationale": "The initialized notification is the synchronization point confirming the handshake. Before it, the capability contract is not in force. Sending tools/call before initialized is the most common integration bug — the server rejects it."
    },
    {
      "id": "Q05",
      "bloom": "application",
      "type": "multiple_choice",
      "prompt": "You are writing a send_email tool. Which JSON Schema directive is a security control, and why?",
      "options": [
        "\"type\": \"object\" — ensures the arguments are a JSON object",
        "\"required\": [\"to\", \"subject\"] — ensures critical fields are present",
        "\"additionalProperties\": false — prevents the model from injecting unexpected fields like a hidden bcc",
        "\"properties\": {...} — documents the expected fields"
      ],
      "answer_index": 2,
      "rationale": "additionalProperties: false forbids extra fields. Without it, a steered model can inject fields the server doesn't expect — e.g., adding bcc to send_email. This is a security control, not a schema nicety."
    },
    {
      "id": "Q06",
      "bloom": "application",
      "type": "multiple_choice",
      "prompt": "Your stdio MCP server keeps getting disconnected by the client in a reconnect loop. You have a console.log('got request') in your request handler. What is the likely cause?",
      "options": [
        "The client is buggy and dropping connections",
        "The console.log writes to stdout, which is the protocol stream — it corrupts the JSON-RPC messages, the client can't parse them, and disconnects",
        "The server is too slow and timing out",
        "The JSON-RPC version is mismatched"
      ],
      "answer_index": 1,
      "rationale": "On stdio transport, stdout IS the protocol stream. A console.log turns into a malformed JSON-RPC message the client tries to parse, fails on, and disconnects. This is the single most common MCP server bug. Fix: log to stderr only."
    },
    {
      "id": "Q07",
      "bloom": "application",
      "type": "multiple_choice",
      "prompt": "A client offers the 'roots' capability (listChanged: true) in its initialize message. The server's initialize response does not advertise roots support. What is the client allowed to do regarding roots?",
      "options": [
        "Query the server's roots — the client offered it",
        "Send roots/list to the server — the server must support it",
        "Nothing regarding roots on this server — the session may only use the intersection of both sides' capabilities",
        "Retry the capability advertisement until the server accepts"
      ],
      "answer_index": 2,
      "rationale": "The negotiation rule: only use what BOTH sides advertised. The session uses the intersection. A capability offered by only one side is forbidden for this session. The client degrades gracefully."
    },
    {
      "id": "Q08",
      "bloom": "application",
      "type": "multiple_choice",
      "prompt": "Your agent subscribed to a live-log resource and now the model's context is flooded with update notifications, drowning out the actual conversation. What is the correct fix?",
      "options": [
        "Increase the model's context window to absorb the updates",
        "On the server, debounce and coalesce updates (e.g., max one per second), subscribe narrowly to specific URIs, and unsubscribe when the model is done",
        "Switch from stdio to HTTP+SSE transport",
        "Disable the resources primitive entirely"
      ],
      "answer_index": 1,
      "rationale": "This is the subscription flood. The fix is server-side debouncing (coalesce rapid changes), narrow subscriptions (specific URIs not wildcards), and aggressive unsubscription. The server should also cap concurrent subscriptions per client."
    },
    {
      "id": "Q09",
      "bloom": "application",
      "type": "multiple_choice",
      "prompt": "A tool call returns { isError: true, content: [{ type: 'text', text: 'Invalid arguments: property \"limit\" must be <= 50' }] }. What class of failure is this and what should the model do?",
      "options": [
        "Class 1 (unknown tool) — the model should stop calling this tool",
        "Class 2 (validation failure) — the model should retry with corrected arguments conforming to the schema",
        "Class 3 (execution failure) — the model should retry or report to the user",
        "A transport error — the model should wait and reconnect"
      ],
      "answer_index": 1,
      "rationale": "This is a Class 2 validation failure: the arguments did not conform to the schema (limit exceeded the maximum). The model's recovery is to retry with corrected arguments. The error message tells the model exactly what to fix."
    },
    {
      "id": "Q10",
      "bloom": "analysis",
      "type": "multiple_choice",
      "prompt": "Why does MCP separate tools from resources rather than treating all server-exposed data as tools the model can call?",
      "options": [
        "Because resources are faster than tools",
        "Because they map to different trust boundaries: tools are model-initiated (untrusted input), resources are user/client-initiated (the model cannot autonomously pull data the user didn't ask for)",
        "Because the protocol needs at least three primitives to be useful",
        "Because tools are for writing and resources are for reading"
      ],
      "answer_index": 1,
      "rationale": "The separation enforces trust boundaries. A tool is model-invoked — input is untrusted and must be safe. A resource is user/client-initiated — the model cannot autonomously read it. Exposing as a tool something that should be a resource lets the model pull data whenever it wants, including when steered to."
    },
    {
      "id": "Q11",
      "bloom": "analysis",
      "type": "multiple_choice",
      "prompt": "A team argues: 'We don't need capability negotiation — our client and server are always the same version.' What is the flaw in this reasoning?",
      "options": [
        "There is no flaw — same-version deployments don't need negotiation",
        "Capability negotiation isn't about version matching — it's about discovering which features each side supports at runtime, so a client works against any server (including future and third-party ones) using whatever subset both offer",
        "The flaw is that negotiation adds latency",
        "The flaw is that negotiation is insecure"
      ],
      "answer_index": 1,
      "rationale": "Capability negotiation is about runtime discovery of supported features, not version matching. A client built once works against every server — including third-party servers and future versions — using the intersection of capabilities. This is what makes MCP a protocol for a platform, not a point-to-point integration."
    },
    {
      "id": "Q12",
      "bloom": "analysis",
      "type": "multiple_choice",
      "prompt": "Compare MCP's approach to Pi's (DD-01) hardcoded tool registry. What is the structural difference, and why does it matter for a platform?",
      "options": [
        "MCP is faster because it uses JSON-RPC instead of function calls",
        "MCP externalizes tools into a discoverable, versioned, independently-deployable service — adding a tool doesn't require rebuilding the agent, and vendor teams can ship servers on their own release cadence",
        "MCP supports more tools than Pi can",
        "MCP is more secure because it uses schemas"
      ],
      "answer_index": 1,
      "rationale": "Pi's tools are compiled-in function references — adding one means editing source, shipping a build, restarting. MCP makes tools external services. A vendor ships a server; the agent discovers it at runtime. This is the structural difference between a personal harness and a platform."
    },
    {
      "id": "Q13",
      "bloom": "analysis",
      "type": "multiple_choice",
      "prompt": "You have 12 MCP servers in production. Server A's 'search' tool takes {query, status}; Server B's same-named 'search' tool takes {keyword, state}. The model gets confused and calls the wrong schema. What is the production discipline that addresses this?",
      "options": [
        "Rename all tools to be unique across the entire platform",
        "Namespace tools by server (e.g., acme-crm__search vs zendesk__search), pin server versions in the registry, and treat schema changes as breaking",
        "Force all servers to use the same schema",
        "Remove one of the servers to avoid the conflict"
      ],
      "answer_index": 1,
      "rationale": "Version drift across servers is the hard platform problem. The discipline: namespace by server so the model knows which server owns the call, pin versions so operators know what changed, and treat schema changes as breaking (version the tool or re-test)."
    },
    {
      "id": "Q14",
      "bloom": "analysis",
      "type": "multiple_choice",
      "prompt": "Why must the server re-validate tool arguments against the JSON Schema on every call, rather than trusting the model (which 'knows' the schema from the tool description)?",
      "options": [
        "Because schema validation is fast and free",
        "Because the model is not a reliable validator — it produces extra fields, wrong types, and missing required fields, especially under adversarial steering. The client may also not have validated. The schema is the server's defense boundary.",
        "Because JSON-RPC requires it",
        "Because the client might be an older version"
      ],
      "answer_index": 1,
      "rationale": "The model is not a reliable schema validator. Under normal operation it may produce malformed arguments; under adversarial steering it may deliberately inject fields. The client may not have validated. Re-validating at the server boundary is the defense — trusting the model or client to validate is an unenforced boundary."
    },
    {
      "id": "Q15",
      "bloom": "analysis",
      "type": "multiple_choice",
      "prompt": "A tool call returns isError: true for three different reasons across three calls: (a) 'Unknown tool: X', (b) 'Invalid arguments: ...', (c) 'Tool execution failed: connection refused'. Why is it important that the model can distinguish these three classes rather than receiving a generic 'tool failed'?",
      "options": [
        "It isn't — the model should retry all failures identically",
        "Each class has a different recovery path: (a) stop calling the tool, (b) retry with corrected arguments, (c) retry or report to user. A generic message prevents the model from choosing the right response, producing a confused loop.",
        "Because the protocol requires three distinct error codes",
        "Because the user needs to see which class of error occurred"
      ],
      "answer_index": 1,
      "rationale": "The three failure classes (unknown tool, validation failure, execution failure) each have a distinct recovery path. If the server conflates them into a generic 'tool failed,' the model cannot distinguish 'I called the wrong thing' from 'I called the right thing with bad args' from 'the right thing broke.' Distinguish the classes; the text payload tells the model which recovery to apply."
    }
  ]
}
