Skip to main content
Glama

mcp-server-example — MCP server for a Markdown notes base

A functional, tested example MCP (Model Context Protocol) server that gives an assistant access to a second brain: a local directory of Markdown notes it can create, read, update, list, search, and measure.

The focus here is not the number of features, but rather showing an honest MCP server: schemas generated from type hints, real sanitization against path traversal, and a test suite that actually calls the tools instead of mocking the call.


What MCP is

The Model Context Protocol is an open protocol that standardizes how an assistant talks to external systems. Instead of each application inventing its own plugin format, the MCP server declares three things — tools (actions the model can execute), resources (data it can read, addressed by URI), and prompts (conversation templates the user can invoke) — and any compatible client discovers and uses all of this on its own. Communication is JSON-RPC, usually over stdio: the client starts the server as a subprocess and exchanges messages through standard input and output.


Related MCP server: Notes MCP Server

What's here

File

What it does

mcp_notas/server.py

Defines the FastMCP server: tools, resources, prompts, and the Pydantic output models.

mcp_notas/storage.py

All disk I/O and identifier sanitization. The only place that builds paths.

mcp_notas/search.py

Text search with per-field ranking (title > tags > body), accent-insensitive.

mcp_notas/__main__.py

Entry point for python3 -m mcp_notas.

tests/test_server.py

45 tests that exercise the real server, including a full MCP session.

requirements.txt

Runtime and test dependencies.

pytest.ini

pytest-asyncio configuration.

Each note is a .md file with minimal front matter:

---
title: Teste env
tags: []
created: 2026-08-25T00:20:24+00:00
updated: 2026-08-25T00:20:24+00:00
---

What the server exposes

Tools

Tool

Arguments

Returns

criar_nota

titulo (required), corpo, tags, slug

The created note, with dates filled in.

ler_nota

slug

The full note (body, tags, dates).

atualizar_nota

slug, corpo, titulo, tags, anexar

The already-updated note.

apagar_nota

slug

Text confirmation.

listar_notas

tag (optional)

Total and summary of each note, without the body.

buscar_notas

consulta, limite

Results ordered by relevance, with excerpt.

estatisticas_base

Counts, most-used tags, longest note.

Resources

URI

Type

Content

notas://index

application/json

Index of the whole base: slug, title, tags, and URI of each note.

notas://{slug}

text/markdown

Full Markdown of a note, with front matter.

Prompts

Prompt

Arguments

What it builds

resumir_nota

slug, tamanho (curto/longo)

A summary request with the note's content already embedded.

sugerir_conexoes

slug, quantidade

Four messages: instruction, starting note, catalog of the other notes, and the assistant's opening.


Installation

git clone <url-do-repositorio> mcp-server-example
cd mcp-server-example
pip install -r requirements.txt

Requires Python 3.11+ and mcp >= 1.27.0.


How to run

The default transport is stdio — that's how an MCP client starts the server:

cd mcp-server-example
python3 -m mcp_notas

The process stays silent waiting for JSON-RPC messages on standard input; that's the correct behavior, not a hang.

The base directory is configurable via the MCP_NOTAS_DIR environment variable (default: ./notas, created automatically):

MCP_NOTAS_DIR=~/meu-second-brain python3 -m mcp_notas

Client configuration

Ready-to-paste block for an MCP client configuration:

{
  "mcpServers": {
    "notas": {
      "command": "python3",
      "args": ["-m", "mcp_notas"],
      "cwd": "/caminho/absoluto/para/mcp-server-example",
      "env": {
        "MCP_NOTAS_DIR": "/caminho/absoluto/para/suas-notas"
      }
    }
  }
}

⚠️ This block has not been tested against a real MCP client in this environment. What was verified here is the programmatic equivalent: the server was started as a subprocess with python3 -m mcp_notas and a ClientSession from the SDK itself completed the stdio handshake, listed the tools, and executed calls (see "Verification status"). The translation of that handshake into a specific client's configuration format has not been exercised.


Usage example

Real outputs, captured by running the server in-process (criar_servidor() + call_tool). The diretorio field was replaced with a generic path; the rest is literal.

>>> criar_nota
{
  "slug": "protocolo-mcp",
  "titulo": "Protocolo MCP",
  "tags": [
    "mcp",
    "protocolo"
  ],
  "corpo": "O Model Context Protocol padroniza como um assistente acessa ferramentas e dados externos.",
  "criada_em": "2026-08-25T00:20:03+00:00",
  "atualizada_em": "2026-08-25T00:20:03+00:00"
}

