Build an MCP Server for Serply Search, News, and Web Scraping
Wrap Serply's Search, News, and Request APIs as MCP tools so Claude Desktop, Claude Code, and any MCP client can search, read news, and scrape pages.


- Why wrap an API in MCP instead of just calling it
- What we’re wrapping
- The /v1/request response shapes are not symmetric
- Setting up the project
- Registering the server with Claude Code
- Registering the server with Claude Desktop
- Where this goes from here
If you’ve spent any time building AI agents in the last year, you’ve probably run into the Model Context Protocol, or MCP. It’s the thing that lets Claude Desktop, Claude Code, and a growing list of other clients talk to external tools through a single, standardized interface instead of every app inventing its own plugin format. Once you’ve written an MCP server, any MCP-aware agent can use it — you’re not locked into one framework’s tool-calling convention.
This post walks through building an MCP server that exposes three Serply capabilities as tools: web search, news search, and full-page scraping. By the end, you’ll have a working server you can register with Claude Desktop or Claude Code, and an agent that can search the live web, pull recent news, and read the full content of any page it finds — not just a two-sentence snippet.
Why wrap an API in MCP instead of just calling it
You could, of course, just have your agent hit https://api.serply.io/v1/search/... with a generic HTTP tool. But that pushes all the API-shape knowledge — auth headers, the odd path-encoded query format, response parsing — into the model’s context on every call, and it means every agent framework you use needs its own glue code to call it correctly.
An MCP server does that translation once. You define serply_search, serply_news, and serply_scrape as clean, typed tools with sane parameter names, and the server handles the actual HTTP request, headers, and response shape internally. The agent just sees “call serply_search with a query” and gets back clean data.
What we’re wrapping
Serply’s API packs the query string directly into the URL path rather than using conventional ?key=value query parameters. A search for search api limited to 100 results looks like this:
GET https://api.serply.io/v1/search/q=search+api&num=100
That q=search+api&num=100 segment is a URL-encoded parameter string, just placed after the path instead of after a ?. Every request also needs an X-Api-Key header. See the Authentication guide for how to get one.
We’ll wrap three endpoints:
GET /v1/search/{query}— organic web search. Returns{"results": [{"title", "link", "description"}], "total": <int>, "answer": null | [...]}. Full details in the Google Search docs.GET /v1/news/{query}— news search, filterable with aceid=US:en-style country/language param. Returns a feed object withentries(each withtitle,link,summary,published,source) plus a top-levelentitiesarray. Full shape in the Google News docs.POST /v1/request— scrape any URL. This one’s worth pausing on, because its two response modes are shaped differently and it’s easy to get wrong.
The /v1/request response shapes are not symmetric
POST /v1/request takes a JSON body — {"url": "...", "response_type": "full" | "markdown"} — but the two modes don’t return the same kind of thing:
response_type: "full"returns a JSON object:{"data": "<html>...full page HTML...</html>"}.response_type: "markdown"returns the markdown text directly as the response body — it is not wrapped in JSON at all. The content type istext/html; charset=utf-8, but the body itself is plain markdown.
If you response.json() a markdown-mode response, you’ll get a parse error, because there’s no JSON there to parse. This trips people up constantly, so our tool wrapper handles both cases explicitly rather than assuming everything comes back as JSON. Full details in the Request docs.
Setting up the project
You’ll need the mcp Python package and httpx for HTTP calls:
pip install mcp httpx
Create serply_mcp_server.py:
import os
import httpx
from mcp.server.fastmcp import FastMCP
SERPLY_API_KEY = os.environ["SERPLY_API_KEY"]
BASE_URL = "https://api.serply.io/v1"
mcp = FastMCP("serply")
def _headers(proxy_location: str | None = None) -> dict:
headers = {"X-Api-Key": SERPLY_API_KEY}
if proxy_location:
headers["X-Proxy-Location"] = proxy_location
return headers
@mcp.tool()
async def serply_search(query: str, num_results: int = 10, proxy_location: str | None = None) -> dict:
"""Search the web via Serply and return organic results.
Args:
query: Plain-language search query, e.g. "best vector databases 2026".
num_results: How many results to request (passed through as `num`).
proxy_location: Optional two-letter proxy region, e.g. "US", "GB", "DE".
"""
path_query = f"q={httpx.QueryParams({'q': query})['q']}&num={num_results}"
url = f"{BASE_URL}/search/{path_query}"
async with httpx.AsyncClient() as client:
resp = await client.get(url, headers=_headers(proxy_location))
resp.raise_for_status()
data = resp.json()
return {
"total": data.get("total"),
"answer": data.get("answer"),
"results": [
{"title": r["title"], "link": r["link"], "description": r["description"]}
for r in data.get("results", [])
],
}
@mcp.tool()
async def serply_news(query: str, ceid: str = "US:en") -> dict:
"""Search recent news via Serply.
Args:
query: News search query, e.g. "federal reserve interest rates".
ceid: Country/language filter in Google's `ceid` format, e.g. "US:en", "GB:en".
"""
encoded_q = httpx.QueryParams({"q": query})["q"]
path_query = f"q={encoded_q}&ceid={ceid}"
url = f"{BASE_URL}/news/{path_query}"
async with httpx.AsyncClient() as client:
resp = await client.get(url, headers=_headers())
resp.raise_for_status()
data = resp.json()
entries = data.get("feed", {}).get("entries", [])
return {
"entries": [
{
"title": e.get("title"),
"link": e.get("link"),
"summary": e.get("summary"),
"published": e.get("published"),
"source": e.get("source"),
}
for e in entries
]
}
@mcp.tool()
async def serply_scrape(url: str, response_type: str = "markdown") -> str:
"""Scrape a URL via Serply and return its content.
Args:
url: The page to scrape.
response_type: "markdown" (default, ideal for feeding to an LLM) or "full" (raw HTML).
"""
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(
f"{BASE_URL}/request",
headers={**_headers(), "Content-Type": "application/json"},
json={"url": url, "response_type": response_type},
)
resp.raise_for_status()
if response_type == "full":
# full mode wraps the HTML in a JSON object: {"data": "<html>...</html>"}
return resp.json()["data"]
else:
# markdown mode returns the markdown text directly as the body — no JSON wrapper
return resp.text
if __name__ == "__main__":
mcp.run()
A few things worth calling out in that code:
serply_scrapebranches onresponse_typebecause, as covered above,fullgives you JSON andmarkdowngives you raw text. Handling both in one tool keeps the agent-facing interface simple — the agent just says which format it wants.- We trim the raw API responses down to the fields an agent actually needs (
title,link,description/summary) rather than passing the whole payload through. Less noise in context, fewer tokens burned per tool call. num_resultsandceidare exposed as tool parameters so the agent can adjust them per query instead of you hardcoding one search shape.
Registering the server with Claude Code
If you’re using Claude Code, adding a local MCP server is a single command:
claude mcp add serply -- python /path/to/serply_mcp_server.py
Make sure SERPLY_API_KEY is set in the environment Claude Code runs in, or pass it through explicitly:
claude mcp add serply --env SERPLY_API_KEY=your_key_here -- python /path/to/serply_mcp_server.py
Registering the server with Claude Desktop
For Claude Desktop, add an entry to your claude_desktop_config.json (on macOS, ~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"serply": {
"command": "python",
"args": ["/path/to/serply_mcp_server.py"],
"env": {
"SERPLY_API_KEY": "your_key_here"
}
}
}
}
Restart Claude Desktop, and you should see serply_search, serply_news, and serply_scrape show up as available tools. Try asking it something like “search for the latest news on the EU AI Act and then scrape the most relevant article for full detail” — that’s the search → news → scrape pipeline these three tools were built to support.
Where this goes from here
Three tools is enough to get a general-purpose research agent off the ground, but it’s a starting point, not the ceiling. Serply also has endpoints for Scholar search, product search, eBay listings, and Google Trends, all of which follow the same path-encoded-query pattern shown here — wrapping any of them as an additional MCP tool is a matter of copying the serply_search pattern and swapping the endpoint and response fields.
The bigger point is architectural: once your API access is behind an MCP server, it stops being “the Serply integration for framework X” and becomes a tool any MCP-compatible agent can pick up, no matter what’s driving it. Check out serply.io and the full API reference for the rest of the available endpoints.