Skip to content

Commit 06e0df0

Browse files
authored
Merge pull request #1 from madebygps/main
Add Python MCP demo: expenses tracker with server, data, devcontainer, and docs
2 parents 6b5af07 + 54112f2 commit 06e0df0

File tree

9 files changed

+1362
-0
lines changed

9 files changed

+1362
-0
lines changed

.devcontainer/devcontainer.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "Python MCP Demos",
3+
"image": "mcr.microsoft.com/devcontainers/python:1-3.12-bullseye",
4+
"features": {
5+
"ghcr.io/va-h/devcontainers-features/uv:1": {},
6+
"ghcr.io/devcontainers/features/node:1": { "version": "lts" }
7+
},
8+
"postCreateCommand": "uv sync",
9+
"forwardPorts": [6277, 6274],
10+
"portsAttributes": {
11+
"6277": {
12+
"label": "MCP Proxy Server",
13+
"visibility": "public",
14+
"onAutoForward": "silent"
15+
},
16+
"6274": {
17+
"label": "MCP Inspector UI",
18+
"visibility": "public",
19+
"onAutoForward": "silent"
20+
}
21+
},
22+
"customizations": {
23+
"vscode": {
24+
"extensions": [
25+
"ms-python.python",
26+
"ms-python.vscode-pylance",
27+
"github.copilot",
28+
"github.copilot-chat"
29+
],
30+
"settings": {
31+
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python"
32+
}
33+
}
34+
},
35+
"remoteUser": "vscode"
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [ -n "${CODESPACE_NAME:-}" ]; then
5+
CODESPACE_URL="https://${CODESPACE_NAME}-6274.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
6+
PROXY_URL="https://${CODESPACE_NAME}-6277.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}"
7+
8+
echo "🚀 Launching MCP Inspector..."
9+
echo ""
10+
echo "📋 Configuration for Inspector UI:"
11+
echo " Inspector Proxy Address: $PROXY_URL"
12+
echo ""
13+
14+
ALLOWED_ORIGINS="$CODESPACE_URL" npx -y @modelcontextprotocol/inspector uv run main.py
15+
else
16+
echo "🚀 Launching MCP Inspector..."
17+
npx -y @modelcontextprotocol/inspector
18+
fi

