PluginWorld
Mc

mcp-server-example

MCP

Provides tools, resources, and prompts for managing a local Markdown notes database, including create, read, update, delete, search, and statistics operations, with robust path traversal protection.

@herickbrandao483-jpg · MIT · updated yesterday

SECURITY

B

SCORE

60

STARS

0

PLUG IN

git clone https://github.com/herickbrandao483-jpg/mcp-server-example.git

See the README to configure this MCP server

README

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.


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 MCPlist_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. Resourceslist_resources, list_resource_templates, leitura do índice JSON, leitura de uma nota individual e as duas formas de traversal.
  5. Promptslist_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 pontacreate_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/ -q45 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

SIMILAR PLUGINS