Skip to main content
Glama

docs-search-mcp

A Model Context Protocol server in Python that gives an AI client (Claude Desktop, Claude Code or any MCP client) five tools: search and read a folder of local notes, fetch a public web page as markdown, and convert times between timezones.

The problem

Tools exposed to a model are an attack surface. A notes tool must not read outside its folder, and a web-fetch tool must not let the model (or a prompt injection) make your machine request internal addresses. This server provides useful tools with those protections built in and tested through a real MCP client.

Related MCP server: vault-mcp-bridge

Tools

Tool

What it does

list_notes

Lists .md/.txt files under the notes folder

search_notes(query, limit)

BM25 keyword search over note sections; returns path, heading, score and snippet

read_note(path, max_chars)

Reads one note; refuses paths outside the folder, symlinks and other file types

fetch_url(url, max_length, start_index, raw)

Fetches a page as markdown, paginated; honours robots.txt; refuses private addresses

convert_time(source_timezone, time, target_timezone, date?)

HH:MM conversion between IANA zones, DST-aware for a given date

All tools are marked read-only through MCP annotations (fetch_url additionally openWorldHint).

Architecture

MCP client (Claude Desktop / Code / your script)
        | JSON-RPC over stdio (or streamable HTTP)
        v
  server.py  (FastMCP: tool registration, schemas from type hints, annotations, error mapping)
     |-- notes.py     BM25 index over sections, safe path resolution
     |-- web.py       SSRF guard -> robots.txt -> size-capped fetch -> HTML to markdown
     '-- timeconv.py  zoneinfo conversion, DST edge cases

Stack: Python 3.10+, the official mcp SDK (1.x, FastMCP), httpx, markdownify, zoneinfo/tzdata, pytest + pytest-asyncio.

Key technical decisions

  • SSRF guard. The host is resolved, non-public addresses are blocked (private, loopback, link-local), and the check is repeated on every redirect hop. Responses are size-capped and redirects are limited.

  • Path traversal and symlink protection. The final path is resolved and verified to stay under the notes root.

  • Errors as tool errors. Invalid input returns isError: true with a message the model can act on; the server keeps running. Logs go to stderr so stdout stays the protocol channel.

  • DST-aware time conversion. An optional date makes offsets correct for that day, and non-existent or ambiguous wall-clock times around DST changes are detected.

  • Section-level BM25. Notes are split by heading so results point at the relevant section, not just the file.

  • FastMCP. Tool schemas are derived from type hints and docstrings, which keeps schema and implementation in sync.

How to run

python -m venv .venv && source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
python -m pytest                                         # 39 tests, no network needed
python examples/client_demo.py                           # starts the server and calls the tools through an MCP client
docs-search-mcp --docs-dir /path/to/your/notes           # run the server (stdio)

Configuration: see .env.example. To use it from Claude Desktop, add examples/claude_desktop_config.json to the MCP config (use the absolute path to docs-search-mcp inside your venv if it is not on PATH) and restart the app.

Example

examples/client_demo_output.txt is real output from client_demo.py: searching "stdio logs stdout" returns the Transports section of mcp-notes.md, and converting 10:00 Warsaw to Dubai on 2026-07-15 returns 12:00 (UTC+2 to UTC+4).

Testing

39 tests cover note search and path safety, URL validation and SSRF cases, robots.txt handling, DST edge cases, and a protocol-level test that starts the server and calls every tool through the official MCP Python client.

Limitations

  • Keyword search only (no embeddings); the index is built at startup, so new notes need a restart

  • The SSRF guard resolves DNS before connecting, so a DNS-rebinding attacker could still race it; do not expose the server to untrusted networks

  • HTML cleanup is simple and JavaScript-rendered pages return little; there is no authentication on the HTTP transport

  • Verified with the official Python MCP client and a local HTTP test server, not inside Claude Desktop

License

MIT, see LICENSE. Third-party attributions: SOURCES.md.

Available Tools

5 tools
convert_timeA
Read-onlyIdempotent

Convert a 24-hour HH:MM time between IANA timezones (e.g. Europe/Warsaw -> Asia/Dubai). Optional date (YYYY-MM-DD) makes daylight-saving correct for that day; default is today.

Inputs: time (required), source_timezone (required), target_timezone (required), date.

fetch_urlA
Read-onlyIdempotent

Fetch a public web page and return it as markdown (or raw text). Honors robots.txt and refuses private/internal addresses. Long pages: pass next_start_index as start_index.

Inputs: url (required), raw, max_length, start_index.

list_notesA
Read-onlyIdempotent

List the note files (.md/.txt) available to search, with sizes in bytes.

Inputs: no named parameters. Returns: result

read_noteA
Read-onlyIdempotent

Read one note by its relative path as returned by list_notes / search_notes.

Inputs: path (required), max_chars.

search_notesA
Read-onlyIdempotent

Keyword-search (BM25) the notes. Returns the best matching sections with a snippet. Use read_note afterwards to see a whole file.

Inputs: query (required), limit. Returns: result

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 5 tools

    Disambiguation3/5

    The four notes/web tools are clearly distinct (list, search, read, fetch), but convert_time is a complete non-sequitur for a docs-search server and muddies the server's overall purpose. An agent may wonder why a timezone converter lives alongside note search.

    Naming Consistency5/5

    All five tools follow a consistent snake_case verb_noun pattern: fetch_url, convert_time, list_notes, search_notes, read_note. Naming is highly predictable and readable.

    Tool Count4/5

    Five tools is a well-scoped, lightweight set for a search server, and the notes trio (list/search/read) earns its place. Slightly penalized because convert_time does not belong to the domain.

    Completeness4/5

    For a read-only search server, list/search/read plus fetch_url covers the core retrieval lifecycle with no obvious dead ends. The only oddity is the unrelated convert_time tool rather than a true missing operation.

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      D
      maintenance
      An MCP server that gives AI agents read and write access to a folder of Markdown notes, supporting full-text search, reading, listing, and writing notes. It is deliberately small, with no dependencies beyond the MCP SDK, and includes proper path traversal protection.
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Read-only MCP bridge that exposes secure search and fetch tools over an Obsidian-compatible Markdown vault, enabling ChatGPT to query notes without write access.
      1
      Apache 2.0
    • F
      license
      Not graded
      quality
      C
      maintenance
      Exposes a folder of Markdown runbooks to an AI assistant via three tools (list_notes, search_notes, read_note) over MCP's streamable-http transport.
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Exposes a Markdown-based second brain as MCP tools, enabling MCP clients like Claude Code, Codex, Claude Desktop, n8n, and bots to read context, search notes, retrieve wiki pages, and optionally register decisions or sources.
      MIT