MCP Pack Integration Guide

MCP Pack Integration Guide

This guide walks you through integrating external MCP (Model Context Protocol) servers into your Intelligence Pack. By the end, you'll have a working pack that calls MCP tools.

Overview

What you'll learn: - How MCP integration works in Huitzo - Configuring MCP servers in your pack manifest - Calling MCP tools from your commands - Handling errors and edge cases

Prerequisites: - Huitzo SDK installed (pip install huitzo-sdk) - Basic familiarity with pack development - An existing pack or willingness to create one


Understanding MCP in Huitzo

What is MCP?

The Model Context Protocol is an open standard for connecting AI systems to external tools and data. MCP servers expose tools (executable functions) that can be called remotely.

How Huitzo Uses MCP

Huitzo consumes MCP servers—it connects to them and makes their tools available via ctx.mcp. This is pure protocol translation; no LLM is required.

Your Pack Command
      │
      ▼
ctx.mcp.call("github", "create_issue", {...})
      │
      ▼
Huitzo MCPClientDriver
      │
      ▼
mcp-server-github (subprocess)
      │
      ▼
GitHub API

Step 1: Choose an MCP Server

MCP has a growing ecosystem of servers. Popular options include:

Server Package Use Case
GitHub mcp-server-github Issues, PRs, repos, gists
PostgreSQL mcp-server-postgres Database queries
Filesystem mcp-server-filesystem Local file operations
Slack mcp-server-slack Messaging, channels
Brave Search mcp-server-brave-search Web search
SQLite mcp-server-sqlite Local database

Browse more at modelcontextprotocol.io/servers.

For this guide, we'll use mcp-server-github as our example.


Step 2: Configure in Manifest

Add the MCP server to your huitzo.yaml:

# huitzo.yaml
pack:
  name: "github-tools"
  namespace: "ghtools"
  version: "1.0.0"
  description: "GitHub integration tools via MCP"

mcp_servers:
  - name: github
    type: stdio
    command: ["uvx", "mcp-server-github"]
    env:
      GITHUB_TOKEN: "${secrets.GITHUB_TOKEN}"

commands:
  - name: "create-issue"
    description: "Create a GitHub issue"
    permissions: ["mcp:call"]
    timeout: 30

secrets:
  user_required:
    - name: "GITHUB_TOKEN"
      description: "Your GitHub personal access token with repo scope"
      help_url: "https://github.com/settings/tokens"

Configuration Explained

Field Purpose
name: github Identifier used in ctx.mcp.call("github", ...)
type: stdio Server runs as a subprocess
command How to start the server (uvx runs Python packages)
env Environment variables passed to the server
${secrets.GITHUB_TOKEN} Interpolated from user's secrets

Version Pinning

Always pin MCP server package versions in production packs. An unpinned package may receive a breaking upstream update that silently changes tool schemas or behavior:

# ✅ Good: Pinned version
command: ["uvx", "mcp-server-github==1.2.3"]

# ❌ Bad: Unpinned - may break on upstream release
command: ["uvx", "mcp-server-github"]

HTTP Transport Alternative

For remote MCP servers:

mcp_servers:
  - name: postgres
    type: http
    url: "https://mcp.example.com/postgres"
    headers:
      Authorization: "Bearer ${secrets.MCP_API_KEY}"
    timeout: 30

Step 3: Declare Required Secrets

Users must provide secrets before using your pack. Declare them clearly:

secrets:
  user_required:
    - name: "GITHUB_TOKEN"
      description: "GitHub personal access token with repo scope"
      help_url: "https://github.com/settings/tokens"

  user_optional:
    - name: "GITHUB_ENTERPRISE_URL"
      description: "GitHub Enterprise URL (optional)"

Best practices: - Write clear descriptions explaining what scope/permissions are needed - Always include help_url pointing to where users get the token - Use user_optional for non-essential features


Step 4: Use MCP Tools in Commands

