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
37 changes: 32 additions & 5 deletions mcp_starter/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,31 @@

from __future__ import annotations

from typing import Annotated

from mcp.server.fastmcp import FastMCP
from pydantic import Field


def register_prompts(mcp: FastMCP) -> None:
"""Register all prompts with the MCP server."""
"""Register all prompts with the MCP server.

Note: The Python MCP SDK's PromptArgument model does not support a 'title' field,
only 'name', 'description', and 'required'. This is a limitation of the SDK compared
to the canonical MCP interface. The 'description' field is used for both purposes.
"""

@mcp.prompt(
title="Greeting Prompt",
description="Generate a greeting in a specific style",
description="Generate a greeting message",
)
def greet(name: str, style: str = "casual") -> str:
def greet(
name: Annotated[str, Field(title="Name", description="Name of the person to greet")],
style: Annotated[
str,
Field(title="Style", description="Greeting style (formal/casual)"),
] = "casual",
) -> str:
"""Generate a greeting prompt.

Args:
Expand All @@ -28,9 +42,22 @@ def greet(name: str, style: str = "casual") -> str:

@mcp.prompt(
title="Code Review",
description="Request a code review with specific focus areas",
description="Review code for potential improvements",
)
def code_review(code: str, language: str, focus: str = "all") -> str:
def code_review(
code: Annotated[str, Field(title="Code", description="The code to review")],
language: Annotated[
str,
Field(title="Language", description="Programming language of the code"),
] = "python",
focus: Annotated[
str,
Field(
title="Focus",
description="What to focus on (security, performance, readability, or all)",
),
] = "all",
) -> str:
"""Generate a code review prompt.

Args:
Expand Down
28 changes: 19 additions & 9 deletions mcp_starter/resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
def register_resources(mcp: FastMCP) -> None:
"""Register all resources and templates with the MCP server."""

@mcp.resource("info://about")
@mcp.resource("about://server", name="About", description="Information about this MCP server")
def about_resource() -> str:
"""Information about this MCP server."""
return """MCP Python Starter v1.0.0
Expand All @@ -31,7 +31,9 @@ def about_resource() -> str:

For more information, visit: https://modelcontextprotocol.io"""

@mcp.resource("file://example.md")
@mcp.resource(
"doc://example", name="Example Document", description="An example document resource"
)
def example_file() -> str:
"""An example markdown document."""
return """# Example Document
Expand All @@ -54,7 +56,11 @@ def example_file() -> str:
- [Python SDK](https://github.com/modelcontextprotocol/python-sdk)
"""

@mcp.resource("greeting://{name}")
@mcp.resource(
"greeting://{name}",
name="Personalized Greeting",
description="A personalized greeting for a specific person",
)
def greeting_template(name: str) -> str:
"""Generate a personalized greeting.

Expand All @@ -63,14 +69,18 @@ def greeting_template(name: str) -> str:
"""
return f"Hello, {name}! This greeting was generated just for you."

@mcp.resource("data://items/{item_id}")
def item_data(item_id: str) -> str:
@mcp.resource(
"item://{id}",
name="Item Data",
description="Data for a specific item by ID",
)
def item_data(id: str) -> str:
"""Get data for a specific item by ID.

Args:
item_id: The item ID to look up
id: The item ID to look up
"""
item = ITEMS_DATA.get(item_id)
item = ITEMS_DATA.get(id)
if not item:
raise ValueError(f"Item not found: {item_id}")
return json.dumps({"id": item_id, **item}, indent=2)
raise ValueError(f"Item not found: {id}")
return json.dumps({"id": id, **item}, indent=2)
6 changes: 3 additions & 3 deletions mcp_starter/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,10 @@

## Available Resources

- **info://about**: Server information
- **file://example.md**: Sample markdown document
- **about://server**: Server information
- **doc://example**: Sample markdown document
- **greeting://{name}**: Personalized greeting template
- **data://items/{item_id}**: Item data by ID
- **item://{id}**: Item data by ID

## Available Prompts

Expand Down
87 changes: 39 additions & 48 deletions mcp_starter/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,12 @@

import asyncio
import random
from typing import Any, Literal
from typing import Annotated, Any, Literal

from mcp.server.fastmcp import Context, FastMCP
from mcp.server.session import ServerSession
from mcp.types import Icon, ToolAnnotations
from pydantic import Field

from .icons import (
ABACUS_ICON,
Expand Down Expand Up @@ -67,12 +68,10 @@ def register_tools(mcp: FastMCP) -> None:
),
],
)
def hello(name: str) -> str:
"""A friendly greeting tool that says hello to someone.

Args:
name: The name to greet
"""
def hello(
name: Annotated[str, Field(title="Name", description="Name of the person to greet")],
) -> str:
"""Say hello to a person"""
return f"Hello, {name}! Welcome to MCP."

