Tutorial Creating Your First MCP - 80-20-Human-In-The-Loop/Community GitHub Wiki

Creating Your First MCP

Build your first MCP server step-by-step, learning the modular patterns that make MCPs powerful, reusable, and aligned with human wisdom.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. The _mcp Folder Pattern
  4. Step 1: Create Your Project Structure
  5. Step 2: Your First Tool
  6. Step 3: Add a Resource
  7. Step 4: Create a Prompt
  8. Step 5: Verify It Works
  9. Step 6: Make It Modular
  10. Step 7: Run Your MCP
  11. Understanding What You Built
  12. Next Steps

Introduction

This tutorial will guide you through creating your first MCP server from scratch. We'll build a Learning Journal MCP - a tool that helps you track what you're learning, reflect on your progress, and maintain human wisdom in your learning journey.

Why a Learning Journal?

This example embodies the 80-20 philosophy:

  • 80% Automation: AI helps organize and analyze your learning
  • 20% Human Wisdom: You decide what's worth learning and why
  • 100% Growth: Both you and the AI learn together

What You'll Learn

  • The modular MCP architecture using the _mcp folder pattern
  • How Tools, Resources, and Prompts work together
  • Testing and running your MCP server
  • Preparing your MCP for community sharing

Prerequisites

You only need:

  • Python 3.10+ installed
  • Basic Python knowledge (functions, decorators)
  • A terminal/command line
  • Curiosity and willingness to learn

Install FastMCP:

pip install fastmcp

Verify installation:

fastmcp version

The _mcp Folder Pattern

Remember from the FastMCP Quickstart Guide: MCPs live in a special _mcp folder. This convention means:

  • Internal Tool: This MCP serves your project
  • Standardized Location: Everyone knows where to look
  • Modular Design: Easy to maintain and share

Our structure will be:

learning-journal/
ā”œā”€ā”€ _mcp/                    # Your MCP lives here
│   ā”œā”€ā”€ main.py             # The coordinator
│   ā”œā”€ā”€ servers/            # Modular components
│   │   ā”œā”€ā”€ __init__.py
│   │   └── journal.py      # Journal tools
│   └── tests/              # Your tests
│       └── test_journal.py
└── journal_entries/        # Where journal data lives

Step 1: Create Your Project Structure

Let's build the foundation:

# Create project directory
mkdir learning-journal
cd learning-journal

# Create the MCP structure
mkdir -p _mcp/servers _mcp/tests journal_entries
touch _mcp/main.py
touch _mcp/servers/__init__.py
touch _mcp/servers/journal.py
touch _mcp/tests/test_journal.py

# Create a sample journal entry
echo "Today I learned about MCP servers!" > journal_entries/day1.md

Step 2: Your First Tool

Start with a simple tool in _mcp/main.py:

"""
Learning Journal MCP - Your personal learning assistant
Following the 80-20 Human in The Loop philosophy
"""

from fastmcp import FastMCP
from datetime import datetime
from pathlib import Path

# Create your MCP server
mcp = FastMCP(
    name="Learning Journal MCP",
    instructions="""
    I help you track and reflect on your learning journey.
    
    **Philosophy**: You decide what's worth learning (20% human wisdom),
    I help organize and analyze (80% automation).
    
    Available capabilities:
    - Record learning entries
    - Review past learnings
    - Generate reflection prompts
    """
)

# Your first tool - a simple action
@mcp.tool
def record_learning(
    topic: str,
    key_insight: str,
    confidence_level: int = 5
) -> str:
    """
    Record something you learned today.
    
    Args:
        topic: What you learned about
        key_insight: The main takeaway
        confidence_level: How well you understand it (1-10)
    
    Returns:
        Confirmation of the recorded entry
    """
    # Create timestamp
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    
    # Build entry
    entry = f"""
## {timestamp} - {topic}

**Key Insight**: {key_insight}
**Confidence Level**: {confidence_level}/10

---
"""
    
    # Save to file (simple persistence)
    journal_path = Path("journal_entries")
    journal_path.mkdir(exist_ok=True)
    
    today = datetime.now().strftime("%Y-%m-%d")
    file_path = journal_path / f"{today}.md"
    
    # Append to today's file
    with open(file_path, "a") as f:
        f.write(entry)
    
    return f"āœ… Recorded learning about '{topic}' (Confidence: {confidence_level}/10)"