.vscode/mcp.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"servers": {
3+
"expenses-mcp": {
4+
"type": "stdio",
5+
"command": "uv",
6+
"cwd": "${workspaceFolder}",
7+
"args": [
8+
"run",
9+
"main.py"
10+
]
11+
}
12+
},
13+
"inputs": []
14+
}

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Python MCP Demos
2+
3+
This repository implements a **minimal MCP expense tracker**.
4+
5+
The Model Context Protocol (MCP) is an open standard that enables LLMs to connect to external data sources and tools.
6+
7+
## Getting Started
8+
9+
### Environment Setup
10+
11+
#### 1. GitHub Codespaces
12+
13+
1. Click the **Code** button
14+
2. Select the **Codespaces** tab
15+
3. Click **Create codespace on main**
16+
4. Wait for the environment to build (dependencies install automatically)
17+
18+
#### 2. Local VS Code Dev Container
19+
20+
**Requirements:** Docker + VS Code + Dev Containers extension
21+
22+
1. Open the repo in VS Code
23+
2. When prompted, select **Reopen in Container** (or run `Dev Containers: Reopen in Container` from the Command Palette)
24+
3. Wait for the container to build
25+
26+
#### 3. Local Machine Without a Dev Container
27+
28+
If you prefer a plain local environment, use **uv** for dependency management:
29+
30+
```bash
31+
uv sync
32+
```
33+
34+
### Run the MCP Server in VS Code
35+
36+
1. Open `.vscode/mcp.json` in the editor
37+
2. Click the **Start** button (▶) above the server name `expenses-mcp`
38+
3. Confirm in the output panel that the server is running
39+
40+
### GitHub Copilot Chat Integration
41+
42+
Make sure the MCP server is running, then:
43+
44+
1. Open the GitHub Copilot Chat panel (bottom right, or via Command Palette: `GitHub Copilot: Focus Chat`)
45+
2. Click the **Tools** icon (wrench) at the bottom of the chat panel
46+
3. Ensure `expenses-mcp` is selected in the list of available tools
47+
4. Ask Copilot to invoke the tool:
48+
- "Use add_expense to record a $12 lunch today paid with visa"
49+
- "Read the expenses resource"
50+
51+
### MCP Inspector
52+
53+
The MCP Inspector is a browser-based visual testing and debugging tool for MCP servers.
54+
55+
**Launch the inspector in GitHub Codespaces:**
56+
57+
1. Run the following command in the terminal:
58+
```bash
59+
.devcontainer/launch-inspector.sh
60+
```
61+
62+
2. Note the **Inspector Proxy Address** and **Session Token** from the terminal output
63+
64+
3. In the **Ports** view, set port **6277** to **PUBLIC** visibility
65+
66+
4. Access the Inspector UI and configure:
67+
- **Transport Type**: `SSE`
68+
- **Inspector Proxy Address**: (from terminal output)
69+
- **Proxy Session Token**: (from terminal output)
70+
- **Command**: `uv`
71+
- **Arguments**: `run main.py`
72+
73+
**Launch the inspector inside of a Dev Container:**
74+
75+
1. Run the following command in the terminal:
76+
```bash
77+
HOST=0.0.0.0 DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector uv run main.py
78+
```
79+
2. Open `http://localhost:6274` in your browser
80+
3. The Inspector should now connect to your MCP server
81+
82+
> **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.
83+
84+
**Launch the inspector locally without Dev Container:**
85+
86+
1. Run the following command in the terminal:
87+
```bash
88+
npx @modelcontextprotocol/inspector uv run main.py
89+
```
90+
2. The Inspector will automatically open in your browser at `http://localhost:6274`
91+
92+
93+
94+
---
95+
96+
97+
## Contributing
98+
99+
Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.

expenses.csv

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
date,amount,category,description,payment_method
2+
2024-08-01,4.50,food,"Morning coffee",AMEX
3+
2024-08-01,12.99,food,"Lunch sandwich",AMEX
4+
2024-08-01,45.00,transport,"Gas station",VISA
5+
2024-08-02,8.75,food,"Breakfast burrito",CASH
6+
2024-08-02,15.00,transport,"Parking downtown",VISA
7+
2024-08-02,25.99,entertainment,"Movie ticket",AMEX
8+
2024-08-03,6.00,food,"Coffee shop",VISA
9+
2024-08-03,32.50,food,"Grocery shopping",VISA
10+
2024-08-03,89.99,entertainment,"Concert ticket",VISA
11+
2024-08-04,5.25,food,"Snack",CASH
12+
2024-08-04,18.00,transport,"Uber ride",VISA
13+
2024-08-04,42.00,food,"Dinner",VISA
14+
2024-08-05,7.50,food,"Coffee and pastry",AMEX
15+
2024-08-05,125.00,shopping,"New shoes",CASH
16+
2024-08-05,22.99,entertainment,"Streaming service",CASH
17+
2025-08-05,50.0,shopping,T-shirt,CASH
18+
2025-08-10,50.0,shopping,phone case,VISA
19+
2025-08-27,50.0,gadget,phone case,AMEX
20+
2025-10-25,50.0,shopping,stuff,AMEX

