simple python project to create an mcp server from the included template
This commit is contained in:
commit
10bde5e0e4
9 changed files with 732 additions and 0 deletions
162
.gitignore
vendored
Normal file
162
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# poetry
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||||
|
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||||
|
# commonly ignored for libraries.
|
||||||
|
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||||
|
#poetry.lock
|
||||||
|
|
||||||
|
# pdm
|
||||||
|
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||||
|
#pdm.lock
|
||||||
|
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||||
|
# in version control.
|
||||||
|
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||||
|
.pdm.toml
|
||||||
|
.pdm-python
|
||||||
|
.pdm-build/
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
77
README.md
Normal file
77
README.md
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
# MCP Create Server
|
||||||
|
|
||||||
|
[](https://pypi.org/project/mcp-create-server/) [](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
Create MCP server projects with no build configuration.
|
||||||
|
|
||||||
|
## Quick Overview
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Using pip
|
||||||
|
pip install mcp-create-server
|
||||||
|
create-mcp-server
|
||||||
|
|
||||||
|
# Or using uvx (recommended)
|
||||||
|
uvx create-mcp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
You don't need to install or configure any dependencies manually. The tool will set up everything you need to create an MCP server.
|
||||||
|
|
||||||
|
## Creating a Server
|
||||||
|
|
||||||
|
**You'll need to have uv >= 0.4.10 installed on your machine.**
|
||||||
|
|
||||||
|
To create a new server, run either of these commands:
|
||||||
|
|
||||||
|
### Using pip
|
||||||
|
```sh
|
||||||
|
pip install mcp-create-server
|
||||||
|
create-mcp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using uvx (recommended)
|
||||||
|
```sh
|
||||||
|
uvx create-mcp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
It will walk you through creating a new MCP server project. When complete, you'll have a new directory with this structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-server/
|
||||||
|
├── README.md
|
||||||
|
├── pyproject.toml
|
||||||
|
├── src/
|
||||||
|
│ └── my_server/
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ └── server.py
|
||||||
|
└── tests/
|
||||||
|
└── __init__.py
|
||||||
|
```
|
||||||
|
|
||||||
|
No configuration or complicated folder structures, only the files you need to run your server.
|
||||||
|
|
||||||
|
Once installation is done, you can start the server:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd my-server
|
||||||
|
uv sync --dev --all-extras
|
||||||
|
uv run my-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Simple command-line interface for creating new projects
|
||||||
|
- Auto-configures Claude Desktop app integration when available
|
||||||
|
- Uses uv for fast, reliable package management
|
||||||
|
- Sets up basic MCP server structure
|
||||||
|
- Includes example endpoints
|
||||||
|
|
||||||
|
## Philosophy
|
||||||
|
|
||||||
|
- **Zero Configuration:** No need to manually set up project structure or dependencies.
|
||||||
|
- **Best Practices:** Follows Python packaging standards and MCP server patterns.
|
||||||
|
- **Batteries Included:** Comes with everything needed to start building an MCP server.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Create MCP Server is open source software [licensed as MIT](https://opensource.org/licenses/MIT).
|
||||||
20
pyproject.toml
Normal file
20
pyproject.toml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
[project]
|
||||||
|
name = "create-mcp-server"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Create an MCP server project from a template."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = ["click>=8.1.7", "packaging>=24.2"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
create-mcp-server = "create_mcp_server:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
artifacts = ["template"]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
dev-dependencies = ["ruff>=0.7.4"]
|
||||||
200
src/create_mcp_server/__init__.py
Normal file
200
src/create_mcp_server/__init__.py
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import click
|
||||||
|
from packaging.version import parse
|
||||||
|
|
||||||
|
MIN_UV_VERSION = '0.4.10'
|
||||||
|
|
||||||
|
def check_uv_version(required_version: str) -> str | None:
|
||||||
|
"""Check if uv is installed and has minimum version"""
|
||||||
|
try:
|
||||||
|
result = subprocess.run(["uv", "--version"], capture_output=True, text=True, check=True)
|
||||||
|
version = result.stdout.strip()
|
||||||
|
match = re.match(r"uv (\d+\.\d+\.\d+)", version)
|
||||||
|
if match:
|
||||||
|
version_num = match.group(1)
|
||||||
|
if parse(version_num) >= parse(required_version):
|
||||||
|
return version
|
||||||
|
return None
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
click.echo("❌ Error: Failed to check uv version.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_uv_installed() -> None:
|
||||||
|
"""Ensure uv is installed at minimum version"""
|
||||||
|
if check_uv_version(MIN_UV_VERSION) is None:
|
||||||
|
click.echo(f"❌ Error: uv >= {MIN_UV_VERSION} is required but not installed.", err=True)
|
||||||
|
click.echo("To install, visit: https://github.com/astral-sh/uv", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_claude_config_path() -> Path | None:
|
||||||
|
"""Get the Claude config directory based on platform"""
|
||||||
|
if sys.platform == "win32":
|
||||||
|
path = Path(Path.home(), "AppData", "Roaming", "Claude")
|
||||||
|
elif sys.platform == "darwin":
|
||||||
|
path = Path(Path.home(), "Library", "Application Support", "Claude")
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if path.exists():
|
||||||
|
return path
|
||||||
|
return None
|
||||||
|
|
||||||
|
def has_claude_app() -> bool:
|
||||||
|
return get_claude_config_path() is not None
|
||||||
|
|
||||||
|
def update_claude_config(project_name: str, project_path: Path) -> bool:
|
||||||
|
"""Add the project to the Claude config if possible"""
|
||||||
|
config_dir = get_claude_config_path()
|
||||||
|
if not config_dir:
|
||||||
|
return False
|
||||||
|
|
||||||
|
config_file = config_dir / "claude_desktop_config.json"
|
||||||
|
if not config_file.exists():
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
config = json.loads(config_file.read_text())
|
||||||
|
if "mcpServers" not in config:
|
||||||
|
config["mcpServers"] = {}
|
||||||
|
|
||||||
|
if project_name in config["mcpServers"]:
|
||||||
|
click.echo(f"⚠️ Warning: {project_name} already exists in Claude.app configuration", err=True)
|
||||||
|
click.echo(f"Settings file location: {config_file}", err=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
config["mcpServers"][project_name] = {
|
||||||
|
"command": "uv",
|
||||||
|
"args": ["--directory", str(project_path), "run", project_name],
|
||||||
|
}
|
||||||
|
|
||||||
|
config_file.write_text(json.dumps(config, indent=2))
|
||||||
|
click.echo(f"✅ Added {project_name} to Claude.app configuration")
|
||||||
|
click.echo(f"Settings file location: {config_file}")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
click.echo("❌ Failed to update Claude.app configuration", err=True)
|
||||||
|
click.echo(f"Settings file location: {config_file}", err=True)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def copy_template(path: Path, name: str) -> None:
|
||||||
|
"""Copy template files into src/<project_name>"""
|
||||||
|
template_dir = Path(__file__).parent / "template"
|
||||||
|
src_dir = next((path / "src").glob("*/__init__.py"), None)
|
||||||
|
if src_dir is None:
|
||||||
|
click.echo("❌ Error: Could not find __init__.py in src directory", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
target_dir = src_dir.parent
|
||||||
|
try:
|
||||||
|
shutil.copytree(template_dir, target_dir, dirs_exist_ok=True)
|
||||||
|
except Exception as e:
|
||||||
|
click.echo(f"❌ Error: Failed to copy template files: {e}", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def create_project(path: Path, name: str, use_claude: bool = True) -> None:
|
||||||
|
"""Create a new project using uv"""
|
||||||
|
path.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["uv", "init", "--name", name, "--package", "--app", "--quiet"], cwd=path, check=True
|
||||||
|
)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
click.echo("❌ Error: Failed to initialize project.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Add mcp dependency using uv add
|
||||||
|
try:
|
||||||
|
subprocess.run(["uv", "add", "mcp"], cwd=path, check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
click.echo("❌ Error: Failed to add mcp dependency.", err=True)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
copy_template(path, name)
|
||||||
|
|
||||||
|
# Check if Claude.app is available
|
||||||
|
if use_claude and has_claude_app() and click.confirm("\nClaude.app detected. Would you like to install the server into Claude.app now?", default=True):
|
||||||
|
update_claude_config(name, path)
|
||||||
|
|
||||||
|
relpath = path.relative_to(Path.cwd())
|
||||||
|
click.echo(f"✅ Created project {name} in {relpath}")
|
||||||
|
click.echo("ℹ️ To install dependencies run:")
|
||||||
|
click.echo(f" cd {relpath}")
|
||||||
|
click.echo(" uv sync --dev --all-extras")
|
||||||
|
|
||||||
|
|
||||||
|
def check_package_name(name: str) -> bool:
|
||||||
|
"""Check if the package name is valid according to pyproject.toml spec"""
|
||||||
|
if not name:
|
||||||
|
click.echo("❌ Project name cannot be empty", err=True)
|
||||||
|
return False
|
||||||
|
if " " in name:
|
||||||
|
click.echo("❌ Project name must not contain spaces", err=True)
|
||||||
|
return False
|
||||||
|
if not all(c.isascii() and (c.isalnum() or c in "_-.") for c in name):
|
||||||
|
click.echo("❌ Project name must consist of ASCII letters, digits, underscores, hyphens, and periods", err=True)
|
||||||
|
return False
|
||||||
|
if name.startswith(("_", "-", ".")) or name.endswith(("_", "-", ".")):
|
||||||
|
click.echo("❌ Project name must not start or end with an underscore, hyphen, or period", err=True)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@click.command()
|
||||||
|
@click.option(
|
||||||
|
"--path",
|
||||||
|
type=click.Path(path_type=Path),
|
||||||
|
help="Directory to create project in",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--name",
|
||||||
|
type=str,
|
||||||
|
help="Project name",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--claudeapp/--no-claudeapp",
|
||||||
|
default=True,
|
||||||
|
help="Enable/disable Claude.app integration",
|
||||||
|
)
|
||||||
|
def main(path: Path | None, name: str | None, claudeapp: bool) -> int:
|
||||||
|
"""Create a new MCP server project"""
|
||||||
|
ensure_uv_installed()
|
||||||
|
|
||||||
|
click.echo("Creating a new MCP server project using uv.")
|
||||||
|
click.echo("This will set up a Python project with MCP dependency.")
|
||||||
|
click.echo("\nLet's begin!\n")
|
||||||
|
|
||||||
|
name = click.prompt("Project name (required)", type=str) if name is None else name
|
||||||
|
|
||||||
|
if name is None:
|
||||||
|
click.echo("❌ Error: Project name cannot be empty", err=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if not check_package_name(name):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
project_path = (Path.cwd() / name) if path is None else path
|
||||||
|
|
||||||
|
# Ask the user if the path is correct if not specified on command line
|
||||||
|
if path is None:
|
||||||
|
click.echo(f"Project will be created at: {project_path}")
|
||||||
|
if not click.confirm("Is this correct?", default=True):
|
||||||
|
project_path = Path(click.prompt("Enter the correct path", type=click.Path(path_type=Path)))
|
||||||
|
|
||||||
|
if project_path is None:
|
||||||
|
click.echo("❌ Error: Invalid path. Project creation aborted.", err=True)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
create_project(project_path, name, claudeapp)
|
||||||
|
|
||||||
|
return 0
|
||||||
5
src/create_mcp_server/__main__.py
Normal file
5
src/create_mcp_server/__main__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from . import main
|
||||||
|
|
||||||
|
sys.exit(main())
|
||||||
5
src/create_mcp_server/template/__init__.py
Normal file
5
src/create_mcp_server/template/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
from . import server
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
def main():
|
||||||
|
asyncio.run(server.main())
|
||||||
3
src/create_mcp_server/template/__main__.py
Normal file
3
src/create_mcp_server/template/__main__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from . import main
|
||||||
|
|
||||||
|
main()
|
||||||
180
src/create_mcp_server/template/server.py
Normal file
180
src/create_mcp_server/template/server.py
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import mcp.types as types
|
||||||
|
from mcp.server import Server
|
||||||
|
from pydantic import AnyUrl
|
||||||
|
import mcp.server.stdio
|
||||||
|
|
||||||
|
|
||||||
|
class ExampleServer(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
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("example-server")
|
||||||
|
|
||||||
|
# Store notes as a simple key-value dict to demonstrate state management
|
||||||
|
self.notes: dict[str, str] = {}
|
||||||
|
|
||||||
|
# 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
|
||||||
|
]
|
||||||
|
|
||||||
|
@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}")
|
||||||
|
|
||||||
|
name = uri.path
|
||||||
|
if name is not None:
|
||||||
|
name = name.lstrip('/')
|
||||||
|
return self.notes[name]
|
||||||
|
raise ValueError(f"Note not found: {name}")
|
||||||
|
|
||||||
|
|
||||||
|
# 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(
|
||||||
|
type="text",
|
||||||
|
text=f"Added note '{note_name}' with content: {content}",
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
server = ExampleServer()
|
||||||
|
|
||||||
|
# Run the server using stdin/stdout streams
|
||||||
|
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
|
||||||
|
await server.run(
|
||||||
|
read_stream, write_stream, server.create_initialization_options()
|
||||||
|
)
|
||||||
80
uv.lock
generated
Normal file
80
uv.lock
generated
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
version = 1
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "platform_system == 'Windows'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "create-mcp-server"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = { editable = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "click", specifier = ">=8.1.7" },
|
||||||
|
{ name = "packaging", specifier = ">=24.2" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "ruff", specifier = ">=0.7.4" }]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "24.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 },
|
||||||
|
]
|
||||||
Loading…
Reference in a new issue