"""MCP server for Navi web tools (search, browse, HTTP)."""
from __future__ import annotations
import json
import os
from typing import Annotated, Any
from mcp.server.fastmcp import FastMCP
from pydantic import Field
from .browse import web_view
from .config import Settings
from .request import http_request
from .search import web_search
INSTRUCTIONS = """
Navi Web MCP server provides web search, browsing, and raw HTTP tools.
Use it when the task involves:
- searching the web for current info, docs, or real-time data;
- opening a URL in a browser to read human-readable content;
- making REST API calls, webhooks, or raw HTTP requests.
Workflow:
1. web_search — find relevant pages or facts.
2. web_view — open promising URLs to read full content.
3. http_request — call APIs or services requiring headers/auth.
All three tools are stateless and work with public URLs.
No session_id or filesystem paths are required.
""".strip()
mcp = FastMCP("navi-web", instructions=INSTRUCTIONS)
def _json(data: Any) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)
def _settings() -> Settings:
return Settings()
@mcp.tool(name="web_search")
async def search_tool(
query: Annotated[str, Field(description="Search query.")],
max_results: Annotated[int, Field(description="Number of results (default 5).")] = 5,
) -> str:
"""Search the web (SearXNG primary, DDG fallback, Brave tertiary)."""
result = await web_search(_settings(), query, max_results)
return _json(result)
@mcp.tool(name="web_view")
async def view_tool(
url: Annotated[str, Field(description="Full URL to open (must start with http:// or https://).")],
screenshot: Annotated[bool, Field(description="Also capture a screenshot.")] = False,
wait_until: Annotated[
str,
Field(
description="When to consider the page loaded: load, domcontentloaded, or networkidle (default)."
),
] = "networkidle",
) -> str:
"""Open a URL in a headless browser and return clean readable text."""
result = await web_view(url, screenshot=screenshot, wait_until=wait_until)
return _json(result)
@mcp.tool(name="http_request")
async def request_tool(
method: Annotated[str, Field(description="HTTP method: GET, POST, PUT, PATCH, DELETE.")],
url: Annotated[str, Field(description="Full URL to request.")],
headers: Annotated[dict[str, str] | None, Field(description="Optional HTTP headers.")] = None,
body: Annotated[dict[str, Any] | None, Field(description="Optional JSON body.")] = None,
params: Annotated[dict[str, str] | None, Field(description="Optional query parameters.")] = None,
) -> str:
"""Make a raw HTTP request."""
result = await http_request(method, url, headers=headers, body=body, params=params)
return _json(result)
def main() -> None:
transport = os.environ.get("NAVI_WEB_MCP_TRANSPORT", "stdio")
if transport not in {"stdio", "sse", "streamable-http"}:
raise SystemExit("NAVI_WEB_MCP_TRANSPORT must be stdio, sse, or streamable-http")
mcp.run(transport=transport) # type: ignore[arg-type]
if __name__ == "__main__":
main()