Skip to content

Commit 545192b

Browse files
committed
Add MCP server
1 parent f226cd3 commit 545192b

File tree

6 files changed

+933
-134
lines changed

6 files changed

+933
-134
lines changed

src/README.md

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Copilot Tips MCP Server
2+
3+
A FastMCP server providing GitHub Copilot tips and tricks via the Model Context Protocol. Built for O'Reilly teaching demonstrations.
4+
5+
## Features
6+
7+
- 📚 **46 curated tips** across 6 categories (Prompting, Shortcuts, Code Gen, Chat, Context, Security)
8+
- 🔍 **Resources** — List categories and stats
9+
- 🛠️ **Tools** — Search, filter, random tips, delete/reset
10+
- 💬 **Prompts** — Task suggestions, category exploration, learning paths
11+
- 🎯 **Elicitations** — Interactive guided discovery with real MCP elicitations
12+
- 🧪 **26 unit tests** with pytest
13+
14+
## Quick Start
15+
16+
### 1. Setup Environment
17+
18+
```powershell
19+
# Windows (PowerShell 7+)
20+
cd src
21+
pwsh .\setup.ps1
22+
```
23+
24+
```bash
25+
# macOS/Linux
26+
cd src
27+
chmod +x setup.sh
28+
./setup.sh
29+
```
30+
31+
This creates a `.venv` virtual environment and installs dependencies.
32+
33+
### 2. Run the Server
34+
35+
```bash
36+
python copilot_tips_server.py
37+
```
38+
39+
The server runs via stdio transport (standard MCP protocol).
40+
41+
### 3. Test with MCP Inspector
42+
43+
The MCP Inspector provides a web UI for testing your server interactively:
44+
45+
```bash
46+
python start_inspector.py
47+
```
48+
49+
This will:
50+
- ✅ Check for Node.js 18+ (shows version)
51+
- ✅ Verify the port is actually available before using it
52+
- ✅ Validate server script and Python executable exist
53+
- 🚀 Launch the inspector web UI
54+
- 🔗 Connect to your MCP server automatically
55+
56+
Open the URL shown in the terminal (e.g., `http://localhost:52341`).
57+
58+
### 4. Run Tests
59+
60+
```bash
61+
pytest test_copilot_tips_server.py -v
62+
```
63+
64+
All 26 tests should pass.
65+
66+
## Using the Inspector
67+
68+
### Testing Resources
69+
70+
1. Click **Resources** tab
71+
2. Click `tips://categories` to see all tip categories
72+
3. Click `tips://stats` to see tip counts by category and difficulty
73+
74+
### Testing Tools
75+
76+
1. Click **Tools** tab
77+
2. Select a tool and fill in parameters:
78+
79+
| Tool | Parameters | Description |
80+
|------|------------|-------------|
81+
| `get_tip_by_id` | `tip_id`: e.g., `prompt-001` | Get specific tip |
82+
| `get_tip_by_topic` | `search_term`: e.g., `chat` | Search tips |
83+
| `get_random_tip` | `category`, `difficulty` (optional) | Random tip |
84+
| `delete_tip` | `tip_id` | Delete tip (in-memory) |
85+
| `reset_tips` | (none) | Restore deleted tips |
86+
| `interactive_tip_finder` | (uses elicitation) | Guided tip discovery |
87+
| `guided_random_tip` | (uses elicitation) | Random tip with prompts |
88+
89+
3. Click **Run** to execute
90+
91+
**Note**: The `interactive_tip_finder` and `guided_random_tip` tools use MCP elicitations to prompt you for input. These require client support for elicitations.
92+
93+
### Testing Prompts
94+
95+
1. Click **Prompts** tab
96+
2. Select a prompt template:
97+
- `tip_suggestion` — Get tips for a specific task
98+
- `category_explorer` — Deep dive into a category
99+
- `learning_path` — Personalized learning plan
100+
101+
## VS Code Integration
102+
103+
The server is pre-configured for VS Code:
104+
105+
- **Debug**: Press `F5` and select "🚀 Run MCP Server"
106+
- **Tasks**: `Ctrl+Shift+P` → "Tasks: Run Task" → choose setup/run tasks
107+
- **MCP Client**: The `.vscode/mcp.json` configures the server for Copilot Chat
108+
109+
### Auto-Activate Virtual Environment
110+
111+
VS Code automatically activates the venv in new terminals. If not working:
112+
1. `Ctrl+Shift+P` → "Python: Select Interpreter"
113+
2. Choose `src/.venv/Scripts/python.exe`
114+
3. Reload window
115+
116+
## Project Structure
117+
118+
```
119+
src/
120+
├── copilot_tips_server.py # Main FastMCP server (resources, tools, prompts)
121+
├── test_copilot_tips_server.py # Pytest test suite (26 tests)
122+
├── start_inspector.py # Robust inspector launcher
123+
├── setup.ps1 # Windows setup script (pwsh)
124+
├── setup.sh # Unix setup script
125+
├── pyproject.toml # Python project config
126+
├── README.md # This file
127+
└── data/
128+
└── copilot_tips.json # Tips database (46 tips, 6 categories)
129+
```
130+
131+
## Data Format
132+
133+
Tips are stored in `data/copilot_tips.json`:
134+
135+
```json
136+
{
137+
"tips": [
138+
{
139+
"id": "prompt-001",
140+
"title": "Provide Top-Level Comments",
141+
"description": "Add a high-level comment...",
142+
"category": "Prompting Techniques",
143+
"difficulty": "beginner"
144+
}
145+
]
146+
}
147+
```
148+
149+
**Categories**: Prompting Techniques, IDE Shortcuts, Code Generation, Chat Features, Workspace Context, Security & Privacy
150+
151+
**Difficulty levels**: beginner, intermediate, advanced
152+
153+
## Troubleshooting
154+
155+
### "Node.js/npx not found"
156+
Install Node.js 18+ from https://nodejs.org/ and restart your terminal. The inspector shows detailed error messages if npx isn't working.
157+
158+
### Import errors in VS Code
159+
The Python extension may not detect the venv. Select the interpreter manually or reload the window.
160+
161+
### "Port already in use"
162+
The inspector now verifies port availability before use. If issues persist, close other inspector instances or processes using ports in the 49152-65535 range.
163+
164+
### Inspector exits immediately
165+
Check the terminal output for error details. Common issues:
166+
- Server script not found (verify `copilot_tips_server.py` exists)
167+
- Python venv not created (run `setup.ps1` or `setup.sh`)
168+
- Node.js version too old (need 18+)
169+
170+
## Requirements
171+
172+
- Python 3.10+
173+
- Node.js 18+ (for MCP Inspector)
174+
- PowerShell 7+ (Windows) or Bash (Unix)
175+
176+
## Architecture
177+
178+
```
179+
┌─────────────────┐ stdio ┌──────────────────────┐
180+
│ MCP Inspector │◄──────────────►│ copilot_tips_server │
181+
│ (Web UI) │ │ (FastMCP/Python) │
182+
└─────────────────┘ └──────────┬───────────┘
183+
184+
185+
┌──────────────────────┐
186+
│ data/copilot_tips │
187+
│ .json (46 tips) │
188+
└──────────────────────┘
189+
```
190+
191+
## License
192+
193+
MIT

