MCP 2026-07-28 one lesson per page
23 lessons
Core conceptFundamentals · InteractionsTrack 1 · 2 / 9

Three primitives, three different bosses

Tools, resources and prompts differ less in shape than in who decides when they are used: the model, the application, or the user.

Listen to this lessonAudio overview in Gemini Notebook · about 15–25 min · opens in a new tab

One code-hosting server, three ways in

Your IDE connects to a code-hosting MCP server. It shows up in three places:

  • Type / in the chat box and you see /review-pr. That's a prompt: you chose to run it.
  • The IDE quietly attaches the repo's README.md to every conversation. That's a resource: the application decided to include it.
  • Mid-conversation the model decides to call create_issue. That's a tool: the model chose it, and the host asks you to confirm.

Get the boss wrong and things feel off. If "delete branch" were a resource, nobody would expect it to do anything. If "review this PR" were a tool, the model might fire it unprompted.

  • Tools are model-controlled: the model discovers and invokes them (with a human able to deny).
  • Resources are application-driven: the host decides how to use them as context. Each has a uri, name, optional mimeType, and returns text or base64 blob. Templates use RFC 6570 URI templates. Annotations include audience ("user", "assistant") and priority (0 to 1). A missing resource MUST return -32602, never an empty contents array. Servers MUST sanitize file paths against traversal.
  • Prompts are user-controlled: "who decides when the prompt is used, not who authors its content". Typically shown as slash commands. prompts/get takes arguments and returns messages.
  • Completion (completion/complete) suggests argument values for a prompt (ref/prompt) or resource template (ref/resource): at most 100 values, plus total and hasMore.
  • Pagination covers the four list methods: an opaque cursor, page size chosen by the server, missing nextCursor means the end. An empty-string cursor is valid and is not the end. An invalid cursor SHOULD get -32602.

Who controls a primitive decides how it's presented and how much it can be trusted to run unprompted. A model-controlled action needs a confirmation step; application context needs relevance rules; user-invoked templates need a menu. Separating them lets each host build the right UI.

A prompt with autocomplete
UserClientServerprompts/listcode_review (args: code, language)types /code_review, language "py"completion/complete {language: "py"}["python","pytorch","pyside"]picks python, pastes codeprompts/get code_review {code, language}messages → sent to the model
Solid arrows are requests, dashed are responses or notifications. Red ✕ is the unsafe or failing step; green is the step that keeps you safe.

Getting a prompt

Examplerequest
{  "jsonrpc": "2.0",  "id": 2,  "method": "prompts/get",  "params": {    "name": "code_review",    "arguments": {      "code": "def hello():\n    print('world')"    }  }}
Exampleresponse
{  "jsonrpc": "2.0",  "id": 2,  "result": {    "resultType": "complete",    "description": "Code review prompt",    "messages": [      {        "role": "user",        "content": {          "type": "text",          "text": "Please review this Python code:\ndef hello():\n    print('world')"        }      }    ]  }}

Verbatim from the spec (request _meta omitted, as the spec does).

Autocomplete for an argument

Examplerequest
{  "jsonrpc": "2.0",  "id": 1,  "method": "completion/complete",  "params": {    "ref": { "type": "ref/prompt", "name": "code_review" },    "argument": { "name": "language", "value": "py" }  }}
Exampleresponse
{  "jsonrpc": "2.0",  "id": 1,  "result": {    "resultType": "complete",    "completion": {      "values": ["python", "pytorch", "pyside"],      "total": 10,      "hasMore": true    }  }}

Reading a resource, and a missing one

Exampleresources/read result
{  "jsonrpc": "2.0",  "id": 2,  "result": {    "resultType": "complete",    "contents": [      {        "uri": "file:///project/src/main.rs",        "mimeType": "text/x-rust",        "text": "fn main() {\n    println!(\"Hello world!\");\n}"      }    ],    "ttlMs": 60000,    "cacheScope": "private"  }}
Errornot found
{  "jsonrpc": "2.0",  "id": 5,  "error": {    "code": -32602,    "message": "Resource not found",    "data": { "uri": "file:///nonexistent.txt" }  }}

A page of results

