Build a custom MCP client
This tutorial builds an MCP client from scratch that uses ScaiGrid as its tool source. The client connects, lists tools, lets an LLM pick one, calls it, and feeds the result back. By the end you'll have a working agent loop where tool discovery and invocation are entirely runtime — no hardcoded endpoint maps.
You need:
- Python 3.10+ or Node 18+.
- The MCP SDK:
pip install mcpornpm install @modelcontextprotocol/sdk. - A ScaiGrid API key (
sgk_...) and the host URL. - An LLM you can call separately — for this tutorial we'll use ScaiGrid's own
inference_chattool as the agent's brain, but you can swap in any client.
1 2 | |
1. Connect and list#
The streamable-HTTP transport opens one long-lived bidirectional connection. List tools right after initialize — the result is filtered to what your token can call.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
The list mixes three sources transparently: core tools, module-contributed tools (anything from modules enabled for your tenant), and remote.* tools from cloud MCP servers your user or tenant has registered through ScaiLink. You don't have to distinguish them in code.
2. Pick the chat tool, run a completion#
1 2 3 4 5 6 7 | |
1 2 3 4 5 6 7 8 9 10 | |
Every tool result is wrapped in a single text content block holding a JSON-encoded payload. Decode it with json.loads / JSON.parse.
3. Filter the catalog for an agent loop#
A real agent doesn't expose all 80+ tools to its LLM — it picks a relevant subset based on the task. Filter the list yourself:
1 2 3 4 5 6 7 8 | |
Pass scoped to your LLM as the available tools. The LLM sees a sensible task-shaped surface and won't hallucinate a call to tenants_create halfway through your research workflow.
4. End-to-end agent loop#
The classic shape: feed user goal → LLM picks tool → call MCP → feed result back → repeat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | |
The TypeScript shape is the same: listTools() → filter to a subset → callTool({ name: "inference_chat", arguments: { ..., tools: toolSpecs } }) → parse the result → if tool_calls returned, call each and feed the result back as a role: "tool" message → repeat with an iteration cap.
Run it with a goal like "Tell me my token spend for today and recommend a cheaper model if I'm above 80% of my budget."
5. Handling remote tools#
If your user (or tenant) has registered cloud MCP servers through ScaiLink, those tools show up in the same list_tools response with names like remote.tenant.slack-acme.post_message. The agent loop above works without modification — call_tool routes the remote.* prefix through ScaiLink's outbound client transparently.
Two things to know:
- The tool's description, input schema, and behaviour come from the upstream server, not ScaiGrid. Inspect them at runtime; they may change.
- Errors from upstream are returned with
codeset to the upstream's error class name plus amessage. Treat them as opaque errors at the agent layer.
6. Hygiene#
Reconnect on transport drops; cap loop iterations as a circuit breaker; re-list tools periodically because modules can be enabled or disabled mid-session; let ScaiMCP enforce permissions instead of caching decisions client-side — treat PERMISSION_DENIED as a normal failure mode.
Done#
You have a working MCP agent that consumes ScaiGrid through a unified, permission-filtered catalog. The same pattern works for any LLM brain — MCP is the transport, the brain is yours.