feat: Add Smithery CLI deployment support

- Add smithery package as dependency
- Create smithery_server.py with @smithery.server() decorator
- Add SmitheryConfigSchema for session config (nextcloud_url, username, app_password)
- Add [tool.smithery] section to pyproject.toml
- Remove manual .well-known/mcp-config endpoint (Smithery handles this)

Smithery CLI will automatically:
- Extract config schema from the decorated function
- Handle session config parsing from query parameters
- Make config accessible via ctx.session_config in tools

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-11-22 18:05:33 +01:00
parent 8d29ce0122
commit b8dc413b73
4 changed files with 123 additions and 45 deletions
-45
View File
@@ -1407,51 +1407,6 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
routes.append(Route("/health/ready", health_ready, methods=["GET"]))
logger.info("Health check endpoints enabled: /health/live, /health/ready")
# ADR-016: MCP config discovery endpoint for Smithery
# Returns the session configuration schema that Smithery uses to render the config UI
def mcp_config(request):
"""MCP configuration discovery endpoint.
Returns the JSON schema for session configuration parameters.
Used by Smithery to render the configuration form for users.
"""
return JSONResponse(
{
"configSchema": {
"type": "object",
"required": ["nextcloud_url", "username", "app_password"],
"properties": {
"nextcloud_url": {
"type": "string",
"title": "Nextcloud URL",
"description": "Your Nextcloud instance URL (e.g., https://cloud.example.com). Must be publicly accessible.",
"pattern": "^https?://.+",
},
"username": {
"type": "string",
"title": "Username",
"description": "Your Nextcloud username",
"minLength": 1,
},
"app_password": {
"type": "string",
"title": "App Password",
"description": "Nextcloud app password. Generate at Settings > Security > App passwords. Do NOT use your main password.",
"minLength": 1,
},
},
},
"exampleConfig": {
"nextcloud_url": "https://cloud.example.com",
"username": "alice",
"app_password": "xxxxx-xxxxx-xxxxx-xxxxx-xxxxx",
},
}
)
routes.append(Route("/.well-known/mcp-config", mcp_config, methods=["GET"]))
logger.info("MCP config discovery endpoint enabled: /.well-known/mcp-config")
# Add test webhook endpoint (for development/testing)
routes.append(
Route("/webhooks/nextcloud", handle_nextcloud_webhook, methods=["POST"])
+82
View File
@@ -0,0 +1,82 @@
"""Smithery server factory for stateless deployment.
ADR-016: This module provides a server factory function decorated with
@smithery.server() for Smithery CLI deployment. Session configuration
is automatically handled by Smithery and accessible via ctx.session_config.
"""
import logging
import os
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel, Field
from smithery.decorators import smithery
from nextcloud_mcp_server.server import (
configure_calendar_tools,
configure_contacts_tools,
configure_cookbook_tools,
configure_deck_tools,
configure_notes_tools,
configure_sharing_tools,
configure_tables_tools,
configure_webdav_tools,
)
logger = logging.getLogger(__name__)
class SmitheryConfigSchema(BaseModel):
"""Configuration schema for Smithery session.
These fields are collected by Smithery's configuration UI and passed
to the server with each request as session_config.
"""
nextcloud_url: str = Field(
...,
description="Your Nextcloud instance URL (e.g., https://cloud.example.com)",
)
username: str = Field(
...,
description="Your Nextcloud username",
)
app_password: str = Field(
...,
description="Nextcloud app password (Settings > Security > App passwords)",
)
@smithery.server(config_schema=SmitheryConfigSchema)
def create_server():
"""Create and return a FastMCP server instance for Smithery deployment.
This function is called by Smithery CLI to create the server.
Session configuration is automatically handled by Smithery and
accessible via ctx.session_config in tool handlers.
"""
# Force Smithery mode
os.environ["SMITHERY_DEPLOYMENT"] = "true"
os.environ["VECTOR_SYNC_ENABLED"] = "false"
logger.info("Creating Nextcloud MCP Server for Smithery deployment")
# Import lifespan after setting env vars
from nextcloud_mcp_server.app import app_lifespan_smithery
# Create FastMCP server with Smithery lifespan
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_smithery)
# Register all core tools (semantic search is skipped in Smithery mode)
configure_notes_tools(mcp)
configure_tables_tools(mcp)
configure_webdav_tools(mcp)
configure_sharing_tools(mcp)
configure_calendar_tools(mcp)
configure_contacts_tools(mcp)
configure_cookbook_tools(mcp)
configure_deck_tools(mcp)
logger.info("Smithery server configured with core Nextcloud tools")
return mcp
+4
View File
@@ -39,6 +39,7 @@ dependencies = [
"pymupdf>=1.26.6",
"pymupdf4llm>=0.2.2",
"pymupdf-layout>=1.26.6",
"smithery>=0.4.4",
]
classifiers = [
"Development Status :: 4 - Beta",
@@ -132,3 +133,6 @@ name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true
[tool.smithery]
server = "nextcloud_mcp_server.smithery_server:create_server"
Generated
+37
View File
@@ -195,6 +195,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/74/f5/9373290775639cb67a2fce7f629a1c240dce9f12fe927bc32b2736e16dfc/argcomplete-3.6.3-py3-none-any.whl", hash = "sha256:f5007b3a600ccac5d25bbce33089211dfd49eab4a7718da3f10e3082525a92ce", size = 43846, upload-time = "2025-10-20T03:33:33.021Z" },
]
[[package]]
name = "art"
version = "6.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d4/7d/7d80509bbd19fb747edef94ba487dbadd2747944774ccc0528ad0d005a36/art-6.5.tar.gz", hash = "sha256:a98d77b42c278697ec6cf4b5bdcdfd997f6b2425332da078d4e31e31377d1844", size = 672902, upload-time = "2025-04-12T17:02:20.279Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/29/57b06fdb3abdf52c621d3ca3caea735e2db4c8d48288ebd26af448e8e247/art-6.5-py3-none-any.whl", hash = "sha256:70706408144c45c666caab690627d5c74aea7b6c7ce8cc968408ddeef8d84afd", size = 610382, upload-time = "2025-04-12T17:02:21.97Z" },
]
[[package]]
name = "asgiref"
version = "3.10.0"
@@ -1967,6 +1976,7 @@ dependencies = [
{ name = "python-json-logger" },
{ name = "pythonvcard4" },
{ name = "qdrant-client" },
{ name = "smithery" },
]
[package.dev-dependencies]
@@ -2015,6 +2025,7 @@ requires-dist = [
{ name = "python-json-logger", specifier = ">=3.2.0" },
{ name = "pythonvcard4", specifier = ">=0.2.0" },
{ name = "qdrant-client", specifier = ">=1.7.0" },
{ name = "smithery", specifier = ">=0.4.4" },
]
[package.metadata.requires-dev]
@@ -3554,6 +3565,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
[[package]]
name = "smithery"
version = "0.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "art" },
{ name = "pydantic" },
{ name = "starlette" },
{ name = "toml" },
{ name = "typer" },
{ name = "uvicorn" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1e/75/d0b0fc1a5c10a20f3e01cefd98276ccbe7b44d74eeb6551bd2f42d8b4768/smithery-0.4.4.tar.gz", hash = "sha256:18ae19af8405e6476ca4984036d4460822ec1647ad2262addb4909d03387d671", size = 17396, upload-time = "2025-10-24T15:47:44.543Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/30/54/a088fa621c9c76a72a70079fac42b822137a1609ec08525168bd8e9f415d/smithery-0.4.4-py3-none-any.whl", hash = "sha256:883d060b3ecc73a2972019760e342f5a04b62edf18e3bc03594a41851319808a", size = 25372, upload-time = "2025-10-24T15:47:43.045Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
@@ -3675,6 +3703,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" },
]
[[package]]
name = "toml"
version = "0.10.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size = 22253, upload-time = "2020-11-01T01:40:22.204Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
]
[[package]]
name = "tomli"
version = "2.3.0"