Move template to flat app pattern
This commit is contained in:
parent
e5c6c63923
commit
35730e4a16
2 changed files with 127 additions and 152 deletions
|
|
@ -6,173 +6,148 @@ from mcp.server import NotificationOptions, Server
|
||||||
from pydantic import AnyUrl
|
from pydantic import AnyUrl
|
||||||
import mcp.server.stdio
|
import mcp.server.stdio
|
||||||
|
|
||||||
|
# Store notes as a simple key-value dict to demonstrate state management
|
||||||
|
notes: dict[str, str] = {}
|
||||||
|
|
||||||
class McpServer(Server):
|
server = Server("{{server_name}}")
|
||||||
|
|
||||||
|
@server.list_resources()
|
||||||
|
async def handle_list_resources() -> list[types.Resource]:
|
||||||
"""
|
"""
|
||||||
Example MCP server demonstrating the three core primitives:
|
List available note resources.
|
||||||
1. Resources - Simple key-value notes storage accessed via custom URIs
|
Each note is exposed as a resource with a custom note:// URI scheme.
|
||||||
2. Prompts - Configurable summarization prompt for notes
|
|
||||||
3. Tools - Ability to add new notes programmatically
|
|
||||||
|
|
||||||
This server shows how to:
|
|
||||||
- Implement resource listing and reading with custom URI schemes
|
|
||||||
- Create parameterized prompts that can access server state
|
|
||||||
- Define tools with JSON schema validation
|
|
||||||
- Send notifications when server state changes
|
|
||||||
"""
|
"""
|
||||||
|
return [
|
||||||
|
types.Resource(
|
||||||
|
uri=AnyUrl(f"note://internal/{name}"),
|
||||||
|
name=f"Note: {name}",
|
||||||
|
description=f"A simple note named {name}",
|
||||||
|
mimeType="text/plain",
|
||||||
|
)
|
||||||
|
for name in notes
|
||||||
|
]
|
||||||
|
|
||||||
def __init__(self):
|
@server.read_resource()
|
||||||
super().__init__("{{server_name}}")
|
async def handle_read_resource(uri: AnyUrl) -> str:
|
||||||
|
"""
|
||||||
|
Read a specific note's content by its URI.
|
||||||
|
The note name is extracted from the URI host component.
|
||||||
|
"""
|
||||||
|
if uri.scheme != "note":
|
||||||
|
raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
|
||||||
|
|
||||||
# Store notes as a simple key-value dict to demonstrate state management
|
name = uri.path
|
||||||
self.notes: dict[str, str] = {}
|
if name is not None:
|
||||||
|
name = name.lstrip("/")
|
||||||
|
return notes[name]
|
||||||
|
raise ValueError(f"Note not found: {name}")
|
||||||
|
|
||||||
# RESOURCE HANDLERS
|
@server.list_prompts()
|
||||||
# Resources allow clients to discover and read content from the server
|
async def handle_list_prompts() -> list[types.Prompt]:
|
||||||
@self.list_resources()
|
"""
|
||||||
async def handle_list_resources() -> list[types.Resource]:
|
List available prompts.
|
||||||
"""
|
Each prompt can have optional arguments to customize its behavior.
|
||||||
List available note resources.
|
"""
|
||||||
Each note is exposed as a resource with a custom note:// URI scheme.
|
return [
|
||||||
"""
|
types.Prompt(
|
||||||
return [
|
name="summarize-notes",
|
||||||
types.Resource(
|
description="Creates a summary of all notes",
|
||||||
uri=AnyUrl(f"note:///{name}"),
|
arguments=[
|
||||||
name=f"Note: {name}",
|
types.PromptArgument(
|
||||||
description=f"A simple note named {name}",
|
name="style",
|
||||||
mimeType="text/plain",
|
description="Style of the summary (brief/detailed)",
|
||||||
|
required=False,
|
||||||
)
|
)
|
||||||
for name in self.notes
|
],
|
||||||
]
|
)
|
||||||
|
]
|
||||||
|
|
||||||
@self.read_resource()
|
@server.get_prompt()
|
||||||
async def handle_read_resource(uri: AnyUrl) -> str:
|
async def handle_get_prompt(
|
||||||
"""
|
name: str, arguments: dict[str, str] | None
|
||||||
Read a specific note's content by its URI.
|
) -> types.GetPromptResult:
|
||||||
The note name is extracted from the URI host component.
|
"""
|
||||||
"""
|
Generate a prompt by combining arguments with server state.
|
||||||
if uri.scheme != "note":
|
The prompt includes all current notes and can be customized via arguments.
|
||||||
raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
|
"""
|
||||||
|
if name != "summarize-notes":
|
||||||
|
raise ValueError(f"Unknown prompt: {name}")
|
||||||
|
|
||||||
name = uri.path
|
style = (arguments or {}).get("style", "brief")
|
||||||
if name is not None:
|
detail_prompt = " Give extensive details." if style == "detailed" else ""
|
||||||
name = name.lstrip("/")
|
|
||||||
return self.notes[name]
|
|
||||||
raise ValueError(f"Note not found: {name}")
|
|
||||||
|
|
||||||
# PROMPT HANDLERS
|
return types.GetPromptResult(
|
||||||
# Prompts define templates that can be filled with server state
|
description="Summarize the current notes",
|
||||||
@self.list_prompts()
|
messages=[
|
||||||
async def handle_list_prompts() -> list[types.Prompt]:
|
types.PromptMessage(
|
||||||
"""
|
role="user",
|
||||||
List available prompts.
|
content=types.TextContent(
|
||||||
Each prompt can have optional arguments to customize its behavior.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
types.Prompt(
|
|
||||||
name="summarize-notes",
|
|
||||||
description="Creates a summary of all notes",
|
|
||||||
arguments=[
|
|
||||||
types.PromptArgument(
|
|
||||||
name="style",
|
|
||||||
description="Style of the summary (brief/detailed)",
|
|
||||||
required=False,
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@self.get_prompt()
|
|
||||||
async def handle_get_prompt(
|
|
||||||
name: str, arguments: dict[str, str] | None
|
|
||||||
) -> types.GetPromptResult:
|
|
||||||
"""
|
|
||||||
Generate a prompt by combining arguments with server state.
|
|
||||||
The prompt includes all current notes and can be customized via arguments.
|
|
||||||
"""
|
|
||||||
if name != "summarize-notes":
|
|
||||||
raise ValueError(f"Unknown prompt: {name}")
|
|
||||||
|
|
||||||
style = (arguments or {}).get("style", "brief")
|
|
||||||
detail_prompt = " Give extensive details." if style == "detailed" else ""
|
|
||||||
|
|
||||||
return types.GetPromptResult(
|
|
||||||
description="Summarize the current notes",
|
|
||||||
messages=[
|
|
||||||
types.PromptMessage(
|
|
||||||
role="user",
|
|
||||||
content=types.TextContent(
|
|
||||||
type="text",
|
|
||||||
text=f"Here are the current notes to summarize:{detail_prompt}\n\n"
|
|
||||||
+ "\n".join(
|
|
||||||
f"- {name}: {content}"
|
|
||||||
for name, content in self.notes.items()
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
# TOOL HANDLERS
|
|
||||||
# Tools allow clients to modify server state in controlled ways
|
|
||||||
@self.list_tools()
|
|
||||||
async def handle_list_tools() -> list[types.Tool]:
|
|
||||||
"""
|
|
||||||
List available tools.
|
|
||||||
Each tool specifies its arguments using JSON Schema validation.
|
|
||||||
"""
|
|
||||||
return [
|
|
||||||
types.Tool(
|
|
||||||
name="add-note",
|
|
||||||
description="Add a new note",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {"type": "string"},
|
|
||||||
"content": {"type": "string"},
|
|
||||||
},
|
|
||||||
"required": ["name", "content"],
|
|
||||||
},
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
@self.call_tool()
|
|
||||||
async def handle_call_tool(
|
|
||||||
name: str, arguments: dict | None
|
|
||||||
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
|
||||||
"""
|
|
||||||
Handle tool execution requests.
|
|
||||||
Tools can modify server state and notify clients of changes.
|
|
||||||
"""
|
|
||||||
if name != "add-note":
|
|
||||||
raise ValueError(f"Unknown tool: {name}")
|
|
||||||
|
|
||||||
if not arguments:
|
|
||||||
raise ValueError("Missing arguments")
|
|
||||||
|
|
||||||
note_name = arguments.get("name")
|
|
||||||
content = arguments.get("content")
|
|
||||||
|
|
||||||
if not note_name or not content:
|
|
||||||
raise ValueError("Missing name or content")
|
|
||||||
|
|
||||||
# Update server state
|
|
||||||
self.notes[note_name] = content
|
|
||||||
|
|
||||||
# Notify clients that resources have changed
|
|
||||||
await self.request_context.session.send_resource_list_changed()
|
|
||||||
|
|
||||||
return [
|
|
||||||
types.TextContent(
|
|
||||||
type="text",
|
type="text",
|
||||||
text=f"Added note '{note_name}' with content: {content}",
|
text=f"Here are the current notes to summarize:{detail_prompt}\n\n"
|
||||||
)
|
+ "\n".join(
|
||||||
]
|
f"- {name}: {content}"
|
||||||
|
for name, content in notes.items()
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def handle_list_tools() -> list[types.Tool]:
|
||||||
|
"""
|
||||||
|
List available tools.
|
||||||
|
Each tool specifies its arguments using JSON Schema validation.
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
types.Tool(
|
||||||
|
name="add-note",
|
||||||
|
description="Add a new note",
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string"},
|
||||||
|
"content": {"type": "string"},
|
||||||
|
},
|
||||||
|
"required": ["name", "content"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_call_tool(
|
||||||
|
name: str, arguments: dict | None
|
||||||
|
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||||
|
"""
|
||||||
|
Handle tool execution requests.
|
||||||
|
Tools can modify server state and notify clients of changes.
|
||||||
|
"""
|
||||||
|
if name != "add-note":
|
||||||
|
raise ValueError(f"Unknown tool: {name}")
|
||||||
|
|
||||||
|
if not arguments:
|
||||||
|
raise ValueError("Missing arguments")
|
||||||
|
|
||||||
|
note_name = arguments.get("name")
|
||||||
|
content = arguments.get("content")
|
||||||
|
|
||||||
|
if not note_name or not content:
|
||||||
|
raise ValueError("Missing name or content")
|
||||||
|
|
||||||
|
# Update server state
|
||||||
|
notes[note_name] = content
|
||||||
|
|
||||||
|
# Notify clients that resources have changed
|
||||||
|
await server.request_context.session.send_resource_list_changed()
|
||||||
|
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=f"Added note '{note_name}' with content: {content}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
async def main():
|
async def main():
|
||||||
server = McpServer()
|
|
||||||
|
|
||||||
# Run the server using stdin/stdout streams
|
# Run the server using stdin/stdout streams
|
||||||
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||||
await server.run(
|
await server.run(
|
||||||
|
|
|
||||||
2
uv.lock
generated
2
uv.lock
generated
|
|
@ -24,7 +24,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "create-mcp-server"
|
name = "create-mcp-server"
|
||||||
version = "1.0.3.dev0"
|
version = "1.0.4.dev0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue