From 2cebc731045e96c9ca3a7b2c747e431a59986809 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Tue, 19 Nov 2024 23:00:59 +0000 Subject: [PATCH 01/13] bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3357203..a9fe32f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.2" +version = "1.0.3.dev0" description = "Create an MCP server project from a template." readme = "README.md" requires-python = ">=3.10" From 6fef3bec6202ef8db436dc10db8b1beea89cdd02 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Wed, 20 Nov 2024 14:03:35 +0000 Subject: [PATCH 02/13] Add project license, URLs, other metadata --- pyproject.toml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a9fe32f..bb7648a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,9 +1,22 @@ [project] name = "create-mcp-server" version = "1.0.3.dev0" -description = "Create an MCP server project from a template." +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"] From 74a6e01a30f6e7bc0b86e9b4e303571c9b7219a6 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 11:59:31 +0000 Subject: [PATCH 03/13] correctly retrieve the script name --- src/create_mcp_server/__init__.py | 20 +++++++++++++++++++ .../template/README.md.jinja2 | 12 ++++++++++- uv.lock | 2 +- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/create_mcp_server/__init__.py b/src/create_mcp_server/__init__.py index 6e1d342..99f57fd 100644 --- a/src/create_mcp_server/__init__.py +++ b/src/create_mcp_server/__init__.py @@ -5,11 +5,25 @@ 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: @@ -123,7 +137,11 @@ 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, @@ -237,6 +255,8 @@ def check_package_name(name: str) -> bool: return True + + @click.command() @click.option( "--path", diff --git a/src/create_mcp_server/template/README.md.jinja2 b/src/create_mcp_server/template/README.md.jinja2 index 644456b..7abfe66 100644 --- a/src/create_mcp_server/template/README.md.jinja2 +++ b/src/create_mcp_server/template/README.md.jinja2 @@ -50,10 +50,20 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 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 {{server_name}} +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/uv.lock b/uv.lock index ba63f9a..372f52f 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.3.dev0" source = { editable = "." } dependencies = [ { name = "click" }, From e84ea45f8f295418d67363f0e8413fc63f0c5ae0 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 12:00:02 +0000 Subject: [PATCH 04/13] v1.0.3 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bb7648a..4e5df7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.3.dev0" +version = "1.0.3" description = "Create an Model Context Protocol server project from a template." readme = "README.md" requires-python = ">=3.10" From 6e43668dbaceeea6766123d4bcb1b18c493edb07 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 12:00:36 +0000 Subject: [PATCH 05/13] bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4e5df7b..5f54454 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.3" +version = "1.0.4.dev0" description = "Create an Model Context Protocol server project from a template." readme = "README.md" requires-python = ">=3.10" From 6559948d349fcd027ad791591068977373799722 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 14:50:08 +0000 Subject: [PATCH 06/13] Resolve the path before using it --- src/create_mcp_server/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/create_mcp_server/__init__.py b/src/create_mcp_server/__init__.py index 99f57fd..c7af81d 100644 --- a/src/create_mcp_server/__init__.py +++ b/src/create_mcp_server/__init__.py @@ -341,6 +341,8 @@ 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) From b56d4c518617f35b3f664f8cddca3761b3e9fe53 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 14:50:23 +0000 Subject: [PATCH 07/13] v1.0.4 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5f54454..cc6c4ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.4.dev0" +version = "1.0.4" description = "Create an Model Context Protocol server project from a template." readme = "README.md" requires-python = ">=3.10" From a40b42b5348a4d2f9eb70736c7fc1f71cd2719f0 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 14:50:56 +0000 Subject: [PATCH 08/13] bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index cc6c4ac..5f54454 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.4" +version = "1.0.4.dev0" description = "Create an Model Context Protocol server project from a template." readme = "README.md" requires-python = ">=3.10" From e5c6c639231b4d935668ab2d29c280a95027c91d Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Thu, 21 Nov 2024 21:28:06 +0000 Subject: [PATCH 09/13] update readme --- .../template/README.md.jinja2 | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/src/create_mcp_server/template/README.md.jinja2 b/src/create_mcp_server/template/README.md.jinja2 index 7abfe66..c35655d 100644 --- a/src/create_mcp_server/template/README.md.jinja2 +++ b/src/create_mcp_server/template/README.md.jinja2 @@ -24,14 +24,22 @@ The server implements one tool: - Takes "name" and "content" as required string arguments - Updates server state and notifies clients of resource changes -## Install +## Configuration -### Claude Desktop +[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", @@ -43,9 +51,51 @@ On Windows: `%APPDATA%/Claude/claude_desktop_config.json` ] } } + ``` +
+ +
+ 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 ``` -## Develop and Debug +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). From 35730e4a16a392b943e019d330d48d1a8cd5f52a Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Sun, 24 Nov 2024 21:02:02 +0000 Subject: [PATCH 10/13] Move template to flat app pattern --- .../template/server.py.jinja2 | 281 ++++++++---------- uv.lock | 2 +- 2 files changed, 129 insertions(+), 154 deletions(-) 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 372f52f..5e0d240 100644 --- a/uv.lock +++ b/uv.lock @@ -24,7 +24,7 @@ wheels = [ [[package]] name = "create-mcp-server" -version = "1.0.3.dev0" +version = "1.0.4.dev0" source = { editable = "." } dependencies = [ { name = "click" }, From 446072a2c0ba897d21d9a5c18237ad2427baeb80 Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Sun, 24 Nov 2024 21:03:28 +0000 Subject: [PATCH 11/13] 1.0.5 --- pyproject.toml | 2 +- src/create_mcp_server/__init__.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5f54454..4fdf847 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.4.dev0" +version = "1.0.5" description = "Create an Model Context Protocol server project from a template." readme = "README.md" requires-python = ">=3.10" diff --git a/src/create_mcp_server/__init__.py b/src/create_mcp_server/__init__.py index c7af81d..b54038d 100644 --- a/src/create_mcp_server/__init__.py +++ b/src/create_mcp_server/__init__.py @@ -24,6 +24,7 @@ class PyProject: 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: @@ -255,8 +256,6 @@ def check_package_name(name: str) -> bool: return True - - @click.command() @click.option( "--path", From 7d921b9302ce45913c07faa5ee2c3efc38f647bd Mon Sep 17 00:00:00 2001 From: David Soria Parra Date: Sun, 24 Nov 2024 21:04:36 +0000 Subject: [PATCH 12/13] bump --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4fdf847..6293767 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.5" +version = "1.0.6.dev0" description = "Create an Model Context Protocol server project from a template." readme = "README.md" requires-python = ">=3.10" From 40083e73016be20a9f7dd8f295ca71d3e3a9dfb1 Mon Sep 17 00:00:00 2001 From: Justin Spahr-Summers Date: Mon, 25 Nov 2024 09:10:40 -0600 Subject: [PATCH 13/13] Create CONTRIBUTING.md --- CONTRIBUTING.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 CONTRIBUTING.md 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.