Skip to main content
Glama

mcp-server-example — servidor MCP para uma base de notas em Markdown

Um servidor MCP (Model Context Protocol) de exemplo, funcional e testado, que dá a um assistente acesso a um second brain: um diretório local de notas em Markdown que ele pode criar, ler, atualizar, listar, buscar e medir.

O foco aqui não é a quantidade de recursos, e sim mostrar um servidor MCP honesto: schemas gerados a partir dos type hints, sanitização de verdade contra path traversal, e uma suíte de testes que chama as ferramentas de verdade em vez de simular a chamada.


O que é MCP

O Model Context Protocol é um protocolo aberto que padroniza como um assistente conversa com sistemas externos. Em vez de cada aplicação inventar seu próprio formato de plugin, o servidor MCP declara três coisas — tools (ações que o modelo pode executar), resources (dados que ele pode ler, endereçados por URI) e prompts (modelos de conversa que o usuário pode invocar) — e qualquer cliente compatível descobre e usa tudo isso sozinho. A comunicação é JSON-RPC, normalmente sobre stdio: o cliente sobe o servidor como um subprocesso e troca mensagens pela entrada e saída padrão.


Related MCP server: mor

O que tem aqui

Arquivo

O que faz

mcp_notas/server.py

Define o servidor FastMCP: tools, resources, prompts e os modelos Pydantic de saída.

mcp_notas/storage.py

Todo o I/O em disco e a sanitização de identificadores. Único ponto que monta caminhos.

mcp_notas/search.py

Busca textual com ranking por campo (título > tags > corpo), insensível a acento.

mcp_notas/__main__.py

Ponto de entrada de python3 -m mcp_notas.

tests/test_server.py

45 testes que exercitam o servidor de verdade, incluindo uma sessão MCP completa.

requirements.txt

Dependências de runtime e de teste.

pytest.ini

Configuração do pytest-asyncio.

Cada nota é um arquivo .md com um front matter mínimo:

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

O que o servidor expõe

Tools

Tool

Argumentos

Devolve

criar_nota

titulo (obrigatório), corpo, tags, slug

A nota criada, com datas preenchidas.

ler_nota

slug

A nota completa (corpo, tags, datas).

atualizar_nota

slug, corpo, titulo, tags, anexar

A nota já atualizada.

apagar_nota

slug

Confirmação em texto.

listar_notas

tag (opcional)

Total e resumo de cada nota, sem o corpo.

buscar_notas

consulta, limite

Resultados ordenados por relevância, com trecho.

estatisticas_base

—

Contagens, tags mais usadas, nota mais longa.

Resources

URI

Tipo

Conteúdo

notas://index

application/json

Índice de toda a base: slug, título, tags e URI de cada nota.

notas://{slug}

text/markdown

Markdown integral de uma nota, com front matter.

Prompts

Prompt

Argumentos

O que monta

resumir_nota

slug, tamanho (curto/longo)

Um pedido de resumo com o conteúdo da nota já embutido.

sugerir_conexoes

slug, quantidade

Quatro mensagens: instrução, nota de partida, catálogo das demais notas e a abertura do assistente.


Instalação

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

Requer Python 3.11+ e mcp >= 1.27.0.


Como rodar

O transporte padrão é stdio — é assim que um cliente MCP sobe o servidor:

cd mcp-server-example
python3 -m mcp_notas

O processo fica em silêncio esperando mensagens JSON-RPC na entrada padrão; isso é o comportamento correto, não um travamento.

O diretório da base é configurável pela variável de ambiente MCP_NOTAS_DIR (padrão: ./notas, criado automaticamente):

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

Configuração no cliente

Bloco pronto para colar na configuração de um cliente MCP:

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

⚠️ Este bloco não foi testado contra um cliente MCP real neste ambiente. O que foi verificado aqui é o equivalente programático: o servidor foi subido como subprocesso com python3 -m mcp_notas e um ClientSession do próprio SDK completou o handshake por stdio, listou as tools e executou chamadas (ver "Status de verificação"). A tradução desse handshake para o formato de configuração de um cliente específico não foi exercitada.


Exemplo de uso

Saídas reais, capturadas rodando o servidor in-process (criar_servidor() + call_tool). O campo diretorio foi trocado por um caminho genérico; o resto é 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."
    }
  ]
}

