W tym praktycznym tutorialu zbudujesz kompletny serwer MCP w Pythonie do zarzadzania notatkami. Uzyjemy FastMCP SDK — wysokopoziomowego API, ktore dramatycznie upraszcza tworzenie serwerow MCP.
- Struktura projektu serwera MCP
- Definiowanie Tools, Resources i Prompts
- Walidacja danych z Pydantic
- Obsluga bledow i logging
- Testowanie z MCP Inspector
- Konfiguracja Claude Desktop
- Deployment i best practices
Czym jest FastMCP?
FastMCP to wysokopoziomowe SDK do budowy serwerow MCP w Pythonie. Inspirowane FastAPI, oferuje dekoratory i automatyczna generacje schematow.
Instalacja i setup
Wymagania
- Python 3.10+
- pip lub uv (zalecane)
- Claude Desktop (do testowania)
Instalacja MCP SDK
# Standardowa instalacja
pip install mcp
# Lub z uv (szybsze)
uv pip install mcp
# Z dodatkowymi zalelznosciami
pip install "mcp[cli]" # dodaje CLI tools
Struktura projektu
notes-mcp/
├── pyproject.toml # Konfiguracja projektu
├── README.md # Dokumentacja
├── .env # Zmienne srodowiskowe (opcjonalne)
└── src/
└── notes_server/
├── __init__.py
├── server.py # Glowny plik serwera
├── models.py # Modele Pydantic
└── storage.py # Warstwa persystencji
Krok 1: Modele danych
Zacznijmy od zdefiniowania modeli Pydantic dla walidacji danych:
# src/notes_server/models.py
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Optional
from enum import Enum
class Priority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
class Note(BaseModel):
"""Model notatki."""
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1)
tags: list[str] = Field(default_factory=list)
priority: Priority = Priority.MEDIUM
created_at: datetime = Field(default_factory=datetime.now)
updated_at: Optional[datetime] = None
class NoteCreate(BaseModel):
"""Model do tworzenia notatki."""
title: str = Field(..., min_length=1, max_length=200)
content: str = Field(..., min_length=1)
tags: list[str] = Field(default_factory=list)
priority: Priority = Priority.MEDIUM
class NoteUpdate(BaseModel):
"""Model do aktualizacji notatki."""
content: Optional[str] = None
tags: Optional[list[str]] = None
priority: Optional[Priority] = None
Krok 2: Warstwa persystencji
Prosta warstwa storage z SQLite:
# src/notes_server/storage.py
import sqlite3
import json
from pathlib import Path
from datetime import datetime
from typing import Optional
from .models import Note, NoteCreate, NoteUpdate
class NotesStorage:
"""SQLite storage dla notatek."""
def __init__(self, db_path: str = "notes.db"):
self.db_path = Path(db_path)
self._init_db()
def _init_db(self):
"""Inicjalizacja bazy danych."""
with sqlite3.connect(self.db_path) as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS notes (
title TEXT PRIMARY KEY,
content TEXT NOT NULL,
tags TEXT DEFAULT '[]',
priority TEXT DEFAULT 'medium',
created_at TEXT NOT NULL,
updated_at TEXT
)
""")
def create(self, note: NoteCreate) -> Note:
"""Utworz nowa notatke."""
now = datetime.now()
with sqlite3.connect(self.db_path) as conn:
conn.execute(
"INSERT INTO notes VALUES (?, ?, ?, ?, ?, ?)",
(note.title, note.content, json.dumps(note.tags),
note.priority.value, now.isoformat(), None)
)
return Note(
title=note.title, content=note.content,
tags=note.tags, priority=note.priority, created_at=now
)
def get(self, title: str) -> Optional[Note]:
"""Pobierz notatke po tytule."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
row = conn.execute(
"SELECT * FROM notes WHERE title = ?", (title,)
).fetchone()
if not row:
return None
return self._row_to_note(row)
def list_all(self) -> list[Note]:
"""Lista wszystkich notatek."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute("SELECT * FROM notes").fetchall()
return [self._row_to_note(r) for r in rows]
def search(self, query: str) -> list[Note]:
"""Szukaj notatek po tresci."""
with sqlite3.connect(self.db_path) as conn:
conn.row_factory = sqlite3.Row
rows = conn.execute(
"SELECT * FROM notes WHERE content LIKE ? OR title LIKE ?",
(f"%{query}%", f"%{query}%")
).fetchall()
return [self._row_to_note(r) for r in rows]
def delete(self, title: str) -> bool:
"""Usun notatke."""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.execute(
"DELETE FROM notes WHERE title = ?", (title,)
)
return cursor.rowcount > 0
def _row_to_note(self, row) -> Note:
return Note(
title=row["title"],
content=row["content"],
tags=json.loads(row["tags"]),
priority=row["priority"],
created_at=datetime.fromisoformat(row["created_at"]),
updated_at=datetime.fromisoformat(row["updated_at"]) if row["updated_at"] else None
)
Krok 3: Kompletny serwer MCP
Teraz glowny plik serwera laczacy wszystko w calossc:
# src/notes_server/server.py
import logging
from mcp.server.fastmcp import FastMCP
from .storage import NotesStorage
from .models import NoteCreate, Priority
# Konfiguracja logowania
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Inicjalizacja FastMCP i storage
mcp = FastMCP("notes", dependencies=["sqlite3"])
storage = NotesStorage()
# ============================================
# TOOLS - Akcje ktore agent moze wykonac
# ============================================
@mcp.tool()
def create_note(
title: str,
content: str,
tags: list[str] = [],
priority: str = "medium"
) -> str:
"""Create a new note with title, content, optional tags and priority.
Args:
title: Unique title for the note (max 200 chars)
content: Main content of the note
tags: Optional list of tags for categorization
priority: Priority level - 'low', 'medium', or 'high'
Returns:
Confirmation message with note details
"""
try:
note_data = NoteCreate(
title=title,
content=content,
tags=tags,
priority=Priority(priority)
)
note = storage.create(note_data)
logger.info(f"Created note: {title}")
return f"Created note '{note.title}' with priority {note.priority.value}"
except Exception as e:
logger.error(f"Error creating note: {e}")
return f"Error: {str(e)}"
@mcp.tool()
def read_note(title: str) -> str:
"""Read a note by its title.
Args:
title: Title of the note to read
Returns:
Note content or error message if not found
"""
note = storage.get(title)
if not note:
return f"Note '{title}' not found"
tags_str = ", ".join(note.tags) if note.tags else "none"
return f"""Title: {note.title}
Priority: {note.priority.value}
Tags: {tags_str}
Created: {note.created_at.strftime('%Y-%m-%d %H:%M')}
{note.content}"""
@mcp.tool()
def list_notes(priority_filter: str = "") -> str:
"""List all notes, optionally filtered by priority.
Args:
priority_filter: Optional filter - 'low', 'medium', 'high', or empty for all
Returns:
Formatted list of note titles with their priorities
"""
notes = storage.list_all()
if priority_filter:
notes = [n for n in notes if n.priority.value == priority_filter]
if not notes:
return "No notes found"
result = [f"Found {len(notes)} note(s):\n"]
for note in notes:
tags = f" [{', '.join(note.tags)}]" if note.tags else ""
result.append(f"- {note.title} ({note.priority.value}){tags}")
return "\n".join(result)
@mcp.tool()
def search_notes(query: str) -> str:
"""Search notes by content or title.
Args:
query: Search term to look for in titles and content
Returns:
List of matching notes with snippets
"""
notes = storage.search(query)
if not notes:
return f"No notes found matching '{query}'"
result = [f"Found {len(notes)} note(s) matching '{query}':\n"]
for note in notes:
snippet = note.content[:100] + "..." if len(note.content) > 100 else note.content
result.append(f"- {note.title}: {snippet}")
return "\n".join(result)
@mcp.tool()
def delete_note(title: str) -> str:
"""Delete a note by title.
Args:
title: Title of the note to delete
Returns:
Confirmation or error message
"""
if storage.delete(title):
logger.info(f"Deleted note: {title}")
return f"Deleted note '{title}'"
return f"Note '{title}' not found"
@mcp.tool()
def get_statistics() -> str:
"""Get statistics about stored notes.
Returns:
Summary of notes by priority and tags
"""
notes = storage.list_all()
if not notes:
return "No notes in storage"
# Zlicz po priorytetach
priorities = {"low": 0, "medium": 0, "high": 0}
all_tags = {}
for note in notes:
priorities[note.priority.value] += 1
for tag in note.tags:
all_tags[tag] = all_tags.get(tag, 0) + 1
result = [f"Total notes: {len(notes)}\n"]
result.append("By priority:")
for p, count in priorities.items():
result.append(f" - {p}: {count}")
if all_tags:
result.append("\nTop tags:")
sorted_tags = sorted(all_tags.items(), key=lambda x: x[1], reverse=True)[:5]
for tag, count in sorted_tags:
result.append(f" - {tag}: {count}")
return "\n".join(result)
# ============================================
# RESOURCES - Dane ktore agent moze czytac
# ============================================
@mcp.resource("notes://list")
def get_notes_list() -> str:
"""Get list of all note titles as a resource."""
notes = storage.list_all()
return "\n".join([n.title for n in notes]) or "No notes"
@mcp.resource("notes://note/{title}")
def get_note_resource(title: str) -> str:
"""Get a specific note as a resource."""
note = storage.get(title)
if not note:
return "Not found"
return note.content
@mcp.resource("notes://tags")
def get_all_tags() -> str:
"""Get list of all unique tags."""
notes = storage.list_all()
tags = set()
for note in notes:
tags.update(note.tags)
return "\n".join(sorted(tags)) or "No tags"
# ============================================
# PROMPTS - Szablony promptow
# ============================================
@mcp.prompt()
def summarize_note(title: str) -> str:
"""Generate a prompt to summarize a specific note."""
note = storage.get(title)
if not note:
return f"Note '{title}' not found"
return f"""Please provide a concise summary of the following note:
Title: {note.title}
Tags: {', '.join(note.tags) or 'none'}
Content:
{note.content}
Provide a 2-3 sentence summary capturing the key points."""
@mcp.prompt()
def analyze_notes() -> str:
"""Generate a prompt to analyze all notes."""
notes = storage.list_all()
if not notes:
return "No notes to analyze"
notes_text = "\n\n".join([
f"## {n.title}\nPriority: {n.priority.value}\n{n.content}"
for n in notes
])
return f"""Analyze the following collection of {len(notes)} notes.
Identify:
1. Common themes
2. Action items
3. Notes that should be prioritized
Notes:
{notes_text}"""
@mcp.prompt()
def suggest_tags(title: str) -> str:
"""Generate a prompt to suggest tags for a note."""
note = storage.get(title)
if not note:
return f"Note '{title}' not found"
return f"""Suggest 3-5 relevant tags for this note:
Title: {note.title}
Content: {note.content}
Current tags: {', '.join(note.tags) or 'none'}
Provide tags as a comma-separated list."""
# Entry point
if __name__ == "__main__":
mcp.run()
Krok 4: Konfiguracja projektu
# pyproject.toml
[project]
name = "notes-mcp"
version = "0.1.0"
description = "MCP server for managing notes"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.0.0",
"pydantic>=2.0.0",
]
[project.scripts]
notes-server = "notes_server.server:mcp.run"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/notes_server"]
Krok 5: Konfiguracja Claude Desktop
Dodaj serwer do konfiguracji Claude Desktop:
macOS
# Otworz plik konfiguracyjny
code ~/Library/Application\ Support/Claude/claude_desktop_config.json
Windows
# Otworz plik konfiguracyjny
code %APPDATA%\Claude\claude_desktop_config.json
Zawartosc konfiguracji
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["-m", "notes_server.server"],
"cwd": "/absolute/path/to/notes-mcp/src",
"env": {
"PYTHONPATH": "/absolute/path/to/notes-mcp/src"
}
}
}
}
Po zmianie konfiguracji musisz zrestartowac Claude Desktop (zamknij i otworz ponownie).
Krok 6: Testowanie
MCP Inspector
Najlepsze narzedzie do debugowania serwerow MCP:
# Uruchom inspector
npx @modelcontextprotocol/inspector python -m notes_server.server
# Otworz przegladarke na http://localhost:5173
Testy jednostkowe
# tests/test_server.py
import pytest
from notes_server.storage import NotesStorage
from notes_server.models import NoteCreate, Priority
@pytest.fixture
def storage(tmp_path):
"""Storage z tymczasowa baza."""
return NotesStorage(str(tmp_path / "test.db"))
def test_create_note(storage):
note = storage.create(NoteCreate(
title="Test",
content="Content",
tags=["test"],
priority=Priority.HIGH
))
assert note.title == "Test"
assert note.priority == Priority.HIGH
def test_search_notes(storage):
storage.create(NoteCreate(title="Python Tips", content="Learn Python"))
storage.create(NoteCreate(title="JavaScript", content="Learn JS"))
results = storage.search("Python")
assert len(results) == 1
assert results[0].title == "Python Tips"
Best practices
- Docstringi — Kazdy tool musi miec dobry opis (LLM go czyta!)
- Walidacja — Uzywaj Pydantic dla wszystkich danych wejsciowych
- Error handling — Zwracaj czytelne komunikaty bledow
- Logging — Loguj wazne operacje dla debugowania
- Idempotentnosc — Toole powinny byc bezpieczne do wielokrotnego wywolania
- Limity — Ogranicz rozmiar danych zwracanych przez toole
- Tooli bez opisow (LLM nie bedzie wiedzial jak ich uzyc)
- Zwracania ogromnych danych (limituj do ~10KB)
- Operacji bez walidacji inputu
- Hardkodowanych sciezek i credentials
Co dalej?
🔒 Autentykacja
Dodaj tokeny API lub OAuth dla bezpiecznego dostepu
📦 Eksport
Dodaj eksport do PDF, Markdown, HTML
🌐 Remote server
Wdraz jako HTTP server z SSE transport
🔍 Full-text search
Zaimplementuj FTS5 w SQLite dla lepszego wyszukiwania
📚 Bibliografia
- Anthropic. (2025). MCP Python SDK Documentation. modelcontextprotocol.io/quickstart/server
- Anthropic. (2025). MCP Python SDK GitHub. github.com/modelcontextprotocol/python-sdk
- Anthropic. (2024). MCP Specification. spec.modelcontextprotocol.io
- Pydantic. (2025). Pydantic Documentation. docs.pydantic.dev
- MCP Community. (2025). MCP Servers Repository. github.com/modelcontextprotocol/servers