>>> listar_notas(tag='mcp')
{
  "total": 1,
  "filtro_tag": "mcp",
  "notas": [
    {
      "slug": "protocolo-mcp",
      "titulo": "Protocolo MCP",
      "tags": [
        "mcp",
        "protocolo"
      ],
      "atualizada_em": "2026-08-25T00:20:03+00:00",
      "resumo": "O Model Context Protocol padroniza como um assistente acessa ferramentas e dados externos.",
      "tamanho": 90
    }
  ]
}

>>> buscar_notas(consulta='protocolo')
{
  "consulta": "protocolo",
  "total": 2,
  "resultados": [
    {
      "slug": "protocolo-mcp",
      "titulo": "Protocolo MCP",
      "tags": [
        "mcp",
        "protocolo"
      ],
      "pontuacao": 8.0,
      "trecho": "O Model Context Protocol padroniza como um assistente acessa ferramentas e dados externos."
    },
    {
      "slug": "memoria-de-longo-prazo",
      "titulo": "Memória de longo prazo",
      "tags": [
        "produtividade"
      ],
      "pontuacao": 1.0,
      "trecho": "Anotações sobre second brain. Cita o protocolo de revisão semanal."
    }
  ]
}

Notice the ranking: the word "protocolo" is in the title and tags of the first note (score 8.0) and only in the body of the second (score 1.0).

>>> estatisticas_base()
{
  "total_de_notas": 2,
  "total_de_caracteres": 156,
  "total_de_palavras": 23,
  "media_de_caracteres": 78.0,
  "total_de_tags": 3,
  "tags_mais_usadas": {
    "mcp": 1,
    "produtividade": 1,
    "protocolo": 1
  },
  "nota_mais_longa": "protocolo-mcp",
  "ultima_atualizacao": "2026-08-25T00:20:03+00:00",
  "diretorio": "/caminho/para/notas"
}

>>> read_resource('notas://index')
{
  "diretorio": "/caminho/para/notas",
  "total": 2,
  "notas": [
    {
      "slug": "memoria-de-longo-prazo",
      "titulo": "Memória de longo prazo",
      "tags": [
        "produtividade"
      ],
      "uri": "notas://memoria-de-longo-prazo"
    },
    {
      "slug": "protocolo-mcp",
      "titulo": "Protocolo MCP",
      "tags": [
        "mcp",
        "protocolo"
      ],
      "uri": "notas://protocolo-mcp"
    }
  ]
}

>>> get_prompt('resumir_nota', {'slug': 'protocolo-mcp'})
Resuma em no máximo 3 bullets.
Não invente informação que não esteja na nota.

# Protocolo MCP
Tags: mcp, protocolo

O Model Context Protocol padroniza como um assistente acessa ferramentas e dados externos.

