Compare commits

...
Author SHA1 Message Date
Justin Spahr-Summers
b07744de4f
Merge pull request #4 from modelcontextprotocol/justin/contributing
Some checks failed
Main branch checks / checks (push) Has been cancelled
Create CONTRIBUTING.md
2024-11-25 09:22:24 -06:00
Justin Spahr-Summers
40083e7301
Create CONTRIBUTING.md 2024-11-25 09:10:40 -06:00
David Soria Parra
7d921b9302
bump 2024-11-24 21:04:36 +00:00
David Soria Parra
446072a2c0
1.0.5 2024-11-24 21:04:03 +00:00
David Soria Parra
35730e4a16
Move template to flat app pattern 2024-11-24 21:02:02 +00:00
David Soria Parra
e5c6c63923
update readme 2024-11-21 21:28:06 +00:00
David Soria Parra
a40b42b534
bump 2024-11-21 14:50:56 +00:00
David Soria Parra
b56d4c5186
v1.0.4 2024-11-21 14:50:23 +00:00
David Soria Parra
6559948d34
Resolve the path before using it 2024-11-21 14:50:08 +00:00
David Soria Parra
6e43668dba
bump 2024-11-21 12:00:36 +00:00
David Soria Parra
e84ea45f8f
v1.0.3 2024-11-21 12:00:02 +00:00
David Soria Parra
74a6e01a30
correctly retrieve the script name 2024-11-21 11:59:52 +00:00
Justin Spahr-Summers
1ccba01c1c
Merge pull request #3 from modelcontextprotocol/justin/project-metadata
Add project license, URLs, other metadata
2024-11-20 21:56:34 +00:00
Justin Spahr-Summers
6fef3bec62
Add project license, URLs, other metadata 2024-11-20 14:03:35 +00:00
David Soria Parra
2cebc73104
bump 2024-11-19 23:00:59 +00:00
David Soria Parra
c8d622e085
v1.0.2 2024-11-19 23:00:22 +00:00
David Soria Parra
eed5b350be
Add templated README to projects 2024-11-19 22:57:31 +00:00
David Soria Parra
84c807a5b8
Merge pull request #2 from modelcontextprotocol/davidsp/README
Add templated README to projects
2024-11-19 22:54:01 +00:00
David Soria Parra
ae6e31527a
Add templated README to projects 2024-11-19 21:03:06 +00:00
David Soria Parra
f2e71e60cb
Update SECURITY.md 2024-11-19 16:50:56 +00:00
David Soria Parra
318a842fc9
Create SECURITY.md 2024-11-19 12:15:18 +00:00
David Soria Parra
aeada27c1f
Create LICENSE 2024-11-18 22:22:49 +00:00
8 changed files with 382 additions and 167 deletions

43
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,43 @@
# 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.

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Anthropic, PBC
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

14
SECURITY.md Normal file
View file

