Implement the client side of an MCP session: initialize the connection, list the server's tools, then call one tool and read its result. To keep the exercise self-contained, you talk to a mock transport that returns canned JSON-RPC responses, so you can focus on the request/response sequence rather than networking.
An MCP client follows the same handshake every time: initialize, discover what the server offers, then invoke tools. The real SDK wraps this in stdio_client and ClientSession, but the logic underneath is a sequence of JSON-RPC requests and responses. Driving that sequence by hand is the clearest way to understand what the SDK does for you.
initialize request and confirm the server responds.tools/list request and print the available tool names.tools/call request for one tool and print the result.transport(request) function, which returns the server's JSON-RPC response.The client completes the handshake, lists tools, and gets a tool result back.
The request helper builds a JSON-RPC message and hands it to the transport, which is exactly the boundary the real SDK draws between protocol logic and the connection. The handshake order is not arbitrary: a client initializes first, then discovers tools, then calls them, because it cannot invoke a tool it has not learned about. Swapping the mock transport for stdio_client and ClientSession connects this same flow to a real server without changing how you initialize, list, and call.
A JSON-RPC response carries either a result or an error, and a robust client checks which it got. Call a tool with a wrong name, detect the error response, and retry with the correct name. Distinguishing result from error is the basic reliability check every MCP client needs.
transport that returns an error object for an unknown tool.error.The first call returns a JSON-RPC error; the client detects it and retries with the valid tool name.
Checking for an error key before reaching for result is mandatory in JSON-RPC, because a response has exactly one of the two. Treating the error as a recoverable signal (retry with a corrected call) rather than a crash is what makes a client resilient to a bad tool name or arguments. Swapping the mock transport for the real stdio_client and ClientSession keeps this same result-or-error handling, which is the part that matters for reliability.