Generic web search returns blog spam alongside official documentation. This tutorial builds an MCP server that constrains search to documentation domains (docs.python.org, react.dev, developer.mozilla.org), giving Claude Code and Cursor precise documentation answers without noise.
Prerequisites
- Node.js 18+ or Python 3.10+
- A Scavio API key
- Basic MCP server knowledge
Walkthrough
Step 1: Design the docs search tool
Define a tool that accepts a query and documentation domain, searching only that domain.
# Concept: Use Google's site: operator via Scavio to restrict results to docs domains
# Example queries:
# 'asyncio event loop site:docs.python.org'
# 'useEffect cleanup site:react.dev'
# 'Array.prototype.map site:developer.mozilla.org'
# Tool schema:
# name: docs_search
# params: { query: string, domain: string (optional, defaults to auto-detect) }
# returns: top 5 results from the specified docs domainStep 2: Implement with FastMCP (Python)
Build the MCP server using FastMCP for minimal boilerplate.
from fastmcp import FastMCP
import requests, os
app = FastMCP('docs-search')
H = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}
DOCS_DOMAINS = {
'python': 'docs.python.org',
'react': 'react.dev',
'mdn': 'developer.mozilla.org',
'node': 'nodejs.org/docs',
'typescript': 'www.typescriptlang.org/docs',
'rust': 'doc.rust-lang.org',
'go': 'pkg.go.dev',
}
@app.tool()
def docs_search(query: str, language: str = 'python') -> str:
"""Search official documentation for a programming language or framework."""
domain = DOCS_DOMAINS.get(language, language)
full_query = f'{query} site:{domain}'
resp = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
json={'platform': 'google', 'query': full_query}, timeout=10)
results = resp.json().get('organic', [])[:5]
lines = []
for r in results:
lines.append(f"## {r.get('title', '')}")
lines.append(f"{r.get('snippet', '')}")
lines.append(f"URL: {r.get('link', '')}")
lines.append('')
return '\n'.join(lines) if lines else 'No documentation found.'
if __name__ == '__main__':
app.run()Step 3: Add to Claude Desktop config
Configure Claude Desktop to use your docs search MCP server.
// Add to claude_desktop_config.json:
{
"mcpServers": {
"docs-search": {
"command": "python",
"args": ["/path/to/docs_search_server.py"]
}
}
}
// Or if using Scavio's hosted MCP directly (no custom server needed):
// Just use site: operator in your queries to Claude:
// 'Search for asyncio event loop on docs.python.org'Step 4: Test and iterate
Verify the docs search returns relevant results and iterate on domains.
# Test queries:
# docs_search('asyncio gather', 'python') -> docs.python.org results
# docs_search('useEffect dependencies', 'react') -> react.dev results
# docs_search('fetch API', 'mdn') -> developer.mozilla.org results
# Add more domains as needed:
# DOCS_DOMAINS['django'] = 'docs.djangoproject.com'
# DOCS_DOMAINS['fastapi'] = 'fastapi.tiangolo.com'
# DOCS_DOMAINS['nextjs'] = 'nextjs.org/docs'Python Example
import requests, os
H = {'x-api-key': os.environ['SCAVIO_API_KEY'], 'Content-Type': 'application/json'}
def docs_search(query, domain='docs.python.org'):
r = requests.post('https://api.scavio.dev/api/v1/search', headers=H,
json={'platform': 'google', 'query': f'{query} site:{domain}'}).json()
return [{'title': x['title'], 'url': x.get('link',''), 'snippet': x.get('snippet','')}
for x in r.get('organic',[])[:5]]JavaScript Example
async function docsSearch(query, domain = 'docs.python.org') {
const r = await fetch('https://api.scavio.dev/api/v1/search', {
method: 'POST', headers: {'x-api-key': process.env.SCAVIO_API_KEY, 'Content-Type': 'application/json'},
body: JSON.stringify({platform: 'google', query: `${query} site:${domain}`})
});
return (await r.json()).organic?.slice(0,5).map(x => ({title: x.title, url: x.link, snippet: x.snippet}));
}Expected Output
A custom MCP documentation search server that constrains results to official docs domains, giving Claude precise documentation access without web noise.