Create a command that calls MCP tools. The pattern follows three steps:

  1. Define args — Pydantic model describing the command's inputs
  2. Call MCP tool — Use ctx.mcp.call(server, tool, arguments) to invoke the external tool
  3. Handle errors — Catch MCPConnectionError (server down) and MCPToolError (tool-level failure)
# pseudocode — calling an MCP tool from a command

@command("your-action", namespace="your-pack")
async def your_action(args, ctx):
    # 1. Call the MCP tool
    try:
        result = await ctx.mcp.call(
            server="<server-name>",          # as configured in manifest
            tool="<tool-name>",              # tool exposed by the MCP server
            arguments={...}                  # tool-specific arguments
        )
    except MCPConnectionError:
        # Server is down or unreachable → suggest retry
        return {"error": "...", "retry": True}
    except MCPToolError as e:
        # Tool returned an error → surface to user
        return {"error": e.message, "code": e.code}

    # 2. Optionally make follow-up MCP calls using the result
    if some_condition:
        await ctx.mcp.call(server="<server-name>", tool="<another-tool>", arguments={...})

    # 3. Return the result
    return {"key": result["value"], ...}

Tip: Run huitzo mcp list-tools <server> during development to discover available tools and their schemas. See Step 5 below.


Step 5: Discover Available Tools

Before writing commands, discover what tools the MCP server provides.

CLI Discovery

# Start a dev session
huitzo pack dev

# List configured MCP servers
huitzo mcp list-servers

# List tools from GitHub server
huitzo mcp list-tools github

# Get schema for a specific tool
huitzo mcp list-tools github --tool create_issue --schema

Programmatic Discovery

# pseudocode — listing available tools at runtime
tools = await ctx.mcp.list_tools("<server-name>")
# Each tool has: name, description, required_args

Step 6: Test Integration

Start Development Session

cd your-pack-directory
huitzo pack dev

Test with curl

# Create an issue
curl -X POST http://localhost:8080/api/v1/commands/ghtools/create-issue \
  -H "Content-Type: application/json" \
  -d '{
    "owner": "your-username",
    "repo": "test-repo",
    "title": "Test issue from MCP",
    "body": "This issue was created via Huitzo MCP integration!"
  }'

Test with CLI

huitzo run ghtools.create-issue \
  --owner=your-username \
  --repo=test-repo \
  --title="Test issue"

Advanced: Multiple Servers

Use multiple MCP servers for complex workflows:

# huitzo.yaml
mcp_servers:
  - name: github
    type: stdio
    command: ["uvx", "mcp-server-github"]
    env:
      GITHUB_TOKEN: "${secrets.GITHUB_TOKEN}"

  - name: slack
    type: stdio
    command: ["uvx", "mcp-server-slack"]
    env:
      SLACK_BOT_TOKEN: "${secrets.SLACK_BOT_TOKEN}"

  - name: postgres
    type: stdio
    command: ["uvx", "mcp-server-postgres"]
    env:
      DATABASE_URL: "${secrets.DATABASE_URL}"
@command("create-and-notify", namespace="workflow")
async def create_and_notify(args: Args, ctx: Context) -> dict:
    """Create GitHub issue and notify Slack channel."""

    # Create issue in GitHub
    issue = await ctx.mcp.call("github", "create_issue", {
        "owner": args.owner,
        "repo": args.repo,
        "title": args.title
    })

    # Log to database
    await ctx.mcp.call("postgres", "query", {
        "sql": "INSERT INTO issue_log (number, title, created_at) VALUES ($1, $2, NOW())",
        "params": [issue["number"], issue["title"]]
    })

    # Notify Slack
    await ctx.mcp.call("slack", "post_message", {
        "channel": args.slack_channel,
        "text": f"New issue created: {issue['html_url']}"
    })

    return {"issue": issue, "notified": True}

Best practice: Limit your pack to 5-10 MCP servers maximum. Each STDIO server spawns a subprocess, consuming memory and CPU. Packs declaring more than 10 servers may experience degraded startup performance and resource exhaustion.