Exampleresources/list with a next page
{  "jsonrpc": "2.0",  "id": "123",  "result": {    "resultType": "complete",    "resources": [...],    "nextCursor": "eyJwYWdlIjogM30=",    "ttlMs": 300000,    "cacheScope": "public"  }}

Send nextCursor back as params.cursor to get the next page. Treat it as opaque: never decode, build or edit it.

  • Stopping pagination early. Clients that assume a fixed page size, or treat "" as the end, silently drop items.
  • Returning empty contents for a missing resource. The client can't tell "empty file" from "doesn't exist". Return -32602.
  • Path traversal. A resource template like file:///project/{path} that accepts ../../etc/passwd. Validate and sanitize.
  • Prompt injection through prompts. Prompt arguments end up in model input; implementations MUST validate prompt inputs and outputs.
First time here? Set up the test helper (once per terminal)
ShellSetup
# 1. In a SECOND terminal, start the reference server (Node 18+, no dependencies)curl -sO https://www.diegozuluaga.dev/mcpa/reference-server.mjsnode reference-server.mjs                 # http://localhost:3000/mcp, logs appear here # 2. In THIS terminal, define the helper every test uses (bash or zsh)export MCP=http://localhost:3000/mcpMETA='"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}'mcp() {  # usage: mcp <method> '<json body>' [extra curl args...]  curl -sS -N "$MCP" \    -H 'Content-Type: application/json' \    -H 'Accept: application/json, text/event-stream' \    -H 'MCP-Protocol-Version: 2026-07-28' \    -H "Authorization: Bearer ${MCP_USER:-alice}" \    -H "Mcp-Method: $1" "${@:3}" -d "$2" \    -w '\nHTTP %{http_code}\n'}# Demo auth: the reference server treats the bearer token as the user's name.# Prefix a command with MCP_USER=bob to act as someone else.
ShellTests
# 1. Prompts: list, then get with and without the required argumentmcp prompts/list '{"jsonrpc":"2.0","id":1,"method":"prompts/list","params":{'"$META"'}}'mcp prompts/get '{"jsonrpc":"2.0","id":2,"method":"prompts/get","params":{"name":"code_review","arguments":{"code":"print(1)","language":"python"},'"$META"'}}' -H 'Mcp-Name: code_review'mcp prompts/get '{"jsonrpc":"2.0","id":3,"method":"prompts/get","params":{"name":"code_review","arguments":{},'"$META"'}}' -H 'Mcp-Name: code_review'# expect: the last one is 400 with -32602 (missing required argument) # 2. Completionmcp completion/complete '{"jsonrpc":"2.0","id":4,"method":"completion/complete","params":{"ref":{"type":"ref/prompt","name":"code_review"},"argument":{"name":"language","value":"py"},'"$META"'}}' # 3. Pagination: the server returns 2 per page; follow nextCursor until it's gonemcp resources/list '{"jsonrpc":"2.0","id":5,"method":"resources/list","params":{'"$META"'}}' | tee /tmp/page1.txtNEXT=$(sed -n 's/.*"nextCursor":"\([^"]*\)".*/\1/p' /tmp/page1.txt)mcp resources/list '{"jsonrpc":"2.0","id":6,"method":"resources/list","params":{"cursor":"'"$NEXT"'",'"$META"'}}'# expect: the last page has no nextCursormcp resources/list '{"jsonrpc":"2.0","id":7,"method":"resources/list","params":{"cursor":"not-a-cursor",'"$META"'}}'# expect: 400 with -32602 (invalid cursor)

Answer all 4 correctly and this lesson is marked as learned.

Q1In an IDE, the user types "/" and picks "review-pr" from a menu. Which primitive is that, and who controls it?

Q2Spot the bug: a client stops paging when a response contains "nextCursor": "".

Q3A client reads file:///missing.txt and the file doesn't exist. What should the server return?

Q4What is the maximum number of values in one completion result?

Feedback or a correction? Email diego [at] diegozuluaga [dot] dev or open an issue on GitHub.

Content CC BY 4.0 · Code MIT

Tip: ← and → move between lessons. Hover any heading and press # to copy a link to it.