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-Id header returned by the server
  • Protocol versioning – uses MCP-Protocol-Version: 2025-06-18 for compatibility negotiation
  • Authentication – attaches Bearer tokens via the Authorization header
  • 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:

  1. Receive structured input from Claude (parsed user intent)
  2. Build domain-specific payloads (GraphQL mutations for ASC 845 swap repricing)
  3. Return execution instructions that Claude routes through the MCP execute tool

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-Id header 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.json with any required API keys in userConfig
  • Implement McpClient as a thin JSON-RPC wrapper handling headers, session IDs, and protocol version 2025-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) and orchestrate_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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →