# MCP Server Integration Example for Claude Plugins: A Complete Walkthrough

> Learn how to integrate your Claude plugin with an MCP server using this example walkthrough. Discover tool discovery and remote execution with JSON-RPC.

- Repository: [Anthropic/claude-plugins-community](https://github.com/anthropics/claude-plugins-community)
- Tags: how-to-guide
- Published: 2026-09-06

---

**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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/tres-finance-plugin/.claude-plugin/plugin.json) (lines 19-26), the `userConfig` section defines API keys that users supply:

```json
{
  "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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/run_report_matrix.py). This version uses only standard library modules for maximum portability:

```python
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`](https://github.com/anthropics/claude-plugins-community/blob/main/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:

```python
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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/.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`](https://github.com/anthropics/claude-plugins-community/blob/main/run_report_matrix.py)** (lines 55-82) and [`orchestrate_reprice.py`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/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`](https://github.com/anthropics/claude-plugins-community/blob/main/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.