Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "Python MCP Demos",
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
"features": {
"ghcr.io/va-h/devcontainers-features/uv:1": {},
"ghcr.io/devcontainers/features/node:1": { "version": "lts" }
},
"postCreateCommand": "uv sync",
"forwardPorts": [6277, 6274],
"portsAttributes": {
"6277": {
"label": "MCP Proxy Server",
"visibility": "public",
"onAutoForward": "silent"
},
"6274": {
"label": "MCP Inspector UI",
"visibility": "public",
"onAutoForward": "silent"
}
},
"customizations": {
"vscode": {
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"github.copilot",
"github.copilot-chat"
],
"settings": {
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
}
}
},
"remoteUser": "vscode"
}
18 changes: 18 additions & 0 deletions .devcontainer/launch-inspector-codespace.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/usr/bin/env bash
set -euo pipefail

if [ -n "${CODESPACE_NAME:-}" ]; then
CODESPACE_URL="https://${CODESPACE_NAME}-6274.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
PROXY_URL="https://${CODESPACE_NAME}-6277.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"

echo "🚀 Launching MCP Inspector..."
echo ""
echo "📋 Configuration for Inspector UI:"
echo " Inspector Proxy Address: $PROXY_URL"
echo ""

ALLOWED_ORIGINS="$CODESPACE_URL" npx -y @modelcontextprotocol/inspector uv run main.py
else
echo "🚀 Launching MCP Inspector..."
npx -y @modelcontextprotocol/inspector
fi
14 changes: 14 additions & 0 deletions .vscode/mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"servers": {
"expenses-mcp": {
"type": "stdio",
"command": "uv",
"cwd": "${workspaceFolder}",
"args": [
"run",
"main.py"
]
}
},
"inputs": []
}
99 changes: 99 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Python MCP Demos

This repository implements a **minimal MCP expense tracker**.

The Model Context Protocol (MCP) is an open standard that enables LLMs to connect to external data sources and tools.

## Getting Started

### Environment Setup

#### 1. GitHub Codespaces

1. Click the **Code** button
2. Select the **Codespaces** tab
3. Click **Create codespace on main**
4. Wait for the environment to build (dependencies install automatically)

#### 2. Local VS Code Dev Container

**Requirements:** Docker + VS Code + Dev Containers extension

1. Open the repo in VS Code
2. When prompted, select **Reopen in Container** (or run `Dev Containers: Reopen in Container` from the Command Palette)
3. Wait for the container to build

#### 3. Local Machine Without a Dev Container

If you prefer a plain local environment, use **uv** for dependency management:

```bash
uv sync
```

### Run the MCP Server in VS Code

1. Open `.vscode/mcp.json` in the editor
2. Click the **Start** button (▶) above the server name `expenses-mcp`
3. Confirm in the output panel that the server is running

### GitHub Copilot Chat Integration

Make sure the MCP server is running, then:

1. Open the GitHub Copilot Chat panel (bottom right, or via Command Palette: `GitHub Copilot: Focus Chat`)
2. Click the **Tools** icon (wrench) at the bottom of the chat panel
3. Ensure `expenses-mcp` is selected in the list of available tools
4. Ask Copilot to invoke the tool:
- "Use add_expense to record a $12 lunch today paid with visa"
- "Read the expenses resource"

### MCP Inspector

The MCP Inspector is a browser-based visual testing and debugging tool for MCP servers.

**Launch the inspector in GitHub Codespaces:**

1. Run the following command in the terminal:
```bash
.devcontainer/launch-inspector.sh
```

2. Note the **Inspector Proxy Address** and **Session Token** from the terminal output

3. In the **Ports** view, set port **6277** to **PUBLIC** visibility

4. Access the Inspector UI and configure:
- **Transport Type**: `SSE`
- **Inspector Proxy Address**: (from terminal output)
- **Proxy Session Token**: (from terminal output)
- **Command**: `uv`
- **Arguments**: `run main.py`

**Launch the inspector inside of a Dev Container:**

1. Run the following command in the terminal:
```bash
HOST=0.0.0.0 DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector uv run main.py
```
2. Open `http://localhost:6274` in your browser
3. The Inspector should now connect to your MCP server

> **Note:** `HOST=0.0.0.0` is required in devcontainer environments to bind the Inspector to all network interfaces, allowing proper communication between the UI and proxy server. `DANGEROUSLY_OMIT_AUTH=true` disables authentication - only use in trusted development environments.

**Launch the inspector locally without Dev Container:**

1. Run the following command in the terminal:
```bash
npx @modelcontextprotocol/inspector uv run main.py
```
2. The Inspector will automatically open in your browser at `http://localhost:6274`



---