Advanced: Wrapping Tools

Add business logic around MCP tools:

async def create_labeled_issue(
    ctx: Context,
    owner: str,
    repo: str,
    title: str,
    labels: list[str],
    assignees: list[str] | None = None
) -> dict:
    """Business logic wrapper for issue creation."""

    # Validate repository exists
    try:
        repo_info = await ctx.mcp.call("github", "get_repository", {
            "owner": owner,
            "repo": repo
        })
    except MCPToolError:
        raise ValidationError(
            field="repo",
            value=f"{owner}/{repo}",
            message="Repository not found or inaccessible"
        )

    # Create the issue
    issue = await ctx.mcp.call("github", "create_issue", {
        "owner": owner,
        "repo": repo,
        "title": title
    })

    # Add labels
    if labels:
        await ctx.mcp.call("github", "add_labels", {
            "owner": owner,
            "repo": repo,
            "issue_number": issue["number"],
            "labels": labels
        })

    # Add assignees
    if assignees:
        await ctx.mcp.call("github", "add_assignees", {
            "owner": owner,
            "repo": repo,
            "issue_number": issue["number"],
            "assignees": assignees
        })

    return issue


@command("create-bug", namespace="ghtools")
async def create_bug(args: BugArgs, ctx: Context) -> dict:
    """Create a bug report with standard labels."""
    return await create_labeled_issue(
        ctx,
        owner=args.owner,
        repo=args.repo,
        title=f"[Bug] {args.title}",
        labels=["bug", "needs-triage"],
        assignees=args.assignees
    )


@command("create-feature", namespace="ghtools")
async def create_feature(args: FeatureArgs, ctx: Context) -> dict:
    """Create a feature request with standard labels."""
    return await create_labeled_issue(
        ctx,
        owner=args.owner,
        repo=args.repo,
        title=f"[Feature] {args.title}",
        labels=["enhancement", "needs-discussion"]
    )

Unit Testing with Mock MCP

For unit tests, mock ctx.mcp.call() to avoid spawning real MCP servers:

import pytest
from unittest.mock import AsyncMock, MagicMock
from github_tools.commands.issues import create_issue, CreateIssueArgs


@pytest.fixture
def mock_ctx():
    ctx = MagicMock()
    ctx.mcp = MagicMock()
    ctx.mcp.call = AsyncMock(return_value={
        "number": 42,
        "html_url": "https://github.com/acme/project/issues/42",
        "title": "Test issue"
    })
    ctx.log = MagicMock()
    return ctx


@pytest.mark.asyncio
async def test_create_issue(mock_ctx):
    args = CreateIssueArgs(owner="acme", repo="project", title="Test issue")
    result = await create_issue(args, mock_ctx)

    assert result["issue_number"] == 42
    mock_ctx.mcp.call.assert_called_once_with(
        server="github",
        tool="create_issue",
        arguments={"owner": "acme", "repo": "project", "title": "Test issue", "body": ""}
    )

Troubleshooting

Server Won't Connect

Symptom: MCPConnectionError: Failed to connect to server

Solutions: 1. Verify the command runs manually: uvx mcp-server-github 2. Check environment variables are set correctly 3. Verify secrets are configured: huitzo secrets list your-pack

Tool Not Found

Symptom: MCPToolError: Tool 'xyz' not found

Solutions: 1. List available tools: huitzo mcp list-tools github 2. Check tool name spelling (case-sensitive) 3. Server version may not have that tool

Authentication Failed

Symptom: Tool returns 401/403 error

Solutions: 1. Verify token has required permissions/scopes 2. Check token hasn't expired 3. Update secret: huitzo secrets set your-pack TOKEN_NAME "new-value"

Timeout Errors

Symptom: MCPTimeoutError: Request timed out

