# The Initialize Handshake and Capabilities — MCP & Agent-to-Agent Protocols

Source: https://www.geekswithgeeks.com/en/mcp-a2a/m-lifecycle

> See how client and server agree on protocol version and features.

## Say hello, agree, then work

Every MCP session starts with an **initialize** request from the client carrying its `protocolVersion`, its `capabilities` (for example whether it supports sampling or roots) and `clientInfo`. The server answers with the version it will use, its own **capabilities** (`tools`, `resources`, `prompts`, with flags such as `listChanged`) and `serverInfo`. The client then sends a **`notifications/initialized`** notification and normal traffic begins. Each side may use **only the features both declared**; this is **capability negotiation**, and it is how protocols stay backward compatible as new features appear. Later the session ends by closing the transport.

## A server to talk to

This small server (built with the official Python SDK, version 2.x) exposes one tool, one resource and one prompt. Save it as `server.py`. The next examples drive it over raw JSON-RPC.

```python
from mcp.server.mcpserver import MCPServer

mcp = MCPServer("demo-orders")

@mcp.tool()
def get_order_status(order_id: str) -> dict:
    """Return the status of ONE order. order_id is exactly 6 digits."""
    if not (order_id.isdigit() and len(order_id) == 6):
        return {"error": "order_id must be exactly 6 digits"}
    return {"order_id": order_id, "status": "shipped"}

@mcp.resource("policy://refunds")
def refund_policy() -> str:
    """The refund policy text."""
    return "Refunds above 5000 rupees need approval."

@mcp.prompt()
def triage(ticket: str) -> str:
    """A prompt template for ticket triage."""
    return f"Classify this ticket as billing, technical or other: {ticket}"

if __name__ == "__main__":
    mcp.run("stdio")

```

## Driving the server by hand, run

I ran this against the real server shown earlier, using the official `mcp` Python SDK 2.2.0 in a virtual environment; the client side is raw JSON-RPC over stdio with no SDK. Save the server as `server.py` in the same folder. SDK names and defaults change between versions, so check the current documentation. The client sends `initialize`, then the `initialized` notification, then lists tools, calls one with valid, invalid and missing arguments, tries an unknown method, and reads a resource and a prompt. The server reports protocol 2025-06-18 and the capabilities `prompts`, `resources`, `tools` (plus an `experimental` entry).

```python
import json, subprocess, sys

proc = subprocess.Popen([sys.executable, "server.py"], stdin=subprocess.PIPE,
                        stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, text=True)

def send(msg):
    proc.stdin.write(json.dumps(msg) + "\n"); proc.stdin.flush()

def call(id_, method, params=None):
    send({"jsonrpc": "2.0", "id": id_, "method": method, **({"params": params} if params is not None else {})})
    return json.loads(proc.stdout.readline())

init = call(1, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
                              "clientInfo": {"name": "demo-client", "version": "0.1"}})
print("server:", init["result"]["serverInfo"]["name"], "| protocol:", init["result"]["protocolVersion"])
print("capabilities:", sorted(init["result"]["capabilities"]))
send({"jsonrpc": "2.0", "method": "notifications/initialized"})     # a notification: no id, no reply

tools = call(2, "tools/list")["result"]["tools"]
print("tools:", [t["name"] for t in tools])
print("input schema:", json.dumps(tools[0]["inputSchema"]))

ok = call(3, "tools/call", {"name": "get_order_status", "arguments": {"order_id": "481516"}})
print("call ok     :", ok["result"]["content"][0]["text"], "| isError:", ok["result"].get("isError"))
bad = call(4, "tools/call", {"name": "get_order_status", "arguments": {"order_id": "12"}})
print("call bad id :", bad["result"]["content"][0]["text"])
missing = call(5, "tools/call", {"name": "get_order_status", "arguments": {}})
print("missing arg :", "isError" , missing["result"].get("isError"), "|", missing["result"]["content"][0]["text"][:60].replace(chr(10), " "))
unknown = call(6, "no/such/method")
print("unknown     :", unknown["error"]["code"], unknown["error"]["message"])

res = call(7, "resources/list")["result"]["resources"]
print("resources:", [r["uri"] for r in res])
print("read:", call(8, "resources/read", {"uri": "policy://refunds"})["result"]["contents"][0]["text"])
pr = call(9, "prompts/get", {"name": "triage", "arguments": {"ticket": "I was charged twice"}})
print("prompt:", pr["result"]["messages"][0]["content"]["text"])
proc.stdin.close(); proc.wait()

```

Output:

```
server: demo-orders | protocol: 2025-06-18
capabilities: ['experimental', 'prompts', 'resources', 'tools']
tools: ['get_order_status']
input schema: {"properties": {"order_id": {"title": "Order Id", "type": "string"}}, "required": ["order_id"], "type": "object", "title": "get_order_statusArguments"}
call ok     : {
  "order_id": "481516",
  "status": "shipped"
} | isError: False
call bad id : {
  "error": "order_id must be exactly 6 digits"
}
missing arg : isError True | Error executing tool get_order_status: 1 validation error fo
unknown     : -32601 Method not found
resources: ['policy://refunds']
read: Refunds above 5000 rupees need approval.
prompt: Classify this ticket as billing, technical or other: I was charged twice
```

## Capability negotiation, run

I ran this with plain Python 3 (standard library only). Only features both sides declared are usable: with no client capabilities just `tools` and `resources` work; sampling needs the client to declare it and the server to want it; asking for `roots` from a client that never declared it is ignored.

```python
def negotiate(client_caps, server_caps):
    """Use only features that BOTH sides declared during initialize."""
    usable = []
    if "tools" in server_caps:
        usable.append("tools")
    if "resources" in server_caps:
        usable.append("resources")
    if "sampling" in client_caps and server_caps.get("wants_sampling"):
        usable.append("sampling (server asks the client's model)")
    if "roots" in client_caps and server_caps.get("wants_roots"):
        usable.append("roots")
    return usable

print(negotiate({}, {"tools": {}, "resources": {}}))
print(negotiate({"sampling": {}}, {"tools": {}, "wants_sampling": True}))
print(negotiate({"sampling": {}}, {"tools": {}, "wants_roots": True}))

```

Output:

```
['tools', 'resources']
['tools', "sampling (server asks the client's model)"]
['tools']
```

**Quiz:** What happens right after the server answers initialize?

- [ ] Nothing can happen again
- [ ] The session is deleted
- [ ] The model is retrained
- [x] The client sends notifications/initialized and normal traffic begins

*Answer:* The client sends notifications/initialized and normal traffic begins. The initialized notification completes the handshake.