And the real stdio handshake, with the server running as a subprocess and a ClientSession from the SDK on the other side (literal output, without the server's INFO logs):

serverInfo: mcp-notas 1.27.0
instructions[:60]: Servidor de uma base local de notas em Markdown. Use 'listar
tools: ['apagar_nota', 'atualizar_nota', 'buscar_notas', 'criar_nota', 'estatisticas_base', 'ler_nota', 'listar_notas']
criar_nota isError: False slug: handshake-stdio
estatisticas: 1 nota(s)
traversal isError: True
traversal msg: Error executing tool ler_nota: Identificador inválido '../../etc/passwd': separadores de caminho não são permitidos. Use

Security

The classic bug in an MCP server that touches files is accepting an identifier from the model and concatenating it directly into the path: Path(base) / slug. With slug = "../../etc/passwd", that hands the entire disk to whoever controls the prompt.

Here the defense lives in mcp_notas/storage.py and has two layers.

1. sanitizar_slug() — allowlist validation. An identifier only passes if it matches ^[a-z0-9][a-z0-9._-]{0,79}$, after explicitly rejecting path separators (/, \), null bytes, Windows drive letters (C:), and any occurrence of ... Requiring it to start with a letter or digit also blocks hidden names like .ssh.

2. BaseDeNotas.caminho() — resolved-path check. After sanitizing, the path is resolved with Path.resolve() and the code verifies that its parent is exactly the base directory. This check is redundant by construction — and that's the point: if the first layer ever has a hole, the leak still doesn't happen.

The canonical attack, actually executed against the tool:

>>> call_tool('ler_nota', {'slug': '../../etc/passwd'})
ToolError: Error executing tool ler_nota: Identificador inválido '../../etc/passwd': separadores de caminho não são permitidos. Use apenas o slug da nota, sem diretórios.

The notas://{slug} resource has the same protection, through two different paths: the raw URI notas://../../etc/passwd doesn't even match the template (Unknown resource), while the percent-encoded form notas://..%2F..%2Fetc%2Fpasswd matches, reaches sanitization, and is blocked there — it's that second, dangerous case that the test covers.

A test also proves on the filesystem that the attack target is never created: after a criar_nota attempt with slug="../vazamento", the base directory remains empty and the file outside it doesn't exist.

Additionally: no API keys, no network access, and the server never reads or writes outside the configured directory.


Tests

$ python3 -m pytest tests/ -q
.............................................                            [100%]
45 passed in 1.48s

Just the path traversal tests:

$ python3 -m pytest tests/ -q -k traversal
.................                                                        [100%]
17 passed, 28 deselected in 0.67s

The suite covers, in order:

  1. Sanitization — 13 parameterized malicious inputs (../../etc/passwd, /etc/passwd, ..\\..\\windows\\system32\\config\\sam, C:\Windows\win.ini, nota\x00.md, empty string…), plus the on-disk proof that nothing is created outside the base.

  2. MCP surfacelist_tools returns exactly the seven tools, and the schemas (required, type, default, outputSchema) are the ones generated from the type hints and docstrings.

  3. Real call of each tool — creation with persistence verified on disk, duplicate, read, read of nonexistent, update, update with anexar, listing with and without tag filter, search with ranking and with limit, statistics, and removal.

  4. Resourceslist_resources, list_resource_templates, reading the JSON index, reading an individual note, and the two traversal forms.

  5. Promptslist_prompts, get_prompt for both prompts, checking that the note's content is actually embedded and that the starting note doesn't appear in the catalog of the others.

  6. End-to-end sessioncreate_connected_server_and_client_session starts a connected MCP client and server in memory; the test lists tools, creates a note, lists, reads a resource, gets a prompt, and confirms isError: True on the traversal attempt.

  7. Isolated storage — front matter round-trip and non-note files being ignored in listing.


Verification status

Everything below was run in this environment, with mcp 1.27.0, pytest 9.1.1, and pytest-asyncio 1.4.0 under Python 3.11.

Verified

  • python3 -m pytest tests/ -q45 passed.

  • The seven tools actually called via FastMCP.call_tool, with results checked.

  • Both resources read via FastMCP.read_resource; both prompts via FastMCP.get_prompt.

  • Full in-memory client↔server MCP session with mcp.shared.memory.create_connected_server_and_client_session.

  • Real stdio handshake: server started as a subprocess (python3 -m mcp_notas) and a ClientSession from the SDK running initialize, list_tools, and call_tool through it.

  • Path traversal rejected in sanitizar_slug, in the tool, in the resource, and on the filesystem.

  • MCP_NOTAS_DIR honored: the created note appeared in the directory pointed to by the variable.

  • All outputs shown in this README were copied from real runs.

⚠️ Not tested

  • The mcpServers block has not been tested against a real MCP client (Claude Desktop, editors, etc.). No client is installed in this environment; what substitutes for that verification is the programmatic stdio handshake described above.

  • The sse and streamable-http transports exist in FastMCP.run, but this project only exercises stdio.

  • No concurrency tests: simultaneous writes to the same note are not coordinated by a lock.

  • No tests on Windows or macOS — Linux only.


License

MIT

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Manages markdown notes in a specified directory, allowing users to create, read, update, and list notes through the Model Context Protocol.
    1
  • A
    license
    Not graded
    quality
    A
    maintenance
    Enables AI assistants to search, read, create, update, and remove personal markdown notes stored locally, providing persistent memory across sessions.
    13
    2
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Enables AI assistants to interact with a local folder of Markdown notes, supporting listing, reading, searching, creating, and appending to notes with strict security boundaries.
    5
    MIT

View all related MCP servers

Related MCP Connectors

  • AI access to your aNotepad online notes: read, search, write, and organize via 22 tools.

  • Read and write your Fresh Jots notes from Claude, Cursor, and any MCP client.

  • Create, validate, edit, export (markdown/svg/png/mermaid), and search JSON Canvas files.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/herickbrandao483-jpg/mcp-server-example'

If you have feedback or need assistance with the MCP directory API, please join our Discord server