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] [project]
name = "create-mcp-server" name = "create-mcp-server"
version = "1.0.6.dev0" version = "1.0.2"
description = "Create an Model Context Protocol server project from a template." description = "Create an MCP server project from a template."
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" 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 = [ dependencies = [
"click>=8.1.7", "click>=8.1.7",
"jinja2>=3.1.4", "jinja2>=3.1.4",
@ -27,10 +14,6 @@ dependencies = [
[project.scripts] [project.scripts]
create-mcp-server = "create_mcp_server:main" create-mcp-server = "create_mcp_server:main"
[project.urls]
Homepage = "https://modelcontextprotocol.io"
Repository = "https://github.com/modelcontextprotocol/create-python-server"
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
@ -39,4 +22,7 @@ build-backend = "hatchling.build"
artifacts = ["src/create_mcp_server/template"] artifacts = ["src/create_mcp_server/template"]
[tool.uv] [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 from pathlib import Path
import click import click
import toml
from packaging.version import parse from packaging.version import parse
MIN_UV_VERSION = "0.4.10" 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: def check_uv_version(required_version: str) -> str | None:
"""Check if uv is installed and has minimum version""" """Check if uv is installed and has minimum version"""
try: try:
@ -138,11 +123,7 @@ def copy_template(
("README.md.jinja2", "README.md", path), ("README.md.jinja2", "README.md", path),
] ]
pyproject = PyProject(path / "pyproject.toml")
bin_name = pyproject.first_binary
template_vars = { template_vars = {
"binary_name": bin_name,
"server_name": name, "server_name": name,
"server_version": version, "server_version": version,
"server_description": description, "server_description": description,
@ -340,8 +321,6 @@ def main(
click.echo("❌ Error: Invalid path. Project creation aborted.", err=True) click.echo("❌ Error: Invalid path. Project creation aborted.", err=True)
return 1 return 1
project_path = project_path.resolve()
create_project(project_path, name, description, version, claudeapp) create_project(project_path, name, description, version, claudeapp)
update_pyproject_settings(project_path, version, description) 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 - Takes "name" and "content" as required string arguments
- Updates server state and notifies clients of resource changes - Updates server state and notifies clients of resource changes
## Configuration ## Install
[TODO: Add configuration details specific to your implementation] ### Claude Desktop
## Quickstart
### Install
#### Claude Desktop
On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json` On MacOS: `~/Library/Application\ Support/Claude/claude_desktop_config.json`
On Windows: `%APPDATA%/Claude/claude_desktop_config.json` On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
<details> ```
<summary>Development/Unpublished Servers Configuration</summary>
```
"mcpServers": { "mcpServers": {
"{{server_name}}": { "{{server_name}}": {
"command": "uv", "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: ## Develop and Debug
```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
Since MCP servers run over stdio, debugging can be challenging. For the best debugging 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). 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: You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command:
```bash ```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. 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 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] = {}
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() This server shows how to:
async def handle_list_resources() -> list[types.Resource]: - 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() def __init__(self):
async def handle_read_resource(uri: AnyUrl) -> str: super().__init__("{{server_name}}")
"""
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}")
name = uri.path # Store notes as a simple key-value dict to demonstrate state management
if name is not None: self.notes: dict[str, str] = {}
name = name.lstrip("/")
return notes[name]
raise ValueError(f"Note not found: {name}")
@server.list_prompts() # RESOURCE HANDLERS
async def handle_list_prompts() -> list[types.Prompt]: # Resources allow clients to discover and read content from the server
""" @self.list_resources()
List available prompts. async def handle_list_resources() -> list[types.Resource]:
Each prompt can have optional arguments to customize its behavior. """
""" List available note resources.
return [ Each note is exposed as a resource with a custom note:// URI scheme.
types.Prompt( """
name="summarize-notes", return [
description="Creates a summary of all notes", types.Resource(
arguments=[ uri=AnyUrl(f"note:///{name}"),
types.PromptArgument( name=f"Note: {name}",
name="style", description=f"A simple note named {name}",
description="Style of the summary (brief/detailed)", mimeType="text/plain",
required=False,
) )
], for name in self.notes
) ]
]
@server.get_prompt() @self.read_resource()
async def handle_get_prompt( async def handle_read_resource(uri: AnyUrl) -> str:
name: str, arguments: dict[str, str] | None """
) -> types.GetPromptResult: Read a specific note's content by its URI.
""" The note name is extracted from the URI host component.
Generate a prompt by combining arguments with server state. """
The prompt includes all current notes and can be customized via arguments. if uri.scheme != "note":
""" raise ValueError(f"Unsupported URI scheme: {uri.scheme}")
if name != "summarize-notes":
raise ValueError(f"Unknown prompt: {name}")
style = (arguments or {}).get("style", "brief") name = uri.path
detail_prompt = " Give extensive details." if style == "detailed" else "" if name is not None:
name = name.lstrip("/")
return self.notes[name]
raise ValueError(f"Note not found: {name}")
return types.GetPromptResult( # PROMPT HANDLERS
description="Summarize the current notes", # Prompts define templates that can be filled with server state
messages=[ @self.list_prompts()
types.PromptMessage( async def handle_list_prompts() -> list[types.Prompt]:
role="user", """
content=types.TextContent( List available prompts.
type="text", Each prompt can have optional arguments to customize its behavior.
text=f"Here are the current notes to summarize:{detail_prompt}\n\n" """
+ "\n".join( return [
f"- {name}: {content}" types.Prompt(
for name, content in notes.items() 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() # TOOL HANDLERS
async def handle_list_tools() -> list[types.Tool]: # Tools allow clients to modify server state in controlled ways
""" @self.list_tools()
List available tools. async def handle_list_tools() -> list[types.Tool]:
Each tool specifies its arguments using JSON Schema validation. """
""" List available tools.
return [ Each tool specifies its arguments using JSON Schema validation.
types.Tool( """
name="add-note", return [
description="Add a new note", types.Tool(
inputSchema={ name="add-note",
"type": "object", description="Add a new note",
"properties": { inputSchema={
"name": {"type": "string"}, "type": "object",
"content": {"type": "string"}, "properties": {
}, "name": {"type": "string"},
"required": ["name", "content"], "content": {"type": "string"},
}, },
) "required": ["name", "content"],
] },
)
]
@server.call_tool() @self.call_tool()
async def handle_call_tool( async def handle_call_tool(
name: str, arguments: dict | None name: str, arguments: dict | None
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
""" """
Handle tool execution requests. Handle tool execution requests.
Tools can modify server state and notify clients of changes. Tools can modify server state and notify clients of changes.
""" """
if name != "add-note": if name != "add-note":
raise ValueError(f"Unknown tool: {name}") raise ValueError(f"Unknown tool: {name}")
if not arguments: if not arguments:
raise ValueError("Missing arguments") raise ValueError("Missing arguments")
note_name = arguments.get("name") note_name = arguments.get("name")
content = arguments.get("content") content = arguments.get("content")
if not note_name or not content: if not note_name or not content:
raise ValueError("Missing name or content") raise ValueError("Missing name or content")
# Update server state # Update server state
notes[note_name] = content self.notes[note_name] = content
# Notify clients that resources have changed # Notify clients that resources have changed
await server.request_context.session.send_resource_list_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(): 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
View file

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