From 9a4b946f67c65f8994ab80953d3de636af5b7c2a Mon Sep 17 00:00:00 2001 From: ParthSareen Date: Thu, 18 Sep 2025 17:03:50 -0700 Subject: [PATCH] mcp --- examples/README.md | 13 +++ examples/mcp_web_search_crawl_server.py | 132 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+) create mode 100644 examples/mcp_web_search_crawl_server.py diff --git a/examples/README.md b/examples/README.md index 3d44fa9..83d76c1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -78,3 +78,16 @@ Requirement: `pip install tqdm` ### Thinking (levels) - Choose the thinking level - [thinking-levels.py](thinking-levels.py) + + +### MCP server - Expose web search and crawl tools to MCP clients +Requires: `pip install mcp` +- [mcp_web_search_crawl_server.py](mcp_web_search_crawl_server.py) + +Run via stdio (for Cursor/Claude MCP): +```sh +python3 examples/mcp_web_search_crawl_server.py +``` + +Optional environment: +- `OLLAMA_API_KEY`: If set, will be passed as an Authorization header for Ollama hosted web search/crawl APIs. diff --git a/examples/mcp_web_search_crawl_server.py b/examples/mcp_web_search_crawl_server.py new file mode 100644 index 0000000..42f2f41 --- /dev/null +++ b/examples/mcp_web_search_crawl_server.py @@ -0,0 +1,132 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "mcp", +# "rich", +# "ollama", +# ] +# /// +""" +Minimal MCP stdio server exposing Ollama web_search and web_crawl as tools. + +This lets MCP clients (e.g., Cursor, Claude Desktop) call these tools. + +Environment: +- OLLAMA_API_KEY (optional): if set, will be used as Authorization header. + +Run directly (stdio transport): + python examples/mcp_web_search_crawl_server.py + +In Cursor/Claude MCP config, point a command to this script. +""" + +from __future__ import annotations + +import asyncio +import os +from typing import Any, Dict, List + +from ollama import Client + +try: + # Preferred high-level API (if available) + from mcp.server.fastmcp import FastMCP # type: ignore + _FASTMCP_AVAILABLE = True +except Exception: + _FASTMCP_AVAILABLE = False + +if not _FASTMCP_AVAILABLE: + # Fallback to the low-level stdio server API + from mcp.server import Server # type: ignore + from mcp.server.stdio import stdio_server # type: ignore + + +def _make_client() -> Client: + headers = {} + api_key = os.getenv("OLLAMA_API_KEY") + if api_key: + headers["Authorization"] = api_key + return Client(headers=headers) + + +client = _make_client() + + +def _web_search_impl(queries: List[str], max_results: int = 3) -> Dict[str, Any]: + res = client.web_search(queries=queries, max_results=max_results) + return res.model_dump() + + +def _web_crawl_impl(urls: List[str]) -> Dict[str, Any]: + res = client.web_crawl(urls=urls) + return res.model_dump() + + +if _FASTMCP_AVAILABLE: + app = FastMCP("ollama-web-tools") + + @app.tool() + def web_search(queries: List[str], max_results: int = 3) -> Dict[str, Any]: + """ + Perform a web search using Ollama's hosted search API. + + Args: + queries: A list of search queries to run. + max_results: Maximum results per query (default: 3). + + Returns: + JSON-serializable dict matching ollama.WebSearchResponse.model_dump() + """ + + return _web_search_impl(queries=queries, max_results=max_results) + + @app.tool() + def web_crawl(urls: List[str]) -> Dict[str, Any]: + """ + Crawl one or more web pages and return extracted content. + + Args: + urls: A list of absolute URLs to crawl. + + Returns: + JSON-serializable dict matching ollama.WebCrawlResponse.model_dump() + """ + + return _web_crawl_impl(urls=urls) + + if __name__ == "__main__": + app.run() + +else: + server = Server("ollama-web-tools") # type: ignore[name-defined] + + @server.tool() # type: ignore[attr-defined] + async def web_search(queries: List[str], max_results: int = 3) -> Dict[str, Any]: + """ + Perform a web search using Ollama's hosted search API. + + Args: + queries: A list of search queries to run. + max_results: Maximum results per query (default: 3). + """ + + return await asyncio.to_thread(_web_search_impl, queries, max_results) + + @server.tool() # type: ignore[attr-defined] + async def web_crawl(urls: List[str]) -> Dict[str, Any]: + """ + Crawl one or more web pages and return extracted content. + + Args: + urls: A list of absolute URLs to crawl. + """ + + return await asyncio.to_thread(_web_crawl_impl, urls) + + async def _main() -> None: + async with stdio_server() as (read, write): # type: ignore[name-defined] + await server.run(read, write) # type: ignore[attr-defined] + + if __name__ == "__main__": + asyncio.run(_main()) +