src/copilot_tips_server.py

Lines changed: 120 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@
2323
import json
2424
import random
2525
from pathlib import Path
26-
from typing import Optional
26+
from typing import Optional, Literal
2727
from collections import Counter
28+
from dataclasses import dataclass
2829

29-
from fastmcp import FastMCP
30+
from fastmcp import FastMCP, Context
3031

3132
# Initialize FastMCP server
3233
mcp = FastMCP(
33-
"Copilot Tips Server",
34-
description="MCP server for GitHub Copilot tips and tricks - O'Reilly teaching example"
34+
name="Copilot Tips Server",
35+
instructions="MCP server for GitHub Copilot tips and tricks. Use tools to search, retrieve, and manage tips. Use resources to get categories and statistics."
3536
)
3637

3738
# Load tips data
@@ -391,67 +392,143 @@ def learning_path(current_skill_level: str) -> str:
391392

392393

393394
# =============================================================================
394-
# ELICITATIONS (Basic examples - client support varies)
395+
# ELICITATIONS - Interactive tools that prompt users for input
395396
# =============================================================================
396397

397-
# Note: Elicitations are a newer MCP feature with limited client support.
398-
# These are included as teaching examples but may fall back to prompts.
398+
# Dataclass for structured tip finder input
399+
@dataclass
400+
class TipFinderPreferences:
401+
"""User preferences for finding tips."""
402+
difficulty: str
403+
category: str
399404

