diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fe64dba --- /dev/null +++ b/CONTRIBUTING.md @@ -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. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3d48435 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..c679ff3 --- /dev/null +++ b/SECURITY.md @@ -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). diff --git a/pyproject.toml b/pyproject.toml index e1b7147..6293767 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] diff --git a/src/create_mcp_server/__init__.py b/src/create_mcp_server/__init__.py index f2c439d..b54038d 100644 --- a/src/create_mcp_server/__init__.py +++ b/src/create_mcp_server/__init__.py @@ -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/""" 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 diff --git a/src/create_mcp_server/template/README.md.jinja2 b/src/create_mcp_server/template/README.md.jinja2 new file mode 100644 index 0000000..c35655d --- /dev/null +++ b/src/create_mcp_server/template/README.md.jinja2 @@ -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` + +
+ Development/Unpublished Servers Configuration + ``` + "mcpServers": { + "{{server_name}}": { + "command": "uv", + "args": [ + "--directory", + "{{server_directory}}", + "run", + "{{server_name}}" + ] + } + } + ``` +
+ +
+ Published Servers Configuration + ``` + "mcpServers": { + "{{server_name}}": { + "command": "uvx", + "args": [ + "{{server_name}}" + ] + } + } + ``` +
+ +## 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 +``` + +Replace `` 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. diff --git a/src/create_mcp_server/template/server.py.jinja2 b/src/create_mcp_server/template/server.py.jinja2 index bd07d11..bc39f5c 100644 --- a/src/create_mcp_server/template/server.py.jinja2 +++ b/src/create_mcp_server/template/server.py.jinja2 @@ -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( diff --git a/uv.lock b/uv.lock index ba63f9a..5e0d240 100644 --- a/uv.lock +++ b/uv.lock @@ -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" },