A Python server
Explain MCP roles and lifecycle and trace a tool-discovery and tool-call exchange under a pinned protocol version.
THE IDEA
A Python server
Start with an ordinary, testable Python function such as add(a, b). Wrap it as a tool with a schema, validate arguments, turn exceptions into error results, and only then expose it with the official SDK, pinned to a version you have checked. The server is not the whole story: the host still needs an authorisation and execution policy deciding which tools a user may call. A schema says what a valid call looks like, not whether it is allowed.
| call | result kind | content | isError |
|---|---|---|---|
| add(2, 3) | ok | 5 | false |
| add(2, "three") | tool_error | Invalid arguments: type:b | true |
| divide(1, 0) | tool_error | division by zero | true |
| delete_file("a.txt") | denied | Permission required: files:write | true |
| format_disk() | protocol_error | Unknown tool: format_disk | true |
The pure function add(a, b) is the easy part. The server still validates arguments against its schema, and the host decides which tools a user may call at all: a schema describes shape, not authority. Pin the SDK version you build against.
Predict first. Does a tool schema alone establish permission to call it?
Chapter context
MCP defines a protocol for applications to connect models with external capabilities. It standardizes exchanges; it does not grant a server universal authority or make every discovered tool appropriate for every user request.
TRY IT YOURSELF
Part 1. Do it hands-on
Predict a server's results by hand, then write the tool dispatcher with validation and a permission policy.
| call | result kind | content | isError |
|---|---|---|---|
| add(2, 3) | ok | 5 | false |
| add(2, "three") | tool_error | Invalid arguments: type:b | true |
| divide(1, 0) | tool_error | division by zero | true |
| delete_file("a.txt") | denied | Permission required: files:write | true |
| format_disk() | protocol_error | Unknown tool: format_disk | true |
The pure function add(a, b) is the easy part. The server still validates arguments against its schema, and the host decides which tools a user may call at all: a schema describes shape, not authority. Pin the SDK version you build against.
- To do: A pure Python function add(a, b) is exposed as a tool. tools/call with a = 2, b = 3: the text in the result's content?
- To do: tools/call with a = 2 and b = "three": isError in the result? Type true or false
- To do: A tool needs the permission files:write, the user granted only files:read. Does the host allow the call? Type allow or deny
- To do: Does a tool schema alone establish permission to call it? Type yes or no
Part 2. Do it in code
- add(a, b) is a pure Python function (PROVIDED). TOOLS maps a tool name to {"fn": function, "schema": {required: [...], properties: {name: {type}}}, "needs": permission string or None}.
- validate(schema, args): problems list: "missing:<name>" for each required name absent, then "type:<name>" for each argument whose declared type (number or string) does not match (bool is not a number).
- call_tool(tools, name, args, granted): granted is the set of permissions the user gave. Return a dict {"kind", "content", "isError"}. Unknown tool: kind "protocol_error", isError True, content "Unknown tool: <name>". The tool needs a permission that was not granted: kind "denied", isError True, content "Permission required: <perm>" (checked BEFORE the arguments). Invalid arguments: kind "tool_error", isError True, content "Invalid arguments: " + ", ".join(problems). If fn raises: kind "tool_error", isError True, content str(exception). Success: kind "ok", isError False, content str(result).
- describe_policy(tools, granted): a sorted list of the tool names the user may call right now.
Stuck, or something not working? You can come back to it any time.
CHECK YOUR UNDERSTANDING
Does a tool schema alone establish permission to call it?
Chapter reference: math, worked example & code
This section belongs to the whole chapter, so it is the same on every lesson in it and may cover a different lesson's topic. The code for the experiment you just ran is inside the experiment itself.
Math and notation
The MCP specification revision examined here is 2026-07-28. Request envelopes use JSON-RPC 2.0, an ID for response correlation, a method, and method-specific parameters that include _meta with the required io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities. A successful result carries resultType, here "complete". Do not treat this date as a claim about every installed client or SDK.
Worked example
A tools/call request with id 7 names add, supplies a=2,b=3 and carries the protocol version and client capabilities in _meta. The server runs the permitted operation and answers with the same id and a content array holding the text 5. The offline example below checks envelope fields and arithmetic; a production server must also implement discovery, schema validation, errors and transport behavior.
Reference code
The following example uses the Python standard library and does not call a remote service.
meta={'io.modelcontextprotocol/protocolVersion':'2026-07-28','io.modelcontextprotocol/clientCapabilities':{}}
request={'jsonrpc':'2.0','id':7,'method':'tools/call','params':{'name':'add','arguments':{'a':2,'b':3},'_meta':meta}}
args=request['params']['arguments']
total=args['a']+args['b']
response={'jsonrpc':'2.0','id':request['id'],'result':{'resultType':'complete','content':[{'type':'text','text':str(total)}],'isError':False}}
assert request['method'] == 'tools/call'
assert request['params']['_meta']['io.modelcontextprotocol/protocolVersion'] == '2026-07-28'
assert response['id'] == 7
assert response['result']['content'][0]['text'] == '5'
assert response['result']['isError'] is False
Python examples are provided for study and local execution; this page does not execute Python.
Common mistakes
MCP is not a language model or an automatic permission system. Resource, prompt and tool capabilities differ. Do not assume an initialize handshake: that belongs to revisions before 2026-07-28. Protocol versions and SDK releases must be matched when turning this draft into runnable setup instructions.