web-research-hub-mcp-server
by Paul-Orlando
README.md
# web-research-hub-mcp-server
### FastAPI · FastMCP · Streamable HTTP · Exa AI · Python
A standalone MCP server that exposes the Web Research Hub's core
research capabilities as standardized tools over Streamable HTTP —
callable by Claude Desktop, Claude.ai, or any MCP-compatible agent
or client.
Built as the infrastructure layer for
[web-research-hub](https://github.com/Paul-Orlando/web-research-hub),
this server separates tool execution from agent reasoning: the LLM
stays in the research app, the tools live here.
---
## 🔗 Live Endpoint
```
https://web-production-a8829.up.railway.app/mcp
```
Health check:
```
https://web-production-a8829.up.railway.app/health
```
---
## Architecture
```
Client (Claude Desktop / Claude.ai / custom agent)
│ POST /mcp (MCP Streamable HTTP)
▼
FastAPI + FastMCP
│
┌─────┴────────────────────────────────────────┐
│ web_search fetch_url calculate export_report │
└─────┬──────────────────────────────────────────┘
│
Exa AI API httpx + BS4 ast (stdlib) fpdf2 / python-docx
```
- **Transport:** Streamable HTTP — clients POST to `/mcp`
- **Health check:** `GET /health` — public, no auth required
- **CORS:** configurable via `CORS_ORIGIN_REGEX` env var
---
## How It Fits the Portfolio
This server is part of a three-tier architecture:
```
Web Research Hub (frontend + FastAPI backend)
→ calls this MCP server as a tool provider
→ agents use web_search and fetch_url during research
→ reports exported via export_report
This MCP Server
→ exposes 4 tools over Streamable HTTP
→ no LLM calls inside — pure tool execution
→ callable by any MCP-compatible client independently
Pinecone Agentic Search MCP Server
→ separate MCP server in this portfolio
→ handles academic/vector search over ArXiv corpus
→ uses SSE transport (contrast: this server uses
Streamable HTTP — the newer MCP spec standard)
```
The two MCP servers in this portfolio demonstrate both transport
patterns (SSE and Streamable HTTP) and two different tool scopes
(single-purpose vector search vs. broader research toolkit).
---
## What Makes This Different from the Pinecone MCP Server
| | Pinecone MCP Server | This Server |
|---|---|---|
| Transport | SSE | Streamable HTTP |
| Tools | 1 (`agentic-search`) | 4 (`web_search`, `fetch_url`, `calculate`, `export_report`) |
| Data source | Pinecone vector store (ArXiv corpus) | Live web (Exa AI) + stdlib |
| LLM calls | Yes (OpenRouter) | None — pure tool execution |
| Purpose | Academic/research paper search | Web research tool layer |
---
## Tool Reference
### `web_search`
Searches the live web via Exa AI and returns structured results
with title, URL, summary, and publication date. Use when the
research query requires current, real-world web sources. Returns
an empty results array on failure, never throws.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `query` | string | required | Search query |
| `num_results` | int | `4` | Max results to return |
| `start_date` | string | `null` | ISO 8601 date filter (e.g. `2024-01-01`) |
| `end_date` | string | `null` | ISO 8601 date filter |
**Response:**
```json
{
"results": [{ "title": "", "url": "", "summary": "", "published_date": "" }],
"query": "...",
"total_results": 4
}
```
Returns `results: []` on failure, never throws.
---
### `fetch_url`
Fetches a URL and returns clean extracted text, stripping HTML
and truncating to the specified character limit. Use to go deeper
on a specific source found during web search. Returns
`success: false` on failure, never throws.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `url` | string | required | Full URL to fetch |
| `max_length` | int | `5000` | Character limit on returned content |
**Response:**
```json
{ "url": "", "content": "", "title": "", "success": true }
```
---
### `calculate`
Evaluates a safe arithmetic expression using Python's AST parser —
no raw `eval()`. Use for numeric reasoning within research tasks
(e.g. percentage changes, cost calculations, financial figures).
Returns `result: null` with an error field on failure, never throws.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `expression` | string | required | e.g. `"(3 + 4) * 2"` |
| `description` | string | `null` | Optional label for context |
Supported operators: `+ - * / ** % //`
**Response:**
```json
{ "expression": "(3 + 4) * 2", "result": 14, "description": null }
```
---
### `export_report`
Exports a markdown string to PDF, DOCX, or MD and returns the
file as a base64-encoded string. Citation links `[text](url)` are
rendered as `text (domain.com)` in PDF and DOCX output so sources
remain identifiable outside the browser. Returns `success: false`
on failure, never throws.
| Parameter | Type | Default | Notes |
|---|---|---|---|
| `content` | string | required | Markdown source |
| `format` | string | required | `"pdf"`, `"docx"`, or `"md"` |
| `title` | string | `null` | Prepended as H1; used in filename |
**Response:**
```json
{
"format": "pdf",
"filename": "report.pdf",
"content_base64": "...",
"success": true
}
```
---
## Design Constraints
**No LLM calls inside this server.**
Every tool is a pure function: input → deterministic output.
The LLM reasoning (planning, orchestration, synthesis) stays in
the Web Research Hub's FastAPI backend agents. This is the correct
MCP pattern — tools are execution units, not reasoning units.
**Every tool is non-throwing.**
All external calls are wrapped in try/except. A tool that throws
breaks the entire MCP session. Every tool returns a valid response
object even on failure.
**Tool descriptions are written as policies, not labels.**
Each tool description specifies what it does, when to use it,
and what it returns on failure — not just a one-line label.
This matches the prompt engineering standard applied across every
agent in this portfolio.
---
## Authentication
Every request to `POST /mcp` must include an `X-API-Key` header:
```
X-API-Key: your-secret-key-here
```
The key is compared against the `MCP_API_KEY` environment variable
on the server. Missing or invalid keys return HTTP 401.
`GET /health` requires no authentication.
---
## Rate Limits
`POST /mcp` is limited to **10 requests per IP address per hour**.
Exceeding the limit returns HTTP 429. `GET /health` is not
rate-limited.
**Note:** one search generates 4-5 `/mcp` calls internally (MCP
handshake + one tool call per subtask), so this limit allows
approximately 2 complete Quick searches per hour.
This is a portfolio demonstration server. To remove these limits,
clone the repo and deploy your own instance with your own API keys.
---
## Environment Variables
| Variable | Required | Description |
|---|---|---|
| `EXA_API_KEY` | ✅ | Exa AI API key — get one at [exa.ai](https://exa.ai) |
| `MCP_API_KEY` | ✅ | Secret key callers must pass as `X-API-Key` header |
| `CORS_ORIGIN_REGEX` | optional | Regex for allowed origins. Default: `https://.*\.vercel\.app` |
---
## Local Development
```bash
# 1. Clone and enter the repo
git clone https://github.com/Paul-Orlando/web-research-hub-mcp-server.git
cd web-research-hub-mcp-server
# 2. Create a virtual environment
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
# 3. Install dependencies
pip install -r requirements.txt
# 4. Configure environment
cp .env.example .env
# Edit .env and add your EXA_API_KEY and MCP_API_KEY
# 5. Run the server
uvicorn main:app --reload
```
Verify it's running:
```bash
curl http://localhost:8000/health
# {"status":"ok","tools":["web_search","fetch_url","calculate","export_report"]}
```
---
## Deployment
### Railway (recommended)
Railway detects the `Procfile` automatically.
1. Push this repo to GitHub
2. New project → Deploy from GitHub repo
3. Root Directory: `/` (not a monorepo)
4. Set `EXA_API_KEY` and `MCP_API_KEY` in the Variables tab
5. Deploy — starts with:
```
uvicorn main:app --host 0.0.0.0 --port $PORT
```
6. Settings → Networking → Generate Domain → set Target Port to
the port shown in deploy logs (typically `8080`)
7. Test: `GET https://your-url.up.railway.app/health`
---
## Connecting to Claude Desktop
Add this to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"web-research-hub": {
"url": "https://web-production-a8829.up.railway.app/mcp",
"headers": {
"X-API-Key": "your-key-here"
}
}
}
}
```
For local development use `http://localhost:8000/mcp` and set
`MCP_API_KEY` in your local `.env`.
---
## Usage Note
This is a portfolio demonstration server with rate limiting and
API key authentication. For production use, clone the repo and
deploy your own instance with your own API keys — the `Procfile`
and Railway deployment instructions above are included for exactly
this purpose.
---
## Roadmap
- [ ] `academic_search` tool — calls the Pinecone Agentic Search
MCP Server internally, making this server an MCP client of
another MCP server in this portfolio (three-tier pattern)
- [ ] Source credibility scoring — weight academic/primary sources
higher than secondary commentary
- [ ] Tool call logging for observability
---
## Related Repos
| Repo | Pattern | Stack |
|---|---|---|
| [web-research-hub](https://github.com/Paul-Orlando/web-research-hub) | Hierarchical 3-Agent Pipeline | Next.js · FastAPI · OpenRouter · Gemini 2.5 Flash · Exa AI |
| [pinecone-mcp-server](https://github.com/Paul-Orlando/pinecone-mcp-server) | Custom MCP Server · Agentic RAG | Node.js · TypeScript · Pinecone · SSE |
| [n8n-mcp-server-agentic-rag](https://github.com/Paul-Orlando/n8n-mcp-server-agentic-rag) | Agentic RAG + MCP Client | Node.js · Express · Pinecone · Gemini Flash 2.5 |
---
## Author
Paul Orlando
Creative Technologist | AI Agent Developer | Data Analytics
🌐 [paulforlando.com](https://www.paulforlando.com) |
💼 [LinkedIn](https://www.linkedin.com/in/paul-orlando-7841b5154) |
🐙 [GitHub](https://github.com/Paul-Orlando)
---
## License
MIT License