fix: Add Smithery lifespan and auth mode detection
- Add SmitheryAppContext dataclass for stateless mode - Add app_lifespan_smithery() with minimal lifespan (no shared state) - Update is_oauth_mode() to detect Smithery mode and return BasicAuth - Use Smithery lifespan when SMITHERY_DEPLOYMENT=true - Add .well-known/mcp-config endpoint for config discovery - Skip document processors in Smithery mode (not enabled) Fixes startup issues in Smithery mode where missing env credentials would incorrectly trigger OAuth mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -266,17 +266,57 @@ class OAuthAppContext:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SmitheryAppContext:
|
||||||
|
"""Application context for Smithery stateless mode.
|
||||||
|
|
||||||
|
ADR-016: No shared client - clients created per-request from session config.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass # No shared state needed - everything comes from session config
|
||||||
|
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def app_lifespan_smithery(server: FastMCP) -> AsyncIterator[SmitheryAppContext]:
|
||||||
|
"""
|
||||||
|
Manage application lifecycle for Smithery stateless mode.
|
||||||
|
|
||||||
|
ADR-016: Minimal lifespan with no shared state.
|
||||||
|
- No shared Nextcloud client (created per-request from session config)
|
||||||
|
- No vector sync (disabled in Smithery mode)
|
||||||
|
- No persistent storage (stateless deployment)
|
||||||
|
- No document processors (not enabled in Smithery mode)
|
||||||
|
"""
|
||||||
|
logger.info("Starting MCP server in Smithery stateless mode")
|
||||||
|
logger.info("Clients will be created per-request from session config")
|
||||||
|
|
||||||
|
try:
|
||||||
|
yield SmitheryAppContext()
|
||||||
|
finally:
|
||||||
|
logger.info("Shutting down Smithery stateless mode")
|
||||||
|
|
||||||
|
|
||||||
def is_oauth_mode() -> bool:
|
def is_oauth_mode() -> bool:
|
||||||
"""
|
"""
|
||||||
Determine if OAuth mode should be used.
|
Determine if OAuth mode should be used.
|
||||||
|
|
||||||
OAuth mode is enabled when:
|
OAuth mode is enabled when:
|
||||||
- NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD are NOT set
|
- NEXTCLOUD_USERNAME and NEXTCLOUD_PASSWORD are NOT set
|
||||||
|
- AND we are NOT in Smithery stateless mode
|
||||||
- Or explicitly enabled via configuration
|
- Or explicitly enabled via configuration
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if OAuth mode, False if BasicAuth mode
|
True if OAuth mode, False if BasicAuth mode
|
||||||
"""
|
"""
|
||||||
|
# ADR-016: Smithery stateless mode uses per-request BasicAuth from session config
|
||||||
|
# It's not OAuth mode even though env credentials aren't set
|
||||||
|
deployment_mode = get_deployment_mode()
|
||||||
|
if deployment_mode == DeploymentMode.SMITHERY_STATELESS:
|
||||||
|
logger.info(
|
||||||
|
"BasicAuth mode (Smithery stateless - credentials from session config)"
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
username = os.getenv("NEXTCLOUD_USERNAME")
|
username = os.getenv("NEXTCLOUD_USERNAME")
|
||||||
password = os.getenv("NEXTCLOUD_PASSWORD")
|
password = os.getenv("NEXTCLOUD_PASSWORD")
|
||||||
|
|
||||||
@@ -860,8 +900,9 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
"OpenTelemetry tracing disabled (set OTEL_EXPORTER_OTLP_ENDPOINT to enable)"
|
"OpenTelemetry tracing disabled (set OTEL_EXPORTER_OTLP_ENDPOINT to enable)"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Determine authentication mode
|
# Determine authentication mode and deployment mode
|
||||||
oauth_enabled = is_oauth_mode()
|
oauth_enabled = is_oauth_mode()
|
||||||
|
deployment_mode = get_deployment_mode()
|
||||||
|
|
||||||
if oauth_enabled:
|
if oauth_enabled:
|
||||||
logger.info("Configuring MCP server for OAuth mode")
|
logger.info("Configuring MCP server for OAuth mode")
|
||||||
@@ -922,8 +963,13 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
auth=auth_settings,
|
auth=auth_settings,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.info("Configuring MCP server for BasicAuth mode")
|
# ADR-016: Use Smithery lifespan for stateless mode, BasicAuth otherwise
|
||||||
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_basic)
|
if deployment_mode == DeploymentMode.SMITHERY_STATELESS:
|
||||||
|
logger.info("Configuring MCP server for Smithery stateless mode")
|
||||||
|
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_smithery)
|
||||||
|
else:
|
||||||
|
logger.info("Configuring MCP server for BasicAuth mode")
|
||||||
|
mcp = FastMCP("Nextcloud MCP", lifespan=app_lifespan_basic)
|
||||||
|
|
||||||
@mcp.resource("nc://capabilities")
|
@mcp.resource("nc://capabilities")
|
||||||
async def nc_get_capabilities():
|
async def nc_get_capabilities():
|
||||||
@@ -1361,6 +1407,51 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
routes.append(Route("/health/ready", health_ready, methods=["GET"]))
|
routes.append(Route("/health/ready", health_ready, methods=["GET"]))
|
||||||
logger.info("Health check endpoints enabled: /health/live, /health/ready")
|
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)
|
# Add test webhook endpoint (for development/testing)
|
||||||
routes.append(
|
routes.append(
|
||||||
Route("/webhooks/nextcloud", handle_nextcloud_webhook, methods=["POST"])
|
Route("/webhooks/nextcloud", handle_nextcloud_webhook, methods=["POST"])
|
||||||
|
|||||||
Reference in New Issue
Block a user