Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Build a web search MCP server as two connected parts: an MCP tool that accepts a query, and an adapter that sends it to a search API you choose. The example below uses the official Python MCP SDK v2 high-level MCPServer interface. Its provider-neutral adapter expects a small, documented JSON contract; because search providers differ, you must adjust the adapter to match your provider’s endpoint, authentication, and response format.

What you are building

The Model Context Protocol (MCP) gives an AI application a standard way to request context from a server. The MCP Python SDK documentation describes it as separating the concern of providing context from the LLM interaction itself. In this tutorial, the server exposes one tool, web_search; when called, that tool validates a query, asks an upstream search API for results, and returns a concise list of titles, URLs, and snippets.

MCP does not perform web search by itself. You need a search backend with an API you are authorized to use. The Microsoft MCP for Beginners example supports the broad pattern of a Python MCP server connected to an external search API, but it does not establish a particular provider, endpoint, authentication method, quota, pricing, or terms. The implementation below therefore does not claim to integrate with or test a named provider.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Choose the SDK line and transport

The official Python SDK documentation presents v2 as its stable line and specifies Python 3.10 or newer. Its v1 documentation labels that line maintenance-only and recommends pinning mcp<2 if you need to stay on v1. This walkthrough targets v2; keep the dependency constraint in your project so an install cannot silently move you onto a different major line.

For development, begin with stdio when the MCP host launches your server as a local process. A deployed service that a client reaches over a URL calls for a network transport; the SDK documents stdio, Streamable HTTP, and SSE. The Python client guide demonstrates Streamable HTTP connections by URL. Choose based on how your MCP client connects, rather than assuming every host supports the same transport.

Install Python dependencies

Create a project and install the v2 SDK line plus httpx, which the adapter uses for outbound HTTP requests. With uv:

uv init web-search-mcp
cd web-search-mcp
uv add "mcp[cli]>=2,<3" httpx

Or with pip in an activated virtual environment:

python -m pip install "mcp[cli]>=2,<3" httpx

The official SDK also documents pip install "mcp[cli]" and uv add "mcp[cli]"; the range above deliberately constrains this example to the v2 major line. For repeatable builds, record the resolved versions in your project’s lock file or requirements file after installation.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Create the MCP server and provider adapter

Save the following as server.py. It registers a typed web_search function using MCPServer and @mcp.tool(). The adapter is intentionally generic: set SEARCH_API_URL to your provider’s search endpoint and configure the provider to accept a GET request with q and limit parameters and, if needed, a bearer token. It expects JSON shaped like {"results":[{"title":"…","url":"…","snippet":"…"}]}. Those are this example’s adapter assumptions, not a claim about any provider’s native API.

import json
import os
from typing import Any
from urllib.parse import urlparse

import httpx
from mcp.server import MCPServer

mcp = MCPServer("web-search")


def search_api_url() -> str:
    value = os.environ.get("SEARCH_API_URL", "").strip()
    if not value:
        raise ValueError("SEARCH_API_URL is not configured")
    parsed = urlparse(value)
    if parsed.scheme != "https" or not parsed.netloc:
        raise ValueError("SEARCH_API_URL must be an absolute HTTPS URL")
    return value


@mcp.tool()
async def web_search(query: str, limit: int = 5) -> str:
    """Search the web and return up to limit results with title, URL, and snippet."""
    query = query.strip()
    if not query:
        raise ValueError("query must not be empty")
    if len(query) > 500:
        raise ValueError("query must be 500 characters or fewer")
    if not 1 <= limit <= 10:
        raise ValueError("limit must be between 1 and 10")

    headers: dict[str, str] = {"Accept": "application/json"}
    api_key = os.environ.get("SEARCH_API_KEY", "").strip()
    if api_key:
        headers["Authorization"] = f"Bearer {api_key}"

    try:
        async with httpx.AsyncClient(timeout=httpx.Timeout(20.0)) as client:
            response = await client.get(
                search_api_url(),
                params={"q": query, "limit": limit},
                headers=headers,
            )
            response.raise_for_status()
            payload: Any = response.json()
    except httpx.TimeoutException as exc:
        raise RuntimeError("Search provider request timed out") from exc
    except httpx.HTTPStatusError as exc:
        raise RuntimeError(
            f"Search provider returned HTTP {exc.response.status_code}"
        ) from exc
    except httpx.RequestError as exc:
        raise RuntimeError("Could not connect to the search provider") from exc
    except json.JSONDecodeError as exc:
        raise RuntimeError("Search provider did not return valid JSON") from exc

    if not isinstance(payload, dict) or not isinstance(payload.get("results"), list):
        raise RuntimeError("Expected a JSON object containing a results array")

    results = []
    for item in payload["results"][:limit]:
        if not isinstance(item, dict):
            continue
        title = item.get("title")
        url = item.get("url")
        snippet = item.get("snippet", "")
        if not isinstance(title, str) or not isinstance(url, str):
            continue
        results.append({
            "title": title[:300],
            "url": url[:2048],
            "snippet": snippet[:1000] if isinstance(snippet, str) else "",
        })

    return json.dumps({"query": query, "results": results}, ensure_ascii=False)


if __name__ == "__main__":
    mcp.run()

Type annotations and the function signature let the high-level SDK derive the tool’s input schema. The function also returns a JSON string, which keeps the provider’s variable result count and fields in one predictable text response. If you want the MCP tool’s schema or structured output to be controlled at a lower level, the SDK offers a low-level Server API; start with MCPServer unless you have a specific reason to manage those details yourself.

Configure the provider contract