main.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import csv
2+
import logging
3+
from datetime import date
4+
from enum import Enum
5+
from pathlib import Path
6+
from typing import Annotated
7+
8+
from fastmcp import FastMCP
9+
10+
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(message)s")
11+
logger = logging.getLogger("ExpensesMCP")
12+
13+
14+
SCRIPT_DIR = Path(__file__).parent
15+
EXPENSES_FILE = SCRIPT_DIR / "expenses.csv"
16+
17+
18+
mcp = FastMCP("Expenses Tracker")
19+
20+
21+
class PaymentMethod(Enum):
22+
AMEX = "amex"
23+
VISA = "visa"
24+
CASH = "cash"
25+
26+
27+
class Category(Enum):
28+
FOOD = "food"
29+
TRANSPORT = "transport"
30+
ENTERTAINMENT = "entertainment"
31+
SHOPPING = "shopping"
32+
GADGET = "gadget"
33+
OTHER = "other"
34+
35+
36+
@mcp.tool
37+
async def add_expense(
38+
date: Annotated[date, "Date of the expense in YYYY-MM-DD format"],
39+
amount: Annotated[float, "Positive numeric amount of the expense"],
40+
category: Annotated[Category, "Category label"],
41+
description: Annotated[str, "Human-readable description of the expense"],
42+
payment_method: Annotated[PaymentMethod, "Payment method used"],
43+
):
44+
"""Add a new expense to the expenses.csv file."""
45+
if amount <= 0:
46+
return "Error: Amount must be positive"
47+
48+
date_iso = date.isoformat()
49+
logger.info(f"Adding expense: ${amount} for {description} on {date_iso}")
50+
51+
try:
52+
file_exists = EXPENSES_FILE.exists()
53+
54+
with open(EXPENSES_FILE, "a", newline="", encoding="utf-8") as file:
55+
writer = csv.writer(file)
56+
57+
if not file_exists:
58+
writer.writerow(
59+
["date", "amount", "category", "description", "payment_method"]
60+
)
61+
62+
writer.writerow(
63+
[date_iso, amount, category.value, description, payment_method.name]
64+
)
65+
66+
return f"Successfully added expense: ${amount} for {description} on {date_iso}"
67+
68+
except Exception as e:
69+
logger.error(f"Error adding expense: {str(e)}")
70+
return "Error: Unable to add expense"
71+
72+
73+
@mcp.resource("resource://expenses")
74+
async def get_expenses_data():
75+
"""Get raw expense data from CSV file"""
76+
logger.info("Expenses data accessed")
77+
78+
try:
79+
with open(EXPENSES_FILE, "r", newline="", encoding="utf-8") as file:
80+
reader = csv.DictReader(file)
81+
expenses_data = list(reader)
82+
83+
csv_content = f"Expense data ({len(expenses_data)} entries):\n\n"
84+
for expense in expenses_data:
85+
csv_content += (
86+
f"Date: {expense['date']}, "
87+
f"Amount: ${expense['amount']}, "
88+
f"Category: {expense['category']}, "
89+
f"Description: {expense['description']}, "
90+
f"Payment: {expense['payment_method']}\n"
91+
)
92+
93+
return csv_content
94+
95+
except FileNotFoundError:
96+
logger.error("Expenses file not found")
97+
return "Error: Expense data unavailable"
98+
except Exception as e:
99+
logger.error(f"Error reading expenses: {str(e)}")
100+
return "Error: Unable to retrieve expense data"
101+
102+
103+
@mcp.prompt
104+
def create_expense_prompt(
105+
date: str,
106+
amount: float,
107+
category: str,
108+
description: str,
109+
payment_method: str
110+
) -> str:
111+
112+
"""Generate a prompt to add a new expense using the add_expense tool."""
113+
114+
logger.info(f"Expense prompt created for: {description}")
115+
116+
return f"""
117+
Please add the following expense:
118+
- Date: {date}
119+
- Amount: ${amount}
120+
- Category: {category}
121+
- Description: {description}
122+
- Payment Method: {payment_method}
123+
Use the `add_expense` tool to record this transaction.
124+
"""
125+
126+
127+
if __name__ == "__main__":
128+
logger.info("MCP Expenses server starting")
129+
mcp.run()

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[project]
2+
name = "python-mcp-demos"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.13"
7+
dependencies = [
8+
"fastmcp>=2.12.5",
9+
]

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
fastmcp>=2.12.5

0 commit comments

Comments
 (0)