# Run the server
if __name__ == "__main__":
    mcp.run()

Test Your First Tool

In a new file _mcp/test_tool.py:

import asyncio
from fastmcp import Client

async def test_tool():
    client = Client("_mcp/main.py")
    
    async with client:
        result = await client.call_tool(
            "record_learning",
            {
                "topic": "MCP Tools",
                "key_insight": "Tools are Python functions that AI can call",
                "confidence_level": 7
            }
        )
        print(result)

asyncio.run(test_tool())

Run it:

# Terminal 1: Start server
python _mcp/main.py

# Terminal 2: Test
python _mcp/test_tool.py

Step 3: Add a Resource

Now let's add a resource to READ our journal entries. Add this to _mcp/main.py:

@mcp.resource("journal://recent")
def get_recent_entries() -> str:
    """
    Read your recent journal entries.
    
    This resource provides read-only access to your learning history.
    """
    journal_path = Path("journal_entries")
    
    if not journal_path.exists():
        return "No journal entries yet. Start learning something!"
    
    # Get all journal files, sorted by date
    files = sorted(journal_path.glob("*.md"), reverse=True)
    
    if not files:
        return "No entries found. Record your first learning!"
    
    # Read the most recent file
    recent_file = files[0]
    with open(recent_file, "r") as f:
        content = f.read()
    
    return f"# Recent Learning Entries\n\n{content}"

@mcp.resource("journal://stats")
def get_learning_stats() -> dict:
    """
    Get statistics about your learning journey.
    """
    journal_path = Path("journal_entries")
    
    if not journal_path.exists():
        return {"total_entries": 0, "days_active": 0}
    
    files = list(journal_path.glob("*.md"))
    total_entries = 0
    
    for file in files:
        with open(file, "r") as f:
            # Count entries (each starts with ##)
            content = f.read()
            total_entries += content.count("\n## ")
    
    return {
        "total_entries": total_entries,
        "days_active": len(files),
        "average_per_day": round(total_entries / len(files), 1) if files else 0
    }

Step 4: Create a Prompt

Add a prompt template for reflection. Add this to _mcp/main.py:

@mcp.prompt
def reflection_prompt(topic: str) -> str:
    """
    Generate a reflection prompt for deeper learning.
    
    This embodies the 20% human wisdom - prompting YOU to think deeper.
    
    Args:
        topic: The topic to reflect on
    
    Returns:
        A thoughtful prompt for reflection
    """
    return f"""
Please help me reflect on my learning about {topic}:

1. What do I understand well about {topic}?
2. What aspects of {topic} are still unclear?
3. How can I apply {topic} in a real project?
4. What would I teach someone else about {topic}?
5. What questions do I still have about {topic}?

Take time to think through each question. True learning comes from reflection.
"""

@mcp.prompt
def learning_plan_prompt(goal: str, current_level: str = "beginner") -> str:
    """
    Create a personalized learning plan.
    
    Args:
        goal: What you want to learn
        current_level: Your current skill level
    """
    return f"""
Help me create a learning plan:

**Goal**: {goal}
**Current Level**: {current_level}

Please suggest:
1. Key concepts to master first
2. Practical projects to build understanding
3. Resources that match my level
4. Milestones to track progress
5. How to know when I'm ready to advance

Remember: The goal is understanding, not just completion.
"""

Step 5: Verify It Works

Let's do a quick verification that everything is working before we dive deeper into testing.

Create _mcp/quick_test.py:

"""
Quick verification of our Learning Journal MCP
"""

import asyncio
from fastmcp import Client

async def verify_mcp():
    client = Client("_mcp/main.py")
    
    async with client:
        print("šŸ” Quick MCP Verification\n")
        
        # Test basic functionality
        result = await client.call_tool(
            "record_learning",
            {
                "topic": "FastMCP Basics",
                "key_insight": "MCPs have tools, resources, and prompts working together",
                "confidence_level": 7
            }
        )
        print(f"āœ… Tool working: {result}")
        
        # Check resources are accessible
        recent = await client.get_resource("journal://recent")
        print(f"āœ… Resource working: Found {len(recent)} characters")
        
        print("\nšŸŽ‰ Your Learning Journal MCP is working!")
        print("Ready for comprehensive testing...")

