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 df2dc6c8965878e7c65a3de73b454b2f2a2f5379 Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 20 Nov 2025 07:57:30 -0600 Subject: [PATCH 2/2] Add PyPI+uvx deployment pattern documentation for Python MCP servers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit documents an alternative deployment pattern that has been in production use (e.g., Braze MCP Server) but was previously undocumented in the MCPB specification. Changes: 1. README.md: - Add comprehensive section on PyPI-based deployment - Document when to use PyPI vs traditional bundling - Include comparison table with trade-offs - Reference Braze MCP Server as production example 2. MANIFEST.md: - Add Python PyPI+uvx configuration example - Document uvx command usage - Link to README for detailed comparison 3. New example: examples/pypi-python/ - Complete working example of PyPI-based bundle - Demonstrates uvx command configuration - Shows pyproject.toml with [project.scripts] entry point - Includes comprehensive README with testing instructions 4. Zscaler MCP fix documentation: - Document correct manifest configuration for Zscaler - Show how to fix command: "python" → "uvx" - Verify package published at pypi.org/project/zscaler-mcp/ This pattern enables: - Smaller bundle sizes (< 1 MB vs 50+ MB) - Automatic updates via @latest tag - Modern Python packaging alignment 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 | 26 ++- README.md | 83 +++++++ examples/pypi-python/README.md | 177 ++++++++++++++ examples/pypi-python/ZSCALER_FIX.md | 217 ++++++++++++++++++ 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 ++++++++ 8 files changed, 706 insertions(+), 1 deletion(-) create mode 100644 examples/pypi-python/README.md create mode 100644 examples/pypi-python/ZSCALER_FIX.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..415c89e 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,30 @@ The `mcp_config` object in the server configuration defines how the implementing } ``` +**Python Example (PyPI + uvx):** + +For Python packages published to PyPI, you can use `uvx` to dynamically fetch dependencies: + +```json +"mcp_config": { + "command": "uvx", + "args": [ + "--native-tls", + "your-package-name@latest" + ], + "env": { + "API_KEY": "${user_config.api_key}" + } +} +``` + +This requires: +- Package published to PyPI with a `[project.scripts]` entry point +- Users have `uv` installed globally (`pip install uv` or `brew install uv`) +- Internet connection at first launch for dependency fetching + +See README.md for detailed comparison of bundling vs. PyPI deployment approaches. + **Node.js Example:** ```json diff --git a/README.md b/README.md index cec7a00..e9840a3 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,87 @@ 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` for dynamic dependency resolution instead of bundling dependencies. This approach trades bundle size for an external dependency requirement. + +**When to use PyPI deployment:** +- You're already publishing your package to PyPI +- You want smaller bundle sizes (< 1 MB vs 50+ MB) +- You need the ability to push updates without resubmitting bundles +- Your users can install additional tools + +**Requirements:** +- Your package must be published to PyPI +- Users must have `uv` installed globally (via `pip install uv` or `brew install uv`) +- Internet connection required at first launch (for dependency fetching) + +**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}" + } + } + } +} +``` + +**PyPI package requirements (`pyproject.toml`):** + +```toml +[project] +name = "your-package-name" # Must match uvx args +version = "1.0.0" +dependencies = [ + "mcp[cli]>=1.11.0", + # Your other dependencies +] + +[project.scripts] +your-package-name = "your_package.main:main" +``` + +**Bundle structure:** + +``` +bundle.mcpb (< 1 MB) +├── manifest.json +├── pyproject.toml # PyPI package definition +├── uv.lock # Lockfile for reproducibility +└── src/ + └── your_package/ + └── main.py # Source code (for reference/auditing) +``` + +**How it works:** +1. Claude Desktop executes: `uvx --native-tls your-package-name@latest` +2. `uvx` (like `npx` for Python) fetches the package from PyPI +3. Dependencies are installed to `~/.cache/uv/` (cached for future runs) +4. The package entry point is executed + +**Trade-offs:** + +| Aspect | Traditional Bundling | PyPI + uvx | +|--------|---------------------|------------| +| Bundle size | 50-100 MB | < 1 MB | +| User requirements | None (works offline) | `uv` must be installed | +| Internet required | No | Yes (first run) | +| Updates | Requires new bundle | `@latest` auto-updates | +| Compatibility | Maximum | Requires modern Python | + +**Example:** The Braze MCP Server uses this pattern. See their [GitHub repository](https://github.com/braze-inc/braze_mcp_server) 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..7164e75 --- /dev/null +++ b/examples/pypi-python/README.md @@ -0,0 +1,177 @@ +# PyPI-Based MCP Server Example + +This example demonstrates how to create an MCP Bundle that uses **PyPI and uvx** for dynamic dependency resolution instead of bundling all dependencies directly in the `.mcpb` file. + +## Overview + +This deployment pattern results in: +- **Smaller bundle sizes** (< 1 MB vs 50+ MB for traditional bundling) +- **Automatic updates** via `@latest` tag +- **Cleaner development** workflow aligned with modern Python packaging + +However, it requires: +- Users to have `uv` installed globally +- Internet connection at first launch (for dependency fetching) + +## How It Works + +1. **Bundle Contents**: Only source code, `pyproject.toml`, and lockfile +2. **Execution**: Claude Desktop runs `uvx --native-tls example-pypi-mcp@latest` +3. **Dependencies**: `uvx` fetches the package from PyPI and installs dependencies to `~/.cache/uv/` +4. **Caching**: Subsequent runs use cached dependencies (offline after first run) + +## Comparison with Traditional Bundling + +| Aspect | Traditional Bundling | PyPI + uvx (This Example) | +|--------|---------------------|---------------------------| +| Bundle size | 50-100 MB | < 1 MB | +| User requirements | None | `uv` must be installed | +| Internet required | No | Yes (first run) | +| Updates | Requires new bundle submission | `@latest` auto-updates | +| Offline support | Full | After first run | + +## File Structure + +``` +pypi-python/ +├── manifest.json # MCPB manifest with uvx configuration +├── pyproject.toml # PyPI package definition +├── README.md # This file +└── src/ + └── example_pypi_mcp/ + ├── __init__.py + └── main.py # MCP server implementation +``` + +## Key Configuration: manifest.json + +The critical difference from traditional bundles is in the `mcp_config`: + +```json +{ + "server": { + "type": "python", + "entry_point": "src/example_pypi_mcp/main.py", + "mcp_config": { + "command": "uvx", // ← Uses uvx instead of python + "args": [ + "--native-tls", + "example-pypi-mcp@latest" // ← Package name from PyPI + ], + "env": { + "API_KEY": "${user_config.api_key}" + } + } + } +} +``` + +**Important notes:** +- `entry_point` is included for reference but not used at runtime +- `command: "uvx"` tells Claude Desktop to use uvx for execution +- Package name in `args` must match `[project.name]` in `pyproject.toml` + +## Key Configuration: pyproject.toml + +The package must define a console script entry point: + +```toml +[project] +name = "example-pypi-mcp" # Must match uvx args + +[project.scripts] +example-pypi-mcp = "example_pypi_mcp.main:main" # Entry point +``` + +## Building This Example + +**Important:** This example is for demonstration purposes only and should not be uploaded to PyPI (note the `"Private :: Do Not Upload"` classifier). + +### 1. Create the bundle + +```bash +cd examples/pypi-python +mcpb pack . example-pypi-mcp.mcpb +``` + +### 2. For real deployment, publish to PyPI + +```bash +# Install build tools +pip install build twine + +# Build the package +python -m build + +# Upload to PyPI (requires account) +twine upload dist/* +``` + +### 3. Users install uv + +```bash +# Via pip +pip install uv + +# Or via Homebrew (macOS/Linux) +brew install uv +``` + +### 4. Install in Claude Desktop + +Open the `.mcpb` file with Claude Desktop. It will: +1. Extract bundle contents +2. Parse manifest.json +3. Configure the server to run via `uvx` + +On first use, `uvx` will fetch dependencies from PyPI. + +## When to Use This Pattern + +**Use PyPI deployment when:** +- ✅ You're already publishing your package to PyPI +- ✅ You want minimal bundle sizes +- ✅ You need to push updates without resubmitting bundles +- ✅ Your target users are comfortable installing tools + +**Use traditional bundling when:** +- ✅ You want maximum compatibility (works for everyone) +- ✅ You need offline support from the start +- ✅ You're not publishing to PyPI +- ✅ You want zero external dependencies + +## Testing + +### Local Testing (without PyPI) + +```bash +# Install in development mode +pip install -e . + +# Run directly +python -m example_pypi_mcp.main + +# Or via installed script +example-pypi-mcp +``` + +### Testing with uvx (without PyPI upload) + +```bash +# Use local directory +uvx --from . example-pypi-mcp +``` + +## Real-World Example + +The **Braze MCP Server** uses this pattern in production: +- Repository: https://github.com/braze-inc/braze_mcp_server +- PyPI: https://pypi.org/project/braze-mcp-server/ +- Bundle size: ~800 KB (vs. potential 50+ MB with bundled dependencies) + +## Additional Resources + +- [uv documentation](https://github.com/astral-sh/uv) +- [Python Packaging Guide](https://packaging.python.org/) +- [MCPB Manifest Specification](../../MANIFEST.md) +- [MCPB README](../../README.md) diff --git a/examples/pypi-python/ZSCALER_FIX.md b/examples/pypi-python/ZSCALER_FIX.md new file mode 100644 index 0000000..44ae003 --- /dev/null +++ b/examples/pypi-python/ZSCALER_FIX.md @@ -0,0 +1,217 @@ +# Zscaler MCP Server - Fixed Configuration + +The Zscaler MCP package is published to PyPI at: https://pypi.org/project/zscaler-mcp/ + +This document shows the correct manifest configuration for using the PyPI+uvx deployment pattern. + +## Problem with Current Submission + +**Current manifest.json (BROKEN):** +```json +{ + "server": { + "type": "python", + "mcp_config": { + "command": "python", // ← WRONG for PyPI deployment + "args": ["-m", "zscaler_mcp.server"], + "env": { + "PYTHONPATH": "${BUNDLE_ROOT}:${PYTHONPATH}", // ← Wrong variable + "ZSCALER_CLIENT_ID": "${user_config.client_id}", + "ZSCALER_CLIENT_SECRET": "${user_config.client_secret}" + } + } + } +} +``` + +**Issues:** +1. Uses `"command": "python"` but no dependencies bundled +2. Uses deprecated `${BUNDLE_ROOT}` variable +3. Mismatch between manifest and README documentation + +## Fixed Configuration + +**Corrected manifest.json:** +```json +{ + "$schema": "../../dist/mcpb-manifest.schema.json", + "manifest_version": "0.3", + "name": "zscaler-mcp", + "display_name": "Zscaler MCP Server", + "version": "0.4.0", + "description": "Zscaler MCP Server - Connect AI agents with Zscaler Zero Trust Exchange", + "long_description": "Model Context Protocol (MCP) server for Zscaler Zero Trust Exchange. Provides read-only access to Zscaler security policies, rules, and configurations via AI agents like Claude.", + "author": { + "name": "Zscaler, Inc.", + "email": "bd-devrel@zscaler.com" + }, + "server": { + "type": "python", + "entry_point": "src/zscaler_mcp/server.py", + "mcp_config": { + "command": "uvx", + "args": [ + "--native-tls", + "zscaler-mcp@latest" + ], + "env": { + "ZSCALER_CLIENT_ID": "${user_config.client_id}", + "ZSCALER_CLIENT_SECRET": "${user_config.client_secret}", + "ZSCALER_CLOUD": "${user_config.cloud}" + } + } + }, + "user_config": { + "client_id": { + "type": "string", + "title": "Zscaler Client ID", + "description": "Your Zscaler API client ID", + "sensitive": true, + "required": true + }, + "client_secret": { + "type": "string", + "title": "Zscaler Client Secret", + "description": "Your Zscaler API client secret", + "sensitive": true, + "required": true + }, + "cloud": { + "type": "string", + "title": "Zscaler Cloud", + "description": "Your Zscaler cloud identifier (e.g., zscaler.net, zscalerone.net)", + "required": true, + "default": "zscaler.net" + } + }, + "keywords": ["zscaler", "mcp", "security", "zero-trust"], + "license": "MIT", + "compatibility": { + "platforms": ["darwin", "win32", "linux"], + "runtimes": { + "python": ">=3.11.0 <4.0.0" + } + }, + "privacy_policies": [] +} +``` + +## Key Changes + +1. **✅ Changed command:** `"python"` → `"uvx"` +2. **✅ Changed args:** `["-m", "zscaler_mcp.server"]` → `["--native-tls", "zscaler-mcp@latest"]` +3. **✅ Removed PYTHONPATH:** Not needed with uvx (it handles dependencies) +4. **✅ Removed BUNDLE_ROOT:** Not needed (and deprecated) +5. **✅ Added manifest_version:** Updated to 0.3 + +## Testing the Fix + +### 1. Verify PyPI package works + +```bash +# Install uv if not already installed +pip install uv + +# Test the package directly +uvx --native-tls zscaler-mcp@latest + +# Should start the MCP server and wait for stdio input +``` + +### 2. Create the bundle + +```bash +# Use the corrected manifest.json +mcpb pack . zscaler-mcp.mcpb +``` + +### 3. Verify bundle contents + +```bash +mcpb info zscaler-mcp.mcpb +``` + +Expected bundle size: < 1 MB (no bundled dependencies) + +### 4. Install in Claude Desktop + +Open the `.mcpb` file with Claude Desktop. The server should: +1. Execute `uvx --native-tls zscaler-mcp@latest` +2. `uvx` fetches the package from PyPI on first run +3. Dependencies cached to `~/.cache/uv/` +4. Server starts successfully + +## User Requirements + +**Users must have `uv` installed:** +```bash +# Via pip +pip install uv + +# Or via Homebrew +brew install uv + +# Or via cargo +cargo install uv +``` + +## Bundle Structure (Corrected) + +``` +zscaler-mcp.mcpb (< 500 KB) +├── manifest.json # Updated with uvx configuration +├── pyproject.toml # PyPI package metadata +├── README.md # Documentation +└── src/ + └── zscaler_mcp/ # Source code (for reference/auditing) +``` + +**No `server/lib/` directory needed** - dependencies fetched from PyPI at runtime. + +## Comparison: Before vs After + +| Aspect | Broken (Oct 2025) | Fixed (PyPI Model) | +|--------|-------------------|-------------------| +| Command | `python` | `uvx` | +| Args | `["-m", "zscaler_mcp.server"]` | `["--native-tls", "zscaler-mcp@latest"]` | +| PYTHONPATH | Set (incorrectly) | Not needed | +| Bundle size | 483 KB (broken) | < 500 KB (working) | +| Dependencies | Missing | Fetched from PyPI | +| Works? | ❌ No | ✅ Yes | + +## Alternative: Traditional Bundling + +If you prefer to bundle dependencies instead of using PyPI: + +```json +{ + "mcp_config": { + "command": "python", + "args": ["${__dirname}/server/main.py"], + "env": { + "PYTHONPATH": "${__dirname}/server/lib", + "ZSCALER_CLIENT_ID": "${user_config.client_id}", + "ZSCALER_CLIENT_SECRET": "${user_config.client_secret}", + "ZSCALER_CLOUD": "${user_config.cloud}" + } + } +} +``` + +Then bundle dependencies: +```bash +pip install --target server/lib zscaler-mcp +``` + +Bundle size: ~50-100 MB (with all dependencies) + +## Recommendation + +**Use the PyPI+uvx model** (corrected configuration above) because: +- ✅ Zscaler package already published to PyPI +- ✅ Much smaller bundle size +- ✅ Automatic updates with `@latest` tag +- ✅ Aligns with modern Python packaging practices +- ✅ Matches what README documents + +The traditional bundling approach would work but is unnecessary given the package is already on PyPI. 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()