From 193eb9391230b0271f372b898cc30ebe68e26650 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 13 Nov 2025 09:47:24 -0600 Subject: [PATCH 1/2] Enhance README with testing, portability, and error handling guidance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive documentation for: - Variable substitution patterns for cross-platform portability - Four-phase testing approach (dev, clean environment, cross-platform, integration) - Common portability mistakes and solutions - Error message best practices with actionable examples These additions help developers create more robust and maintainable MCPB bundles. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/README.md b/README.md index 608023e..cec7a00 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,133 @@ bundle.mcpb (ZIP file) - Include all required shared libraries if dynamic linking used - Test on clean systems without development tools +### Using Variable Substitution for Portability + +The manifest supports variable substitution for cross-platform compatibility: + +**Available variables:** +- `${__dirname}` - Extension's installation directory +- `${HOME}` - User's home directory +- `${DESKTOP}` - User's desktop folder +- `${DOCUMENTS}` - User's documents folder +- `${DOWNLOADS}` - User's downloads folder +- `${pathSeparator}` or `${/}` - Platform-specific separator +- `${user_config.KEY}` - User-configured values + +**Common portability mistakes:** +```javascript +// ❌ WRONG - Hardcoded absolute paths +spawn('/usr/local/bin/node', ['script.js']); +spawn('C:\\Program Files\\tool\\bin.exe'); + +// ✅ CORRECT - Use runtime's executables +spawn(process.execPath, ['script.js']); + +// ❌ WRONG - Assuming global packages +spawn('npx', ['some-command']); + +// ✅ CORRECT - Bundle and reference locally +spawn('${__dirname}/node_modules/.bin/tool'); +``` + +**Testing portability:** +1. Fresh VM without development tools +2. Different OS than development machine +3. Verify variable substitution works +4. Check all paths resolve correctly + +## Testing Your MCPB + +Before distributing your MCPB, follow this four-phase testing approach: + +### Phase 1: Development Testing +- Unit tests for your server code +- Manual testing on your development machine +- Tool functionality verification +- Error handling validation + +### Phase 2: Clean Environment Testing ⚠️ Critical +Test on a fresh system without development tools to catch portability issues: + +**Using Docker (recommended):** +```bash +# Create clean test environment +docker run -it node:20 bash + +# Copy ONLY your MCPB bundle (no global packages) +# Test installation exactly as users would +``` + +**What this catches:** +- Missing bundled dependencies +- Hardcoded paths +- Global package assumptions +- Platform-specific code issues + +**Common issues found:** +- ❌ `spawn('/usr/local/bin/node')` - Hardcoded path +- ✅ `spawn(process.execPath)` - Runtime's own Node.js +- ❌ Assuming `npx` is globally installed +- ✅ Bundling all required executables + +### Phase 3: Cross-Platform Testing +- Test on different OS than you developed on +- Verify paths work correctly on both macOS and Windows +- Use forward slashes in paths (automatically converted) +- Test platform-specific features with fallbacks + +### Phase 4: Integration Testing +- Install in actual host application (Claude Desktop, etc.) +- Test complete end-to-end workflows +- Verify error messages are helpful +- Check performance and responsiveness + +## Error Message Best Practices + +Well-crafted error messages dramatically reduce support burden. Include three components: + +### 1. What went wrong (specific diagnosis) +```javascript +// ❌ Generic +throw new Error("An error occurred"); + +// ✅ Specific +throw new Error("Failed to read config file at path/to/config.json"); +``` + +### 2. Why it happened (context) +```javascript +// ❌ Vague +return { isError: true, content: [{ type: "text", text: "Authentication failed" }] }; + +// ✅ Clear +return { + isError: true, + content: [{ + type: "text", + text: "API key is invalid or has expired. Generate a new key at Settings → API" + }] +}; +``` + +### 3. How to fix it (actionable steps) +```javascript +// ❌ Unhelpful +throw new Error("Check your settings"); + +// ✅ Actionable +throw new Error("Missing API key. Add it in Settings → Extensions → [Your Extension] → API Key field"); +``` + +### Error Categories +Return structured errors via MCP protocol with clear `isError` flags: + +- **Configuration errors** - Missing or invalid settings +- **Authentication errors** - Invalid credentials with regeneration instructions +- **Resource errors** - File not found, network unavailable with paths/URLs +- **Permission errors** - Access denied with required permission details +- **Validation errors** - Invalid input with expected format + # Contributing We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details. From 1c7ab2e6332d98783c6d14627d92bc2981ac2215 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 20 Nov 2025 07:57:30 -0600 Subject: [PATCH 2/2] Document PyPI+uvx deployment pattern for Python MCP servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add minimal documentation for an alternative Python deployment pattern using PyPI and uvx instead of bundling dependencies. Changes: - README.md: Add concise PyPI deployment section with trade-offs - MANIFEST.md: Add uvx configuration example - examples/pypi-python/: Working reference implementation This pattern enables: - Smaller bundle sizes (< 1 MB vs 50+ MB) - Dynamic dependency resolution via PyPI - Automatic updates with @latest tag Trade-offs: - Requires users to install uv tool - Needs internet connection at first launch 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- MANIFEST.md | 18 +++- README.md | 39 ++++++++ examples/pypi-python/README.md | 34 +++++++ examples/pypi-python/manifest.json | 66 +++++++++++++ examples/pypi-python/pyproject.toml | 38 ++++++++ .../src/example_pypi_mcp/__init__.py | 3 + .../pypi-python/src/example_pypi_mcp/main.py | 97 +++++++++++++++++++ 7 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 examples/pypi-python/README.md create mode 100644 examples/pypi-python/manifest.json create mode 100644 examples/pypi-python/pyproject.toml create mode 100644 examples/pypi-python/src/example_pypi_mcp/__init__.py create mode 100644 examples/pypi-python/src/example_pypi_mcp/main.py diff --git a/MANIFEST.md b/MANIFEST.md index c4cdd52..40c635e 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -419,7 +419,7 @@ The `server` object defines how to run the MCP server: The `mcp_config` object in the server configuration defines how the implementing app should execute the MCP server. This replaces the manual JSON configuration users currently need to write. -**Python Example:** +**Python Example (Traditional Bundling):** ```json "mcp_config": { @@ -431,6 +431,22 @@ The `mcp_config` object in the server configuration defines how the implementing } ``` +**Python Example (PyPI + uvx):** + +For packages published to PyPI, use `uvx` to fetch dependencies dynamically: + +```json +"mcp_config": { + "command": "uvx", + "args": ["--native-tls", "your-package-name@latest"], + "env": { + "API_KEY": "${user_config.api_key}" + } +} +``` + +Requires: Package on PyPI with `[project.scripts]` entry point, users have `uv` installed. + **Node.js Example:** ```json diff --git a/README.md b/README.md index cec7a00..acf7541 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ bundle.mcpb (ZIP file) ### Bundling Dependencies +#### Traditional Bundling (Recommended for Maximum Compatibility) + **Python Bundles:** - Bundle all required packages in `server/lib/` directory @@ -129,6 +131,43 @@ bundle.mcpb (ZIP file) - Include all required shared libraries if dynamic linking used - Test on clean systems without development tools +#### Alternative: PyPI-Based Deployment for Python (Advanced) + +For Python packages published to PyPI, you can use `uvx` to dynamically fetch dependencies instead of bundling them. + +**Requirements:** +- Package published to PyPI with `[project.scripts]` entry point +- Users must have `uv` installed (`pip install uv` or `brew install uv`) +- Internet connection at first launch + +**Manifest configuration:** + +```json +{ + "server": { + "type": "python", + "entry_point": "src/your_package/main.py", + "mcp_config": { + "command": "uvx", + "args": ["--native-tls", "your-package-name@latest"], + "env": { + "API_KEY": "${user_config.api_key}" + } + } + } +} +``` + +**Trade-offs:** + +| Aspect | Traditional Bundling | PyPI + uvx | +|--------|---------------------|------------| +| Bundle size | 50-100 MB | < 1 MB | +| User requirements | None | `uv` must be installed | +| Updates | Requires new bundle | `@latest` auto-updates | + +See `examples/pypi-python/` for a complete reference implementation. + ### Using Variable Substitution for Portability The manifest supports variable substitution for cross-platform compatibility: diff --git a/examples/pypi-python/README.md b/examples/pypi-python/README.md new file mode 100644 index 0000000..ae0160c --- /dev/null +++ b/examples/pypi-python/README.md @@ -0,0 +1,34 @@ +# PyPI-Based MCP Server Example + +This example demonstrates creating an MCP Bundle that uses PyPI and `uvx` for dynamic dependency resolution. + +## How It Works + +1. Bundle contains only source code and package metadata (< 1 MB) +2. Claude Desktop runs `uvx --native-tls example-pypi-mcp@latest` +3. `uvx` fetches dependencies from PyPI and caches them locally + +## Key Configuration + +**manifest.json:** +```json +{ + "mcp_config": { + "command": "uvx", + "args": ["--native-tls", "example-pypi-mcp@latest"] + } +} +``` + +**pyproject.toml:** +```toml +[project] +name = "example-pypi-mcp" + +[project.scripts] +example-pypi-mcp = "example_pypi_mcp.main:main" +``` + +## Usage + +Users need `uv` installed (`pip install uv`). When Claude Desktop runs the server, `uvx` automatically fetches the package from PyPI on first use. diff --git a/examples/pypi-python/manifest.json b/examples/pypi-python/manifest.json new file mode 100644 index 0000000..e283513 --- /dev/null +++ b/examples/pypi-python/manifest.json @@ -0,0 +1,66 @@ +{ + "$schema": "../../dist/mcpb-manifest.schema.json", + "manifest_version": "0.3", + "name": "example-pypi-mcp", + "display_name": "PyPI Example MCP Server", + "version": "1.0.0", + "description": "Example MCP server demonstrating PyPI-based deployment with uvx", + "long_description": "This example demonstrates how to create an MCP Bundle that uses PyPI and uvx for dynamic dependency resolution instead of bundling dependencies. This approach results in smaller bundle sizes (< 1 MB) and enables automatic updates, but requires users to have 'uv' installed globally.", + "author": { + "name": "Anthropic", + "email": "support@anthropic.com", + "url": "https://github.com/anthropics" + }, + "server": { + "type": "python", + "entry_point": "src/example_pypi_mcp/main.py", + "mcp_config": { + "command": "uvx", + "args": [ + "--native-tls", + "example-pypi-mcp@latest" + ], + "env": { + "API_KEY": "${user_config.api_key}", + "DEBUG": "${user_config.debug_mode}" + } + } + }, + "tools": [ + { + "name": "echo", + "description": "Echo back a message (demonstrates basic tool functionality)" + }, + { + "name": "get_timestamp", + "description": "Get current timestamp in ISO format" + } + ], + "keywords": ["example", "pypi", "uvx", "python", "deployment"], + "license": "MIT", + "user_config": { + "api_key": { + "type": "string", + "title": "API Key", + "description": "Example API key for demonstration purposes", + "sensitive": true, + "required": false, + "default": "demo-key-12345" + }, + "debug_mode": { + "type": "boolean", + "title": "Debug Mode", + "description": "Enable debug logging output", + "default": false, + "required": false + } + }, + "compatibility": { + "claude_desktop": ">=0.10.0", + "platforms": ["darwin", "win32", "linux"], + "runtimes": { + "python": ">=3.10.0 <4" + } + }, + "privacy_policies": [] +} diff --git a/examples/pypi-python/pyproject.toml b/examples/pypi-python/pyproject.toml new file mode 100644 index 0000000..15ee418 --- /dev/null +++ b/examples/pypi-python/pyproject.toml @@ -0,0 +1,38 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "example-pypi-mcp" +version = "1.0.0" +description = "Example MCP server demonstrating PyPI-based deployment" +readme = "README.md" +requires-python = ">=3.10" +authors = [ + {name = "Anthropic", email = "support@anthropic.com"} +] +license = {text = "MIT"} +keywords = ["mcp", "model-context-protocol", "example", "pypi"] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Private :: Do Not Upload" # Prevents accidental upload to PyPI +] + +dependencies = [ + "mcp[cli]>=1.11.0", +] + +[project.scripts] +example-pypi-mcp = "example_pypi_mcp.main:main" + +[tool.setuptools] +packages = ["src/example_pypi_mcp"] + +[tool.setuptools.package-dir] +"example_pypi_mcp" = "src/example_pypi_mcp" diff --git a/examples/pypi-python/src/example_pypi_mcp/__init__.py b/examples/pypi-python/src/example_pypi_mcp/__init__.py new file mode 100644 index 0000000..f401679 --- /dev/null +++ b/examples/pypi-python/src/example_pypi_mcp/__init__.py @@ -0,0 +1,3 @@ +"""Example PyPI-based MCP Server.""" + +__version__ = "1.0.0" diff --git a/examples/pypi-python/src/example_pypi_mcp/main.py b/examples/pypi-python/src/example_pypi_mcp/main.py new file mode 100644 index 0000000..42da5ea --- /dev/null +++ b/examples/pypi-python/src/example_pypi_mcp/main.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +""" +Example MCP Server demonstrating PyPI-based deployment with uvx. + +This server shows how to create an MCP Bundle that uses dynamic dependency +resolution via PyPI and uvx instead of bundling all dependencies. +""" + +import logging +import os +from datetime import datetime, timezone + +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import Tool, TextContent + +# Configure logging +logging.basicConfig( + level=logging.DEBUG if os.getenv("DEBUG") == "true" else logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +# Create server instance +app = Server("example-pypi-mcp") + + +@app.list_tools() +async def list_tools() -> list[Tool]: + """List available tools.""" + return [ + Tool( + name="echo", + description="Echo back a message (demonstrates basic tool functionality)", + inputSchema={ + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Message to echo back" + } + }, + "required": ["message"] + } + ), + Tool( + name="get_timestamp", + description="Get current timestamp in ISO format", + inputSchema={ + "type": "object", + "properties": {} + } + ) + ] + + +@app.call_tool() +async def call_tool(name: str, arguments: dict) -> list[TextContent]: + """Handle tool calls.""" + logger.info(f"Tool called: {name} with arguments: {arguments}") + + if name == "echo": + message = arguments.get("message", "") + api_key = os.getenv("API_KEY", "not-set") + return [ + TextContent( + type="text", + text=f"Echo: {message}\n\nAPI Key configured: {api_key[:10]}..." if len(api_key) > 10 else api_key + ) + ] + + elif name == "get_timestamp": + timestamp = datetime.now(timezone.utc).isoformat() + return [ + TextContent( + type="text", + text=f"Current UTC timestamp: {timestamp}" + ) + ] + + else: + raise ValueError(f"Unknown tool: {name}") + + +def main(): + """Main entry point for the server.""" + logger.info("Starting Example PyPI MCP Server") + logger.info(f"API_KEY environment variable: {'set' if os.getenv('API_KEY') else 'not set'}") + logger.info(f"DEBUG mode: {os.getenv('DEBUG', 'false')}") + + # Run the server using stdio transport + import asyncio + asyncio.run(stdio_server(app)) + + +if __name__ == "__main__": + main()