
Context7 Efficient
- 15 installs
- 1 repo stars
- Updated January 27, 2026
- bilalmk/todo_correct
context7-efficient is a Claude Code skill that fetches library documentation via the Context7 MCP server and filters it through a shell pipeline to cut token usage.
About
This skill fetches library and framework documentation through the Context7 MCP server using a shell pipeline. The pipeline keeps the full documentation response inside a subprocess and returns only filtered code examples, API signatures, and key notes to the model. A developer uses it when they need syntax reference or code examples for libraries like React, Next.js, Prisma, or Express while writing code. It reduces token usage by filtering documentation before it enters the model context.
- Fetches library docs via Context7 MCP through a shell pipeline that filters output before it reaches the model
- Claims ~77% token reduction (205 vs 934 tokens per query)
- Ships fetch-docs.sh orchestrator plus code-block, signature, and notes extractors
Context7 Efficient by the numbers
- 15 all-time installs (skills.sh)
- Ranked #1,080 of 1,879 Documentation skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
context7-efficient capabilities & compatibility
- Capabilities
- documentation · token optimization
- Works with
- openai
- Use cases
- documentation · token optimization
- Runs
- Runs locally
- Pricing
- Free
What context7-efficient says it does
Token-efficient library documentation fetcher using Context7 MCP with 86.8% token savings through intelligent shell pipeline filtering.
Fetch library documentation with automatic 77% token reduction via shell pipeline.
npx skills add https://github.com/bilalmk/todo_correct --skill context7-efficientAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 15 |
|---|---|
| repo stars | ★ 1 |
| Last updated | January 27, 2026 |
| Repository | bilalmk/todo_correct ↗ |
What it does
Fetch filtered library documentation and code examples for a framework while writing code, without loading full docs into context.
Who is it for?
Looking up code examples or API syntax for a specific library while coding.
When should I use this skill?
The user asks about library documentation, needs code examples, or wants API usage patterns for a framework.
What you get
Returns only code examples, API signatures, and key notes for a requested library and topic.
By the numbers
- Claims 77% token reduction (205 vs 934 tokens per query)
- Lists 9 common library IDs
Files
Context7 Efficient Documentation Fetcher
Fetch library documentation with automatic 77% token reduction via shell pipeline.
Quick Start
Always use the token-efficient shell pipeline:
# Automatic library resolution + filtering
bash scripts/fetch-docs.sh --library <library-name> --topic <topic>
# Examples:
bash scripts/fetch-docs.sh --library react --topic useState
bash scripts/fetch-docs.sh --library nextjs --topic routing
bash scripts/fetch-docs.sh --library prisma --topic queriesResult: Returns ~205 tokens instead of ~934 tokens (77% savings).
Standard Workflow
For any documentation request, follow this workflow:
1. Identify Library and Topic
Extract from user query:
- Library: React, Next.js, Prisma, Express, etc.
- Topic: Specific feature (hooks, routing, queries, etc.)
2. Fetch with Shell Pipeline
bash scripts/fetch-docs.sh --library <library> --topic <topic> --verboseThe --verbose flag shows token savings statistics.
3. Use Filtered Output
The script automatically:
- Fetches full documentation (934 tokens, stays in subprocess)
- Filters to code examples + API signatures + key notes
- Returns only essential content (205 tokens to Claude)
Parameters
Basic Usage
bash scripts/fetch-docs.sh [OPTIONS]Required (pick one):
--library <name>- Library name (e.g., "react", "nextjs")--library-id <id>- Direct Context7 ID (faster, skips resolution)
Optional:
--topic <topic>- Specific feature to focus on--mode <code|info>- code for examples (default), info for concepts--page <1-10>- Pagination for more results--verbose- Show token savings statistics
Mode Selection
Code Mode (default): Returns code examples + API signatures
--mode codeInfo Mode: Returns conceptual explanations + fewer examples
--mode infoCommon Library IDs
Use --library-id for faster lookup (skips resolution):
React: /reactjs/react.dev
Next.js: /vercel/next.js
Express: /expressjs/express
Prisma: /prisma/docs
MongoDB: /mongodb/docs
Fastify: /fastify/fastify
NestJS: /nestjs/docs
Vue.js: /vuejs/docs
Svelte: /sveltejs/siteWorkflow Patterns
Pattern 1: Quick Code Examples
User asks: "Show me React useState examples"
bash scripts/fetch-docs.sh --library react --topic useState --verboseReturns: 5 code examples + API signatures + notes (~205 tokens)
Pattern 2: Learning New Library
User asks: "How do I get started with Prisma?"
# Step 1: Get overview
bash scripts/fetch-docs.sh --library prisma --topic "getting started" --mode info
# Step 2: Get code examples
bash scripts/fetch-docs.sh --library prisma --topic queries --mode codePattern 3: Specific Feature Lookup
User asks: "How does Next.js routing work?"
bash scripts/fetch-docs.sh --library-id /vercel/next.js --topic routingUsing --library-id is faster when you know the exact ID.
Pattern 4: Deep Exploration
User needs comprehensive information:
# Page 1: Basic examples
bash scripts/fetch-docs.sh --library react --topic hooks --page 1
# Page 2: Advanced patterns
bash scripts/fetch-docs.sh --library react --topic hooks --page 2Token Efficiency
How it works:
1. fetch-docs.sh calls fetch-raw.sh (which uses mcp-client.py) 2. Full response (934 tokens) stays in subprocess memory 3. Shell filters (awk/grep/sed) extract essentials (0 LLM tokens used) 4. Returns filtered output (205 tokens) to Claude
Savings:
- Direct MCP: 934 tokens per query
- This approach: 205 tokens per query
- 77% reduction
Do NOT use `mcp-client.py` directly - it bypasses filtering and wastes tokens.
Advanced: Library Resolution
If library name fails, try variations:
# Try different formats
--library "next.js" # with dot
--library "nextjs" # without dot
--library "next" # short form
# Or search manually
bash scripts/fetch-docs.sh --library "your-library" --verbose
# Check output for suggested library IDsTroubleshooting
| Issue | Solution |
|---|---|
| Library not found | Try name variations or use broader search term |
| No results | Use --mode info or broader topic |
| Need more examples | Increase page: --page 2 |
| Want full context | Use --mode info for explanations |
References
For detailed Context7 MCP tool documentation, see:
- references/context7-tools.md - Complete tool reference
Implementation Notes
Components (for reference only, use fetch-docs.sh):
mcp-client.py- Universal MCP client (foundation)fetch-raw.sh- MCP wrapperextract-code-blocks.sh- Code example filter (awk)extract-signatures.sh- API signature filter (awk)extract-notes.sh- Important notes filter (grep)fetch-docs.sh- Main orchestrator (ALWAYS USE THIS)
Architecture: Shell pipeline processes documentation in subprocess, keeping full response out of Claude's context. Only filtered essentials enter the LLM context, achieving 77% token savings with 100% functionality preserved.
Context7 MCP Tools
2 tools available
resolve-library-id
Resolves a package/product name to a Context7-compatible library ID and returns a list of matching libraries.
You MUST call this function before 'get-library-docs' to obtain a valid Context7-compatible library ID UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.
Selection Process
1. Analyze the query to understand what library/package the user is looking for 2. Return the most relevant match based on:
- Name similarity to the query (exact matches prioritized)
- Description relevance to the query's intent
- Documentation coverage (prioritize libraries with higher Code Snippet counts)
- Source reputation (consider libraries with High or Medium reputation more authoritative)
- Benchmark Score: Quality indicator (100 is the highest score)
Parameters
- `libraryName` (
string) (required): Library name to search for and retrieve a Context7-compatible library ID
Response Format
Returns a list of matching libraries with:
- Title
- Context7-compatible library ID (e.g.,
/reactjs/react.dev) - Code Snippets count
- Source Reputation (High/Medium/Low)
- Benchmark Score
- Description
Examples
# Find React library
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t resolve-library-id \
-p '{"libraryName": "react"}'
# Find Next.js library
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t resolve-library-id \
-p '{"libraryName": "next.js"}'
# Find MongoDB library
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t resolve-library-id \
-p '{"libraryName": "mongodb"}'<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"libraryName": {
"type": "string",
"description": "Library name to search for and retrieve a Context7-compatible library ID."
}
},
"required": ["libraryName"]
}</details>
get-library-docs
Fetches up-to-date documentation for a library. You must call 'resolve-library-id' first to obtain the exact Context7-compatible library ID required to use this tool, UNLESS the user explicitly provides a library ID in the format '/org/project' or '/org/project/version' in their query.
Use mode='code' (default) for API references and code examples, or mode='info' for conceptual guides, narrative information, and architectural questions.
Parameters
- `context7CompatibleLibraryID` (
string) (required): Exact Context7-compatible library ID (e.g., '/mongodb/docs', '/vercel/next.js', '/supabase/supabase', '/vercel/next.js/v14.3.0-canary.87') retrieved from 'resolve-library-id' or directly from user query in the format '/org/project' or '/org/project/version'
- `topic` (
string) (optional): Topic to focus documentation on (e.g., 'hooks', 'routing')
- `mode` (
string) (optional, default: "code"): Documentation mode code: API references and code examples (default)info: Conceptual guides, narrative information, and architectural questions
- `page` (
integer) (optional, default: 1): Page number for pagination (start: 1, default: 1). If the context is not sufficient, try page=2, page=3, page=4, etc. with the same topic. Range: 1-10
Examples
# Get React hooks documentation (code mode)
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/reactjs/react.dev", "topic": "hooks", "mode": "code", "page": 1}'
# Get conceptual information about Next.js routing
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/vercel/nextjs.org", "topic": "routing", "mode": "info"}'
# Get MongoDB aggregation examples
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/mongodb/docs", "topic": "aggregation", "mode": "code"}'
# Get additional pages for more details
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/reactjs/react.dev", "topic": "hooks", "mode": "code", "page": 2}'<details> <summary>Full Schema</summary>
{
"type": "object",
"properties": {
"context7CompatibleLibraryID": {
"type": "string",
"description": "Exact Context7-compatible library ID (e.g., '/mongodb/docs', '/vercel/next.js', '/supabase/supabase', '/vercel/next.js/v14.3.0-canary.87') retrieved from 'resolve-library-id' or directly from user query in the format '/org/project' or '/org/project/version'."
},
"topic": {
"type": "string",
"description": "Topic to focus documentation on (e.g., 'hooks', 'routing')."
},
"mode": {
"type": "string",
"enum": ["code", "info"],
"default": "code",
"description": "Documentation mode: 'code' for API references and code examples (default), 'info' for conceptual guides, narrative information, and architectural questions."
},
"page": {
"type": "integer",
"minimum": 1,
"maximum": 10,
"default": 1,
"description": "Page number for pagination (start: 1, default: 1). If the context is not sufficient, try page=2, page=3, page=4, etc. with the same topic."
}
},
"required": ["context7CompatibleLibraryID"]
}</details>
Usage Patterns
Pattern 1: Unknown Library
When you don't know the exact library ID:
# Step 1: Resolve library name
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t resolve-library-id -p '{"libraryName": "express"}'
# Step 2: Use returned ID to fetch docs
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/expressjs/expressjs.com", "topic": "middleware"}'Pattern 2: Known Library ID
When you know the library ID:
# Direct fetch (skip resolve step)
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/reactjs/react.dev", "topic": "useState"}'Pattern 3: Exploring Multiple Topics
# Get overview first
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/prisma/docs", "topic": "getting started", "mode": "info"}'
# Then drill into specifics
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/prisma/docs", "topic": "queries", "mode": "code"}'Pattern 4: Pagination for Deep Research
# Get first page
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/reactjs/react.dev", "topic": "hooks", "page": 1}'
# Get additional pages as needed
python3 scripts/mcp-client.py call -s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p '{"context7CompatibleLibraryID": "/reactjs/react.dev", "topic": "hooks", "page": 2}'Common Library IDs
Quick reference for popular libraries:
| Library | Context7 ID |
|---|---|
| React | /reactjs/react.dev |
| Next.js | /vercel/nextjs.org |
| Express | /expressjs/expressjs.com |
| MongoDB | /mongodb/docs |
| Prisma | /prisma/docs |
| Vue | /vuejs/docs |
| Svelte | /sveltejs/svelte.dev |
| FastAPI | /tiangolo/fastapi |
| Django | /django/docs |
Tips
1. Library Resolution: Always use resolve-library-id first unless you have the exact ID 2. Mode Selection: Use code mode for examples, info mode for concepts 3. Topic Specificity: More specific topics yield better results 4. Pagination: If results are insufficient, try page: 2 or refine the topic 5. Fallback: If no results, try broader topics or switch modes
#!/bin/bash
# Extract code blocks from documentation text
# Uses awk for maximum efficiency (0 LLM tokens!)
set -euo pipefail
MAX_BLOCKS="${1:-5}"
# Use awk to extract code blocks between ``` markers
awk -v max="$MAX_BLOCKS" '
BEGIN {
count = 0
in_block = 0
block = ""
lang = ""
}
/^```/ {
if (in_block) {
# End of code block
if (count < max && length(block) > 20) {
count++
print "### Example " count
if (lang != "") {
print "```" lang
} else {
print "```"
}
print block
print "```\n"
}
block = ""
lang = ""
in_block = 0
} else {
# Start of code block - extract language
in_block = 1
lang = substr($0, 4) # Get language after ```
}
next
}
in_block {
if (block != "") {
block = block "\n" $0
} else {
block = $0
}
}
END {
if (count == 0) {
print "# No code blocks found"
}
}
'
#!/bin/bash
# Extract important notes and warnings using grep
# Filters for key informational content
set -euo pipefail
MAX_NOTES="${1:-3}"
# Use grep to find lines with important keywords
grep -iE '(important|note:|warning:|caution:|tip:|remember:|must|should not|deprecated|breaking change)' | \
head -n "$MAX_NOTES" | \
sed 's/^/- /' || echo "- No important notes found"
#!/bin/bash
# Extract API signatures using awk
# Finds function declarations, interfaces, types
set -euo pipefail
MAX_SIGS="${1:-3}"
# Use awk to find common API patterns
awk -v max="$MAX_SIGS" '
BEGIN { count = 0 }
# Function declarations
/^(export )?(async )?(function|const|let|var) [a-zA-Z_$][a-zA-Z0-9_$]*.*\(/ {
if (count < max) {
print "- `" $0 "`"
count++
}
}
# Interface definitions
/^(export )?interface [a-zA-Z_$]/ {
if (count < max) {
sig = $0
getline
while ($0 ~ /^ / && count < max) {
sig = sig " " $0
getline
}
print "- `" sig "`"
count++
}
}
# Type definitions
/^(export )?type [a-zA-Z_$][a-zA-Z0-9_$]* =/ {
if (count < max) {
print "- `" $0 "`"
count++
}
}
'
#!/bin/bash
# Main orchestrator: Token-efficient documentation fetcher
#
# This script achieves 94% token savings by:
# 1. Fetching raw docs (5,500 tokens stay in shell)
# 2. Filtering with grep/awk/sed (0 LLM tokens!)
# 3. Returning condensed output (~350 tokens to Claude)
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Parse arguments
LIBRARY_ID=""
TOPIC=""
MODE="code"
PAGE=1
VERBOSE=0
usage() {
cat << USAGE
Usage: $0 [OPTIONS]
Token-efficient documentation fetcher using Context7 MCP
OPTIONS:
--library-id ID Context7 library ID (e.g., /reactjs/react.dev)
--library NAME Library name (will resolve to ID)
--topic TOPIC Topic to focus on (e.g., hooks, routing)
--mode MODE Mode: code (default) or info
--page NUM Page number (1-10, default: 1)
--verbose, -v Show token statistics
--help, -h Show this help
EXAMPLES:
# Quick lookup with known library ID
$0 --library-id /reactjs/react.dev --topic useState
# Resolve library name first
$0 --library react --topic hooks --mode code
# Get conceptual info
$0 --library-id /prisma/docs --topic "getting started" --mode info
# Pagination
$0 --library react --topic hooks --page 2 --verbose
USAGE
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--library-id)
LIBRARY_ID="$2"
shift 2
;;
--library)
LIBRARY_NAME="$2"
shift 2
;;
--topic)
TOPIC="$2"
shift 2
;;
--mode)
MODE="$2"
shift 2
;;
--page)
PAGE="$2"
shift 2
;;
-v|--verbose)
VERBOSE=1
shift
;;
-h|--help)
usage
;;
*)
echo "Unknown option: $1" >&2
usage
;;
esac
done
# Resolve library if name provided
if [ -n "${LIBRARY_NAME:-}" ] && [ -z "$LIBRARY_ID" ]; then
[ $VERBOSE -eq 1 ] && echo "🔍 Resolving library: $LIBRARY_NAME..." >&2
# Call resolve-library-id
RESOLVE_JSON=$(python3 "$SCRIPT_DIR/mcp-client.py" call \
-s "npx -y @upstash/context7-mcp" \
-t resolve-library-id \
-p "{\"libraryName\": \"$LIBRARY_NAME\"}" 2>/dev/null)
# Extract text from JSON (fallback to Python if jq not available)
if command -v jq &> /dev/null; then
RESOLVE_TEXT=$(echo "$RESOLVE_JSON" | jq -r '.content[0].text')
else
RESOLVE_TEXT=$(echo "$RESOLVE_JSON" | python3 -c 'import sys, json; data=json.load(sys.stdin); print(data.get("content", [{}])[0].get("text", ""))')
fi
# Extract first library ID using grep
LIBRARY_ID=$(echo "$RESOLVE_TEXT" | grep -oP 'Context7-compatible library ID:\s*\K[/\w.-]+' | head -n 1)
[ $VERBOSE -eq 1 ] && echo "✅ Resolved to: $LIBRARY_ID" >&2
fi
# Validate library ID
if [ -z "$LIBRARY_ID" ]; then
echo "Error: Must specify --library-id or --library" >&2
usage
fi
# Step 1: Fetch raw documentation (stays in shell memory!)
[ $VERBOSE -eq 1 ] && echo "📚 Fetching documentation..." >&2
RAW_JSON=$("$SCRIPT_DIR/fetch-raw.sh" "$LIBRARY_ID" "$TOPIC" "$MODE" "$PAGE")
# Step 2: Extract text from JSON (using Python if jq not available)
if command -v jq &> /dev/null; then
RAW_TEXT=$(echo "$RAW_JSON" | jq -r '.content[0].text // empty')
else
RAW_TEXT=$(echo "$RAW_JSON" | python3 -c 'import sys, json; data=json.load(sys.stdin); print(data.get("content", [{}])[0].get("text", ""))')
fi
if [ -z "$RAW_TEXT" ]; then
echo "Error: No documentation received from Context7" >&2
exit 1
fi
# Calculate raw token count (approximate: words * 1.3)
if [ $VERBOSE -eq 1 ]; then
RAW_WORDS=$(echo "$RAW_TEXT" | wc -w)
RAW_TOKENS=$(echo "$RAW_WORDS * 1.3" | bc | cut -d. -f1)
echo "📊 Raw response: ~$RAW_WORDS words (~$RAW_TOKENS tokens)" >&2
fi
# Step 3: Filter using shell tools (0 LLM tokens!)
# This is where the magic happens - all processing stays in shell
OUTPUT=""
if [ "$MODE" = "code" ]; then
# Code mode: Extract code examples and API signatures
# Extract code blocks
CODE_BLOCKS=$(echo "$RAW_TEXT" | "$SCRIPT_DIR/extract-code-blocks.sh" 5)
if [ -n "$CODE_BLOCKS" ] && [ "$CODE_BLOCKS" != "# No code blocks found" ]; then
OUTPUT+="## Code Examples\n\n$CODE_BLOCKS\n"
fi
# Extract API signatures
SIGNATURES=$(echo "$RAW_TEXT" | "$SCRIPT_DIR/extract-signatures.sh" 3)
if [ -n "$SIGNATURES" ]; then
OUTPUT+="\n## API Signatures\n\n$SIGNATURES\n"
fi
else
# Info mode: Extract conceptual content
# Get fewer code examples (2 max)
CODE_BLOCKS=$(echo "$RAW_TEXT" | "$SCRIPT_DIR/extract-code-blocks.sh" 2)
if [ -n "$CODE_BLOCKS" ] && [ "$CODE_BLOCKS" != "# No code blocks found" ]; then
OUTPUT+="## Examples\n\n$CODE_BLOCKS\n"
fi
# Extract key paragraphs (first 3 substantial paragraphs)
OVERVIEW=$(echo "$RAW_TEXT" | \
awk 'BEGIN{RS=""; FS="\n"} length($0) > 200 && !/```/{print; if(++count>=3) exit}')
if [ -n "$OVERVIEW" ]; then
OUTPUT+="\n## Overview\n\n$OVERVIEW\n"
fi
fi
# Always add important notes
NOTES=$(echo "$RAW_TEXT" | "$SCRIPT_DIR/extract-notes.sh" 3)
if [ -n "$NOTES" ]; then
OUTPUT+="\n## Important Notes\n\n$NOTES\n"
fi
# Fallback if no content extracted
if [ -z "$OUTPUT" ]; then
OUTPUT=$(echo "$RAW_TEXT" | head -c 500)
OUTPUT+="\n\n[Response truncated for brevity...]"
fi
# Step 4: Output filtered content (this is what enters Claude's context!)
echo -e "$OUTPUT"
# Calculate filtered token count and savings
if [ $VERBOSE -eq 1 ]; then
FILTERED_WORDS=$(echo -e "$OUTPUT" | wc -w)
FILTERED_TOKENS=$(echo "$FILTERED_WORDS * 1.3" | bc | cut -d. -f1)
SAVINGS=$(echo "scale=1; (($RAW_TOKENS - $FILTERED_TOKENS) / $RAW_TOKENS) * 100" | bc)
echo "" >&2
echo "✨ Filtered output: ~$FILTERED_WORDS words (~$FILTERED_TOKENS tokens)" >&2
echo "💰 Token savings: ${SAVINGS}%" >&2
fi
#!/bin/bash
# Fetch raw documentation from Context7 MCP
# Output: JSON response (stays in shell, doesn't enter Claude context)
set -euo pipefail
LIBRARY_ID="${1:?Error: Library ID required}"
TOPIC="${2:-}"
MODE="${3:-code}"
PAGE="${4:-1}"
# Build parameters JSON
PARAMS=$(cat <<JSON
{
"context7CompatibleLibraryID": "$LIBRARY_ID",
"mode": "$MODE",
"page": $PAGE
JSON
)
# Add topic if provided
if [ -n "$TOPIC" ]; then
PARAMS=$(cat <<JSON
{
"context7CompatibleLibraryID": "$LIBRARY_ID",
"topic": "$TOPIC",
"mode": "$MODE",
"page": $PAGE
}
JSON
)
fi
# Call MCP server (response stays in this subprocess!)
python3 "$(dirname "$0")/mcp-client.py" call \
-s "npx -y @upstash/context7-mcp" \
-t get-library-docs \
-p "$PARAMS" 2>/dev/null
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Universal MCP Client - Bundle with any skill that needs MCP access.
Supports both HTTP and stdio transports for connecting to MCP servers.
Usage:
# List available tools from an HTTP MCP server
python mcp-client.py list --url http://localhost:8080
# List tools from a stdio MCP server
python mcp-client.py list --stdio "npx -y @modelcontextprotocol/server-github"
# Call a tool
python mcp-client.py call --url http://localhost:8080 --tool create_issue \
--params '{"title": "Bug report", "body": "Details..."}'
# Emit tool schemas as markdown (for caching in references/)
python mcp-client.py emit --url http://localhost:8080
# Emit as JSON (for programmatic use)
python mcp-client.py emit --url http://localhost:8080 --format json
"""
import argparse
import json
import subprocess
import sys
import threading
import queue
from typing import Optional, Any
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
class MCPClientError(Exception):
"""Base exception for MCP client errors."""
pass
class HTTPTransport:
"""MCP client using HTTP transport (streamable HTTP with session support)."""
def __init__(self, url: str, headers: Optional[dict] = None):
url = url.rstrip('/')
# Playwright MCP and other streamable HTTP servers use /mcp endpoint
if not url.endswith('/mcp'):
url = url + '/mcp'
self.url = url
self.headers = headers or {}
self._request_id = 0
self._session_id: Optional[str] = None
self._initialized = False
def _next_id(self) -> int:
self._request_id += 1
return self._request_id
def _ensure_initialized(self):
"""Initialize the session if not already done."""
if self._initialized:
return
payload = {
"jsonrpc": "2.0",
"id": self._next_id(),
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "mcp-client", "version": "1.0.0"}
}
}
data = json.dumps(payload).encode('utf-8')
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
**self.headers
}
req = Request(self.url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=30) as resp:
# Check for session ID in response headers
self._session_id = resp.headers.get('Mcp-Session-Id')
response = self._parse_response(resp.read().decode('utf-8'))
except HTTPError as e:
body = e.read().decode('utf-8') if e.fp else str(e)
raise MCPClientError(f"HTTP {e.code}: {body}")
except URLError as e:
raise MCPClientError(f"Connection failed: {e.reason}")
if "error" in response:
err = response["error"]
raise MCPClientError(f"Initialize failed: {err.get('message')}")
self._initialized = True
# Send initialized notification
self._send_notification("notifications/initialized")
def _parse_response(self, body: str) -> dict:
"""Parse response body, handling SSE format if needed."""
body = body.strip()
# Handle SSE format (event stream)
if body.startswith('event:') or body.startswith('data:'):
for line in body.split('\n'):
if line.startswith('data:'):
json_data = line[5:].strip()
if json_data:
return json.loads(json_data)
raise MCPClientError("No data in SSE response")
# Regular JSON response
return json.loads(body)
def _send_notification(self, method: str, params: Optional[dict] = None):
"""Send a notification (no response expected)."""
payload = {
"jsonrpc": "2.0",
"method": method,
}
if params:
payload["params"] = params
data = json.dumps(payload).encode('utf-8')
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
**self.headers
}
if self._session_id:
headers["Mcp-Session-Id"] = self._session_id
req = Request(self.url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=30) as resp:
pass # Notifications don't return data
except (HTTPError, URLError):
pass # Ignore notification errors
def request(self, method: str, params: Optional[dict] = None) -> dict:
"""Send a JSON-RPC request to the MCP server."""
self._ensure_initialized()
payload = {
"jsonrpc": "2.0",
"id": self._next_id(),
"method": method,
}
if params:
payload["params"] = params
data = json.dumps(payload).encode('utf-8')
headers = {
"Content-Type": "application/json",
"Accept": "application/json, text/event-stream",
**self.headers
}
if self._session_id:
headers["Mcp-Session-Id"] = self._session_id
req = Request(self.url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=30) as resp:
response = self._parse_response(resp.read().decode('utf-8'))
except HTTPError as e:
body = e.read().decode('utf-8') if e.fp else str(e)
raise MCPClientError(f"HTTP {e.code}: {body}")
except URLError as e:
raise MCPClientError(f"Connection failed: {e.reason}")
if "error" in response:
err = response["error"]
raise MCPClientError(f"MCP error {err.get('code')}: {err.get('message')}")
return response.get("result", {})
class StdioTransport:
"""MCP client using stdio transport (for local MCP servers)."""
def __init__(self, command: str):
self.command = command
self._request_id = 0
self._process: Optional[subprocess.Popen] = None
self._response_queue: queue.Queue = queue.Queue()
self._reader_thread: Optional[threading.Thread] = None
def _next_id(self) -> int:
self._request_id += 1
return self._request_id
def _start(self):
"""Start the MCP server process."""
if self._process is not None:
return
self._process = subprocess.Popen(
self.command,
shell=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
bufsize=1
)
# Start reader thread
self._reader_thread = threading.Thread(target=self._read_responses, daemon=True)
self._reader_thread.start()
# Send initialize request
self._send({
"jsonrpc": "2.0",
"id": self._next_id(),
"method": "initialize",
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "mcp-client", "version": "1.0.0"}
}
})
# Wait for initialize response
try:
resp = self._response_queue.get(timeout=10)
if "error" in resp:
raise MCPClientError(f"Initialize failed: {resp['error']}")
except queue.Empty:
raise MCPClientError("Timeout waiting for server initialization")
# Send initialized notification
self._send({
"jsonrpc": "2.0",
"method": "notifications/initialized"
})
def _read_responses(self):
"""Background thread to read responses from the server."""
while self._process and self._process.poll() is None:
try:
line = self._process.stdout.readline()
if not line:
break
line = line.strip()
if line:
try:
msg = json.loads(line)
# Only queue responses (messages with id), not notifications
if "id" in msg:
self._response_queue.put(msg)
except json.JSONDecodeError:
pass # Ignore non-JSON output
except Exception:
break
def _send(self, message: dict):
"""Send a message to the server."""
if self._process is None:
raise MCPClientError("Process not started")
line = json.dumps(message) + "\n"
self._process.stdin.write(line)
self._process.stdin.flush()
def request(self, method: str, params: Optional[dict] = None) -> dict:
"""Send a JSON-RPC request and wait for response."""
self._start()
req_id = self._next_id()
payload = {
"jsonrpc": "2.0",
"id": req_id,
"method": method,
}
if params:
payload["params"] = params
self._send(payload)
# Wait for response with matching id
try:
while True:
resp = self._response_queue.get(timeout=30)
if resp.get("id") == req_id:
if "error" in resp:
err = resp["error"]
raise MCPClientError(f"MCP error {err.get('code')}: {err.get('message')}")
return resp.get("result", {})
except queue.Empty:
raise MCPClientError(f"Timeout waiting for response to {method}")
def close(self):
"""Shutdown the server process."""
if self._process:
self._process.terminate()
self._process.wait(timeout=5)
self._process = None
class MCPClient:
"""High-level MCP client that works with any transport."""
def __init__(self, transport):
self.transport = transport
def list_tools(self) -> list[dict]:
"""Get list of available tools from the server."""
result = self.transport.request("tools/list")
return result.get("tools", [])
def call_tool(self, name: str, arguments: Optional[dict] = None) -> Any:
"""Call a tool and return the result."""
params = {"name": name}
if arguments:
params["arguments"] = arguments
result = self.transport.request("tools/call", params)
return result
def list_resources(self) -> list[dict]:
"""Get list of available resources."""
result = self.transport.request("resources/list")
return result.get("resources", [])
def list_prompts(self) -> list[dict]:
"""Get list of available prompts."""
result = self.transport.request("prompts/list")
return result.get("prompts", [])
def emit_markdown(tools: list[dict]) -> str:
"""Generate markdown documentation for tools."""
lines = ["# MCP Server Tools\n"]
lines.append(f"*{len(tools)} tools available*\n")
for tool in tools:
name = tool.get("name", "unnamed")
desc = tool.get("description", "No description")
schema = tool.get("inputSchema", {})
annotations = tool.get("annotations", {})
lines.append(f"## `{name}`\n")
lines.append(f"{desc}\n")
# Add annotations if present
if annotations:
flags = []
if annotations.get("readOnlyHint"):
flags.append("read-only")
if annotations.get("destructiveHint"):
flags.append("destructive")
if annotations.get("idempotentHint"):
flags.append("idempotent")
if flags:
lines.append(f"*Flags: {', '.join(flags)}*\n")
# Add input schema
if schema.get("properties"):
lines.append("### Parameters\n")
required = set(schema.get("required", []))
for prop_name, prop_def in schema["properties"].items():
req_marker = " *(required)*" if prop_name in required else ""
prop_type = prop_def.get("type", "any")
prop_desc = prop_def.get("description", "")
lines.append(f"- **`{prop_name}`** (`{prop_type}`){req_marker}: {prop_desc}")
lines.append("")
# Add full schema as collapsible
lines.append("<details>")
lines.append("<summary>Full Schema</summary>\n")
lines.append("```json")
lines.append(json.dumps(schema, indent=2))
lines.append("```")
lines.append("</details>\n")
return "\n".join(lines)
def emit_json(tools: list[dict]) -> str:
"""Generate JSON output for tools."""
return json.dumps({"tools": tools}, indent=2)
def main():
parser = argparse.ArgumentParser(
description="Universal MCP Client - connect to any MCP server",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__
)
# Commands
subparsers = parser.add_subparsers(dest="command", required=True)
# list command
list_parser = subparsers.add_parser("list", help="List available tools")
list_parser.add_argument("--verbose", "-v", action="store_true", help="Show full tool details")
# call command
call_parser = subparsers.add_parser("call", help="Call a tool")
call_parser.add_argument("--tool", "-t", required=True, help="Tool name")
call_parser.add_argument("--params", "-p", default="{}", help="JSON parameters")
# emit command
emit_parser = subparsers.add_parser("emit", help="Emit tool schemas as documentation")
emit_parser.add_argument("--format", "-f", choices=["markdown", "json"], default="markdown")
# resources command
subparsers.add_parser("resources", help="List available resources")
# prompts command
subparsers.add_parser("prompts", help="List available prompts")
# Transport options (added to all subparsers)
for sub in [list_parser, call_parser, emit_parser]:
transport_group = sub.add_mutually_exclusive_group(required=True)
transport_group.add_argument("--url", "-u", help="HTTP URL of MCP server")
transport_group.add_argument("--stdio", "-s", help="Command to start stdio MCP server")
sub.add_argument("--header", "-H", action="append", default=[],
help="HTTP header (format: 'Name: Value')")
args = parser.parse_args()
# Create transport
transport = None
try:
if hasattr(args, 'url') and args.url:
headers = {}
for h in args.header:
if ':' in h:
key, value = h.split(':', 1)
headers[key.strip()] = value.strip()
transport = HTTPTransport(args.url, headers)
elif hasattr(args, 'stdio') and args.stdio:
transport = StdioTransport(args.stdio)
else:
parser.error("Must specify --url or --stdio")
client = MCPClient(transport)
# Execute command
if args.command == "list":
tools = client.list_tools()
if args.verbose:
print(json.dumps(tools, indent=2))
else:
for tool in tools:
desc = tool.get("description", "")[:60]
print(f" {tool['name']}: {desc}...")
elif args.command == "call":
params = json.loads(args.params)
result = client.call_tool(args.tool, params)
print(json.dumps(result, indent=2))
elif args.command == "emit":
tools = client.list_tools()
if args.format == "markdown":
print(emit_markdown(tools))
else:
print(emit_json(tools))
elif args.command == "resources":
resources = client.list_resources()
print(json.dumps(resources, indent=2))
elif args.command == "prompts":
prompts = client.list_prompts()
print(json.dumps(prompts, indent=2))
except MCPClientError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
except json.JSONDecodeError as e:
print(f"Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
sys.exit(130)
finally:
if transport and hasattr(transport, 'close'):
transport.close()
if __name__ == "__main__":
main()
#!/bin/bash
# Start Context7 MCP server for context7-efficient skill
# Usage: ./start-server.sh [port]
PORT=${1:-8809}
PID_FILE="/tmp/context7-mcp-${PORT}.pid"
# Check if already running
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "Context7 MCP already running on port $PORT (PID: $(cat $PID_FILE))"
exit 0
fi
# Start server
npx -y @upstash/context7-mcp --port "$PORT" &
echo $! > "$PID_FILE"
sleep 2
if kill -0 $(cat "$PID_FILE") 2>/dev/null; then
echo "Context7 MCP started on port $PORT (PID: $(cat $PID_FILE))"
else
echo "Failed to start Context7 MCP"
rm -f "$PID_FILE"
exit 1
fi
#!/bin/bash
# Stop Context7 MCP server
# Usage: ./stop-server.sh [port]
PORT=${1:-8809}
PID_FILE="/tmp/context7-mcp-${PORT}.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
# Kill the server
kill "$PID" 2>/dev/null
sleep 1
# Force kill if still running
kill -9 "$PID" 2>/dev/null || true
echo "Context7 MCP stopped (was PID: $PID)"
else
echo "Context7 MCP not running (stale PID file)"
fi
rm -f "$PID_FILE"
else
# Try to find and kill by process name
pkill -f "context7-mcp.*--port.*${PORT}" 2>/dev/null && echo "Context7 MCP stopped" || echo "Context7 MCP not running"
fi