diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index fe64dba..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 6293767..cc6c4ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "create-mcp-server" -version = "1.0.6.dev0" +version = "1.0.4" 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 b54038d..c7af81d 100644 --- a/src/create_mcp_server/__init__.py +++ b/src/create_mcp_server/__init__.py @@ -24,7 +24,6 @@ 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: @@ -256,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 c35655d..7abfe66 100644 --- a/src/create_mcp_server/template/README.md.jinja2 +++ b/src/create_mcp_server/template/README.md.jinja2 @@ -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` -
- Development/Unpublished Servers Configuration - ``` +``` "mcpServers": { "{{server_name}}": { "command": "uv", @@ -51,51 +43,9 @@ 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 ``` -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). diff --git a/src/create_mcp_server/template/server.py.jinja2 b/src/create_mcp_server/template/server.py.jinja2 index bc39f5c..bd07d11 100644 --- a/src/create_mcp_server/template/server.py.jinja2 +++ b/src/create_mcp_server/template/server.py.jinja2 @@ -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( diff --git a/uv.lock b/uv.lock index 5e0d240..372f52f 100644 --- a/uv.lock +++ b/uv.lock @@ -24,7 +24,7 @@ wheels = [ [[package]] name = "create-mcp-server" -version = "1.0.4.dev0" +version = "1.0.3.dev0" source = { editable = "." } dependencies = [ { name = "click" },