if __name__ == "__main__":
    asyncio.run(verify_mcp())

Run it to verify everything works:

python _mcp/quick_test.py

Next Step: Now that we've built our Learning Journal MCP, it's time to learn how to test it properly. Head to MCP Testing Strategies to learn comprehensive testing patterns that will ensure your MCP is reliable, maintainable, and preserves the 80-20 philosophy.


Step 6: Make It Modular

Following the pattern from the FastMCP Quickstart, let's split into modules.

Create _mcp/servers/journal.py:

"""
Journal module - Core learning journal functionality
"""

from fastmcp import FastMCP
from datetime import datetime
from pathlib import Path

# Create a modular server
journal_server = FastMCP("Journal Tools")

@journal_server.tool
def record_learning(
    topic: str,
    key_insight: str,
    confidence_level: int = 5
) -> str:
    """Record something you learned today."""
    timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    
    entry = f"""
## {timestamp} - {topic}

**Key Insight**: {key_insight}
**Confidence Level**: {confidence_level}/10

---
"""
    
    journal_path = Path("journal_entries")
    journal_path.mkdir(exist_ok=True)
    
    today = datetime.now().strftime("%Y-%m-%d")
    file_path = journal_path / f"{today}.md"
    
    with open(file_path, "a") as f:
        f.write(entry)
    
    return f"āœ… Recorded learning about '{topic}'"

@journal_server.resource("journal://recent")
def get_recent_entries() -> str:
    """Read your recent journal entries."""
    journal_path = Path("journal_entries")
    
    if not journal_path.exists():
        return "No journal entries yet."
    
    files = sorted(journal_path.glob("*.md"), reverse=True)
    
    if not files:
        return "No entries found."
    
    with open(files[0], "r") as f:
        return f.read()

@journal_server.resource("journal://stats")
def get_learning_stats() -> dict:
    """Get statistics about your learning journey."""
    journal_path = Path("journal_entries")
    
    if not journal_path.exists():
        return {"total_entries": 0}
    
    files = list(journal_path.glob("*.md"))
    total_entries = sum(
        open(f).read().count("\n## ") for f in files
    )
    
    return {
        "total_entries": total_entries,
        "days_active": len(files)
    }

Update _mcp/main.py to use the module:

"""
Learning Journal MCP - Main Coordinator
"""

from fastmcp import FastMCP
from servers.journal import journal_server

# Create the main server
mcp = FastMCP(
    name="Learning Journal MCP",
    instructions="""
    Your personal learning assistant following the 80-20 philosophy.
    
    I help track and reflect on your learning journey.
    You decide what's worth learning (human wisdom).
    I help organize and analyze (AI assistance).
    """
)

# Mount the journal module
mcp.mount(journal_server)

# Add main-level prompts
@mcp.prompt
def reflection_prompt(topic: str) -> str:
    """Generate a reflection prompt for deeper learning."""
    return f"""
Reflect on your learning about {topic}:

1. What do you understand well?
2. What's still unclear?
3. How can you apply this?
4. What would you teach others?
5. What questions remain?

True learning comes from reflection.
"""

if __name__ == "__main__":
    mcp.run()

Step 7: Run Your MCP

Method 1: Python Direct

python _mcp/main.py

Method 2: FastMCP CLI

# Run with stdio (default)
fastmcp run _mcp/main.py

# Run with HTTP transport
fastmcp run _mcp/main.py --transport http --port 8000

# Run with development inspector
fastmcp dev _mcp/main.py

Method 3: Programmatic with Different Transports

Update the end of _mcp/main.py:

if __name__ == "__main__":
    import sys
    
    if "--http" in sys.argv:
        # Run as HTTP server
        mcp.run(transport="http", port=8000)
    else:
        # Default to stdio
        mcp.run()

Understanding What You Built

