Newer
Older
navi-1 / mcp-servers / navi-web / app / mcp_server.py
"""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()