MCP Client Overview
Jiki provides two levels of MCP (Model Context Protocol) clients based on a hybrid inheritance/protocol approach:
BaseMCPClient: Abstract base class defining the structure and shared utilities for MCP clients. Designed for extension if you need custom transport logic or behaviors.JikiClient: The standard, concrete, full-featured client inheriting fromBaseMCPClient. Implements the completeIMCPClientprotocol usingfastmcp. Recommended for most use cases.
Deprecated Aliases: For backward compatibility, MCPClient aliases BaseMCPClient and EnhancedMCPClient aliases JikiClient, but these will be removed in a future version and emit DeprecationWarning.
1. BaseMCPClient (Abstract Base)
Subclass BaseMCPClient when you need to build a custom MCP client, for example:
- To integrate with a transport library other than fastmcp.
- To add custom retry logic around RPC calls.
- To implement specialized error handling or logging.
You would inherit from BaseMCPClient and implement the abstract _call_rpc method to handle the actual communication.
# Conceptual example - Do not run directly
from jiki.mcp_client import BaseMCPClient
from typing import Any
# import my_custom_rpc_library # Assume this exists
class MyCustomClient(BaseMCPClient):
def __init__(self, endpoint: str):
super().__init__(connection_info=endpoint)
# self._rpc_conn = my_custom_rpc_library.connect(endpoint)
async def _call_rpc(self, method: str, params: dict | None = None) -> Any:
# Implementation using the custom library
# response = await self._rpc_conn.call(method, params)
# if response.is_error:
# raise ConnectionError(f"RPC Error: {response.error_message}")
# return response.result
pass # Placeholder
# You would then need to implement the full IMCPClient interface methods
# often by calling self._call_rpc and self._process_mcp_result
- Extensibility: Provides a clear structure for building custom clients.
- Shared Logic: Inherits utility methods like
_process_mcp_result(for parsing standard MCP results).
When to use BaseMCPClient
- You are building a significantly different client implementation (e.g., different transport).
- You need fine-grained control over the RPC call mechanism itself.
- You are comfortable implementing the required abstract methods and potentially the public interface methods (
discover_tools, etc.) using_call_rpc.
2. JikiClient (Standard Concrete Client)
This is the recommended client for standard Jiki usage. It bundles key MCP capabilities using fastmcp:
- Tool discovery
- Resource listing & reading
- Roots management
- Interaction tracing
- Spec-compliant handshakes
Construction
It's typically constructed internally by the Jiki() factory function based on the mcp_mode, mcp_script_path, or mcp_url arguments provided.
# Example of how Jiki() might create it internally for stdio
from jiki.mcp_client import JikiClient
connection_info = {"type": "stdio", "script_path": "servers/calculator_server.py"}
client = JikiClient(connection_info=connection_info)
# Example for SSE
# connection_info_sse = {"type": "sse", "url": "http://localhost:8000/mcp"}
# client_sse = JikiClient(connection_info=connection_info_sse)
Key Methods (used by JikiOrchestrator)
initialize(): Handles the MCP handshake.discover_tools(): Retrieves tool schemas.execute_tool_call(): Runs a specific tool.list_resources(),read_resource(): Manages resources.list_roots(),send_roots_list_changed(): Manages roots.
Tracing & Server Log Capture
The JikiClient automatically logs all MCP interactions when a TraceLogger is provided (via Jiki(trace=True)). This includes:
- JSON‑RPC handshake records
- <mcp_tool_call> blocks
- <mcp_tool_result> blocks
- Server‑side log entries emitted via the utilities/logging/log notification (per the MCP spec)
Each saved trace bundle includes handshakes, tool calls/results, and any server-side log entries.
# Example trace output fragment
# (Assuming logger is active)
# MCP call log
print("[DEBUG] Calling MCP method: tools/call")
# ... JSON-RPC request logged ...
# MCP result log
print("[DEBUG] Received MCP result for tools/call")
# ... JSON-RPC response logged ...
# Or for handshakes:
print("[DEBUG] Performing MCP initialize handshake...")
# Traces are stored by the TraceLogger passed to Jiki()
# Example of accessing traces after a run:
# from jiki import Jiki
# jiki_instance = Jiki(trace=True, ...)
# jiki_instance.process("...")
# traces = jiki_instance.get_traces()
# for t in traces:
# print(t)
Choosing the Right Client
| Feature | BaseMCPClient (Abstract) |
JikiClient (Concrete) |
|---|---|---|
| Handshake management | No (must be impl. by subclass) | Yes (via initialize) |
| Tool discovery | No (must be impl. by subclass) | Yes (discover_tools) |
| Resource & roots | No (must be impl. by subclass) | Yes (built-in methods) |
| Tracing & logging | No (subclass responsibility) | Yes (built-in, needs logger) |
| Transport Agnostic | Yes | No (uses fastmcp) |
| Ready to Use | No | Yes |
| Extensibility Point | Yes | Limited (composition pref.) |
Default via Jiki() |
No | Yes |
Use JikiClient (implicitly via Jiki()) for a complete, ready‑to‑use MCP implementation based on fastmcp. Subclass BaseMCPClient only if you need to build a custom client with different transport or core RPC logic.
3. Easy to Learn, Hard to Master
- Zero boilerplate: The
Jiki()factory handles client creation and configuration automatically for the common case. - Protocol‑driven: Swap in your own transport (by subclassing
BaseMCPClient), tool client, or root manager by implementing the corresponding interface and manually constructingJikiOrchestrator. - Advanced tracing:
JikiClient(when given a logger byJiki()) automatically logs every handshake, call, and result for deep inspection. - Error resilience: Catches and formats JSON‑RPC errors, falling back to human‑readable messages.
With Jiki(), you get a robust MCP implementation in a few lines—yet the underlying BaseMCPClient structure allows plugging in custom behavior when needed.
# Easy to learn: default orchestrator creation via Jiki()
from jiki import Jiki
# Jiki() uses JikiClient internally
orchestrator = Jiki(
auto_discover_tools=True,
mcp_script_path="servers/calculator_server.py"
)
# ... use orchestrator ...
# Harder to master: manually creating orchestrator with custom client
from jiki.orchestrator import JikiOrchestrator
from jiki.models.litellm import LiteLLMModel
# from my_custom_client import MyCustomClient # Assume this exists
# model = LiteLLMModel(...)
# custom_client = MyCustomClient(...)
# tools = [...]
# orch = JikiOrchestrator(
# model=model,
# mcp_client=custom_client,
# tools_config=tools
# )