Skip to content
Open
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
25 changes: 25 additions & 0 deletions src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ A FastMCP server providing GitHub Copilot tips and tricks via the Model Context
- πŸ› οΈ **Tools** β€” Search, filter, random tips, delete/reset
- πŸ’¬ **Prompts** β€” Task suggestions, category exploration, learning paths
- 🎯 **Elicitations** β€” Interactive guided discovery with real MCP elicitations
- πŸ“ **Logging** β€” Structured logging to file and console for debugging
- πŸ§ͺ **26 unit tests** with pytest

## Quick Start
Expand Down Expand Up @@ -72,6 +73,30 @@ pytest test_copilot_tips_server.py -v

All 26 tests should pass.

## Logging

The server includes structured logging to help with debugging and monitoring:

- **Log Location**: `logs/mcp_server.log`
- **Log Level**: INFO (configurable via code)
- **Outputs**: Both file and console

Logs include:
- Server initialization
- Tool invocations with parameters
- Search results and patterns
- Errors and warnings
- Tip access patterns

View logs:
```bash
# Tail the log file
tail -f logs/mcp_server.log

# On Windows PowerShell
Get-Content logs/mcp_server.log -Wait -Tail 20
```

## Using the Inspector

### Testing Resources
Expand Down
32 changes: 31 additions & 1 deletion src/copilot_tips_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"""

import json
import logging
import random
from pathlib import Path
from typing import Optional, Literal
Expand All @@ -29,6 +30,22 @@

from fastmcp import FastMCP, Context

# Configure logging
LOG_DIR = Path(__file__).parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
Comment on lines 30 to +35
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log directory path uses Path(__file__).parent / 'logs', which places logs in the src/ directory. Consider using a project root-relative path or making this configurable, as logs are typically stored at the project root or in a dedicated directory outside the source tree.

Suggested change
from fastmcp import FastMCP, Context
# Configure logging
LOG_DIR = Path(__file__).parent / "logs"
LOG_DIR.mkdir(exist_ok=True)
import os
from fastmcp import FastMCP, Context
# Configure logging
# Prefer a project-root-relative logs directory, with optional override via environment variable.
PROJECT_ROOT = Path(__file__).resolve().parent.parent
DEFAULT_LOG_DIR = PROJECT_ROOT / "logs"
LOG_DIR = Path(os.getenv("COPILOT_TIPS_LOG_DIR", str(DEFAULT_LOG_DIR)))
LOG_DIR.mkdir(parents=True, exist_ok=True)

Copilot uses AI. Check for mistakes.

logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(LOG_DIR / "mcp_server.log"),
logging.StreamHandler()
]
)
Comment on lines +37 to +44
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log file will grow indefinitely without rotation. Consider using RotatingFileHandler or TimedRotatingFileHandler from logging.handlers to prevent disk space issues in long-running deployments. For example: RotatingFileHandler(LOG_DIR / 'mcp_server.log', maxBytes=10485760, backupCount=5) for 10MB files with 5 backups.

Copilot uses AI. Check for mistakes.

logger = logging.getLogger("copilot_tips_server")
logger.info("Initializing Copilot Tips MCP Server")

# Initialize FastMCP server
mcp = FastMCP(
name="Copilot Tips Server",
Expand All @@ -41,10 +58,14 @@

def load_tips() -> list[dict]:
"""Load tips from the JSON data file."""
logger.debug(f"Loading tips from {DATA_FILE}")
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using f-strings in log statements creates the formatted string even when the log level would filter it out. Use lazy formatting with logger.debug('Loading tips from %s', DATA_FILE) to avoid unnecessary string construction when DEBUG level is disabled.

Suggested change
logger.debug(f"Loading tips from {DATA_FILE}")
logger.debug("Loading tips from %s", DATA_FILE)

Copilot uses AI. Check for mistakes.
if DATA_FILE.exists():
with open(DATA_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return data.get("tips", [])
tips = data.get("tips", [])
logger.info(f"Loaded {len(tips)} tips from data file")
return tips
logger.warning(f"Tips file not found at {DATA_FILE}")
return []


Expand Down Expand Up @@ -135,15 +156,18 @@ def get_tip_by_id(tip_id: str) -> dict:
Returns:
The tip object if found, or an error message if not found.
"""
logger.info(f"Tool invoked: get_tip_by_id(tip_id={tip_id})")
tips = get_tips_store()

for tip in tips:
if tip["id"].lower() == tip_id.lower():
logger.debug(f"Found tip: {tip['title']}")
return {
"success": True,
"tip": tip
}

logger.warning(f"Tip not found: {tip_id}")
return {
"success": False,
"error": f"Tip with ID '{tip_id}' not found",
Expand All @@ -168,6 +192,7 @@ def get_tip_by_topic(
Returns:
List of matching tips sorted by relevance.
"""
logger.info(f"Tool invoked: get_tip_by_topic(search_term='{search_term}', category={category}, difficulty={difficulty})")
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using f-strings in log statements creates the formatted string even when the log level would filter it out. Use lazy formatting with logger.info('Tool invoked: get_tip_by_topic(search_term=%r, category=%s, difficulty=%s)', search_term, category, difficulty) to avoid unnecessary string construction.

Copilot uses AI. Check for mistakes.
tips = get_tips_store()
search_lower = search_term.lower()

Expand Down Expand Up @@ -211,12 +236,14 @@ def get_tip_by_topic(
matches.sort(key=lambda x: x["relevance"], reverse=True)

if not matches:
logger.info(f"No tips found for search term: '{search_term}'")
return {
"success": False,
"error": f"No tips found matching '{search_term}'",
"suggestion": "Try broader search terms or remove filters"
}

logger.info(f"Found {len(matches)} matching tips for '{search_term}'")
return {
"success": True,
"count": len(matches),
Expand Down Expand Up @@ -280,18 +307,21 @@ def delete_tip(tip_id: str) -> dict:
Returns:
Confirmation of deletion or error if tip not found.
"""
logger.info(f"Tool invoked: delete_tip(tip_id={tip_id})")
tips = get_tips_store()

for i, tip in enumerate(tips):
if tip["id"].lower() == tip_id.lower():
deleted_tip = tips.pop(i)
logger.info(f"Deleted tip: {tip_id} - '{deleted_tip['title']}'")
return {
"success": True,
"message": f"Tip '{tip_id}' deleted successfully",
"deleted_tip": deleted_tip,
"remaining_count": len(tips)
}

logger.warning(f"Cannot delete tip: {tip_id} not found")
return {
"success": False,
"error": f"Tip with ID '{tip_id}' not found"
Expand Down
Loading