Before launching, confirm the provider’s actual API documentation and make the adapter match it. At minimum, verify the request URL and method, parameter names, authentication header or query parameter, response JSON shape, and provider-specific error behavior. If its results are nested differently, change the extraction under payload["results"]; if it uses a different query parameter, change params. Do not send credentials in a URL unless the provider requires it, and do not commit keys to source control.

Set the environment for a local run. Substitute the real HTTPS endpoint and key mechanism required by the search API you selected:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
export SEARCH_API_URL="https://your-provider.example/search"
export SEARCH_API_KEY="your-secret-key"
uv run mcp dev server.py

The endpoint above is illustrative, not a real service URL. In PowerShell, use $env:SEARCH_API_URL="..." and $env:SEARCH_API_KEY="..." before running the development command. If your provider uses different authentication, adapt the header construction rather than assuming bearer authentication.

Run the server and inspect the tool

  1. Start the development workflow. Run uv run mcp dev server.py from the project directory. The SDK documents this command as the development-server and MCP Inspector workflow. With pip, use the corresponding installed mcp command in the same activated environment.
  2. Check tool discovery. In MCP Inspector, confirm the server advertises web_search, with a string query and numeric limit. The Python type hints supply the input schema; the docstring describes what the tool does.
  3. Call it with a small result limit. Try a short, non-sensitive query and limit between 1 and 10. A successful response is JSON text with the original query and a results array containing title, URL, and snippet fields.
  4. Check failure behavior deliberately. Try a blank query, an invalid limit, an unset endpoint, and an unreachable endpoint. Input validation should reject malformed tool arguments; configuration, timeout, and HTTP failures should surface as errors rather than being mistaken for empty search results.

Adapt the design for production

Keep provider-specific policy out of the MCP interface

The MCP tool’s stable contract can stay small while the adapter evolves. If your chosen API uses pagination, locale, safe-search controls, filters, or a different maximum result count, add only the inputs the provider supports and document their meaning in the tool description. Validate them before making a request. Do not advertise a capability merely because the MCP schema can represent it.

Protect secrets and control data exposure

  • Load credentials from environment variables or your deployment’s secret manager; avoid hard-coded keys, logs containing authorization headers, and checked-in local environment files.
  • Apply access control at the host or service boundary appropriate to your deployment. The example is a tool adapter, not an authentication system for a publicly reachable service.
  • Return only the fields the agent needs. Truncate unusually long provider text, as the example does, and consider whether queries or snippets could contain sensitive information before logging or retaining them.
  • Review the provider’s usage terms and data handling policy yourself. This tutorial does not establish provider retention, training, geographic availability, or usage rights.

Make latency and failures visible

The example sets a 20-second HTTP timeout so an upstream request cannot wait indefinitely. Set this in light of the MCP host’s own timeout and the provider’s documented response characteristics. A server-side timeout does not guarantee the client will wait that long. In a deployed system, log a request identifier, elapsed time, and error category without logging secrets; distinguish an upstream error from a legitimate zero-result response.

Retries can help with transient network failures, but they can also multiply latency and API usage. Add bounded retries only after checking the provider’s retry guidance and whether the request is safe to repeat. Likewise, caching can reduce repeated calls but may return stale results and may be restricted by provider terms; set a deliberate TTL only if your provider permits it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Pick a deployment transport intentionally

Use stdio for a local process that an MCP host starts and communicates with through standard input and output. Use Streamable HTTP or SSE when clients connect to a network service, after checking which transport your host and deployment support. The SDK documents all three; its client guide demonstrates Streamable HTTP using a URL. A transport change affects how clients connect, not the provider adapter’s search semantics.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting

Symptom Likely cause What to check
The server will not import MCPServer or start The installed SDK line may not match the v2 example, or the interpreter may be older than Python 3.10. Check python --version and the installed mcp version in the same environment used to run the server. Install the documented v2 line; if intentionally using v1, follow its maintenance-line documentation and pin mcp<2 rather than mixing APIs.
Inspector does not show web_search The server failed during import or did not launch through the expected dev command. Read the terminal error, confirm the command targets the correct server.py, and ensure the @mcp.tool() function is defined before mcp.run().
SEARCH_API_URL is not configured The environment variable is missing from the process environment. Set it in the shell or service that launches the MCP process; setting it in a different terminal or account will not update an already-running process.
Provider returns HTTP 401 or 403 The key may be missing, invalid, or sent using the wrong authentication scheme. Compare the provider’s current API documentation with the example’s bearer header and update the adapter. Never paste a real secret into diagnostic output.
Provider returns 400 or 404 The endpoint or parameter names may not match the provider’s API. Verify the URL, HTTP method, query parameter names, and required fields. The example’s q and limit parameters are an adapter contract, not universal search API parameters.
Response is reported as invalid JSON or missing results The provider may return a different content type or schema, or an error object instead of results. Inspect a safely redacted response according to the provider docs, then update the parsing logic and error mapping. Do not log authorization headers or sensitive query data.
The client disconnects before a response The upstream call may exceed the host or network transport timeout. Compare host, proxy, and provider timeouts; reduce work per call or use a supported asynchronous pattern if the chosen service needs longer jobs.

Or skip the browser setup

If your next step is capturing a webpage rather than searching an index, ScreenshotNeo is a website screenshot API and MCP server for developers. It is not a web-search backend and does not replace the web_search tool above. Its one-request screenshot example is:

ScreenshotNeo API documentation

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

ScreenshotNeo accepts cookie or consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers indicate the page verdict and billing status. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month with no card.

Frequently Asked Questions

Can this server search the web without an API provider?

No. MCP defines the callable tool interface; a separate search backend must supply the indexed results.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Can I use the low-level Server API instead?

Yes. The SDK provides that option when you need exact schema or result control; the high-level MCPServer interface is the simpler starting point.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.