@ -0,0 +1,14 @@
# Security Policy
Thank you for helping us keep this software secure.
## Reporting Security Issues
This software is maintained by [Anthropic](https://www.anthropic.com/) as part of the Model Context Protocol project.
The security of our systems and user data is Anthropic’s top priority. We appreciate the work of security researchers acting in good faith in identifying and reporting potential vulnerabilities.
Our security program is managed on HackerOne and we ask that any validated vulnerability in this functionality be reported through their [submission form](https://hackerone.com/anthropic-vdp/reports/new?type=team&report_type=vulnerability).
## Vulnerability Disclosure Program
Our Vulnerability Program Guidelines are defined on our [HackerOne program page](https://hackerone.com/anthropic-vdp).

View file

@ -1,9 +1,22 @@
[project]
name = "create-mcp-server"
version = "1.0.1.dev0"
description = "Create an MCP server project from a template."
version = "1.0.6.dev0"
description = "Create an Model Context Protocol 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",
@ -14,6 +27,10 @@ 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"
@ -22,7 +39,4 @@ 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,11 +5,26 @@ 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:
@ -105,7 +120,9 @@ def get_package_directory(path: Path) -> Path:
return src_dir.parent
def copy_template(path: Path, name: str, version: str = "0.1.0") -> None:
def copy_template(
path: Path, name: str, description: str, version: str = "0.1.0"
) -> None:
"""Copy template files into src/<project_name>"""
template_dir = Path(__file__).parent / "template"
@ -116,18 +133,28 @@ def copy_template(path: Path, name: str, version: str = "0.1.0") -> None:
env = Environment(loader=FileSystemLoader(str(template_dir)))
files = [
("__init__.py.jinja2", "__init__.py"),
("server.py.jinja2", "server.py"),
("__init__.py.jinja2", "__init__.py", target_dir),
("server.py.jinja2", "server.py", target_dir),
("README.md.jinja2", "README.md", path),
]
template_vars = {"server_name": name, "server_version": version}
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,
"server_directory": str(path.resolve()),
}
try:
for template_file, output_file in files:
for template_file, output_file, output_dir in files:
template = env.get_template(template_file)
rendered = template.render(**template_vars)
out_path = target_dir / output_file
out_path = output_dir / output_file
out_path.write_text(rendered)
except Exception as e:
@ -136,7 +163,7 @@ def copy_template(path: Path, name: str, version: str = "0.1.0") -> None:
def create_project(
path: Path, name: str, version: str, use_claude: bool = True
path: Path, name: str, description: str, version: str, use_claude: bool = True
) -> None:
"""Create a new project using uv"""
path.mkdir(parents=True, exist_ok=True)
@ -158,7 +185,7 @@ def create_project(
click.echo("❌ Error: Failed to add mcp dependency.", err=True)
sys.exit(1)
copy_template(path, name, version)
copy_template(path, name, description, version)
# Check if Claude.app is available
if (
@ -313,7 +340,9 @@ def main(
click.echo("❌ Error: Invalid path. Project creation aborted.", err=True)
return 1
create_project(project_path, name, version, claudeapp)
project_path = project_path.resolve()
create_project(project_path, name, description, version, claudeapp)
update_pyproject_settings(project_path, version, description)
return 0

View file

@ -0,0 +1,119 @@
# {{server_name}} MCP server
{{server_description}}
## Components
### Resources
The server implements a simple note storage system with:
- Custom note:// URI scheme for accessing individual notes
- Each note resource has a name, description and text/plain mimetype
### Prompts
The server provides a single prompt:
- summarize-notes: Creates summaries of all stored notes
- Optional "style" argument to control detail level (brief/detailed)
- Generates prompt combining all current notes with style preference
### Tools
The server implements one tool:
- add-note: Adds a new note to the server
- Takes "name" and "content" as required string arguments
- Updates server state and notifies clients of resource changes
## Configuration
[TODO: Add configuration details specific to your implementation]
## Quickstart
### Install
#### 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",
"args": [
"--directory",
"{{server_directory}}",
"run",
"{{server_name}}"
]
}
}
```
</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
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}}
```
{% 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,173 +6,148 @@ 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] = {}
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:
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
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
]
def __init__(self):
super().__init__("{{server_name}}")
@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}")
# Store notes as a simple key-value dict to demonstrate state management
self.notes: dict[str, str] = {}
name = uri.path
if name is not None:
name = name.lstrip("/")
return notes[name]
raise ValueError(f"Note not found: {name}")
# 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",
@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,
)
for name in self.notes
]
],
)
]
@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}")
@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}")
name = uri.path
if name is not None:
name = name.lstrip("/")
return self.notes[name]
raise ValueError(f"Note not found: {name}")
style = (arguments or {}).get("style", "brief")
detail_prompt = " Give extensive details." if style == "detailed" else ""
# 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()
),
),
)
],
)
# 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(
return types.GetPromptResult(
description="Summarize the current notes",
messages=[
types.PromptMessage(
role="user",
content=types.TextContent(
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():
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.1.dev0"
version = "1.0.4.dev0"
source = { editable = "." }
dependencies = [
{ name = "click" },