## Contributing

Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.
20 changes: 20 additions & 0 deletions expenses.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
date,amount,category,description,payment_method
2024-08-01,4.50,food,"Morning coffee",AMEX
2024-08-01,12.99,food,"Lunch sandwich",AMEX
2024-08-01,45.00,transport,"Gas station",VISA
2024-08-02,8.75,food,"Breakfast burrito",CASH
2024-08-02,15.00,transport,"Parking downtown",VISA
2024-08-02,25.99,entertainment,"Movie ticket",AMEX
2024-08-03,6.00,food,"Coffee shop",VISA
2024-08-03,32.50,food,"Grocery shopping",VISA
2024-08-03,89.99,entertainment,"Concert ticket",VISA
2024-08-04,5.25,food,"Snack",CASH
2024-08-04,18.00,transport,"Uber ride",VISA
2024-08-04,42.00,food,"Dinner",VISA
2024-08-05,7.50,food,"Coffee and pastry",AMEX
2024-08-05,125.00,shopping,"New shoes",CASH
2024-08-05,22.99,entertainment,"Streaming service",CASH
2025-08-05,50.0,shopping,T-shirt,CASH
2025-08-10,50.0,shopping,phone case,VISA
2025-08-27,50.0,gadget,phone case,AMEX
2025-10-25,50.0,shopping,stuff,AMEX
129 changes: 129 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import csv
import logging
from datetime import date
from enum import Enum
from pathlib import Path
from typing import Annotated

from fastmcp import FastMCP

logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
logger = logging.getLogger("ExpensesMCP")


SCRIPT_DIR = Path(__file__).parent
EXPENSES_FILE = SCRIPT_DIR / "expenses.csv"


mcp = FastMCP("Expenses Tracker")


class PaymentMethod(Enum):
AMEX = "amex"
VISA = "visa"
CASH = "cash"


class Category(Enum):
FOOD = "food"
TRANSPORT = "transport"
ENTERTAINMENT = "entertainment"
SHOPPING = "shopping"
GADGET = "gadget"
OTHER = "other"


@mcp.tool
async def add_expense(
date: Annotated[date, "Date of the expense in YYYY-MM-DD format"],
amount: Annotated[float, "Positive numeric amount of the expense"],
category: Annotated[Category, "Category label"],
description: Annotated[str, "Human-readable description of the expense"],
payment_method: Annotated[PaymentMethod, "Payment method used"],
):
"""Add a new expense to the expenses.csv file."""
if amount <= 0:
return "Error: Amount must be positive"

date_iso = date.isoformat()
logger.info(f"Adding expense: ${amount} for {description} on {date_iso}")

try:
file_exists = EXPENSES_FILE.exists()

with open(EXPENSES_FILE, "a", newline="", encoding="utf-8") as file:
writer = csv.writer(file)

if not file_exists:
writer.writerow(
["date", "amount", "category", "description", "payment_method"]
)

writer.writerow(
[date_iso, amount, category.value, description, payment_method.name]
)

return f"Successfully added expense: ${amount} for {description} on {date_iso}"

except Exception as e:
logger.error(f"Error adding expense: {str(e)}")
return "Error: Unable to add expense"


@mcp.resource("resource://expenses")
async def get_expenses_data():
"""Get raw expense data from CSV file"""
logger.info("Expenses data accessed")

try:
with open(EXPENSES_FILE, "r", newline="", encoding="utf-8") as file:
reader = csv.DictReader(file)
expenses_data = list(reader)

csv_content = f"Expense data ({len(expenses_data)} entries):\n\n"
for expense in expenses_data:
csv_content += (
f"Date: {expense['date']}, "
f"Amount: ${expense['amount']}, "
f"Category: {expense['category']}, "
f"Description: {expense['description']}, "
f"Payment: {expense['payment_method']}\n"
)

return csv_content

except FileNotFoundError:
logger.error("Expenses file not found")
return "Error: Expense data unavailable"
except Exception as e:
logger.error(f"Error reading expenses: {str(e)}")
return "Error: Unable to retrieve expense data"


@mcp.prompt
def create_expense_prompt(
date: str,
amount: float,
category: str,
description: str,
payment_method: str
) -> str:

"""Generate a prompt to add a new expense using the add_expense tool."""

logger.info(f"Expense prompt created for: {description}")

return f"""
Please add the following expense:
- Date: {date}
- Amount: ${amount}
- Category: {category}
- Description: {description}
- Payment Method: {payment_method}
Use the `add_expense` tool to record this transaction.
"""


if __name__ == "__main__":
logger.info("MCP Expenses server starting")
mcp.run()
9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[project]
name = "python-mcp-demos"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"fastmcp>=2.12.5",
]
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fastmcp>=2.12.5
Loading