Repare no ranking: a palavra "protocolo" está no título e nas tags da primeira nota (pontuação 8.0) e apenas no corpo da segunda (pontuação 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.

E o handshake real por stdio, com o servidor rodando como subprocesso e um ClientSession do SDK do outro lado (saída literal, sem os logs INFO do servidor):

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

Segurança

O bug clássico de servidor MCP que mexe em arquivos é aceitar um identificador vindo do modelo e concatená-lo direto no caminho: Path(base) / slug. Com slug = "../../etc/passwd", isso entrega o disco inteiro para quem controlar o prompt.

Aqui a defesa está em mcp_notas/storage.py e tem duas camadas.

1. sanitizar_slug() — validação por lista de permissão. Um identificador só passa se casar com ^[a-z0-9][a-z0-9._-]{0,79}$, depois de rejeitar explicitamente separadores de caminho (/, \), byte nulo, letras de unidade do Windows (C:) e qualquer ocorrência de ... Exigir que comece por letra ou dígito também derruba nomes ocultos como .ssh.

2. BaseDeNotas.caminho() — verificação do caminho resolvido. Depois de sanitizar, o caminho é resolvido com Path.resolve() e o código confere que o pai dele é exatamente o diretório da base. Essa checagem é redundante por construção — e é esse o ponto: se algum dia a primeira camada tiver um furo, o vazamento ainda não acontece.

O ataque canônico, executado de verdade contra a 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.

O resource notas://{slug} tem a mesma proteção, e por dois caminhos diferentes: a URI crua notas://../../etc/passwd nem casa com o template (Unknown resource), enquanto a forma percent-encoded notas://..%2F..%2Fetc%2Fpasswd casa, chega à sanitização e é barrada lá — é esse segundo caso, o perigoso, que o teste cobre.

Um teste também prova no sistema de arquivos que o alvo do ataque não chega a ser criado: depois de uma tentativa de criar_nota com slug="../vazamento", o diretório da base continua vazio e o arquivo fora dele não existe.

Além disso: nenhuma chave de API, nenhum acesso de rede, e o servidor nunca lê ou escreve fora do diretório configurado.


Testes

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

Só os testes de path traversal:

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

A suíte cobre, em ordem:

  1. Sanitização — 13 entradas maliciosas parametrizadas (../../etc/passwd, /etc/passwd, ..\\..\\windows\\system32\\config\\sam, C:\Windows\win.ini, nota\x00.md, string vazia…), mais a prova em disco de que nada é criado fora da base.

  2. Superfície MCP — list_tools devolve exatamente as sete tools, e os schemas (required, type, default, outputSchema) são os gerados a partir dos type hints e docstrings.

  3. Chamada real de cada tool — criação com persistência verificada em disco, duplicata, leitura, leitura de inexistente, atualização, atualização com anexar, listagem com e sem filtro de tag, busca com ranking e com limite, estatísticas e remoção.

  4. Resources — list_resources, list_resource_templates, leitura do índice JSON, leitura de uma nota individual e as duas formas de traversal.

  5. Prompts — list_prompts, get_prompt dos dois prompts, conferindo que o conteúdo da nota é realmente embutido e que a nota de partida não aparece no catálogo das outras.

  6. Sessão ponta a ponta — create_connected_server_and_client_session sobe um cliente e um servidor MCP conectados em memória; o teste lista tools, cria nota, lista, lê resource, pega prompt e confirma isError: True na tentativa de traversal.

  7. Armazenamento isolado — round-trip do front matter e arquivos que não são notas sendo ignorados na listagem.


Status de verificação

Tudo abaixo foi executado neste ambiente, com mcp 1.27.0, pytest 9.1.1 e pytest-asyncio 1.4.0 sob Python 3.11.

✅ Verificado

  • python3 -m pytest tests/ -q → 45 passed.

  • As sete tools chamadas de verdade via FastMCP.call_tool, com os resultados conferidos.

  • Os dois resources lidos via FastMCP.read_resource; os dois prompts via FastMCP.get_prompt.

  • Sessão MCP completa cliente↔servidor em memória com mcp.shared.memory.create_connected_server_and_client_session.

  • Handshake stdio real: servidor subido como subprocesso (python3 -m mcp_notas) e um ClientSession do SDK executando initialize, list_tools e call_tool por ele.

  • Path traversal rejeitado em sanitizar_slug, na tool, no resource e no sistema de arquivos.

  • MCP_NOTAS_DIR respeitado: a nota criada apareceu no diretório apontado pela variável.

  • Todas as saídas mostradas neste README foram copiadas de execuções reais.

⚠️ Não testado

  • O bloco mcpServers não foi testado contra um cliente MCP real (Claude Desktop, editores, etc.). Não há nenhum cliente instalado neste ambiente; o que substitui essa verificação é o handshake stdio programático descrito acima.

  • Os transportes sse e streamable-http existem em FastMCP.run, mas este projeto só exercita stdio.

  • Sem testes de concorrência: escritas simultâneas na mesma nota não são coordenadas por lock.

  • Sem testes em Windows ou macOS — só Linux.


Licença

MIT

Related MCP Connectors

Related MCP Servers

  • 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.
    18 npm
    2
    MIT
  • A
    license
    A
    quality
    D
    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
  • A
    license
    A
    quality
    B
    maintenance
    Provides an AI agent with a searchable markdown notes vault, offering tools to create, read, list, search, update, and delete notes stored as plain .md files.
    6
    MIT