# Mcp Client

> Consume MCP (Model Context Protocol) servers over stdio (local) or Streamable HTTP (remote): initialize handshake, call tools, read resources, get prompts, pagination/timeouts/errors, OAuth/bearer auth, plus Claude Desktop/Code, Cursor, OpenClaw config. Use when wiring an agent to an MCP server or debugging a transport/auth failure.

- **Type:** Skill
- **Install:** `agentstack add skill-san-npm-skills-ws-mcp-client`
- **Verified:** Pending review
- **Seller:** [san-npm](https://agentstack.voostack.com/s/san-npm)
- **Installs:** 0
- **Category:** [Security](https://agentstack.voostack.com/c/security)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [san-npm](https://github.com/san-npm)
- **Source:** https://github.com/san-npm/skills-ws/tree/main/skills/mcp-client
- **Website:** https://skills-ws.vercel.app

## Install

```sh
agentstack add skill-san-npm-skills-ws-mcp-client
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# MCP Client — Consuming Model Context Protocol Servers

> **Transport policy (MCP spec):** Use **stdio** for local subprocess servers and **Streamable HTTP** (`StreamableHTTPClientTransport`, endpoint usually `/mcp`) for remote servers. The old **HTTP+SSE** transport was deprecated in spec revision `2025-03-26` and superseded by Streamable HTTP; keep it only as a *legacy fallback* for old servers (endpoint usually `/sse`). WebSocket transport was removed. As of Jun 2026 the latest spec revision is `2025-11-25` — verify at https://modelcontextprotocol.io/specification.

This skill makes an agent expert at being an **MCP client**: discovering a server's capabilities, calling its tools, reading its resources, using its prompts, and doing so safely with auth, timeouts, retries, and cost control. It is provider-agnostic; one specific public server (`mcp.skills.ws`) appears only as an optional worked example at the end.

For building the *server* side, see the sibling skill `mcp-server-builder`. For agent orchestration around these tool calls, see `ai-agent-building`. For wallet/payment flows (x402), see `wallet-integration` and `defi-integration`.

## What this skill covers

1. Transports: stdio (local) vs Streamable HTTP (remote), with a Streamable-HTTP-then-SSE fallback for legacy servers.
2. Connection lifecycle: `initialize` handshake, protocol-version negotiation, capability discovery, clean shutdown.
3. Primitives: tools (`tools/list`, `tools/call`), resources (`resources/list`, `resources/read`, templates, subscriptions), prompts (`prompts/list`, `prompts/get`), and client-provided capabilities (roots, sampling, elicitation).
4. Robustness: cursor pagination, progress notifications, cancellation, timeouts, JSON-RPC error codes, retries with backoff.
5. Auth: OAuth 2.1 (the spec's standard for remote HTTP servers) plus provider-specific bearer/API-key headers; token storage and least privilege.
6. Client configuration for Claude Desktop, Claude Code, Cursor, and OpenClaw (stdio + Streamable HTTP).
7. Cost, caching, and security best practices.

---

## 1. Transports

MCP is JSON-RPC 2.0 messages over a transport. You pick the transport based on *where the server runs*.

| Transport | Use for | Endpoint shape | SDK class (TS) | SDK helper (Py) |
|-----------|---------|----------------|----------------|-----------------|
| **stdio** | Local subprocess (a CLI you spawn) | command + args | `StdioClientTransport` | `stdio_client` |
| **Streamable HTTP** | Remote server over the network (preferred) | `https://host/mcp` | `StreamableHTTPClientTransport` | `streamablehttp_client` |
| HTTP+SSE *(legacy)* | Old remote servers built pre-2025-03-26 | `https://host/sse` | `SSEClientTransport` | `sse_client` |

**stdio** — the server is a process you launch; messages flow over stdin/stdout, framed as newline-delimited JSON-RPC. Most "install an MCP server" instructions (`npx @scope/server`, `uvx some-server`) are stdio. Logging must go to **stderr** — never stdout (stdout is the protocol channel).

**Streamable HTTP** — a single HTTP endpoint (commonly `/mcp`). The client `POST`s JSON-RPC requests; the server may answer with a single `application/json` body or upgrade to an SSE stream (`text/event-stream`) for streaming/server-initiated messages. After the `initialize` response, the client must echo the negotiated version on every request via the `MCP-Protocol-Version` header, and persist any `Mcp-Session-Id` the server returns. This replaces the deprecated two-endpoint SSE transport.

**Legacy SSE** — two endpoints (a GET SSE stream + a POST channel). Only for servers that predate Streamable HTTP. Detect-and-fallback (see §2.3); don't build new clients on it.

### Choosing and detecting

- If you control the launch command → **stdio**.
- If you have a URL → try **Streamable HTTP** first, fall back to **SSE** only if the server rejects it (HTTP 400/404/405 on the `initialize` POST).
- Public server registries (the MCP registry `server.json`) list `remotes[]` entries typed `streamable-http` or `sse`; prefer the `streamable-http` entry.

---

## 2. Programmatic client (official SDK)

Install: `npm i @modelcontextprotocol/sdk` (TypeScript) or `pip install mcp` / `uv add mcp` (Python). The TypeScript SDK uses subpath imports under `@modelcontextprotocol/sdk/...`. (As of Jun 2026; check the current import paths and package name at https://github.com/modelcontextprotocol/typescript-sdk and https://github.com/modelcontextprotocol/python-sdk before pinning.)

### 2.1 Connect to a remote server (Streamable HTTP) — TypeScript

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';

const client = new Client(
  { name: 'my-agent', version: '1.0.0' },
  // Advertise the client capabilities you actually implement (see §3.4).
  { capabilities: { /* roots: { listChanged: true }, sampling: {}, elicitation: {} */ } }
);

const transport = new StreamableHTTPClientTransport(
  new URL('https://api.example.com/mcp'),
  {
    // Provider-specific static headers (API key, etc.). For OAuth, prefer authProvider — see §5.
    requestInit: {
      headers: new Headers({ Authorization: `Bearer ${process.env.MCP_TOKEN}` }),
    },
  }
);

await client.connect(transport); // performs the initialize handshake for you
// ... use the client (see §3) ...
await client.close();
```

`client.connect()` runs the full `initialize` exchange: it sends the client's protocol version + capabilities + `clientInfo`, receives the server's negotiated version + capabilities + `serverInfo`, and sends the `notifications/initialized` ack. You don't hand-roll JSON-RPC.

### 2.2 Connect to a local server (stdio) — TypeScript

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

const transport = new StdioClientTransport({
  command: 'npx',
  args: ['-y', '@scope/some-mcp-server'],
  env: { ...process.env, SOME_API_KEY: process.env.SOME_API_KEY ?? '' }, // pass only what's needed
});

const client = new Client({ name: 'my-agent', version: '1.0.0' });
await client.connect(transport);
```

Secrets reach a local server via its **environment**, not the command line (args are visible in `ps`). Pass an explicit `env` allowlist rather than leaking your whole environment.

### 2.3 Streamable HTTP with SSE fallback (support old + new servers)

This is the canonical compatibility pattern from the SDK docs:

```typescript
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';

async function connectRemote(url: string) {
  const baseUrl = new URL(url);
  try {
    // Preferred: modern Streamable HTTP
    const client = new Client({ name: 'my-agent', version: '1.0.0' });
    const transport = new StreamableHTTPClientTransport(baseUrl);
    await client.connect(transport);
    return { client, transport };
  } catch {
    // Legacy fallback: old HTTP+SSE servers (deprecated 2025-03-26)
    const client = new Client({ name: 'my-agent', version: '1.0.0' });
    const transport = new SSEClientTransport(baseUrl);
    await client.connect(transport);
    return { client, transport };
  }
}
```

### 2.4 Python client (Streamable HTTP, with stdio + legacy SSE shown)

```python
import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.client.streamable_http import streamablehttp_client  # preferred for remote
# from mcp.client.sse import sse_client                       # legacy fallback only

async def remote():
    # Provider-specific auth headers; for OAuth see the SDK auth helpers (§5).
    headers = {"Authorization": "Bearer "}
    async with streamablehttp_client(
        "https://api.example.com/mcp", headers=headers, timeout=30
    ) as (read, write, _get_session_id):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print("tools:", [t.name for t in tools.tools])
            result = await session.call_tool("dns_lookup", {"domain": "example.com"})
            print(result.content)

async def local():
    params = StdioServerParameters(command="uvx", args=["some-mcp-server"], env=None)
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            print((await session.list_tools()).tools)

asyncio.run(remote())
```

> SDK note (Jun 2026): newer Python SDK releases also expose a `streamable_http_client(url, http_client=httpx.AsyncClient(...))` form where you configure headers/timeout/auth on an `httpx.AsyncClient` (set `follow_redirects=True`). Use whichever your installed `mcp` version documents — `python -c "import mcp; print(mcp.__version__)"` then check that version's `docs/`.

---

## 3. Using server capabilities

After `initialize`, only call primitives the server actually advertised in its capabilities. Calling a method the server didn't declare returns a JSON-RPC error (`-32601 Method not found`).

### 3.1 Tools — discover and call

```typescript
// List (paginate — never assume one page; see §3.5)
const { tools } = await client.listTools();
console.log(tools.map(t => `${t.name}: ${t.description}`));
// Each tool has a JSON Schema `inputSchema`; validate arguments against it before calling.

// Call
const result = await client.callTool({
  name: 'dns_lookup',
  arguments: { domain: 'example.com', type: 'MX' },
});

// Read structured + unstructured output. `content` is an array of typed blocks.
for (const block of result.content) {
  if (block.type === 'text') console.log(block.text);
  // other block types: 'image' (data+mimeType), 'resource', 'resource_link', 'audio'
}
// Tools signal failures via result.isError === true (NOT a transport/JSON-RPC error).
// Modern servers may also return `result.structuredContent` (typed JSON) — prefer it when present.
if (result.isError) {
  throw new Error('Tool reported an error: ' + JSON.stringify(result.content));
}
```

**Two error channels — keep them straight:**
- **Protocol errors** (bad params, unknown method, transport down) → thrown as an `McpError` carrying a JSON-RPC code (v1 SDK; v2 splits this into local `SdkError` vs server-side `ProtocolError`).
- **Tool execution errors** (the DNS lookup failed, the API 500'd) → returned *successfully* with `result.isError === true` and a human-readable message in `content`. The model is meant to see and react to these, so don't mask them.

### 3.2 Resources — list, read, templates, subscribe

```typescript
// List concrete resources (paginate on nextCursor)
const { resources } = await client.listResources();
// Read one by URI
const { contents } = await client.readResource({ uri: 'config://app/settings' });
for (const item of contents) {
  // item.uri, item.mimeType, and either item.text or item.blob (base64)
  console.log(item.uri, item.mimeType);
}

// Resource templates (RFC 6570 URI templates) for parameterized reads
const { resourceTemplates } = await client.listResourceTemplates();
// e.g. uriTemplate: "github://repos/{owner}/{repo}/issues/{id}"

// If the server's resources capability advertises `subscribe: true`:
await client.subscribeResource({ uri: 'log://app/today' });
client.setNotificationHandler(
  /* ResourceUpdatedNotificationSchema */ undefined as any,
  (n) => console.log('resource changed:', n.params.uri)
);
```

Resources are **app-controlled context** (read-only data the host chooses to feed the model), distinct from tools (model-invoked actions). Don't treat a `resources/read` as a side-effecting call.

### 3.3 Prompts — list and fill

```typescript
const { prompts } = await client.listPrompts(); // each has name + argument schema
const { messages } = await client.getPrompt({
  name: 'review-code',
  arguments: { code: 'console.log("hello")' },
});
// `messages` is a ready-to-send array of {role, content} you forward to the model.
```

Prompts are **user-controlled** templates (often surfaced as slash commands / menu items). Let the user pick them; don't auto-invoke silently.

### 3.4 Client-provided capabilities (the reverse direction)

A server can call *back* into your client if you advertised the capability in `initialize`:
- **roots** — you expose filesystem roots the server may operate within (register a `ListRootsRequest` handler).
- **sampling** — the server asks *your* model to generate text (`sampling/createMessage`). Gate this behind user approval and a token/cost budget; a malicious server could otherwise drive your LLM spend.
- **elicitation** — the server asks the user for structured input mid-call (you render a form from a JSON Schema and return the answer). Never auto-fill secrets; show the user what's being requested.

Only advertise what you actually implement and intend to honor.

### 3.5 Pagination, progress, cancellation, timeouts

```typescript
// Pagination: list endpoints return nextCursor; loop until it's undefined.
const allTools = [];
let cursor: string | undefined;
do {
  const page = await client.listTools({ cursor });
  allTools.push(...page.tools);
  cursor = page.nextCursor;
} while (cursor);

// Per-call timeout (default is 60s). On timeout the SDK sends a cancellation to the server.
// v1 SDK (npm i @modelcontextprotocol/sdk): McpError + ErrorCode from .../sdk/types.js.
// v2 renamed these — local errors become `SdkError`/`SdkErrorCode` and protocol errors
// `ProtocolError`/`ProtocolErrorCode`, both imported from `@modelcontextprotocol/client`.
// Check your installed major version and import accordingly.
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
try {
  const r = await client.callTool(
    { name: 'slow-operation', arguments: {} },
    { timeout: 120_000 } // override default 60s
  );
} catch (e) {
  if (e instanceof McpError && e.code === ErrorCode.RequestTimeout) {
    console.error('timed out');
  } else { throw e; }
}

// Progress + long-running work: pass onprogress; reset the timeout as progress arrives,
// but cap total wall-clock with maxTotalTimeout so a stalled server can't hang forever.
await client.callTool(
  { name: 'long-operation', arguments: {} },
  {
    onprogress: ({ progress, total }) => console.log(`${progress}/${total ?? '?'}`),
    resetTimeoutOnProgress: true,
    maxTotalTimeout: 600_000,
  }
);

// Manual cancellation via AbortSignal:
const ac = new AbortController();
const p = client.callTool({ name: 'x', arguments: {} }, { signal: ac.signal });
// ac.abort();  // sends notifications/cancelled to the server
```

### 3.6 JSON-RPC / SDK error codes

| Code | Name | Typical cause | Client action |
|------|------|---------------|---------------|
| `-32700` | Parse error | Malformed JSON on the wire | Bug in transport/serialization; report |
| `-32600` | Invalid request | Bad JSON-RPC envelope | Fix request shape |
| `-32601` | Method not found | Called a capability the server didn't advertise | Check `initialize` capabilities first |
| `-32602` | Invalid params | Arguments fail the tool's `inputSchema` | Validate args against the schema |
| `-32603` | Internal error | Server-side exception | Retry with backoff; if persistent, report |
| `-32002` | Resource not found | Bad/expired resource URI | Re-list resources |
| `-32001` | Request timeout (SDK) | No response within `timeout` | Backoff/retry; raise timeout for slow tools |

Distinguish these (protocol failures) from `result.isError === true` (the tool ran but failed). Retry only idempotent operations; never blindly retry a tool that may have side effects.

---

## 4. Configuring AI clients

Below: **local (stdio)** and **remote

…

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [san-npm](https://github.com/san-npm)
- **Source:** [san-npm/skills-ws](https://github.com/san-npm/skills-ws)
- **License:** MIT
- **Homepage:** https://skills-ws.vercel.app

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** yes
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: flagged — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-san-npm-skills-ws-mcp-client
- Seller: https://agentstack.voostack.com/s/san-npm
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
