Skip to content
This repository was archived by the owner on Mar 24, 2025. It is now read-only.

Commit 40dacf6

Browse files
zcklyclaude
andcommitted
feat: add template selection support
Adds a --template flag to choose between a minimal blank template and the notes app example. Moves the notes app code to a separate template. Makes the blank template the default for new projects. 🤖 Generated with Claude CLI. Co-Authored-By: Claude <[email protected]>
1 parent b07744d commit 40dacf6

File tree

3 files changed

+182
-133
lines changed

3 files changed

+182
-133
lines changed

src/create_mcp_server/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,12 @@ def get_package_directory(path: Path) -> Path:
121121

122122

123123
def copy_template(
124-
path: Path, name: str, description: str, version: str = "0.1.0"
124+
path: Path, name: str, description: str, version: str = "0.1.0", template: str = "blank"
125125
) -> None:
126126
"""Copy template files into src/<project_name>"""
127127
template_dir = Path(__file__).parent / "template"
128+
if template == "notes":
129+
template_dir = template_dir / "notes"
128130

129131
target_dir = get_package_directory(path)
130132

@@ -277,6 +279,12 @@ def check_package_name(name: str) -> bool:
277279
type=str,
278280
help="Project description",
279281
)
282+
@click.option(
283+
"--template",
284+
type=click.Choice(["blank", "notes"]),
285+
default="blank",
286+
help="Project template to use",
287+
)
280288
@click.option(
281289
"--claudeapp/--no-claudeapp",
282290
default=True,
@@ -287,6 +295,7 @@ def main(
287295
name: str | None,
288296
version: str | None,
289297
description: str | None,
298+
template: str,
290299
claudeapp: bool,
291300
) -> int:
292301
"""Create a new MCP server project"""
@@ -343,6 +352,7 @@ def main(
343352
project_path = project_path.resolve()
344353

345354
create_project(project_path, name, description, version, claudeapp)
355+
copy_template(project_path, name, description, version, template)
346356
update_pyproject_settings(project_path, version, description)
347357

348358
return 0
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import asyncio
2+
3+
from mcp.server.models import InitializationOptions
4+
import mcp.types as types
5+
from mcp.server import NotificationOptions, Server
6+
from pydantic import AnyUrl
7+
import mcp.server.stdio
8+
9+
# Store notes as a simple key-value dict to demonstrate state management
10+
notes: dict[str, str] = {}
11+
12+
server = Server("{{server_name}}")
13+
14+
@server.list_resources()
15+
async def handle_list_resources() -> list[types.Resource]:
16+
"""
17+
List available note resources.
18+
Each note is exposed as a resource with a custom note:// URI scheme.
19+
"""
20+
return [
21+
types.Resource(
22+
uri=AnyUrl(f"note://internal/{name}"),
23+
name=f"Note: {name}",
24+
description=f"A simple note named {name}",
25+
mimeType="text/plain",
26+
)
27+
for name in notes
28+
]
29+
30+
@server.read_resource()
31+
async def handle_read_resource(uri: AnyUrl) -> str:
32+
"""
33+
Read a specific note's content by its URI.
34+
The note name is extracted from the URI host component.
35+
"""
36+
if uri.scheme != "note":
37+
raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
38+
39+
name = uri.path
40+
if name is not None:
41+
name = name.lstrip("/")
42+
return notes[name]
43+
raise ValueError(f"Note not found: {name}")
44+
45+
@server.list_prompts()
46+
async def handle_list_prompts() -> list[types.Prompt]:
47+
"""
48+
List available prompts.
49+
Each prompt can have optional arguments to customize its behavior.
50+
"""
51+
return [
52+
types.Prompt(
53+
name="summarize-notes",
54+
description="Creates a summary of all notes",
55+
arguments=[
56+
types.PromptArgument(
57+
name="style",
58+
description="Style of the summary (brief/detailed)",
59+
required=False,
60+
)
61+
],
62+
)
63+
]
64+
65+
@server.get_prompt()
66+
async def handle_get_prompt(
67+
name: str, arguments: dict[str, str] | None
68+
) -> types.GetPromptResult:
69+
"""
70+
Generate a prompt by combining arguments with server state.
71+
The prompt includes all current notes and can be customized via arguments.
72+
"""
73+
if name != "summarize-notes":
74+
raise ValueError(f"Unknown prompt: {name}")
75+
76+
style = (arguments or {}).get("style", "brief")
77+
detail_prompt = " Give extensive details." if style == "detailed" else ""
78+
79+
return types.GetPromptResult(
80+
description="Summarize the current notes",
81+
messages=[
82+
types.PromptMessage(
83+
role="user",
84+
content=types.TextContent(
85+
type="text",
86+
text=f"Here are the current notes to summarize:{detail_prompt}\n\n"
87+
+ "\n".join(
88+
f"- {name}: {content}"
89+
for name, content in notes.items()
90+
),
91+
),
92+
)
93+
],
94+
)
95+
96+
@server.list_tools()
97+
async def handle_list_tools() -> list[types.Tool]:
98+
"""
99+
List available tools.
100+
Each tool specifies its arguments using JSON Schema validation.
101+
"""
102+
return [
103+
types.Tool(
104+
name="add-note",
105+
description="Add a new note",
106+
inputSchema={
107+
"type": "object",
108+
"properties": {
109+
"name": {"type": "string"},
110+
"content": {"type": "string"},
111+
},
112+
"required": ["name", "content"],
113+
},
114+
)
115+
]
116+
117+
@server.call_tool()
118+
async def handle_call_tool(
119+
name: str, arguments: dict | None
120+
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
121+
"""
122+
Handle tool execution requests.
123+
Tools can modify server state and notify clients of changes.
124+
"""
125+
if name != "add-note":
126+
raise ValueError(f"Unknown tool: {name}")
127+
128+
if not arguments:
129+
raise ValueError("Missing arguments")
130+
131+
note_name = arguments.get("name")
132+
content = arguments.get("content")
133+
134+
if not note_name or not content:
135+
raise ValueError("Missing name or content")
136+
137+
# Update server state
138+
notes[note_name] = content
139+
140+
# Notify clients that resources have changed
141+
await server.request_context.session.send_resource_list_changed()
142+
143+
return [
144+
types.TextContent(
145+
type="text",
146+
text=f"Added note '{note_name}' with content: {content}",
147+
)
148+
]
149+
150+
async def main():
151+
# Run the server using stdin/stdout streams
152+
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
153+
await server.run(
154+
read_stream,
155+
write_stream,
156+
InitializationOptions(
157+
server_name="{{server_name}}",
158+
server_version="{{server_version}}",
159+
capabilities=server.get_capabilities(
160+
notification_options=NotificationOptions(),
161+
experimental_capabilities={},
162+
),
163+
),
164+
)

src/create_mcp_server/template/server.py.jinja2

Lines changed: 7 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -3,149 +3,24 @@ import asyncio
33
from mcp.server.models import InitializationOptions
44
import mcp.types as types
55
from mcp.server import NotificationOptions, Server
6-
from pydantic import AnyUrl
76
import mcp.server.stdio
87

9-
# Store notes as a simple key-value dict to demonstrate state management
10-
notes: dict[str, str] = {}
11-
128
server = Server("{{server_name}}")
139

1410
@server.list_resources()
1511
async def handle_list_resources() -> list[types.Resource]:
16-
"""
17-
List available note resources.
18-
Each note is exposed as a resource with a custom note:// URI scheme.
19-
"""
20-
return [
21-
types.Resource(
22-
uri=AnyUrl(f"note://internal/{name}"),
23-
name=f"Note: {name}",
24-
description=f"A simple note named {name}",
25-
mimeType="text/plain",
26-
)
27-
for name in notes
28-
]
29-
30-
@server.read_resource()
31-
async def handle_read_resource(uri: AnyUrl) -> str:
32-
"""
33-
Read a specific note's content by its URI.
34-
The note name is extracted from the URI host component.
35-
"""
36-
if uri.scheme != "note":
37-
raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
38-
39-
name = uri.path
40-
if name is not None:
41-
name = name.lstrip("/")
42-
return notes[name]
43-
raise ValueError(f"Note not found: {name}")
12+
"""List available resources."""
13+
return []
4414

4515
@server.list_prompts()
4616
async def handle_list_prompts() -> list[types.Prompt]:
47-
"""
48-
List available prompts.
49-
Each prompt can have optional arguments to customize its behavior.
50-
"""
51-
return [
52-
types.Prompt(
53-
name="summarize-notes",
54-
description="Creates a summary of all notes",
55-
arguments=[
56-
types.PromptArgument(
57-
name="style",
58-
description="Style of the summary (brief/detailed)",
59-
required=False,
60-
)
61-
],
62-
)
63-
]
64-
65-
@server.get_prompt()
66-
async def handle_get_prompt(
67-
name: str, arguments: dict[str, str] | None
68-
) -> types.GetPromptResult:
69-
"""
70-
Generate a prompt by combining arguments with server state.
71-
The prompt includes all current notes and can be customized via arguments.
72-
"""
73-
if name != "summarize-notes":
74-
raise ValueError(f"Unknown prompt: {name}")
75-
76-
style = (arguments or {}).get("style", "brief")
77-
detail_prompt = " Give extensive details." if style == "detailed" else ""
78-
79-
return types.GetPromptResult(
80-
description="Summarize the current notes",
81-
messages=[
82-
types.PromptMessage(
83-
role="user",
84-
content=types.TextContent(
85-
type="text",
86-
text=f"Here are the current notes to summarize:{detail_prompt}\n\n"
87-
+ "\n".join(
88-
f"- {name}: {content}"
89-
for name, content in notes.items()
90-
),
91-
),
92-
)
93-
],
94-
)
17+
"""List available prompts."""
18+
return []
9519

9620
@server.list_tools()
9721
async def handle_list_tools() -> list[types.Tool]:
98-
"""
99-
List available tools.
100-
Each tool specifies its arguments using JSON Schema validation.
101-
"""
102-
return [
103-
types.Tool(
104-
name="add-note",
105-
description="Add a new note",
106-
inputSchema={
107-
"type": "object",
108-
"properties": {
109-
"name": {"type": "string"},
110-
"content": {"type": "string"},
111-
},
112-
"required": ["name", "content"],
113-
},
114-
)
115-
]
116-
117-
@server.call_tool()
118-
async def handle_call_tool(
119-
name: str, arguments: dict | None
120-
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
121-
"""
122-
Handle tool execution requests.
123-
Tools can modify server state and notify clients of changes.
124-
"""
125-
if name != "add-note":
126-
raise ValueError(f"Unknown tool: {name}")
127-
128-
if not arguments:
129-
raise ValueError("Missing arguments")
130-
131-
note_name = arguments.get("name")
132-
content = arguments.get("content")
133-
134-
if not note_name or not content:
135-
raise ValueError("Missing name or content")
136-
137-
# Update server state
138-
notes[note_name] = content
139-
140-
# Notify clients that resources have changed
141-
await server.request_context.session.send_resource_list_changed()
142-
143-
return [
144-
types.TextContent(
145-
type="text",
146-
text=f"Added note '{note_name}' with content: {content}",
147-
)
148-
]
22+
"""List available tools."""
23+
return []
14924

15025
async def main():
15126
# Run the server using stdin/stdout streams
@@ -161,4 +36,4 @@ async def main():
16136
experimental_capabilities={},
16237
),
16338
),
164-
)
39+
)

0 commit comments

Comments
 (0)