MCP Server Integration Example for Claude Plugins: A Complete Walkthrough
Claude plugins communicate with MCP (Model-Code Protocol) servers through a minimal JSON-RPC client that handles initialization, tool discovery, and remote execution.
The anthropics/claude-plugins-community repository demonstrates this pattern in the Tres Finance plugin, where a plugin invokes remote GraphQL queries and data exports via an MCP endpoint. This guide walks through the complete integration using production-tested code from that implementation.
Understanding the MCP Integration Architecture
MCP server integration in Claude plugins follows a three-layer architecture. Each layer has a specific responsibility and communicates through well-defined interfaces.
Layer 1: Plugin Declaration
Every Claude plugin begins with a .claude-plugin/plugin.json file. This registers the plugin with Claude's system and optionally exposes user-configurable secrets.
In the Tres Finance plugin, located at tres-finance-plugin/.claude-plugin/plugin.json (lines 19-26), the userConfig section defines API keys that users supply:
{
"id": "tres-finance",
"name": "Tres Finance",
"userConfig": {
"TRES_API_KEY": {
"type": "string",
"description": "Your Tres Finance API key",
"required": true
}
}
}
Claude injects these values as environment variables when running skill scripts.
Layer 2: The MCP Client
The McpClient class in tres-finance-plugin/skills/tres-report-create/tests/run_report_matrix.py (lines 55-82) provides the core communication layer. It wraps standard HTTP requests with MCP-specific headers and JSON-RPC formatting.
Key responsibilities include:
- Session management – stores and reuses the
Mcp-Session-Idheader returned by the server - Protocol versioning – uses
MCP-Protocol-Version: 2025-06-18for compatibility negotiation - Authentication – attaches Bearer tokens via the
Authorizationheader - RPC marshaling – formats requests as JSON-RPC 2.0 and parses responses
Layer 3: Skill Scripts
Skill scripts consume the MCP client to perform domain-specific work. The orchestrate_reprice.py script (lines 14-19) demonstrates how a skill receives data from Claude, constructs GraphQL mutations, and returns payloads for MCP execution.
Building the MCP Client from Scratch
Below is a production-ready McpClient implementation distilled from run_report_matrix.py. This version uses only standard library modules for maximum portability:
import json
import urllib.request
import urllib.error
PROTOCOL_VERSION = "2025-06-18"
class McpClient:
def __init__(self, url: str, token: str):
self._url = url
self._token = token
self._session_id = None
self._next_id = 0
def _headers(self):
"""Build request headers with authentication and session state."""
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
"Authorization": f"Bearer {self._token}",
"MCP-Protocol-Version": PROTOCOL_VERSION,
}
if self._session_id:
headers["Mcp-Session-Id"] = self._session_id
return headers
def _post(self, payload: dict):
"""Execute HTTP POST and return status, headers, and body."""
data = json.dumps(payload).encode()
request = urllib.request.Request(
self._url,
data=data,
headers=self._headers(),
method="POST"
)
try:
with urllib.request.urlopen(request, timeout=120) as response:
raw_body = response.read().decode()
headers = {k.lower(): v for k, v in response.headers.items()}
return response.status, headers, raw_body
except urllib.error.HTTPError as exc:
return exc.code, {}, exc.read().decode()
def _rpc(self, method: str, params: dict):
"""Execute JSON-RPC call and handle session/response state."""
self._next_id += 1
payload = {
"jsonrpc": "2.0",
"id": self._next_id,
"method": method,
"params": params
}
status, headers, body = self._post(payload)
# Capture session ID for subsequent calls
if "mcp-session-id" in headers:
self._session_id = headers["mcp-session-id"]
if status >= 400:
raise RuntimeError(f"{method} -> HTTP {status}: {body[:200]}")
result = json.loads(body)
if "error" in result:
raise RuntimeError(f"{method} -> JSON-RPC error: {result['error']}")
return result.get("result", {})
def initialize(self):
"""Start MCP session and return server capabilities."""
return self._rpc("initialize", {
"protocolVersion": PROTOCOL_VERSION,
"capabilities": {},
"clientInfo": {"name": "claude-plugin-client", "version": "1.0.0"},
})
def list_tools(self):
"""Discover available tools from the MCP server."""
return self._rpc("tools/list", {})
def call_tool(self, name: str, arguments: dict):
"""Execute a specific tool with the provided arguments."""
return self._rpc("tools/call", {"name": name, "arguments": arguments})
This implementation mirrors the production code in run_report_matrix.py while remaining self-contained for adaptation to other plugins.
Complete MCP Server Integration Example
The following end-to-end example demonstrates the full integration pattern: initialization, tool discovery, and remote query execution against the Tres Finance MCP endpoint:
import os
# Configuration loaded from plugin.json userConfig
MCP_URL = "https://ai.tres.finance/mcp"
API_TOKEN = os.environ.get("TRES_API_KEY", "your-api-key-here")
def main():
# Step 1: Create client and initialize session
client = McpClient(MCP_URL, API_TOKEN)
server_info = client.initialize()
print(f"Connected: {server_info.get('name')} v{server_info.get('version')}")
# Step 2: Discover available tools
tools_response = client.list_tools()
available_tools = tools_response.get("tools", [])
# Find the execution tool (typically named 'execute_query' or similar)
execute_tool = next(
(t for t in available_tools if "execute" in t["name"].lower()),
None
)
if not execute_tool:
raise RuntimeError("No execution tool found in available tools")
print(f"Discovered tool: {execute_tool['name']}")
print(f"Description: {execute_tool.get('description', 'N/A')}")
# Step 3: Execute remote GraphQL query
graphql_query = """
query OrganizationBalance($orgId: ID!) {
organizationBalance(organizationId: $orgId) {
assets {
name
balance
usdValue
}
totalUsdValue
}
}
"""
result = client.call_tool(execute_tool["name"], {
"query": graphql_query,
"variables": {"orgId": "org_demo_001"}
})
# Step 4: Process structured response
content = result.get("content", [])
for item in content:
if item.get("type") == "text":
data = json.loads(item.get("text", "{}"))
print(json.dumps(data, indent=2))
if __name__ == "__main__":
main()
The response from call_tool contains either content (text descriptions) or structuredContent (typed data objects), depending on the tool implementation.
Integrating MCP Calls into Skill Workflows
Skill scripts in the Tres Finance plugin demonstrate how to bridge Claude's natural language processing with MCP remote execution. The orchestrate_reprice.py script (lines 14-19) follows this pattern:
- Receive structured input from Claude (parsed user intent)
- Build domain-specific payloads (GraphQL mutations for ASC 845 swap repricing)
- Return execution instructions that Claude routes through the MCP
executetool
This creates a clean separation: the skill script handles business logic while the MCP client handles transport and protocol concerns.
Error Handling and Session Management
The production implementation in run_report_matrix.py includes several resilience patterns:
- Automatic session recovery – the
Mcp-Session-Idheader binds requests to server-side state - Timeout configuration – 120-second default for long-running queries
- Structured error extraction – the helper
extract_graphql()function (lines 52-66) pulls{data, errors}from tool results
For plugins requiring robust operation, wrap MCP calls in retry logic with exponential backoff, particularly for initialization and tool discovery phases.
Summary
- Declare your plugin in
.claude-plugin/plugin.jsonwith any required API keys inuserConfig - Implement
McpClientas a thin JSON-RPC wrapper handling headers, session IDs, and protocol version2025-06-18 - Call
initialize()before any tool operations to establish server session state - Use
list_tools()to discover available remote capabilities dynamically - Execute work via
call_tool()with structured arguments matching the tool's schema - Reference
run_report_matrix.py(lines 55-82) andorchestrate_reprice.py(lines 14-19) for production patterns
Frequently Asked Questions
What protocol version does the MCP client use?
The Tres Finance plugin uses 2025-06-18 as the MCP-Protocol-Version header, defined as a constant in run_report_matrix.py (line 35). This version string is sent with every request for server-side compatibility negotiation.
How does session management work in MCP integration?
The MCP server returns a Mcp-Session-Id header after the initialize call. The client stores this value and includes it in subsequent requests via the same header name, as implemented in the _headers() method (lines 58-65 of run_report_matrix.py).
Can a Claude plugin use multiple MCP servers?
Yes. Instantiate separate McpClient instances with different URLs and tokens. Each maintains independent session state. The plugin architecture does not limit the number of remote endpoints, though each requires its own initialization sequence.
Where should API credentials be stored?
Define them in plugin.json's userConfig section. Claude injects these as environment variables when executing skill scripts. Access them via os.environ rather than hardcoding, as shown in the complete example above.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →