AgentStack
SKILL unreviewed MIT Self-run

Mcp Client

skill-san-npm-skills-ws-mcp-client · by san-npm

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.

No reviews yet
0 installs
13 views
0.0% view→install

Install

$ agentstack add skill-san-npm-skills-ws-mcp-client

Open-source listing — not yet scanned by AgentStack. Follow the source repository for install instructions.

Security review

⚠ Flagged

2 finding(s); flagged for manual review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures
  • high Dangerous shell/eval execution.
  • high Destructive filesystem operation.

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution Used
  • Environment & secrets Used
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Mcp Client? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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 POSTs 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

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

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:

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)

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

// 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

// 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

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

// 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.

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

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.