The Three Components Working Together

  1. Tools (record_learning): ACTIONS that modify state

    • The AI can help you record learnings
    • You decide what's worth recording
  2. Resources (journal://recent, journal://stats): READ-ONLY data

    • The AI can read your journal
    • Provides context for conversations
  3. Prompts (reflection_prompt): TEMPLATES for interaction

    • Guide the AI to help you reflect
    • Preserve human thinking in the process

The 80-20 Philosophy in Action

  • 80% Automation:

    • Organizing entries by date
    • Calculating statistics
    • Formatting data consistently
  • 20% Human Wisdom:

    • Deciding what to learn
    • Reflecting on understanding
    • Setting confidence levels
  • 100% Human Responsibility:

    • You own your learning journey
    • AI assists but doesn't replace thinking

The Modular Architecture

Following the community pattern:

  • main.py coordinates everything
  • servers/journal.py contains focused functionality
  • Easy to add new modules (e.g., servers/quiz.py)
  • Ready for PyPI distribution

Next Steps

1. Enhance Your MCP

Add more features while preserving the 80-20 balance:

# servers/review.py
review_server = FastMCP("Review Tools")

@review_server.tool
def schedule_review(topic: str, days_later: int = 3) -> str:
    """Schedule a review reminder (spaced repetition)."""
    # Implementation here
    
@review_server.tool
def quiz_me(topic: str) -> dict:
    """Generate a quiz based on past learnings."""
    # Human decides if they understood

2. Connect to AI Clients

Configure for Claude Code, Cursor, or other clients:

// For Claude Code (claude_desktop_config.json)
{
  "mcpServers": {
    "learning-journal": {
      "command": "python",
      "args": ["path/to/_mcp/main.py"]
    }
  }
}

3. Share with the Community

Prepare for PyPI distribution:

# setup.py or pyproject.toml
# Package as learning_journal_mcp
# So others can: from learning_journal_mcp import journal_mcp

4. Explore Advanced Patterns

  • Conditional Loading: Load modules based on environment
  • Authentication: Add user-specific journals
  • HTTP Transport: Deploy as a web service
  • Composition: Combine with other community MCPs

Common Patterns to Remember

Always Test Incrementally

# Test each component separately
async def test_tools(): ...
async def test_resources(): ...
async def test_prompts(): ...

Handle Errors Gracefully

@mcp.tool
def safe_record(topic: str) -> str:
    try:
        # Your logic
    except Exception as e:
        return f"āŒ Could not record: {str(e)}"

Document for Three Audiences

@mcp.tool
def complex_tool(
    data: str,  # For beginners: "Your input text"
    mode: str = "analyze"  # For experts: Processing mode
) -> dict:  # For AI: Returns structured results
    """
    Beginners: This tool helps analyze your data
    Experts: Implements NLP pipeline with configurable modes
    AI Agents: Use mode='summary' for concise output
    """

Troubleshooting

MCP Won't Start

  • Check Python version: python --version (need 3.10+)
  • Verify FastMCP installed: pip show fastmcp
  • Check for import errors: python -c "from fastmcp import FastMCP"

Tools Not Working

  • Ensure function has proper type hints
  • Check return types are JSON-serializable
  • Verify no *args or **kwargs

Resources Return Empty

  • Check file paths are correct
  • Ensure proper permissions
  • Verify Path objects resolve correctly

Summary

Congratulations! You've built your first MCP with:

āœ… Tools for taking action (recording learnings)
āœ… Resources for reading data (journal entries, stats)
āœ… Prompts for structured interaction (reflection)
āœ… Modular architecture ready for growth
āœ… 80-20 philosophy preserving human wisdom

Your Learning Journal MCP is now ready to help you learn while keeping YOU in control of what matters.


Resources


What's Next?

šŸŽ‰ Congratulations! You've successfully built your first Learning Journal MCP. But we're not done yet.

Next Chapter: MCP Testing Strategies (Chapter 2)

Learn how to properly test your Learning Journal MCP with comprehensive pytest strategies, ensuring it's reliable, maintainable, and preserves the 80-20 philosophy.


Remember: The goal isn't to automate learning, but to enhance it. Keep human wisdom at the center of everything you build.