Solutions: 1. Increase timeout in manifest or call: yaml mcp_servers: - name: postgres type: stdio command: ["uvx", "mcp-server-postgres"] # No timeout field for STDIO, use call-level timeout python result = await ctx.mcp.call("postgres", "query", {...}, timeout=120) 2. Check if the operation is genuinely slow 3. Consider breaking into smaller operations


Complete Example

Here's a complete pack that integrates with GitHub via MCP:

Directory Structure

github-tools/
├── pyproject.toml
├── huitzo.yaml
├── README.md
└── src/
    └── github_tools/
        ├── __init__.py
        └── commands/
            ├── __init__.py
            ├── issues.py
            └── repos.py

huitzo.yaml

pack:
  name: "github-tools"
  namespace: "ghtools"
  version: "1.0.0"
  description: "GitHub integration tools via MCP"
  author: "Your Name"
  visibility: "organization"

mcp_servers:
  - name: github
    type: stdio
    command: ["uvx", "mcp-server-github"]
    env:
      GITHUB_TOKEN: "${secrets.GITHUB_TOKEN}"

commands:
  - name: "create-issue"
    description: "Create a GitHub issue"
    permissions: ["mcp:call"]
    timeout: 30

  - name: "list-issues"
    description: "List issues in a repository"
    permissions: ["mcp:call"]
    timeout: 30

  - name: "list-repos"
    description: "List repositories for a user/org"
    permissions: ["mcp:call"]
    timeout: 30

secrets:
  user_required:
    - name: "GITHUB_TOKEN"
      description: "GitHub personal access token with repo scope"
      help_url: "https://github.com/settings/tokens"

metadata:
  keywords: ["github", "issues", "repositories", "mcp"]
  category: "developer"

src/github_tools/commands/issues.py

"""
Module: GitHub Issues Commands
Description: Commands for managing GitHub issues via MCP.

Implements:
    - docs/sdk/mcp.md#ctx-mcp-call
    - docs/guides/mcp-pack-integration.md

See Also:
    - docs/sdk/context.md#ctx-mcp
"""

from pydantic import BaseModel, Field
from huitzo_sdk import command, Context
from huitzo_sdk.errors import MCPConnectionError, MCPToolError


class CreateIssueArgs(BaseModel):
    owner: str = Field(..., description="Repository owner")
    repo: str = Field(..., description="Repository name")
    title: str = Field(..., description="Issue title")
    body: str = Field(default="", description="Issue body")


class ListIssuesArgs(BaseModel):
    owner: str = Field(..., description="Repository owner")
    repo: str = Field(..., description="Repository name")
    state: str = Field(default="open", description="Issue state: open, closed, all")
    limit: int = Field(default=10, ge=1, le=100, description="Max issues to return")


@command("create-issue", namespace="ghtools")
async def create_issue(args: CreateIssueArgs, ctx: Context) -> dict:
    """Create a GitHub issue."""
    try:
        result = await ctx.mcp.call("github", "create_issue", {
            "owner": args.owner,
            "repo": args.repo,
            "title": args.title,
            "body": args.body
        })
        return {
            "issue_number": result["number"],
            "url": result["html_url"]
        }
    except MCPConnectionError:
        return {"error": "GitHub unavailable", "retry": True}
    except MCPToolError as e:
        return {"error": str(e.message)}


@command("list-issues", namespace="ghtools")
async def list_issues(args: ListIssuesArgs, ctx: Context) -> dict:
    """List issues in a repository."""
    try:
        result = await ctx.mcp.call("github", "list_issues", {
            "owner": args.owner,
            "repo": args.repo,
            "state": args.state,
            "per_page": args.limit
        })
        return {
            "count": len(result.get("issues", [])),
            "issues": [
                {
                    "number": i["number"],
                    "title": i["title"],
                    "state": i["state"],
                    "url": i["html_url"]
                }
                for i in result.get("issues", [])
            ]
        }
    except MCPConnectionError:
        return {"error": "GitHub unavailable", "retry": True}
    except MCPToolError as e:
        return {"error": str(e.message)}