diff --git a/README.md b/README.md index 577c0b2..7f02f2b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +

+ Nextcloud MCP Server +

+ # Nextcloud MCP Server [![Docker Image](https://img.shields.io/badge/docker-ghcr.io/cbcoutinho/nextcloud--mcp--server-blue)](https://github.com/cbcoutinho/nextcloud-mcp-server/pkgs/container/nextcloud-mcp-server) @@ -29,6 +33,12 @@ docker run -p 127.0.0.1:8000:8000 --env-file .env --rm \ # 3. Test the connection curl http://127.0.0.1:8000/health/ready + +# 4. Connect to the endpoint +http://127.0.0.1:8000/sse + +# 4. Or with --transport streamable-http +http://127.0.0.1:8000/mcp ``` **Next Steps:** diff --git a/nextcloud_mcp_server/auth/templates/base.html b/nextcloud_mcp_server/auth/templates/base.html new file mode 100644 index 0000000..163f4c4 --- /dev/null +++ b/nextcloud_mcp_server/auth/templates/base.html @@ -0,0 +1,524 @@ + + + + + + + + + {% block title %}Nextcloud MCP Server{% endblock %} + + + + + + + + {% block extra_head %}{% endblock %} + + + + + +
+ + + Nextcloud MCP Server + +
+ + + {% block content %}{% endblock %} + + {% block scripts %}{% endblock %} + + diff --git a/nextcloud_mcp_server/auth/templates/error.html b/nextcloud_mcp_server/auth/templates/error.html new file mode 100644 index 0000000..3d7405f --- /dev/null +++ b/nextcloud_mcp_server/auth/templates/error.html @@ -0,0 +1,19 @@ +{% extends "base.html" %} + +{% block title %}{{ error_title|default('Error') }} - Nextcloud MCP Server{% endblock %} + +{% block content %} +

{{ error_title|default('Error') }}

+ +
+ Error: {{ error_message }} +
+ +{% if login_url %} +

Login again

+{% endif %} + +{% if back_url %} +

Go Back

+{% endif %} +{% endblock %} diff --git a/nextcloud_mcp_server/auth/templates/success.html b/nextcloud_mcp_server/auth/templates/success.html new file mode 100644 index 0000000..24eea31 --- /dev/null +++ b/nextcloud_mcp_server/auth/templates/success.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} + +{% block title %}{{ success_title|default('Success') }} - Nextcloud MCP Server{% endblock %} + +{% block extra_head %} +{% if redirect_url and redirect_delay %} + +{% endif %} +{% endblock %} + +{% block content %} +
+

{{ success_title|default('✓ Success') }}

+ {% for message in success_messages %} +

{{ message }}

+ {% endfor %} + {% if redirect_url %} +

Redirecting...

+ {% endif %} +
+{% endblock %} diff --git a/nextcloud_mcp_server/auth/templates/user_info.html b/nextcloud_mcp_server/auth/templates/user_info.html new file mode 100644 index 0000000..f107eed --- /dev/null +++ b/nextcloud_mcp_server/auth/templates/user_info.html @@ -0,0 +1,325 @@ +{% extends "base.html" %} + +{% block title %}Nextcloud MCP Server{% endblock %} + +{% block extra_head %} + + + + + + + + +{% endblock %} + +{% block extra_styles %} + /* Smooth htmx transitions */ + .htmx-swapping { + opacity: 0; + transition: opacity 200ms ease-out; + } + + .htmx-settling { + opacity: 1; + transition: opacity 200ms ease-in; + } + + /* Logout button styling */ + .logout-section { + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid var(--color-border); + } +{% endblock %} + +{% block content %} +
+ + + + +
+
+ +
+
+

User Information

+ {{ user_info_tab_html|safe }} +
+
+ + {% if show_vector_sync_tab %} + +
+
+

Vector Sync Status

+ {{ vector_sync_tab_html|safe }} +
+
+ + +
+
+

Vector Visualization

+
+

Loading vector visualization...

+
+
+
+ {% endif %} + + {% if show_webhooks_tab %} + +
+
+

Webhook Management

+ {{ webhooks_tab_html|safe }} +
+
+ {% endif %} +
+
+
+ + +{% endblock %} diff --git a/nextcloud_mcp_server/auth/templates/vector_viz.html b/nextcloud_mcp_server/auth/templates/vector_viz.html index 561afb2..c9ce688 100644 --- a/nextcloud_mcp_server/auth/templates/vector_viz.html +++ b/nextcloud_mcp_server/auth/templates/vector_viz.html @@ -1,38 +1,59 @@
-
-

Vector Visualization

-
- Testing search algorithms on your indexed documents. User: {{ username }} -
+
+ +
+
+
+
+ + +
- -
- -
- - -
- -
-
+
-
- +
+
-
- +
+ +
-
-
-
-

Advanced Options

- -
+
+
- -
+ +
-
-
- - -
+
+ + +
-
- - -
+
+ +
- - -
-

- BM25 Hybrid Search: Combines dense semantic vectors with sparse BM25 keyword vectors. -

-

- RRF: Reciprocal Rank Fusion - Rank-based fusion producing scores in [0.0, 1.0] -

-

- DBSF: Distribution-Based Score Fusion - Sums normalized scores (can exceed 1.0) -

-
-
- -
- -
-
-
- Executing search and computing PCA projection... -
-
+
-
-
-

Search Results ()

