Budowa serwera MCS

⏱ Czas czytania: ~18 minut | 📊 Poziom: Sredniozaawansowany | 📅 Aktualizacja: Grudzien 2025

W tym praktycznym tutorialu zbudujesz kompletny serwer MCP w Pythonie do zarzadzania notatkami. Uzyjemy FastMCP SDK — wysokopoziomowego API, ktore dramatycznie upraszcza tworzenie serwerow MCP.

🎯 Czego sie nauczysz:
  • 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.

Cecha Low-level SDK FastMCP
Definiowanie tooli Reczne schematy JSON Dekoratory + type hints
Walidacja Manualna Automatyczna (Pydantic)
Linie kodu ~100+ na tool ~10 na tool
Krzywa uczenia Stroma Lagodna

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"
      }
    }
  }
}
⚠ Wazne:

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
Tools Resources Prompts create_note read_note list_notes search_notes delete_note get_statistics Response: “Created note ‘Shopping List’ with priority medium” MCP Inspector – interaktywne testowanie serwera

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

✅ Dobre praktyki:
  1. Docstringi — Kazdy tool musi miec dobry opis (LLM go czyta!)
  2. Walidacja — Uzywaj Pydantic dla wszystkich danych wejsciowych
  3. Error handling — Zwracaj czytelne komunikaty bledow
  4. Logging — Loguj wazne operacje dla debugowania
  5. Idempotentnosc — Toole powinny byc bezpieczne do wielokrotnego wywolania
  6. Limity — Ogranicz rozmiar danych zwracanych przez toole
❌ Unikaj:
  • 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

  1. Anthropic. (2025). MCP Python SDK Documentation. modelcontextprotocol.io/quickstart/server
  2. Anthropic. (2025). MCP Python SDK GitHub. github.com/modelcontextprotocol/python-sdk
  3. Anthropic. (2024). MCP Specification. spec.modelcontextprotocol.io
  4. Pydantic. (2025). Pydantic Documentation. docs.pydantic.dev
  5. MCP Community. (2025). MCP Servers Repository. github.com/modelcontextprotocol/servers