400-
@mcp.prompt()
401-
def interactive_tip_finder() -> str:
405+
406+
@mcp.tool
407+
async def interactive_tip_finder(ctx: Context) -> dict:
402408
"""
403-
Interactive prompt that guides users through finding the right tip.
409+
Interactively gather user preferences and find matching tips.
404410
405-
This serves as a fallback for elicitations where client support is limited.
411+
Uses MCP elicitation to prompt the user for their experience level
412+
and area of interest, then returns relevant tips.
413+
414+
Note: Requires client support for elicitations.
406415
"""
407-
return """Let me help you find the perfect GitHub Copilot tip!
416+
# Step 1: Get difficulty preference
417+
difficulty_result = await ctx.elicit(
418+
message="What's your experience level with GitHub Copilot?",
419+
response_type=Literal["beginner", "intermediate", "advanced"]
420+
)
421+
422+
if difficulty_result.action != "accept":
423+
return {
424+
"success": False,
425+
"message": "Tip finder cancelled - no difficulty selected"
426+
}
408427

409-
Please answer these questions:
428+
difficulty = difficulty_result.data
429+
430+
# Step 2: Get category preference
431+
category_result = await ctx.elicit(
432+
message="Which area would you like tips for?",
433+
response_type=Literal[
434+
"Prompting Techniques",
435+
"IDE Shortcuts",
436+
"Code Generation",
437+
"Chat Features",
438+
"Workspace Context",
439+
"Security & Privacy"
440+
]
441+
)
442+
443+
if category_result.action != "accept":
444+
return {
445+
"success": False,
446+
"message": "Tip finder cancelled - no category selected"
447+
}
410448

411-
1. **What's your experience level?**
412-
- Beginner (just getting started)
413-
- Intermediate (comfortable with basics)
414-
- Advanced (looking for power-user tips)
449+
category = category_result.data
415450

416-
2. **What area are you interested in?**
417-
- Prompting Techniques (writing better prompts)
418-
- IDE Shortcuts (keyboard efficiency)
419-
- Code Generation (getting better code output)
420-
- Chat Features (using Copilot Chat effectively)
421-
- Workspace Context (leveraging project context)
422-
- Security & Privacy (safe Copilot usage)
451+
# Step 3: Find matching tips
452+
tips = get_tips_store()
453+
matching = [
454+
t for t in tips
455+
if t["difficulty"] == difficulty and t["category"] == category
456+
]
423457

424-
3. **What's your specific goal right now?**
425-
(Describe what you're trying to accomplish)
458+
if not matching:
459+
# Fall back to just category match
460+
matching = [t for t in tips if t["category"] == category]
426461

427-
Once you answer, I'll search for the most relevant tips and provide personalized recommendations!"""
462+
return {
463+
"success": True,
464+
"preferences": {
465+
"difficulty": difficulty,
466+
"category": category
467+
},
468+
"tips": matching[:5],
469+
"total_matches": len(matching)
470+
}
428471

429472

430-
@mcp.prompt()
431-
def quiz_me(category: Optional[str] = None) -> str:
473+
@mcp.tool
474+
async def guided_random_tip(ctx: Context) -> dict:
432475
"""
433-
Generate a quiz prompt to test knowledge of Copilot tips.
476+
Get a random tip with optional guided filtering via elicitation.
434477
435-
Args:
436-
category: Optional category to focus the quiz on
478+
Prompts the user to optionally filter by category before
479+
returning a random tip.
437480
438-
Returns:
439-
A formatted quiz prompt.
481+
Note: Requires client support for elicitations.
440482
"""
441-
category_text = f' in the "{category}" category' if category else ""
483+
# Ask if user wants to filter
484+
filter_result = await ctx.elicit(
485+
message="Would you like to filter tips by category, or get a completely random tip?",
486+
response_type=Literal["filter by category", "completely random"]
487+
)
442488

443-
return f"""Let's test your GitHub Copilot knowledge{category_text}!
489+
if filter_result.action != "accept":
490+
return {
491+
"success": False,
492+
"message": "Cancelled"
493+
}
444494

445-
I'll ask you questions about best practices and tips. For each question:
446-
1. I'll describe a scenario
447-
2. You tell me which tip or technique would be most helpful
448-
3. I'll provide feedback and explain the best approach
495+
tips = get_tips_store()
449496

450-
Ready? Let's start with an easy one!
497+
if filter_result.data == "filter by category":
498+
# Get category preference
499+
category_result = await ctx.elicit(
500+
message="Select a category:",
501+
response_type=Literal[
502+
"Prompting Techniques",
503+
"IDE Shortcuts",
504+
"Code Generation",
505+
"Chat Features",
506+
"Workspace Context",
507+
"Security & Privacy"
508+
]
509+
)
510+
511+
if category_result.action != "accept":
512+
return {
513+
"success": False,
514+
"message": "Category selection cancelled"
515+
}
451516

452-
**Question 1:** You're writing a new function but Copilot's suggestions don't match what you need. What's the FIRST thing you should try?
517+
tips = [t for t in tips if t["category"] == category_result.data]
453518

454-
(Answer, and I'll give you feedback and move to the next question!)"""
519+
if not tips:
520+
return {
521+
"success": False,
522+
"error": f"No tips in category '{category_result.data}'"
523+
}
524+
525+
selected = random.choice(tips)
526+
527+
return {
528+
"success": True,
529+
"tip": selected,
530+
"pool_size": len(tips)
531+
}
455532

456533

457534
# =============================================================================

0 commit comments

Comments
 (0)