Compare commits

..
6 changed files with 163 additions and 276 deletions

View file

@ -1,43 +0,0 @@
# Contributing
Thank you for your interest in contributing to `create-mcp-server`! This tool helps developers quickly scaffold new MCP (Model Context Protocol) servers.
## Getting Started
1. Fork the repository
2. Clone your fork: `git clone https://github.com/YOUR_USERNAME/create-python-server.git`
3. Install dependencies: `uv sync --dev --all-extras`
## Development
- Make your changes in the `src` directory
- Test your changes by creating a new server: `uv run -m create_mcp_server test-server`
- Run type checking: `uv run pyright`
- Run linting: `uv run ruff check .`
## Pull Requests
1. Create a new branch for your changes
2. Make your changes
3. Ensure type checking and linting pass
4. Test server creation using your changes
5. Submit a pull request with a clear description of your changes
## Code of Conduct
This project follows our [Code of Conduct](CODE_OF_CONDUCT.md). Please review it before contributing.
## Reporting Issues
- Use the [GitHub issue tracker](https://github.com/modelcontextprotocol/create-python-server/issues)
- Provide clear reproduction steps
- Include relevant system information
- Specify the version you're using
## Security Issues
Please review our [Security Policy](SECURITY.md) for reporting security vulnerabilities.
## License
By contributing, you agree that your contributions will be licensed under the MIT License.

View file

@ -1,22 +1,9 @@
[project]
name = "create-mcp-server"
version = "1.0.6.dev0"
description = "Create an Model Context Protocol server project from a template."
version = "1.0.2"
description = "Create an MCP server project from a template."
readme = "README.md"
requires-python = ">=3.10"
authors = [{ name = "Anthropic, PBC." }]
maintainers = [
{ name = "David Soria Parra", email = "davidsp@anthropic.com" },
{ name = "Justin Spahr-Summers", email = "justin@anthropic.com" },
]
license = { text = "MIT" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
]
dependencies = [
"click>=8.1.7",
"jinja2>=3.1.4",
@ -27,10 +14,6 @@ dependencies = [
[project.scripts]
create-mcp-server = "create_mcp_server:main"
[project.urls]
Homepage = "https://modelcontextprotocol.io"
Repository = "https://github.com/modelcontextprotocol/create-python-server"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
@ -39,4 +22,7 @@ build-backend = "hatchling.build"
artifacts = ["src/create_mcp_server/template"]
[tool.uv]
dev-dependencies = ["pyright>=1.1.389", "ruff>=0.7.4"]
dev-dependencies = [
"pyright>=1.1.389",
"ruff>=0.7.4",
]

View file

@ -5,26 +5,11 @@ import sys
from pathlib import Path
import click
import toml
from packaging.version import parse
MIN_UV_VERSION = "0.4.10"
class PyProject:
def __init__(self, path: Path):
self.data = toml.load(path)
@property
def name(self) -> str:
return self.data["project"]["name"]
@property
def first_binary(self) -> str | None:
scripts = self.data["project"].get("scripts", {})
return next(iter(scripts.keys()), None)
def check_uv_version(required_version: str) -> str | None:
"""Check if uv is installed and has minimum version"""
try:
@ -138,11 +123,7 @@ def copy_template(
("README.md.jinja2", "README.md", path),
]
pyproject = PyProject(path / "pyproject.toml")
bin_name = pyproject.first_binary
template_vars = {
"binary_name": bin_name,
"server_name": name,
"server_version": version,
"server_description": description,
@ -340,8 +321,6 @@ def main(
click.echo("❌ Error: Invalid path. Project creation aborted.", err=True)
return 1
project_path = project_path.resolve()
create_project(project_path, name, description, version, claudeapp)
update_pyproject_settings(project_path, version, description)

View file

@ -24,22 +24,14 @@ The server implements one tool:
- Takes "name" and "content" as required string arguments
- Updates server state and notifies clients of resource changes
## Configuration
## Install
[TODO: Add configuration details specific to your implementation]
## Quickstart
### Install
#### Claude Desktop
### Claude Desktop
On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
<details>
<summary>Development/Unpublished Servers Configuration</summary>
```
```
"mcpServers": {
"{{server_name}}": {
"command": "uv",
@ -51,69 +43,17 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
]
}
}
```
</details>
<details>
<summary>Published Servers Configuration</summary>
```
"mcpServers": {
"{{server_name}}": {
"command": "uvx",
"args": [
"{{server_name}}"
]
}
}
```
</details>
## Development
### Building and Publishing
To prepare the package for distribution:
1. Sync dependencies and update lockfile:
```bash
uv sync
```
2. Build package distributions:
```bash
uv build
```
This will create source and wheel distributions in the `dist/` directory.
3. Publish to PyPI:
```bash
uv publish
```
Note: You'll need to set PyPI credentials via environment variables or command flags:
- Token: `--token` or `UV_PUBLISH_TOKEN`
- Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD`
### Debugging
## Develop and Debug
Since MCP servers run over stdio, debugging can be challenging. For the best debugging
experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector).
{% if binary_name %}
You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
```bash
npx @modelcontextprotocol/inspector uv --directory {{server_directory}} run {{binary_name}}
npx @modelcontextprotocol/inspector uv --directory {{server_directory}} run {{server_name}}
```
{% else %}
This project does not contain a script to start. However, you can still use the MCP Inspector for debugging. To start the inspector, use the following general command structure:
```bash
npx @modelcontextprotocol/inspector <command-to-run-your-server>
```
Replace `<command-to-run-your-server>` with the specific command you use to start your MCP server.
{% endif %}
Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging.

View file

@ -6,148 +6,173 @@ from mcp.server import NotificationOptions, Server
from pydantic import AnyUrl
import mcp.server.stdio
# Store notes as a simple key-value dict to demonstrate state management
notes: dict[str, str] = {}
server = Server("{{server_name}}")
class McpServer(Server):
"""
Example MCP server demonstrating the three core primitives:
1. Resources - Simple key-value notes storage accessed via custom URIs
2. Prompts - Configurable summarization prompt for notes
3. Tools - Ability to add new notes programmatically
@server.list_resources()
async def handle_list_resources() -> list[types.Resource]:
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
"""
List available note resources.
Each note is exposed as a resource with a custom note:// URI scheme.
"""
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
]
@server.read_resource()
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}")
def __init__(self):
super().__init__("{{server_name}}")
name = uri.path
if name is not None:
name = name.lstrip("/")
return notes[name]
raise ValueError(f"Note not found: {name}")
# Store notes as a simple key-value dict to demonstrate state management
self.notes: dict[str, str] = {}
@server.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
"""
List available prompts.
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,
# RESOURCE HANDLERS
# Resources allow clients to discover and read content from the server
@self.list_resources()
async def handle_list_resources() -> list[types.Resource]:
"""
List available note resources.
Each note is exposed as a resource with a custom note:// URI scheme.
"""
return [
types.Resource(
uri=AnyUrl(f"note:///{name}"),
name=f"Note: {name}",
description=f"A simple note named {name}",
mimeType="text/plain",
)
],
)
]
for name in self.notes
]
@server.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}")
@self.read_resource()
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}")
style = (arguments or {}).get("style", "brief")
detail_prompt = " Give extensive details." if style == "detailed" else ""
name = uri.path
if name is not None:
name = name.lstrip("/")
return self.notes[name]
raise ValueError(f"Note not found: {name}")
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 notes.items()
),
),
# PROMPT HANDLERS
# Prompts define templates that can be filled with server state
@self.list_prompts()
async def handle_list_prompts() -> list[types.Prompt]:
"""
List available prompts.
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()
),
),
)
],
)
],
)
@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"],
},
)
]
# 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"],
},
)
]
@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}")
@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")
if not arguments:
raise ValueError("Missing arguments")
note_name = arguments.get("name")
content = arguments.get("content")
note_name = arguments.get("name")
content = arguments.get("content")
if not note_name or not content:
raise ValueError("Missing name or content")
if not note_name or not content:
raise ValueError("Missing name or content")
# Update server state
notes[note_name] = content
# Update server state
self.notes[note_name] = content
# Notify clients that resources have changed
await server.request_context.session.send_resource_list_changed()
# Notify clients that resources have changed
await self.request_context.session.send_resource_list_changed()
return [
types.TextContent(
type="text",
text=f"Added note '{note_name}' with content: {content}",
)
]
return [
types.TextContent(
type="text",
text=f"Added note '{note_name}' with content: {content}",
)
]
async def main():
server = McpServer()
# Run the server using stdin/stdout streams
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(

2
uv.lock generated
View file

@ -24,7 +24,7 @@ wheels = [
[[package]]
name = "create-mcp-server"
version = "1.0.4.dev0"
version = "1.0.1.dev0"
source = { editable = "." }
dependencies = [
{ name = "click" },