+ +
+
+
+ Executing search and computing PCA projection... +
+
+
+
+ + +
+

Search Results ()

Loading results... @@ -335,5 +342,6 @@
-
-
+
+
+
diff --git a/nextcloud_mcp_server/auth/userinfo_routes.py b/nextcloud_mcp_server/auth/userinfo_routes.py index d57806c..c84c039 100644 --- a/nextcloud_mcp_server/auth/userinfo_routes.py +++ b/nextcloud_mcp_server/auth/userinfo_routes.py @@ -9,15 +9,21 @@ For OAuth mode: Requires browser-based OAuth login to establish session. import logging import os +from pathlib import Path from typing import Any import httpx +from jinja2 import Environment, FileSystemLoader from starlette.authentication import requires from starlette.requests import Request from starlette.responses import HTMLResponse, JSONResponse logger = logging.getLogger(__name__) +# Setup Jinja2 environment for templates +_template_dir = Path(__file__).parent / "templates" +_jinja_env = Environment(loader=FileSystemLoader(_template_dir)) + async def _get_authenticated_client_for_userinfo(request: Request) -> httpx.AsyncClient: """Get an authenticated HTTP client for user info page operations. @@ -431,51 +437,14 @@ async def user_info_html(request: Request) -> HTMLResponse: oauth_ctx = getattr(request.app.state, "oauth_context", None) login_url = str(request.url_for("oauth_login")) if oauth_ctx else "/oauth/login" - error_html = f""" - - - - - - Error - Nextcloud MCP Server - - - -
-

Error Retrieving User Info

-
- Error: {user_context["error"]} -
-

Login again

-
- - - """ - return HTMLResponse(content=error_html) + template = _jinja_env.get_template("error.html") + return HTMLResponse( + content=template.render( + error_title="Error Retrieving User Info", + error_message=user_context["error"], + login_url=login_url, + ) + ) # Build HTML response auth_mode = user_context.get("auth_mode", "unknown") @@ -654,457 +623,19 @@ async def user_info_html(request: Request) -> HTMLResponse:
""" - html_content = f""" - - - - - - Nextcloud MCP Server - - - - - - - - - - - - - - - - -
-

Nextcloud MCP Server

- - -
- - { - "" - if not show_vector_sync_tab - else ''' - - ''' - } - { - "" - if not show_vector_sync_tab - else ''' - - ''' - } - { - "" - if not show_webhooks_tab - else ''' - - ''' - } -
- - -
- -
- {user_info_tab_html} -
- - { - "" - if not show_vector_sync_tab - else f''' - -
- {vector_sync_tab_html} -
- ''' - } - - { - "" - if not show_vector_sync_tab - else ''' - -
-
-

Loading vector visualization...

-
-
- ''' - } - - { - "" - if not show_webhooks_tab - else f''' - -
- {webhooks_tab_html} -
- ''' - } -
- - { - f'' - if auth_mode == "oauth" - else "" - } -
- - - """ - - return HTMLResponse(content=html_content) + # Render template + template = _jinja_env.get_template("user_info.html") + return HTMLResponse( + content=template.render( + user_info_tab_html=user_info_tab_html, + vector_sync_tab_html=vector_sync_tab_html, + webhooks_tab_html=webhooks_tab_html, + show_vector_sync_tab=show_vector_sync_tab, + show_webhooks_tab=show_webhooks_tab, + logout_url=logout_url if auth_mode == "oauth" else None, + nextcloud_host_for_links=nextcloud_host_for_links, + ) + ) @requires("authenticated", redirect="oauth_login") @@ -1124,17 +655,12 @@ async def revoke_session(request: Request) -> HTMLResponse: oauth_ctx = getattr(request.app.state, "oauth_context", None) if not oauth_ctx: + template = _jinja_env.get_template("error.html") return HTMLResponse( - """ - - - Error - -

Error

-

OAuth mode not enabled

- - - """, + content=template.render( + error_title="Error", + error_message="OAuth mode not enabled", + ), status_code=400, ) @@ -1142,17 +668,12 @@ async def revoke_session(request: Request) -> HTMLResponse: session_id = request.cookies.get("mcp_session") if not storage or not session_id: + template = _jinja_env.get_template("error.html") return HTMLResponse( - """ - - - Error - -

Error

-

Session not found

- - - """, + content=template.render( + error_title="Error", + error_message="Session not found", + ), status_code=400, ) @@ -1165,57 +686,26 @@ async def revoke_session(request: Request) -> HTMLResponse: # Redirect back to user page user_page_url = str(request.url_for("user_info_html")) + template = _jinja_env.get_template("success.html") return HTMLResponse( - f""" - - - - - - Background Access Revoked - - - -
-

✓ Background Access Revoked

-

Your refresh token has been deleted successfully.

-

Browser session remains active.

-

Redirecting back to user page...

-
- - - """ + content=template.render( + success_title="✓ Background Access Revoked", + success_messages=[ + "Your refresh token has been deleted successfully.", + "Browser session remains active.", + ], + redirect_url=user_page_url, + redirect_delay=2, + ) ) except Exception as e: logger.error(f"Failed to revoke background access: {e}") + template = _jinja_env.get_template("error.html") return HTMLResponse( - f""" - - - Error - -

Error

-

Failed to revoke background access: {e}

- - - """, + content=template.render( + error_title="Error", + error_message=f"Failed to revoke background access: {e}", + ), status_code=500, )