docs-search-mcp
Use this MCP server to give an AI client read-only access to local notes, web pages, and timezone conversion with built-in safety guards.
list_notes: list available.md/.txtnote files with sizes.search_notes(query, limit): BM25 keyword search over note sections, returning paths, headings, scores, and snippets.read_note(path, max_chars): read one note, refusing paths outside the notes folder, symlinks, or unsupported file types.fetch_url(url, max_length, start_index, raw): fetch public web pages as markdown/raw text, paginated, honoring robots.txt and blocking private/internal addresses.convert_time(source_timezone, time, target_timezone, date?): convert HH:MM between IANA timezones, DST-aware when a date is provided.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@docs-search-mcpsearch my notes for BM25 keyword search"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 |
| Lists |
| BM25 keyword search over note sections; returns path, heading, score and snippet |
| Reads one note; refuses paths outside the folder, symlinks and other file types |
| Fetches a page as markdown, paginated; honours robots.txt; refuses private addresses |
| 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 casesStack: 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: truewith 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
datemakes 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 toolsconvert_timeARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| date | No | ||
| time | Yes | ||
| source_timezone | Yes | ||
| target_timezone | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already establish readOnly/idempotent/non-destructive, and the description adds genuine behavioral context: DST correctness depends on the supplied date, and the result is day-sensitive. It does not describe invalid-timezone error behavior or the exact returned format, which keeps it short of a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Two sentences, front-loaded with the core operation and followed by the one non-obvious qualifier (date/DST). No filler or repetition of the tool name.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a 4-parameter utility with no output schema and no schema-level descriptions, it supplies all input formats and behavior. The only omission is the shape of the returned value (e.g. converted HH:MM plus offset), which is implied but not stated.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the load and does: it gives the time format (HH:MM 24-hour), the date format (YYYY-MM-DD) and its default/effect, and shows IANA timezone syntax via the Europe/Warsaw -> Asia/Dubai example, covering source and target zones.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Convert), resource (a 24-hour HH:MM time), and a scoped transformation between named IANA timezones, with a concrete example. Unambiguous and trivially distinguishable from the note/URL siblings.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explains when the optional date matters (DST-correct conversion for that day) and what the default is (today), which is the only real usage decision for this tool. No explicit exclusions, but there is no competing alternative tool to route away from.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
fetch_urlARead-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.
| Name | Required | Description | Default |
|---|---|---|---|
| raw | No | ||
| url | Yes | ||
| max_length | No | ||
| start_index | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, openWorld and non-destructive, so the safety profile is covered. The description adds genuinely useful behavior beyond that: robots.txt compliance, refusal of private/internal addresses, and a pagination convention for long pages. It stops short of mentioning rate limits, timeouts, or error modes.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three compact sentences, front-loaded with the core action and output format, then constraints, then the pagination hint. No filler, every clause earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, and the description correctly compensates by stating the return format (markdown/raw text). Combined with the constraint disclosure and pagination guidance, an agent has everything needed to invoke this tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must carry the load. It explains the raw flag ('or raw text') and the start_index pagination parameter explicitly, which is meaningful added value; only max_length goes unexplained, though its name and default are largely self-describing.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (fetch) and resource (public web page) plus the return format (markdown or raw text). No sibling tool overlaps, so no differentiation is needed, and an agent can immediately tell what this does.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear operating context and constraints (honors robots.txt, refuses private/internal addresses) and hints at the pagination workflow for long pages. It doesn't name explicit alternatives, but no sibling tool is a plausible substitute, so there is little to exclude.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_notesARead-onlyIdempotent
List the note files (.md/.txt) available to search, with sizes in bytes.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds the file-type filter and the byte-size output detail, but says nothing about scope (which directory/workspace is listed) or ordering.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no filler; the file-type constraint and output unit are packed in without waste.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be explained, and a zero-parameter tool has little else to specify. The only gap is the unstated scope of what 'available' means (which directory or store is enumerated).
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so there is no parameter semantics to document and the baseline is 4. Schema coverage is also 100%, leaving nothing for the description to compensate for.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('List the note files') and adds useful scope detail: only .md/.txt files, with sizes in bytes. It does not explicitly contrast with sibling search_notes or read_note, but 'available to search' hints at the relationship between the listed corpus and search_notes.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The phrase 'available to search' implies this is a discovery step preceding search_notes, but the description never states when to use this versus search_notes or read_note, nor any exclusions. Usage must be inferred from the sibling names.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
read_noteARead-onlyIdempotent
Read one note by its relative path as returned by list_notes / search_notes.
| Name | Required | Description | Default |
|---|---|---|---|
| path | Yes | ||
| max_chars | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true and destructiveHint=false, so the safety profile is fully covered without description help. The description adds no behavioral context of its own — nothing about truncation via max_chars, missing-path errors, or what the note content looks like — so it neither helps nor contradicts.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
A single front-loaded sentence with no filler; the core action and its input contract are stated immediately.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
There is no output schema, so the description is the only place to explain the return value (note content, truncation behavior, format), and it does not. For a simple two-parameter read tool this is survivable but leaves an agent guessing about truncation and error cases.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate, and it only partly does: it clarifies that 'path' is relative and produced by sibling tools, but says nothing about 'max_chars' or what happens when the note exceeds it. One of two parameters is meaningfully explained.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (read) and a precisely scoped resource (one note), which immediately separates it from list_notes and search_notes that operate on collections. The addition of the relative-path provenance makes the operation unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description tells the agent where the required path comes from (list_notes / search_notes), which is the key usage dependency. It stops short of naming when not to use it (e.g., fetch_url for external content) or stating prerequisites, so it is clear context without exclusions.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_notesARead-onlyIdempotent
Keyword-search (BM25) the notes. Returns the best matching sections with a snippet. Use read_note afterwards to see a whole file.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | ||
| query | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, so the safety profile is covered. The description adds meaningful behavioral context beyond annotations: BM25 keyword matching, snippet-level results rather than whole notes, and the need for read_note to get full files.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three tight sentences with zero waste: action, return shape, and follow-up. The main verb is front-loaded and every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Output schema and annotations cover return values and safety, so the description needn't explain those. However, with 0% schema description coverage, the missing explanation of query syntax and limit semantics is a real gap for an agent trying to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0%, so the description must compensate for both parameters. It only hints that 'query' is a keyword search and says nothing about the 'limit' parameter (e.g., max results, default behavior), leaving the primary parameter semantics substantially undocumented.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource with the search mechanism: 'Keyword-search (BM25) the notes.' It also clarifies the return granularity ('best matching sections with a snippet'), which distinguishes it from list_notes (browse all) and read_note (whole file). An agent can tell what this tool does without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly names the alternative read_note and the condition for using it ('afterwards to see a whole file'), giving clear follow-up guidance. However, it does not state when to prefer search_notes over list_notes or when not to use it, leaving some sibling selection to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
5 tool updates
v0.1.0- First observed
convert_time - First observed
fetch_url - First observed
list_notes - First observed
read_note - First observed
search_notes
TDQS
Scored across 5 tools
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.
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.
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.
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.
Maintenance
Related MCP Connectors
Serve a folder of Markdown notes as an MCP server: hybrid search, reading, and sourced answers.
Read-only MCP server: let AI agents read your ORANO saved-video library, tasks, and memory.
Read-only MCP tools for AI agent discovery, structured resources, and NIULAI information.
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceAn 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
- AlicenseNot gradedqualityBmaintenanceRead-only MCP bridge that exposes secure search and fetch tools over an Obsidian-compatible Markdown vault, enabling ChatGPT to query notes without write access.1Apache 2.0
- FlicenseNot gradedqualityCmaintenanceExposes a folder of Markdown runbooks to an AI assistant via three tools (list_notes, search_notes, read_note) over MCP's streamable-http transport.-
- AlicenseNot gradedqualityCmaintenanceExposes 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