@mcp.tool(
Expand All @@ -91,15 +90,13 @@ def hello(name: str) -> str:
),
],
)
def get_weather(location: str) -> dict[str, Any]:
"""Get current weather for a location (simulated).

Args:
location: City name or coordinates
"""
def get_weather(
city: Annotated[str, Field(title="City", description="City name to get weather for")],
) -> dict[str, Any]:
"""Get the current weather for a city"""
conditions = ["sunny", "cloudy", "rainy", "windy"]
return {
"location": location,
"location": city,
"temperature": round(15 + random.random() * 20),
"unit": "celsius",
"conditions": random.choice(conditions),
Expand All @@ -123,16 +120,15 @@ def get_weather(location: str) -> dict[str, Any]:
],
)
async def ask_llm(
prompt: str,
prompt: Annotated[
str, Field(title="Prompt", description="The question or prompt to send to the LLM")
],
ctx: Context[ServerSession, None],
max_tokens: int = 100,
maxTokens: Annotated[
int, Field(title="Max Tokens", description="Maximum tokens in response")
] = 100,
) -> str:
"""Ask the connected LLM a question using sampling.

Args:
prompt: The question or prompt for the LLM
max_tokens: Maximum tokens in response
"""
"""Ask the connected LLM a question using sampling"""
try:
result = await ctx.session.create_message(
messages=[
Expand All @@ -141,7 +137,7 @@ async def ask_llm(
"content": {"type": "text", "text": prompt},
}
],
max_tokens=max_tokens,
max_tokens=maxTokens,
)
if result.content.type == "text":
return f"LLM Response: {result.content.text}"
Expand All @@ -166,17 +162,12 @@ async def ask_llm(
],
)
async def long_task(
task_name: str,
taskName: Annotated[str, Field(title="Task Name", description="Name for this task")],
ctx: Context[ServerSession, None],
steps: Annotated[int, Field(title="Steps", description="Number of steps to simulate")] = 5,
) -> str:
"""A task that takes 5 seconds and reports progress along the way.

Args:
task_name: Name for this task
"""
steps = 5

await ctx.info(f"Starting task: {task_name}")
"""Simulate a long-running task with progress updates"""
await ctx.info(f"Starting task: {taskName}")

for i in range(steps):
await ctx.report_progress(
Expand All @@ -188,7 +179,7 @@ async def long_task(

await ctx.report_progress(progress=1.0, total=1.0, message="Complete!")

return f'Task "{task_name}" completed successfully after {steps} steps!'
return f'Task "{taskName}" completed successfully after {steps} steps!'

@mcp.tool(
annotations=ToolAnnotations(
Expand All @@ -207,7 +198,7 @@ async def long_task(
],
)
async def load_bonus_tool(ctx: Context[ServerSession, None]) -> str:
"""Dynamically loads a bonus tool that wasn't available at startup."""
"""Dynamically register a new bonus tool"""
global _bonus_tool_loaded

if _bonus_tool_loaded:
Expand Down Expand Up @@ -284,14 +275,16 @@ def bonus_calculator(a: float, b: float, operation: Operation) -> str:
),
)
async def confirm_action(
action: str,
action: Annotated[
str, Field(title="Action", description="Description of the action to confirm")
],
ctx: Context[ServerSession, None],
destructive: Annotated[
bool,
Field(title="Destructive", description="Whether the action is destructive"),
] = False,
) -> str:
"""Demonstrates elicitation - requests user confirmation before proceeding.

Args:
action: The action to confirm with the user
"""
"""Request user confirmation before proceeding"""
try:
# Form elicitation: Display a structured form with typed fields
# The client renders this as a dialog/form based on the JSON schema
Expand Down Expand Up @@ -338,17 +331,15 @@ async def confirm_action(
),
)
async def get_feedback(
question: Annotated[
str, Field(title="Question", description="The question to ask the user")
],
ctx: Context[ServerSession, None],
topic: str = "",
) -> str:
"""Demonstrates URL elicitation - opens a feedback form in the browser.

Args:
topic: Optional topic for the feedback
"""
"""Request feedback from the user"""
feedback_url = "https://github.com/SamMorrowDrums/mcp-starters/issues/new?template=workshop-feedback.yml"
if topic:
feedback_url += f"&title={topic}"
if question:
feedback_url += f"&title={question}"

try:
# URL elicitation: Open a web page in the user's browser
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "RUF"]
ignore = ["E501"]
ignore = ["E501", "N803"] # N803: Allow camelCase for MCP cross-language consistency

[tool.ruff.format]
quote-style = "double"
Expand Down