Learn MCP - Client SDK & MCP Inspector
Episode 7 of 23

Learn MCP - Client SDK & MCP Inspector

This episode builds the MCP client side: connecting to servers via stdio and HTTP in TypeScript and Python, error handling and streaming patterns, then exploring the MCP Inspector for interactive testing, transport debugging, and schema validation.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 6 we built MCP servers — currency-tools in TypeScript and weather-server in Python — running over the stdio transport and ready to test. But a server means nothing without something calling it. In this episode we build the client side: a Client SDK that connects to servers, lists tools, calls them, and handles errors and streaming.

This episode's roadmap: understand the client's position in the MCP architecture, implement a client in TypeScript (stdio and HTTP) and Python, error handling and streaming patterns, then meet the MCP Inspector — the official tool for interactively testing servers without writing any code.

The Client's Role in the MCP Architecture

Recall the architecture from episode 2: Host (the user-facing application), Client (a one-to-one connection with a server), and Server. A single host like an editor or CLI agent can run many clients, one per server. Because connections are one-to-one, it's the client that holds the transport, sends JSON-RPC requests, and receives responses and notifications from the server.

The client's main jobs: establish a connection to the server (via stdio for local processes, or HTTP for remote servers), surface the catalog of tools/resources/prompts the server exposes, run tools/call with parsed arguments, and handle JSON-RPC errors and streaming messages.

Info

One golden rule: the client and server may come from different SDKs. A TypeScript client doesn't care whether the server is Python — what matters is that both speak JSON-RPC per the same specification.

TypeScript: Client over stdio

For a local server (subprocess), use StdioClientTransport:

client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
 
const transport = new StdioClientTransport({
  command: "node",
  args: ["dist/server.js"],
});
 
const client = new Client({ name: "test-client", version: "1.0.0" });
await client.connect(transport);
 
const { tools } = await client.listTools();
console.log(tools.map((tool) => tool.name));
 
const result = await client.callTool({
  name: "convert-currency",
  arguments: { from: "USD", to: "IDR", amount: 100 },
});
console.log(result.content);
 
await client.close();

The flow: create a transport that manages the child process node dist/server.js, create a Client object with an identity, connect with client.connect(transport), then explore with client.listTools() and execute with client.callTool({:ts}...). Don't forget client.close() to cleanly release the child process and connection.

TypeScript: Client over HTTP

Remote servers (we deploy them in episode 9) can't be reached over stdio. Instead, use StreamableHTTPClientTransport, which sends JSON-RPC requests via POST and receives responses or streaming over SSE:

client-http.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
 
const transport = new StreamableHTTPClientTransport(
  new URL("https://mcp.example.com/mcp"),
  {
    requestInit: {
      headers: { Authorization: "Bearer access-token" },
    },
  }
);
 
const client = new Client({ name: "web-client", version: "1.0.0" });
await client.connect(transport);

After connect, the rest of the code is identical to the stdio version — listTools, callTool, and friends don't care about the transport behind the scenes. Note the requestInit option for injecting HTTP headers like Authorization; this matters when the remote server is protected by OAuth, which we'll cover in episode 10.

Python: Async Client

In Python, the common pattern uses async with to manage the connection lifecycle and the session together:

Pythonclient.py
import asyncio
 
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
 
 
async def main():
    params = StdioServerParameters(
        command="python", args=["weather_server.py"]
    )
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([tool.name for tool in tools.tools])
 
            result = await session.call_tool(
                "get_weather", {"city": "Jakarta"}
            )
            print(result.content)
 
 
asyncio.run(main())

stdio_client(params) opens the child process, then ClientSession wraps it with a JSON-RPC layer. session.initialize() agrees on the version and capabilities before other requests may be sent — in the stateless era of 2026-07-28 this information is carried per request, but the SDK still handles it transparently for you. For an HTTP server, swap the transport for streamable_http_client(url) from the mcp.client.streamable_http module; everything else is the same.

Error Handling & Streaming Patterns

Network connections always can fail, and servers can reject arguments. Always wrap tool calls in try/catch and inspect the JSON-RPC error:

error-handling.ts
try {
  const result = await client.callTool({
    name: "convert-currency",
    arguments: { from: "USD", to: "IDR", amount: -100 },
  });
  console.log(result.content);
} catch (error) {
  console.error("Panggilan gagal:", error);
}

A healthy server returns a structured JSON-RPC error: code, message, and data. Distinguish failure types — -32700 parse error, -32601 unknown method, -32602 invalid arguments, and application-specific codes defined by the server — so the messages shown to users are accurate.

For long-running results, the server can send progress notifications. Register a notification handler once, then let the SDK invoke it whenever a message arrives:

notifications.ts
client.setNotificationHandler((notification) => {
  console.log("Notifikasi dari server:", notification);
});

Notifications have no id — they're one-way calls. Use them for progress, logs, or status updates; don't assume a notification always arrives before the main response completes.

MCP Inspector: Interactive Testing

The MCP Inspector is the official tool for testing servers without writing any client. It runs your server, presents a web UI in the browser, and lets you explore all capabilities.

npx @modelcontextprotocol/inspector python weather_server.py

npx @modelcontextprotocol/inspector opens a dashboard-like page showing: the list of Tools / Resources / Prompts along with their input schemas, a Call tool form generated from the schema (which also validates the schema — invalid arguments are rejected immediately), a raw JSON-RPC request/response panel for transport debugging, and server logs shown live.

Success

Before integrating a server into any application, make a habit of testing it in the Inspector first. If a server passes in the Inspector, most integration problems with other clients are already preventable.

Conclusion

Episode 7 completes the client side of the MCP story: StdioClientTransport and StreamableHTTPClientTransport in TypeScript, the async ClientSession in Python, JSON-RPC error handling and notification patterns, plus the MCP Inspector as your primary debugging weapon. Now you can call the servers built in episode 6 from two different languages.

Key takeaways:

  • Cross-language client and server: as long as they speak the same JSON-RPC, they're always compatible.
  • The transport determines how you connect: stdio for local processes, streamable HTTP for remote.
  • Always handle errors: inspect the JSON-RPC code and message, not just catch the exception.
  • Inspector for everything: test servers without writing code, complete with schema validation and a raw message inspector.
  • One client, one server: big hosts run many clients — design the client as a lightweight unit.

In the next episode 8 we move up to a modern feature of the 2026-07-28 specification: Multi-Round-Trip Requests (MRTR) — how a server can request additional confirmation or authorization in the middle of a tool call, replacing the reliance on long SSE streams. See you there!

Learn MCP - Client SDK & MCP Inspector | Learning MCP