Module 8: Project — Real-World MCP Server
MCP Server Design and Architecture
MCP Server Design and Architecture
Capsule description
Before writing a single line of code, you design. Not for formality's sake — because the decisions you make now determine whether your server will be maintainable or a tangle. Which data to expose as resources. Which operations to turn into tools. Which repetitive interactions to automate with prompts. How to organize the files. How to handle errors.
This capsule guides you through the design process. When you finish, you'll have a complete architecture document and the file structure ready to implement.
The design process
Step 1: Define the domain
Your MCP server revolves around a domain — a coherent set of data and operations. The domain determines everything else.
Questions you must answer:
- What data does it handle? — Tasks, products, users, files, repos, issues...
- What operations do you need? — Create, read, update, delete, search, report, analyze...
- What questions do you ask frequently? — "How many pending tasks are there?", "What's the best-selling product?", "Which files were modified yesterday?"
- What data should Claude Code be able to see without you asking explicitly? — These are your resources.
- What actions should Claude Code be able to execute when you ask? — These are your tools.
- Which interactions do you repeat constantly? — These are candidates for prompts.
Step 2: Map primitives to the domain
Once you're clear about the domain, map each need to the correct primitive:
| Need | Primitive | Why |
|---|---|---|
| "See what tables exist" | Resource | Static/lookup data, no side effects |
| "See a table's schema" | Resource | Metadata that Claude Code needs to understand the structure |
| "Read a table's records" | Resource | Read access to data |
| "Create a new record" | Tool | Operation with side effects (modifies the database) |
| "Update a record" | Tool | Operation with side effects |
| "Delete a record" | Tool | Operation with side effects |
| "Run a custom SQL query" | Tool | Flexible operation that can have side effects |
| "Search records by criteria" | Tool | Parameterized operation |
| "Analyze a table and generate a report" | Prompt | Multi-step interaction that combines resources and tools |
| "Generate a weekly summary" | Prompt | Reusable template with parameters |
The simple rule
- Does Claude Code need to read it to have context? → Resource
- Does Claude Code need to execute something that changes data? → Tool
- Is it a multi-step interaction that you repeat? → Prompt
Design for Option A: SQLite + Python
This is the main example. If you chose Option B or C, read this section to understand the process and then adapt it to yours.
Domain: Task Manager
A task management system with categories and tags. Simple enough to implement in a capsule, complex enough to demonstrate all the primitives.
Database schema
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
description TEXT,
color TEXT DEFAULT '#6B7280',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending' CHECK(status IN ('pending', 'in_progress', 'completed', 'cancelled')),
priority TEXT DEFAULT 'medium' CHECK(priority IN ('low', 'medium', 'high', 'critical')),
category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
due_date TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE tags (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
);
CREATE TABLE task_tags (
task_id INTEGER REFERENCES tasks(id) ON DELETE CASCADE,
tag_id INTEGER REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (task_id, tag_id)
);
Why this schema:
categoriesandtagsgive 1:N and M:N relationships, demonstrating queries with JOINsstatusandprioritywith CHECK constraints demonstrate validation at the database leveldue_dateallows temporal filters ("overdue tasks", "this week's tasks")- It's enough for 5+ interesting tools without being overwhelming
Planned resources
| URI | Description | What it returns |
|---|---|---|
taskdb://tables | List of tables in the database | Names and record count |
taskdb://table/{name}/schema | Schema of a specific table | Columns, types, constraints |
taskdb://stats | General statistics | Count by status, category, priority |
taskdb://tasks/overdue | Overdue tasks | Tasks with a past due_date and status != completed |
taskdb://categories | All the categories | List with task count per category |
Design decisions:
- The URIs use the
taskdb://prefix for clear namespacing taskdb://tablesis the entry point — Claude Code reads it first to understand the structuretaskdb://statsgives a quick summary without needing to run queriestaskdb://tasks/overdueis a derived resource — calculated data, not a table dump
Planned tools
| Tool | Description | Inputs | Side effects |
|---|---|---|---|
create_task | Create a new task | title, description?, category_id?, priority?, due_date?, tags? | INSERT into tasks + task_tags |
list_tasks | List tasks with filters | status?, priority?, category_id?, limit? | None (read only) |
update_task | Update a task's fields | task_id, fields to update | UPDATE on tasks |
delete_task | Delete a task | task_id | DELETE on tasks + task_tags |
search_tasks | Search tasks by text | query, search_in? (title/description/both) | None |
create_category | Create a category | name, description?, color? | INSERT into categories |
run_query | Run a custom SQL query | sql (SELECT only) | None (read only) |
get_task_summary | Summary by period | period (today/week/month) | None |
Design decisions:
list_tasksis a tool because it has complex filters that need parametersrun_queryallows arbitrary queries but restricted to SELECT (no modifications)get_task_summarycould be a resource, but it needs a parameter (period), so it's a tool- Each tool that modifies data returns the modified object for confirmation
Planned prompts
| Prompt | Description | Parameters |
|---|---|---|
analyze_table | Analyzes a table and suggests improvements | table_name |
weekly_report | Generates a report of the week's tasks | week_start? (default: this week) |
optimize_query | Analyzes and optimizes a SQL query | sql_query |
Design decisions:
analyze_tablecombines resources (schema, data) with the model's analysisweekly_reportstandardizes an interaction you'd do regularlyoptimize_queryis advanced — Claude Code analyzes the query, consults the schema, and suggests improvements
Prompt template: analyze_table
@mcp.prompt()
async def analyze_table(table_name: str) -> str:
"""Analyzes the structure and data of a table, suggests improvements."""
return f"""Analyze the table '{table_name}' in the database.
Please:
1. Read the resource taskdb://table/{table_name}/schema to see the structure
2. Use the run_query tool to count records: SELECT COUNT(*) FROM {table_name}
3. Use the run_query tool to see a sample: SELECT * FROM {table_name} LIMIT 5
4. Read the resource taskdb://stats for general context
With that information, generate an analysis that includes:
- Table structure (columns, types, constraints)
- Data volume
- Distribution of values in key columns
- Possible schema improvements (indexes, additional constraints)
- Useful queries for this table"""
Prompt template: weekly_report
@mcp.prompt()
async def weekly_report(week_start: str = "") -> str:
"""Generates a weekly report of tasks."""
date_filter = f"for the week of {week_start}" if week_start else "for this week"
return f"""Generate a productivity report {date_filter}.
Please:
1. Use the get_task_summary tool with period='week'
2. Use the list_tasks tool with status='completed' to see finished tasks
3. Read the resource taskdb://tasks/overdue for overdue tasks
4. Read the resource taskdb://stats for general context
With that information, generate a report that includes:
- Executive summary (completed vs pending tasks)
- Tasks completed this week (with category)
- Priority pending tasks
- Overdue tasks that require attention
- Metrics: completion rate, distribution by priority
- Recommendations for next week"""
Design for Option B: File System + TypeScript
If you chose Option B, here's the corresponding design.
Domain: Project Analyzer
A server that analyzes code projects — structure, dependencies, metrics.
Planned resources
| URI | Description |
|---|---|
project://info | Project metadata (name, type, size) |
project://structure | Directory tree |
project://dependencies | List of dependencies (package.json / requirements.txt) |
project://stats | Metrics: lines of code, files by type, total size |
Planned tools
| Tool | Description |
|---|---|
list_directory | Lists the content of a directory with filters |
read_file | Reads a file's content (with a size limit) |
search_content | Searches text in the project's files |
analyze_file | Metrics of a file: LOC, complexity, imports |
find_unused_files | Detects files that aren't imported anywhere |
generate_tree | Generates a visual tree of the project |
Planned prompts
| Prompt | Description |
|---|---|
project_review | Complete analysis of a project directory |
dependency_audit | Reviews dependencies: versions, vulnerabilities, unused |
Example of interaction with Claude Code
You: "Analyze the structure of my project in ./my-app"
Claude Code: [uses resource project://structure] → reads the directory tree
Claude Code: [uses tool analyze_file for the main files]
Claude Code: "Your project my-app has 23 TypeScript files in 8 directories.
Most of the code is in src/components/ (42%). There are 3 files that aren't
imported anywhere: utils/legacy.ts, helpers/deprecated.ts, types/old.ts."
File structure
project-analyzer/
├── src/
│ ├── index.ts # Entry point, MCP registrations
│ ├── tools/
│ │ ├── filesystem.ts # Read/search tools
│ │ └── analysis.ts # Analysis tools
│ ├── resources/
│ │ └── project.ts # Project resources
│ └── utils/
│ ├── tree.ts # Tree generator
│ └── metrics.ts # Metric calculations
├── tests/
│ ├── tools.test.ts
│ ├── resources.test.ts
│ └── integration.test.ts
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.md
Design for Option C: External API
If you chose Option C, here's the design framework. The example uses Todoist but adapt it to your API.
Domain: Todoist Integration
A server that connects Claude Code with the Todoist API for task management.
Planned resources
| URI | Description |
|---|---|
todoist://projects | List of projects |
todoist://project/{id} | Details of a project |
todoist://labels | All the available labels |
todoist://stats | Productivity statistics |
Planned tools
| Tool | Description |
|---|---|
list_tasks | Lists tasks with filters (project, label, priority) |
create_task | Creates a new task |
complete_task | Marks a task as completed |
update_task | Updates an existing task |
move_task | Moves a task to another project |
search_tasks | Searches tasks by text |
Planned prompts
| Prompt | Description |
|---|---|
daily_plan | Generates the day's plan based on pending tasks |
project_status | Status report of a specific project |
Special considerations for external APIs
- Authentication: You need an API token. Pass it as an environment variable, never hardcoded.
- Rate limiting: Respect the API's limits. Implement retry with backoff.
- Latency: Requests to external APIs are slower than a local database. Consider caching.
- Availability: The API can go down. Your server must handle timeouts and connection errors gracefully.
- Pagination: Many APIs paginate results. Your tool must handle pages or set a reasonable limit.
- Versioning: APIs change. Pin the API version you use (e.g.,
X-API-Versionheader).
File structure: Option A (main example)
task-manager-mcp/
├── src/
│ ├── __init__.py
│ ├── server.py # Entry point — FastMCP instance and registrations
│ ├── database.py # SQLite connection, table setup, seed data
│ ├── models.py # Pydantic models for inputs and outputs
│ ├── tools/
│ │ ├── __init__.py
│ │ ├── tasks.py # Task CRUD tools
│ │ ├── categories.py # Category tools
│ │ └── queries.py # Query and report tools
│ └── resources/
│ ├── __init__.py
│ └── database.py # Database resources
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures (in-memory database)
│ ├── test_tools_tasks.py # Task tool tests
│ ├── test_tools_categories.py
│ ├── test_tools_queries.py
│ ├── test_resources.py # Resource tests
│ └── test_integration.py # End-to-end tests
├── data/
│ └── tasks.db # SQLite database (generated by the server)
├── requirements.txt
└── README.md
Why this structure
src/server.pyas the entry point: A single file where everything is registered. Easy to find and modify.src/database.pyseparated: The database connection and setup is independent of MCP. You can test the database without the server.src/models.pycentralized: All the Pydantic models in one place. Avoids circular imports.src/tools/in its own directory: Each group of tools in its file. When you have 8+ tools, a single file becomes hard to navigate.src/resources/separated from tools: Resources and tools have distinct responsibilities. Separating them makes the difference explicit.tests/conftest.pywith fixtures: The testing database (in memory) is configured once and reused across all the tests.data/for the database: The SQLite database doesn't go insrc/. It's a data artifact, not code.
Files that shouldn't exist
src/utils.py— Too generic. If you need utilities, name them by what they do.src/helpers.py— Same problem. Helper for what?src/config.py— For a project of this size, configurations go in environment variables or inserver.py.
Pydantic Models design
The models are the contract between Claude Code and your server. Designing them well now saves you problems later.
Input models (for tools)
from pydantic import BaseModel, Field
from typing import Optional
from enum import Enum
class TaskStatus(str, Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class TaskPriority(str, Enum):
LOW = "low"
MEDIUM = "medium"
HIGH = "high"
CRITICAL = "critical"
class CreateTaskInput(BaseModel):
title: str = Field(min_length=1, max_length=200, description="Task title")
description: Optional[str] = Field(default=None, max_length=2000, description="Detailed description")
status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
priority: TaskPriority = Field(default=TaskPriority.MEDIUM, description="Priority: low, medium, high, critical")
category_id: Optional[int] = Field(default=None, description="Category ID")
due_date: Optional[str] = Field(default=None, description="Due date in YYYY-MM-DD format")
tags: Optional[list[str]] = Field(default=None, description="List of tags for the task")
class UpdateTaskInput(BaseModel):
task_id: int = Field(description="ID of the task to update")
title: Optional[str] = Field(default=None, min_length=1, max_length=200)
description: Optional[str] = Field(default=None, max_length=2000)
status: Optional[TaskStatus] = Field(default=None)
priority: Optional[TaskPriority] = Field(default=None)
category_id: Optional[int] = Field(default=None)
due_date: Optional[str] = Field(default=None)
class ListTasksInput(BaseModel):
status: Optional[TaskStatus] = Field(default=None, description="Filter by status")
priority: Optional[TaskPriority] = Field(default=None, description="Filter by priority")
category_id: Optional[int] = Field(default=None, description="Filter by category")
limit: int = Field(default=20, ge=1, le=100, description="Maximum results")
class SearchTasksInput(BaseModel):
query: str = Field(min_length=1, description="Text to search")
search_in: str = Field(default="both", description="Search in: title, description, both")
class CreateCategoryInput(BaseModel):
name: str = Field(min_length=1, max_length=50, description="Category name")
description: Optional[str] = Field(default=None, max_length=200)
color: str = Field(default="#6B7280", pattern=r"^#[0-9A-Fa-f]{6}$", description="Hex color, e.g. '#FF5733'")
class RunQueryInput(BaseModel):
sql: str = Field(min_length=1, description="SQL query (only SELECT allowed)")
class TaskSummaryInput(BaseModel):
period: str = Field(description="Period: today, week, month")
Design principles for the models
-
Each field has a
description— Claude Code uses these descriptions to understand what to send. A clear description = fewer errors from the model. -
Explicit constraints —
min_length,max_length,ge,le,pattern. They validate automatically before your code touches the database. -
Reasonable defaults —
statusdefaultpending,prioritydefaultmedium,limitdefault20. Claude Code doesn't need to specify everything all the time. -
Enums for fixed values —
TaskStatusandTaskPriorityas Enums. Claude Code can't send an invalid status. -
Optional for editable fields — In
UpdateTaskInput, everything is Optional excepttask_id. You only update what you send.
Error handling design
Define how your server handles errors before implementing. The errors fall into predictable categories:
Error categories
| Category | Example | How to handle |
|---|---|---|
| Not found | Task with ID 999 doesn't exist | Return a clear message: "Task with ID 999 not found" |
| Validation | Priority "super_high" isn't valid | Pydantic handles it automatically. Returns a descriptive error. |
| Database | Corrupt or locked SQLite file | Catch sqlite3.Error, return a generic message + logging |
| Query | SQL syntax error in run_query | Catch sqlite3.OperationalError, return the SQLite error |
| Permission | Query with DELETE/DROP in run_query | Detect before executing, return "Only SELECT queries allowed" |
| Connection | External API doesn't respond (Option C) | Timeout + retry + message: "API unavailable, try later" |
Consistent error pattern
All the errors return the same JSON format:
import json
def error_response(error_type: str, message: str, details: str = "") -> str:
result = {
"error": True,
"error_type": error_type,
"message": message,
}
if details:
result["details"] = details
return json.dumps(result, ensure_ascii=False)
Usage example:
async def delete_task(task_id: int) -> str:
conn = get_connection()
cursor = conn.execute("SELECT id FROM tasks WHERE id = ?", (task_id,))
if not cursor.fetchone():
return error_response("not_found", f"Task with ID {task_id} not found")
conn.execute("DELETE FROM tasks WHERE id = ?", (task_id,))
conn.commit()
return json.dumps({"deleted": True, "task_id": task_id})
Claude Code receives clear errors that it can communicate to the user. "Task with ID 999 not found" is infinitely better than a Python stack trace.
Tool descriptions design
Your tools' descriptions are more important than they seem. Claude Code reads them to decide when to use each tool. A vague description = Claude Code uses the tool incorrectly or doesn't use it when it should.
Good vs bad descriptions
# ❌ Bad: too vague
async def list_tasks() -> str:
"""Lists tasks."""
...
# ❌ Bad: too technical
async def list_tasks(input: ListTasksInput) -> str:
"""Runs SELECT * FROM tasks with optional WHERE filters on status, priority and category_id."""
...
# ✅ Good: clear and usage-oriented
async def list_tasks(input: ListTasksInput) -> str:
"""Lists tasks with optional filters by status, priority and category.
Without filters it returns the 20 most recent tasks. Use status='completed'
to see only the finished ones, priority='high' for the urgent ones.
"""
...
Principles for descriptions
- First sentence = what the tool does. Claude Code often only reads the first line.
- Second sentence = how to use it. Example of common parameters.
- Don't explain the implementation. Claude Code doesn't need to know that you use SQLite underneath.
- Mention the available filters. If the tool accepts filters, list the most useful ones.
- Mention edge cases. "If not found, returns an error message."
Apply the same to resources — the description you pass to the @mcp.resource() decorator tells Claude Code what data it contains.
URI design for resources
The resource URIs should be predictable and descriptive. Follow these conventions:
Conventions
{prefix}://{entity} → List or summary of the entity
{prefix}://{entity}/{id} → Specific entity by ID
{prefix}://{entity}/{id}/{sub-entity} → Sub-entity of an entity
{prefix}://{metadata-type} → System metadata
Examples for Task Manager
taskdb://tables → List of tables
taskdb://table/tasks/schema → Schema of the tasks table
taskdb://table/categories/schema → Schema of the categories table
taskdb://stats → General statistics
taskdb://tasks/overdue → Overdue tasks (derived resource)
taskdb://categories → List of categories
URI anti-patterns
taskdb://getTaskById/5 → Don't use verbs in resource URIs
taskdb://all-the-tables → Don't use ambiguous names
taskdb://data → Too generic
taskdb://tasks?status=pending → Resources don't have query params; use a tool
Design decisions to document
Before implementing, document your decisions. These questions help you think about the trade-offs:
Decisions checklist
-
Which operations to expose as tools vs resources?
- Tools: parameterized operations or ones with side effects
- Resources: context data that Claude Code reads to understand the situation
-
Does the
run_querytool allow arbitrary queries?- Yes, but only SELECT. It rejects INSERT/UPDATE/DELETE/DROP.
- Trade-off: flexibility vs security. For a local server it's acceptable.
-
Do the tools return the complete object or just confirmation?
- Complete object after create/update. Claude Code needs to see what was created/changed.
- Only confirmation after delete (the object no longer exists).
-
How to handle the database in tests?
- In-memory database (
sqlite3.connect(":memory:")) for tests. - File database for the real server.
- A pytest fixture that creates and destroys the database for each test.
- In-memory database (
-
Is seed data included?
- Yes. The server creates tables and optionally loads example data.
- Useful for demos and so the user sees data immediately.
Your turn: design your server
If you're following along with Option A (SQLite + Python), the designs in this capsule are your starting point. You can use them as they are or modify them for your domain.
If you chose Option B or C, use the design frameworks above to create your own plan:
Design template
Copy and complete it for your project:
## My MCP Server: [Name]
### Domain
[Describe what data it handles and what problem it solves]
### Stack
- Language: [Python / TypeScript]
- Data source: [SQLite / File system / External API]
- Validation: [Pydantic / Zod]
- Testing: [pytest / Vitest]
### Resources (minimum 3)
| URI | Description | What it returns |
|-----|-------------|-------------|
| | | |
### Tools (minimum 5)
| Tool | Description | Inputs | Side effects |
|------|-------------|--------|--------------|
| | | | |
### Prompts (minimum 2)
| Prompt | Description | Parameters |
|--------|-------------|------------|
| | | |
### File structure
[Directory tree]
### Design decisions
1. [Decision] → [Reason]
2. [Decision] → [Reason]
3. [Decision] → [Reason]
Create the file structure
Once you have the design, create the empty structure. For Option A:
mkdir task-manager-mcp
cd task-manager-mcp
python -m venv .venv
source .venv/bin/activate # Linux/macOS
# .venv\Scripts\activate # Windows
pip install "mcp[cli]" pydantic pytest pytest-asyncio
mkdir -p src/tools src/resources tests data
touch src/__init__.py
touch src/server.py
touch src/database.py
touch src/models.py
touch src/tools/__init__.py
touch src/tools/tasks.py
touch src/tools/categories.py
touch src/tools/queries.py
touch src/resources/__init__.py
touch src/resources/database.py
touch tests/__init__.py
touch tests/conftest.py
touch tests/test_tools_tasks.py
touch tests/test_tools_categories.py
touch tests/test_tools_queries.py
touch tests/test_resources.py
touch tests/test_integration.py
requirements.txt
mcp[cli]>=1.0.0
pydantic>=2.0.0
pytest>=8.0.0
pytest-asyncio>=0.23.0
.gitignore
.venv/
__pycache__/
*.pyc
data/tasks.db
.pytest_cache/
*.egg-info/
dist/
build/
Don't version the database (data/tasks.db) — it's generated automatically. Don't version the virtualenv or the Python caches. If your project uses API tokens, never include them in the repo — use .env and add it to .gitignore.
Verify that the environment works
python -c "import mcp; print(f'MCP SDK version: {mcp.__version__}')"
python -c "import pydantic; print(f'Pydantic version: {pydantic.__version__}')"
pytest --version
If all three commands run without error, your environment is ready.
Milestone of this capsule
When you finish this capsule, you should have:
- Domain defined — You know exactly what data and operations your server handles
- Resources planned — List of at least 3 resources with defined URIs
- Tools planned — List of at least 5 tools with defined inputs
- Prompts planned — List of at least 2 prompts with parameters
- Pydantic/Zod models designed — Validation schemas for each tool input
- File structure created — All the files/directories exist (empty)
- Environment configured — SDK, dependencies, and testing framework installed
- Decisions documented — You know why you chose each resource, tool, and prompt
If all of this is checked, you're ready to implement. Capsule 03 is where the code takes shape.
Common errors in the design phase
Designing too many tools from the start
It's tempting to plan 15 tools before writing code. The problem: half of those tools will change when you start implementing. Plan the 5-8 core ones, implement, and then add if you need more.
Confusing resources with tools
A frequent error is creating a resource for something that requires variable parameters. If Claude Code needs to send a status filter, it's a tool, not a resource. Resources are for context data with fixed URIs or with simple templates like {table_name}.
Forgetting the descriptions
Leaving descriptions empty or with a single "Lists tasks" is like naming a function doStuff(). Claude Code needs detailed descriptions to decide when to use each tool. Invest time in writing them well now — it saves you debugging later.
Not designing the error handling
"I'll handle it later" is the phrase that precedes stack traces in production. Define the error pattern now (consistent JSON format, predictable categories) and all your tools follow it.
Making the schema too complex
For a 4-hour project, 3-4 tables is ideal. More than that and you spend more time on the database than on MCP. Remember: the goal is to demonstrate MCP, not to design an enterprise schema.
Troubleshooting
"I don't know which domain to choose"
Think of a task you do weekly with data. Do you manage tasks? Do you keep an inventory? Do you organize notes? Any of those works. If you really have no preference, use the Task Manager from this capsule.
"Do I need exactly the file structure that's shown?"
No. The structure is a proven suggestion. If you prefer to put everything in a single file to start, do it. You can refactor later. What matters is that it works.
"Can I add more tools/resources later?"
Yes. The design isn't final. Implement the minimum first (3 resources, 5 tools, 2 prompts). If you have time to spare and want to add more, do it. The rubric gives extra points for going beyond the minimum.
"How do I decide if something is a resource or a tool?"
Ask yourself: "Does Claude Code need parameters to get this?" If yes, it's probably a tool. If no, it's a resource. Another question: "Does this modify data?" If yes, it's necessarily a tool.
"Are prompts mandatory?"
Yes, at least 2. Prompts demonstrate that you understand the third primitive. Besides, they're extremely useful in practice — they turn 3-step interactions into 1.
"Can I use PostgreSQL instead of SQLite?"
You can, but it adds complexity (installation, connection, credentials). SQLite comes included with Python and needs no setup. For this project, SQLite is the recommended option. If you already have PostgreSQL configured and prefer it, go ahead.
Summary
- You designed the complete architecture of the MCP server: database schema, models, and folder structure
- The schema uses 3 related tables: tasks, categories, and task_tags
- The Pydantic models define clear contracts for data input and output
- You planned the 3 primitives: Resources (reading data), Tools (CRUD with side effects), Prompts (reusable templates)
- The folder structure separates responsibilities:
database/,tools/,resources/,prompts/ - The design prioritizes clarity over optimization — a server that's easy to understand and maintain
Resources
- SQLite Documentation — Complete reference of SQL and data types
- Pydantic v2 — Fields — Documentation of Field constraints
- MCP Resources Specification — Official specification of Resources
- MCP Tools Specification — Official specification of Tools
- MCP Prompts Specification — Official specification of Prompts
- Python Enums — Enums for status and priority
- Zod Documentation — Validation for TypeScript (Option B)
- MCP URI Design — URI conventions