Building Custom MCP Clients
Learn the responsibilities of a host application that discovers and invokes MCP capabilities.
Client as policy mediator
A custom MCP client sits between the host, model, user, and server. It should negotiate the protocol, discover capabilities, normalize schemas, enforce host policy, request approval, invoke operations, and translate results into a controlled model context. Do not make the client a transparent tunnel. A transparent tunnel lets server metadata or model output bypass product rules. Keep server connection state separate from conversation state so reconnects and user sessions cannot accidentally share authority.
Discover and curate
When a server connects, inspect its protocol version, capabilities, names, descriptions, and schemas. Store a reviewed or allowlisted representation and expose only the tools appropriate to the current user, tenant, and task. Names and descriptions are untrusted input and may contain misleading instructions. Detect collisions, unexpected capability growth, and schema changes. If discovery fails or a server offers an incompatible version, present a clear degraded state instead of guessing how to call an unknown interface.
def requires_confirmation(tool: str) -> bool:
return tool in {"create_ticket", "send_email", "delete_record"}
def invoke(tool: str, arguments: dict, approved: bool = False):
if requires_confirmation(tool) and not approved:
return {"status": "needs_confirmation", "tool": tool, "arguments": arguments}
return call_mcp_tool(tool, arguments)Validate model arguments
The model should receive a compact, accurate schema, but the client must parse and validate its proposed arguments before sending them. Reject unknown fields when strictness is safer, enforce enum and length constraints, canonicalize identifiers, and check user or tenant scope. Preserve the original proposal for audit while logging only a redacted normalized summary. Never interpolate arguments directly into shell commands, URLs, database queries, or markup. Argument validation is a deterministic checkpoint in an otherwise probabilistic workflow.
Authorize and approve
Use policy code to classify operations as read-only, sensitive read, reversible write, irreversible write, external communication, or administrative. Apply permissions before showing approval, then display the exact tool, target, arguments, and expected side effect. Bind confirmation to a request identifier and expire it after a short period. For repeated low-risk reads, a user may grant a narrow session permission. For high-impact actions, require fresh confirmation and server-side verification of the approved payload.
Normalize tool results
A server result may contain text, structured values, resource links, errors, or unexpected content. Check its shape, size, encoding, and declared status before passing it to the model or UI. Preserve provenance and identifiers so answers can cite what was used. Mark returned text as untrusted data and prevent it from becoming host instructions. Redact secrets and private fields according to policy. A malformed or oversized result should produce a bounded error, not crash the conversation or consume the entire context window.
Design for disconnects
Connections can close during discovery, execution, streaming, or cancellation. Track connection state, request IDs, deadlines, and whether an operation may have committed before the response was lost. Retry only safe idempotent calls, and use idempotency keys for supported writes. Recover discovery state after reconnecting and re-check capability versions. Give users a truthful status such as “completion unknown” when the client cannot determine whether a side effect happened. Do not silently repeat a potentially destructive request.
Test the complete host loop
Client tests should cover trusted and untrusted servers, capability changes, duplicate names, invalid schemas, malicious descriptions, prompt injection in results, authorization boundaries, approval cancellation, timeouts, partial streams, malformed payloads, and reconnects. Add contract tests against representative servers and an end-to-end test with the actual model adapter. Measure tool latency, token overhead, approval frequency, and error rates. The finished client should fail closed on privilege questions while remaining clear enough that users can recover from ordinary connectivity failures.