Skip to main content

Connect a custom client

If you're building your own agent rather than using Claude Desktop or Claude Code, connect directly with the official MCP SDK. Both TypeScript and Python SDKs ship with Streamable-HTTP transport built in.

TypeScript​

npm install @modelcontextprotocol/sdk
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" },
{ capabilities: {} },
);

// No trailing slash on the URL — /mcp/ gets redirected and the handshake fails
await client.connect(
new StreamableHTTPClientTransport(new URL("https://mcp.edited.com/mcp"), {
requestInit: { headers: { "x-api-key": "YOUR_API_KEY" } },
}),
);

// Discover what's available
const { tools } = await client.listTools();

// Call a tool
const result = await client.callTool({
name: "mdmcp-search_brands",
arguments: { query: "Nike", search_limit: 5 },
});

// Refusals reach you on two channels, so check both — see Concepts → Response
// shape. `isError` means the call didn't complete: there is no structured
// payload and the text is prose. Otherwise the payload carries exactly one of
// `result` or `error`.
if (result.isError) throw new Error(result.content?.[0]?.text ?? "call failed");
const body = result.structuredContent;
if (body.error) throw Object.assign(new Error(body.error.message), body.error);
const brands = body.result;
console.log(brands);
// → [{ name: "Nike", slug: "nike" }, ...]

await client.close();

Python​

pip install mcp
import asyncio
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client

async def main():
headers = {"x-api-key": "YOUR_API_KEY"}
# No trailing slash on the URL — /mcp/ gets redirected and the handshake fails
async with streamablehttp_client("https://mcp.edited.com/mcp", headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
result = await session.call_tool(
"mdmcp-search_brands",
{"query": "Nike", "search_limit": 5},
)
# Refusals reach you on two channels — see Concepts → Response shape.
# isError means the call didn't complete, and structuredContent is
# None there, so check it before touching the payload.
if result.isError:
text = result.content[0].text if result.content else "call failed"
raise RuntimeError(text)
body = result.structuredContent
if "error" in body:
raise RuntimeError(
f"{body['error']['code']}: {body['error']['message']}"
)
print(body["result"])

asyncio.run(main())
warning

Using the fastmcp client instead? Read .structured_content, not .data. .data unwraps correctly on success but yields None on a refusal, with the parse error swallowed and is_error still False — see Concepts → Response shape.

Using your agent's framework​

Most agent frameworks speak MCP natively — point them at the same endpoint and pass your key as the x-api-key header. That header detail matters: EDITED MCP authenticates with x-api-key, not a bearer token, so the framework has to let you set an arbitrary request header.

FrameworkConnect with x-api-key over Streamable HTTP
OpenAI Agents SDKMCPServerStreamableHttp(params={"url": "…", "headers": {"x-api-key": "…"}}) — docs
LangChainMultiServerMCPClient with "transport": "streamable_http" and a "headers" dict — docs
LlamaIndexBasicMCPClient(url, headers={"x-api-key": "…"}) + McpToolSpec — docs
Pydantic AIMCPServerStreamableHTTP(url, headers={"x-api-key": "…"}) — docs
Vercel AI SDKcreateMCPClient with an http transport that takes a headers map — docs
Mastranew MCPClient({ servers: { edited: { url, requestInit: { headers: { "x-api-key": "…" } } } } }) — docs

In each case the URL is the same MCP endpoint and the framework handles the JSON-RPC handshake; you supply the x-api-key header.

Claude via the Anthropic API

Anthropic's hosted MCP connector (the mcp_servers parameter on the Messages API) only forwards a bearer token via authorization_token, so it can't send the x-api-key EDITED MCP requires. To call EDITED MCP from Claude, connect with the official MCP SDK client yourself (the TypeScript / Python examples above) and hand the resulting tools to the model with Anthropic's client-side MCP helpers — or use the ready-made app integrations in Connect Claude Code and Connect Claude Desktop.

What to do with the result​

The identifier-resolution tools return objects with stable identifiers (slug for retailers/brands, id for everything else). Feed those identifiers into the market-data query tools to pull the actual data — see Recipes → Example workflow for an end-to-end demo.

Troubleshooting​

401 Unauthorized on every call

  • Missing or wrong x-api-key, or a key that isn't enabled for MCP. Set the header on the transport (TS) or pass headers= (Python), as shown above — see Authentication & access for how to get a key.

Connection refused / timeout

  • Wrong URL, or the endpoint isn't reachable from your network. Confirm with curl -I https://mcp.edited.com/mcp — an HTTP status (e.g. 401) means it's reachable; a connection error means it isn't.

Handshake fails with a 307 or the connection dies after the first call

  • The URL has a trailing slash. Use https://mcp.edited.com/mcp — the /mcp/ form is redirected in a way most HTTP clients can't follow (see Concepts → Transports).

MCP error -32600

  • The envelope wasn't usable. On a tools/call, the usual cause is no usable name: either you omitted it, or you passed params positionally — a non-object params is read as empty, so name goes missing with it. If instead every call fails with HTTP 400, the body isn't reaching the server as JSON-RPC at all; check you're sending JSON rather than form-encoded. It is not the MCP-Protocol-Version header — this gateway never validates it, so adding or removing it won't change the result. See Operations → Errors.

The handshake reports a protocol version I didn't ask for

  • Expected. The revision is negotiated: offer one of 2024-11-05, 2025-03-26 or 2025-06-18 and you get it back; offer anything else and you get 2025-06-18, the newest we speak. Read result.protocolVersion for what you actually got — and note that negotiating an older revision doesn't reduce what the server sends. See Operations → Versioning.

Empty result on a real query

  • Vector search is misspelling-tolerant but not magic. Try a more specific query or batch several phrasings — see Concepts → Tools.

For the full error model — HTTP statuses, JSON-RPC codes, and retry guidance — see Operations → Errors. For cross-client issues that aren't specific to your setup, see Operations → Troubleshooting.