diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index 02caa9d..930fddc 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -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"]) diff --git a/nextcloud_mcp_server/smithery_server.py b/nextcloud_mcp_server/smithery_server.py new file mode 100644 index 0000000..7c3de61 --- /dev/null +++ b/nextcloud_mcp_server/smithery_server.py @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 5481b0e..9c089b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/uv.lock b/uv.lock index 75307d2..3561db3 100644 --- a/uv.lock +++ b/uv.lock @@ -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"