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:
- Define args — Pydantic model describing the command's inputs
- Call MCP tool — Use
ctx.mcp.call(server, tool, arguments)to invoke the external tool - Handle errors — Catch
MCPConnectionError(server down) andMCPToolError(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)}
Related Documentation¶
- MCP Architecture — How MCP fits into Huitzo
- MCP SDK Reference — Complete
ctx.mcpAPI documentation - Pack Manifest - MCP Servers — Configuration reference
- Error Handling — MCP error types
- Your First Pack — Pack development basics