
Mcp Client
- 116 installs
- 805 repo stars
- Updated January 24, 2026
- coleam00/second-brain-skills
Connects to any MCP server with progressive disclosure via a Python client, loading tool schemas on demand instead of dumping them into context.
About
Wraps external MCP servers (Zapier, GitHub, filesystem, Sequential Thinking, etc.) as a skill, listing servers and tools and executing tool calls through a bundled Python client. A developer uses it to interact with MCP servers without context bloat.
- Progressive disclosure loads tool schemas on demand
- Config resolved from env, references, or project .mcp.json
Mcp Client by the numbers
- 116 all-time installs (skills.sh)
- Ranked #3,865 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/coleam00/second-brain-skills --skill mcp-clientAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 116 |
|---|---|
| repo stars | ★ 805 |
| Last updated | January 24, 2026 |
| Repository | coleam00/second-brain-skills ↗ |
What it does
Connects to any MCP server with progressive disclosure via a Python client, loading tool schemas on demand instead of dumping them into context.
Files
Universal MCP Client
Connect to any MCP server with progressive disclosure - load tool schemas on-demand instead of dumping thousands of tokens into context upfront.
Skill Location
This skill is located at: .claude/skills/mcp-client/
Script path: .claude/skills/mcp-client/scripts/mcp_client.py
Configuration
The script looks for config in this order: 1. MCP_CONFIG_PATH env var (custom path) 2. `references/mcp-config.json` (this skill's config - recommended) 3. .mcp.json in project root 4. ~/.claude.json
Your config file: .claude/skills/mcp-client/references/mcp-config.json
Edit this file to add your API keys. The example file (example-mcp-config.json) is kept as a reference template.
If the user hasn't provided their Zapier API key yet, ask them for it.
Running Commands
All commands use the script at .claude/skills/mcp-client/scripts/mcp_client.py:
# List configured servers
python .claude/skills/mcp-client/scripts/mcp_client.py servers
# List tools from a server
python .claude/skills/mcp-client/scripts/mcp_client.py tools <server_name>
# Call a tool
python .claude/skills/mcp-client/scripts/mcp_client.py call <server> <tool> '{"arg": "value"}'Workflow
1. Check config exists - Run servers command. If error, create .mcp.json 2. List servers - See what MCP servers are configured 3. List tools - Get tool schemas from a specific server 4. Call tool - Execute a tool with arguments
Commands Reference
| Command | Description |
|---|---|
servers | List all configured MCP servers |
tools <server> | List tools with full parameter schemas |
call <server> <tool> '<json>' | Execute a tool with arguments |
Example: Zapier
# 1. List servers to confirm Zapier is configured
python .claude/skills/mcp-client/scripts/mcp_client.py servers
# 2. List Zapier tools
python .claude/skills/mcp-client/scripts/mcp_client.py tools zapier
# 3. Call a Zapier tool
python .claude/skills/mcp-client/scripts/mcp_client.py call zapier <tool_name> '{"param": "value"}'Example: Sequential Thinking
# 1. List tools
python .claude/skills/mcp-client/scripts/mcp_client.py tools sequential-thinking
# 2. Use sequential thinking
python .claude/skills/mcp-client/scripts/mcp_client.py call sequential-thinking sequentialthinking '{"thought": "Breaking down the problem...", "thoughtNumber": 1, "totalThoughts": 5, "nextThoughtNeeded": true}'Config Format
Config file format (references/mcp-config.json):
{
"mcpServers": {
"zapier": {
"url": "https://mcp.zapier.com/api/v1/connect",
"api_key": "your-api-key"
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
}
}
}Transport detection:
url+api_key→ FastMCP with Bearer auth (Zapier)command+args→ stdio (local servers like sequential-thinking)urlending in/sse→ SSE transporturlending in/mcp→ Streamable HTTP
Error Handling
Errors return JSON:
{"error": "message", "type": "configuration|validation|connection"}configuration- Config file not found. Create.mcp.jsonvalidation- Invalid server or tool nameconnection- Failed to connect to server
Dependencies
pip install mcp fastmcpReferences
references/example-mcp-config.json- Template config filereferences/mcp-servers.md- Common server configurationsreferences/python-mcp-sdk.md- Python SDK documentation
{
"mcpServers": {
"zapier": {
"url": "https://mcp.zapier.com/api/v1/connect",
"api_key": "YOUR_MCP_API_KEY",
"_comment": "Get your API key from https://mcp.zapier.com/"
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
}
}
}
Common MCP Server Configurations
Reference configurations for popular MCP servers. Copy relevant sections to your .mcp.json.
Remote Servers (FastMCP/SSE/HTTP)
Zapier MCP
Connects to 8,000+ apps with 30,000+ actions. Get your API key from mcp.zapier.com.
Current Format (FastMCP with Bearer Auth):
{
"zapier": {
"url": "https://mcp.zapier.com/api/v1/connect",
"api_key": "YOUR_MCP_API_KEY"
}
}Getting Your API Key: 1. Go to mcp.zapier.com 2. Sign in with your Zapier account 3. Configure which actions/apps to expose 4. Copy the generated MCP API key
How it works: The script uses FastMCP client with StreamableHttpTransport and Bearer token authentication. When api_key is present in config, it automatically uses this transport.
Security: Treat your API key like a password - it grants access to your configured Zapier actions.
Token Cost: One MCP tool call = 2 Zapier tasks from your plan quota.
Legacy SSE Format (if needed):
{
"zapier": {
"url": "https://actions.zapier.com/mcp/YOUR_MCP_SERVER_KEY/sse"
}
}---
Local Servers (stdio)
Sequential Thinking
Structured problem-solving through dynamic, reflective thinking process. Useful for breaking down complex problems into steps.
{
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
}
}Docker Alternative:
{
"sequential-thinking": {
"command": "docker",
"args": ["run", "--rm", "-i", "mcp/sequentialthinking"]
}
}Environment Variable: Set DISABLE_THOUGHT_LOGGING=true to disable verbose output.
Tool: sequentialthinking
thought(string): Current thinking stepthoughtNumber(int): Current step numbertotalThoughts(int): Expected total stepsnextThoughtNeeded(bool): Whether more steps needed
---
GitHub MCP
Access GitHub repositories, issues, PRs, and more.
{
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxxxxxxxxxxx"
}
}
}Getting Token: 1. Go to GitHub Settings > Developer Settings > Personal Access Tokens 2. Generate token with appropriate scopes (repo, read:org, etc.)
---
Filesystem MCP
Read/write access to local filesystem paths.
{
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/allowed/path"]
}
}Security: Only the specified path (and subdirectories) are accessible.
---
Memory MCP
Persistent key-value memory for conversations.
{
"memory": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-memory"]
}
}---
PostgreSQL MCP
Query PostgreSQL databases.
{
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres"],
"env": {
"POSTGRES_CONNECTION_STRING": "postgresql://user:pass@host:5432/db"
}
}
}---
Brave Search MCP
Web search via Brave Search API.
{
"brave-search": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"],
"env": {
"BRAVE_API_KEY": "your-api-key"
}
}
}---
Puppeteer MCP
Browser automation for web scraping and testing.
{
"puppeteer": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-puppeteer"]
}
}---
Custom Python Servers
For custom MCP servers written in Python:
{
"my-server": {
"command": "python",
"args": ["path/to/my_mcp_server.py"],
"env": {
"MY_API_KEY": "xxx",
"DEBUG": "true"
},
"cwd": "/path/to/working/directory"
}
}With uv (recommended):
{
"my-server": {
"command": "uv",
"args": ["run", "my_mcp_server.py"],
"cwd": "/path/to/project"
}
}---
Full Example Config
{
"mcpServers": {
"zapier": {
"url": "https://actions.zapier.com/mcp/YOUR_KEY/sse"
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_xxx"
}
},
"filesystem": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "./data"]
}
}
}---
Transport Reference
| Server Type | Transport | Config Key | Example |
|---|---|---|---|
| Remote (SSE) | SSE | url | https://...../sse |
| Remote (HTTP) | Streamable HTTP | url | https://...../mcp |
| Local (Node.js) | stdio | command | npx |
| Local (Python) | stdio | command | python |
| Local (Docker) | stdio | command | docker |
---
Resources
Python MCP SDK Reference
The official Python SDK for building and consuming MCP servers.
Installation
pip install mcpCurrent stable version: 1.25.x (pin to mcp>=1.25,<2 for stability)
Note: v2 release anticipated Q1 2026. v1.x continues to receive bug fixes.
Core Concepts
MCP (Model Context Protocol) provides a standardized way to connect AI models to external data sources and tools.
Think of it like USB-C for AI - a universal connector that works across different tools and services.
Client Usage (Consuming MCP Servers)
Session Creation
from mcp import ClientSession
# For stdio transport
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client
server_params = StdioServerParameters(
command="npx",
args=["-y", "@modelcontextprotocol/server-github"],
env={"GITHUB_TOKEN": "xxx"}
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Use session...SSE Transport (Remote Servers)
from mcp import ClientSession
from mcp.client.sse import sse_client
async with sse_client(url, headers=headers, timeout=30) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
# Use session...Streamable HTTP Transport
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
# Use session...Listing Tools
result = await session.list_tools()
for tool in result.tools:
print(f"Tool: {tool.name}")
print(f"Description: {tool.description}")
print(f"Schema: {tool.inputSchema}")Calling Tools
result = await session.call_tool("tool_name", {"arg1": "value1"})
# Extract content
for item in result.content:
if hasattr(item, 'text'):
print(item.text)
elif hasattr(item, 'data'):
print(item.data)Listing Resources
result = await session.list_resources()
for resource in result.resources:
print(f"Resource: {resource.uri}")
print(f"Name: {resource.name}")Reading Resources
result = await session.read_resource("resource://uri")
for content in result.contents:
print(content.text)---
Server Creation (Building MCP Servers)
FastMCP (Recommended)
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("My Server")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers together."""
return a + b
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting."""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()Tool Decorators
@mcp.tool()
def search_database(query: str, limit: int = 10) -> list[dict]:
"""
Search the database for matching records.
Args:
query: Search query string
limit: Maximum results to return
Returns:
List of matching records
"""
# Implementation
return resultsType hints are converted to JSON Schema automatically.
Resource Decorators
@mcp.resource("config://settings")
def get_settings() -> str:
"""Return application settings as JSON."""
return json.dumps(settings)
@mcp.resource("file://{path}")
def read_file(path: str) -> str:
"""Read a file from the filesystem."""
return Path(path).read_text()Prompt Decorators
@mcp.prompt()
def code_review(code: str, language: str = "python") -> str:
"""Generate a code review prompt."""
return f"Please review this {language} code:\n\n```{language}\n{code}\n```"---
Transport Configuration
stdio (Local Servers)
Default for local subprocess servers.
from mcp import StdioServerParameters
params = StdioServerParameters(
command="python",
args=["server.py"],
env={"DEBUG": "true"},
cwd="/path/to/dir"
)SSE (Remote - Legacy)
Server-Sent Events for remote HTTP connections.
from mcp.client.sse import sse_client
async with sse_client(
url="https://example.com/mcp/sse",
headers={"Authorization": "Bearer token"},
timeout=60
) as (read, write):
# ...Streamable HTTP (Remote - Modern)
Modern HTTP transport with better connection handling.
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(
url="https://example.com/mcp",
headers={"Authorization": "Bearer token"}
) as (read, write, session_info):
# ...---
Error Handling
from mcp.types import McpError
try:
result = await session.call_tool("tool", args)
except McpError as e:
print(f"MCP Error: {e.code} - {e.message}")
except ConnectionError as e:
print(f"Connection failed: {e}")
except TimeoutError as e:
print(f"Request timed out: {e}")---
Authentication
MCP SDK supports OAuth 2.1 for resource server functionality.
from mcp.server.auth import bearer_auth
@mcp.tool()
@bearer_auth(scopes=["read:data"])
def protected_tool(data: str) -> str:
"""A tool requiring authentication."""
return process(data)---
Best Practices
1. Use context managers - Always use async with for sessions 2. Handle errors - Wrap calls in try/except for graceful degradation 3. Set timeouts - Prevent hanging on unresponsive servers 4. Type your tools - Let the SDK generate JSON Schema from type hints 5. Document tools - Docstrings become tool descriptions
---
Resources
#!/usr/bin/env python3
"""
Universal MCP Client - Connect to any MCP server from config files.
Supports all MCP transports:
- stdio: Local subprocess servers
- sse: HTTP + Server-Sent Events (remote)
- streamable_http: Modern HTTP transport (remote)
- fastmcp: FastMCP client with Bearer auth (Zapier, etc.)
Config Resolution (priority order):
1. MCP_CONFIG_PATH env var (path to config file)
2. MCP_CONFIG env var (inline JSON)
3. .mcp.json in current directory
4. ~/.claude.json (Claude Code user config)
Usage:
python mcp_client.py servers # List configured servers
python mcp_client.py tools <server> # List tools with schemas
python mcp_client.py call <server> <tool> '{"args"}' # Execute a tool
Environment:
MCP_CONFIG_PATH: Path to MCP config file
MCP_CONFIG: Inline JSON config (for simple setups)
"""
import asyncio
import json
import os
import sys
from pathlib import Path
from typing import Any, Optional
from contextlib import asynccontextmanager
# =============================================================================
# Config Loading
# =============================================================================
def find_config_file() -> Optional[Path]:
"""Find MCP config file in standard locations."""
# Priority 1: Environment variable path
if env_path := os.environ.get("MCP_CONFIG_PATH"):
path = Path(env_path).expanduser()
if path.exists():
return path
# Priority 2: mcp-config.json in skill's references folder (user's actual config)
script_dir = Path(__file__).parent
skill_config = script_dir.parent / "references" / "mcp-config.json"
if skill_config.exists():
return skill_config
# Priority 3: .mcp.json in current directory
local_config = Path(".mcp.json")
if local_config.exists():
return local_config
# Priority 4: ~/.claude.json (Claude Code config)
claude_config = Path.home() / ".claude.json"
if claude_config.exists():
return claude_config
return None
def load_config() -> dict:
"""Load MCP server configuration."""
# Priority 1: Inline JSON from environment
if env_config := os.environ.get("MCP_CONFIG"):
try:
config = json.loads(env_config)
return config.get("mcpServers", config)
except json.JSONDecodeError as e:
raise ValueError(f"Invalid MCP_CONFIG JSON: {e}")
# Priority 2: Config file
config_path = find_config_file()
if not config_path:
raise FileNotFoundError(
"No MCP config found. Set MCP_CONFIG_PATH, MCP_CONFIG, "
"or create .mcp.json in the current directory."
)
with open(config_path) as f:
config = json.load(f)
# Handle both formats: {"mcpServers": {...}} and direct {...}
return config.get("mcpServers", config)
def get_server_config(servers: dict, server_name: str) -> dict:
"""Get configuration for a specific server."""
if server_name not in servers:
available = ", ".join(servers.keys())
raise ValueError(f"Server '{server_name}' not found. Available: {available}")
return servers[server_name]
# =============================================================================
# Transport Detection & Connection
# =============================================================================
def detect_transport(config: dict) -> str:
"""Detect transport type from server config."""
# Explicit type takes precedence
if explicit_type := config.get("type"):
type_map = {
"stdio": "stdio",
"sse": "sse",
"http": "streamable_http",
"streamable_http": "streamable_http",
"streamable-http": "streamable_http",
"fastmcp": "fastmcp",
}
return type_map.get(explicit_type.lower(), explicit_type.lower())
# Infer from config keys
if "command" in config:
return "stdio"
if "url" in config:
# FastMCP when api_key is present (Zapier-style Bearer auth)
if "api_key" in config:
return "fastmcp"
url = config["url"]
if url.endswith("/mcp"):
return "streamable_http"
if url.endswith("/sse"):
return "sse"
# Default to SSE for remote servers
return "sse"
raise ValueError("Cannot detect transport: config must have 'command' or 'url'")
@asynccontextmanager
async def create_session(config: dict):
"""Create MCP client session based on server config."""
transport = detect_transport(config)
if transport == "fastmcp":
# FastMCP client with Bearer auth (Zapier, etc.)
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
url = config["url"]
api_key = config["api_key"]
transport_obj = StreamableHttpTransport(
url,
headers={"Authorization": f"Bearer {api_key}"}
)
client = Client(transport=transport_obj)
async with client:
# Wrap FastMCP client in adapter for unified interface
yield FastMCPSessionAdapter(client)
elif transport == "stdio":
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
# Build environment with current env + config env
env = {**os.environ}
if config_env := config.get("env"):
env.update(config_env)
server_params = StdioServerParameters(
command=config["command"],
args=config.get("args", []),
env=env,
cwd=config.get("cwd"),
)
async with stdio_client(server_params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
yield session
elif transport == "sse":
from mcp import ClientSession
from mcp.client.sse import sse_client
url = config["url"]
headers = config.get("headers")
timeout = config.get("timeout", 30)
async with sse_client(url, headers=headers, timeout=timeout) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
yield session
elif transport == "streamable_http":
from mcp import ClientSession
from mcp.client.streamable_http import streamablehttp_client
url = config["url"]
headers = config.get("headers")
timeout = config.get("timeout", 30)
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
yield session
else:
raise ValueError(f"Unsupported transport: {transport}")
class FastMCPSessionAdapter:
"""Adapter to give FastMCP Client a similar interface to mcp ClientSession."""
def __init__(self, client):
self.client = client
async def list_tools(self):
"""List tools, returning object with .tools attribute."""
tools = await self.client.list_tools()
return type('ToolsResult', (), {'tools': tools})()
async def call_tool(self, name: str, arguments: dict):
"""Call a tool and return result."""
result = await self.client.call_tool(name, arguments)
return result
# =============================================================================
# Commands
# =============================================================================
def cmd_servers(servers: dict) -> list[dict]:
"""List all configured MCP servers."""
result = []
for name, config in servers.items():
transport = detect_transport(config)
info = {
"name": name,
"transport": transport,
}
if transport == "stdio":
info["command"] = config.get("command")
else:
info["url"] = config.get("url")
result.append(info)
return result
async def cmd_tools(servers: dict, server_name: str) -> list[dict]:
"""List all tools from a server with full schemas."""
config = get_server_config(servers, server_name)
async with create_session(config) as session:
result = await session.list_tools()
tools = []
for tool in result.tools:
tools.append({
"name": tool.name,
"description": tool.description,
"parameters": tool.inputSchema,
})
return tools
async def cmd_call(servers: dict, server_name: str, tool_name: str, arguments: dict) -> Any:
"""Execute a tool on a server."""
config = get_server_config(servers, server_name)
async with create_session(config) as session:
result = await session.call_tool(tool_name, arguments)
# Extract content from result
if hasattr(result, 'content'):
contents = []
for item in result.content:
if hasattr(item, 'text'):
contents.append(item.text)
elif hasattr(item, 'data'):
contents.append({"type": "data", "data": item.data})
else:
contents.append(str(item))
return contents[0] if len(contents) == 1 else contents
return result
# =============================================================================
# CLI Interface
# =============================================================================
def print_json(data: Any) -> None:
"""Print data as formatted JSON."""
print(json.dumps(data, indent=2, default=str))
def print_error(message: str, error_type: str = "error") -> None:
"""Print error as JSON."""
print(json.dumps({"error": message, "type": error_type}))
def print_usage():
"""Print usage information."""
usage = """Usage: mcp_client.py <command> [args]
Commands:
servers List configured MCP servers
tools <server> List tools with full schemas
call <server> <tool> '<json>' Execute a tool with arguments
Examples:
python mcp_client.py servers
python mcp_client.py tools github
python mcp_client.py call github search_repos '{"query": "python mcp"}'
Config sources (checked in order):
1. MCP_CONFIG_PATH environment variable
2. MCP_CONFIG environment variable (inline JSON)
3. .mcp.json in current directory
4. ~/.claude.json"""
print(usage)
async def main():
if len(sys.argv) < 2:
print_usage()
sys.exit(1)
command = sys.argv[1].lower()
if command in ("--help", "-h", "help"):
print_usage()
sys.exit(0)
# Load config
try:
servers = load_config()
except (FileNotFoundError, ValueError) as e:
print_error(str(e), "configuration")
sys.exit(1)
try:
if command == "servers":
result = cmd_servers(servers)
print_json(result)
elif command == "tools":
if len(sys.argv) < 3:
print_error("Usage: tools <server_name>", "usage")
sys.exit(1)
server_name = sys.argv[2]
result = await cmd_tools(servers, server_name)
print_json(result)
elif command == "call":
if len(sys.argv) < 4:
print_error("Usage: call <server> <tool> [json_args]", "usage")
sys.exit(1)
server_name = sys.argv[2]
tool_name = sys.argv[3]
args = {}
if len(sys.argv) >= 5:
try:
args = json.loads(sys.argv[4])
except json.JSONDecodeError as e:
print_error(f"Invalid JSON arguments: {e}", "invalid_args")
sys.exit(1)
result = await cmd_call(servers, server_name, tool_name, args)
print_json(result)
else:
print_error(f"Unknown command: {command}", "usage")
print_usage()
sys.exit(1)
except ValueError as e:
print_error(str(e), "validation")
sys.exit(1)
except ConnectionError as e:
print_error(f"Connection failed: {e}", "connection")
sys.exit(1)
except Exception as e:
print_error(f"Error: {e}", "error")
sys.exit(1)
if __name__ == "__main__":
asyncio.run(main())