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())
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.
| Framework | Connect with x-api-key over Streamable HTTP |
|---|---|
| OpenAI Agents SDK | MCPServerStreamableHttp(params={"url": "…", "headers": {"x-api-key": "…"}}) — docs |
| LangChain | MultiServerMCPClient with "transport": "streamable_http" and a "headers" dict — docs |
| LlamaIndex | BasicMCPClient(url, headers={"x-api-key": "…"}) + McpToolSpec — docs |
| Pydantic AI | MCPServerStreamableHTTP(url, headers={"x-api-key": "…"}) — docs |
| Vercel AI SDK | createMCPClient with an http transport that takes a headers map — docs |
| Mastra | new 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.
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 passheaders=(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 usablename: either you omitted it, or you passedparamspositionally — a non-objectparamsis read as empty, sonamegoes missing with it. If instead every call fails with HTTP400, the body isn't reaching the server as JSON-RPC at all; check you're sending JSON rather than form-encoded. It is not theMCP-Protocol-Versionheader — 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-26or2025-06-18and you get it back; offer anything else and you get2025-06-18, the newest we speak. Readresult.protocolVersionfor 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.