Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 00e72d24a6 | |||
| dc78d92e5b | |||
| 86891173b2 | |||
| 73b3d80026 | |||
| 26099d643d | |||
| 56a5c63994 | |||
| 92c8e1e41d | |||
| dd12c957f6 | |||
| 74e2ab2440 | |||
| d124144424 | |||
| 39259ef282 | |||
| 14a59fdff3 | |||
| 2f138e7539 | |||
| 2baacc0ae8 | |||
| ff3123a190 | |||
| 2c37ad165e |
@@ -85,4 +85,4 @@ jobs:
|
|||||||
NEXTCLOUD_USERNAME: "admin"
|
NEXTCLOUD_USERNAME: "admin"
|
||||||
NEXTCLOUD_PASSWORD: "admin"
|
NEXTCLOUD_PASSWORD: "admin"
|
||||||
run: |
|
run: |
|
||||||
uv run pytest -v --log-cli-level=WARN --ignore=tests/manual
|
uv run pytest -v --log-cli-level=WARN -m smoke
|
||||||
|
|||||||
@@ -1,3 +1,24 @@
|
|||||||
|
## v0.34.2 (2025-11-13)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- Use NEXTCLOUD_OIDC_CLIENT_ID/SECRET env vars consistently
|
||||||
|
|
||||||
|
## v0.34.1 (2025-11-13)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- return all notes when search query is empty
|
||||||
|
|
||||||
|
## v0.34.0 (2025-11-13)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- Complete Phase 5 - Instrument all 93 MCP tools
|
||||||
|
- Add instrumentation decorator and apply to notes tools (Phase 5)
|
||||||
|
- Add OAuth token and database metrics (Phases 3-4)
|
||||||
|
- Add metrics instrumentation for queue, health, and database operations
|
||||||
|
|
||||||
## v0.33.1 (2025-11-13)
|
## v0.33.1 (2025-11-13)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
|||||||
name: nextcloud-mcp-server
|
name: nextcloud-mcp-server
|
||||||
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
||||||
type: application
|
type: application
|
||||||
version: 0.33.1
|
version: 0.34.2
|
||||||
appVersion: "0.33.1"
|
appVersion: "0.34.2"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
- mcp
|
- mcp
|
||||||
|
|||||||
+2
-3
@@ -3,7 +3,7 @@ services:
|
|||||||
# https://hub.docker.com/_/mariadb
|
# https://hub.docker.com/_/mariadb
|
||||||
db:
|
db:
|
||||||
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
|
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
|
||||||
image: docker.io/library/mariadb:lts@sha256:404ebf26ed7a56fbab05c29f6f1e70188e5eadb51bba8cee8d355775776deb08
|
image: docker.io/library/mariadb:lts@sha256:6b848cb24fbbd87429917f6c4422ac53c343e85692eb0fef86553e99e4f422f3
|
||||||
restart: always
|
restart: always
|
||||||
command: --transaction-isolation=READ-COMMITTED
|
command: --transaction-isolation=READ-COMMITTED
|
||||||
volumes:
|
volumes:
|
||||||
@@ -69,7 +69,6 @@ services:
|
|||||||
|
|
||||||
mcp:
|
mcp:
|
||||||
build: .
|
build: .
|
||||||
command: ["--transport", "streamable-http"]
|
|
||||||
restart: always
|
restart: always
|
||||||
depends_on:
|
depends_on:
|
||||||
app:
|
app:
|
||||||
@@ -156,7 +155,7 @@ services:
|
|||||||
- oauth-tokens:/app/data
|
- oauth-tokens:/app/data
|
||||||
|
|
||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:26.4.4@sha256:c6459d5fae1b759f5d667ebdc6237ab3121379c3494e213898569014ede1846d
|
image: quay.io/keycloak/keycloak:26.4.5@sha256:653852bfdea2be6e958b9e90a976eff1c6de34edd55f2f679bdc48ef16bc528e
|
||||||
command:
|
command:
|
||||||
- "start-dev"
|
- "start-dev"
|
||||||
- "--import-realm"
|
- "--import-realm"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -507,9 +507,9 @@ async def setup_oauth_config():
|
|||||||
- External IdP mode: OIDC_DISCOVERY_URL points to external provider
|
- External IdP mode: OIDC_DISCOVERY_URL points to external provider
|
||||||
→ External IdP for OAuth, Nextcloud user_oidc validates tokens and provides API access
|
→ External IdP for OAuth, Nextcloud user_oidc validates tokens and provides API access
|
||||||
|
|
||||||
Uses generic OIDC environment variables:
|
Uses OIDC environment variables:
|
||||||
- OIDC_DISCOVERY_URL: OIDC discovery endpoint (optional, defaults to NEXTCLOUD_HOST)
|
- OIDC_DISCOVERY_URL: OIDC discovery endpoint (optional, defaults to NEXTCLOUD_HOST)
|
||||||
- OIDC_CLIENT_ID / OIDC_CLIENT_SECRET: Static credentials (optional, uses DCR if not provided)
|
- NEXTCLOUD_OIDC_CLIENT_ID / NEXTCLOUD_OIDC_CLIENT_SECRET: Static credentials (optional, uses DCR if not provided)
|
||||||
- NEXTCLOUD_OIDC_SCOPES: Requested OAuth scopes
|
- NEXTCLOUD_OIDC_SCOPES: Requested OAuth scopes
|
||||||
|
|
||||||
This is done synchronously before FastMCP initialization because FastMCP
|
This is done synchronously before FastMCP initialization because FastMCP
|
||||||
@@ -633,19 +633,21 @@ async def setup_oauth_config():
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Load client credentials (static or dynamic registration)
|
# Load client credentials (static or dynamic registration)
|
||||||
client_id = os.getenv("OIDC_CLIENT_ID")
|
client_id = os.getenv("NEXTCLOUD_OIDC_CLIENT_ID")
|
||||||
client_secret = os.getenv("OIDC_CLIENT_SECRET")
|
client_secret = os.getenv("NEXTCLOUD_OIDC_CLIENT_SECRET")
|
||||||
|
|
||||||
if client_id and client_secret:
|
if client_id and client_secret:
|
||||||
logger.info(f"Using static OIDC client credentials: {client_id}")
|
logger.info(f"Using static OIDC client credentials: {client_id}")
|
||||||
elif registration_endpoint:
|
elif registration_endpoint:
|
||||||
logger.info("OIDC_CLIENT_ID not set, attempting Dynamic Client Registration")
|
logger.info(
|
||||||
|
"NEXTCLOUD_OIDC_CLIENT_ID not set, attempting Dynamic Client Registration"
|
||||||
|
)
|
||||||
client_id, client_secret = await load_oauth_client_credentials(
|
client_id, client_secret = await load_oauth_client_credentials(
|
||||||
nextcloud_host=nextcloud_host, registration_endpoint=registration_endpoint
|
nextcloud_host=nextcloud_host, registration_endpoint=registration_endpoint
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"OIDC_CLIENT_ID and OIDC_CLIENT_SECRET environment variables are required "
|
"NEXTCLOUD_OIDC_CLIENT_ID and NEXTCLOUD_OIDC_CLIENT_SECRET environment variables are required "
|
||||||
"when the OIDC provider does not support Dynamic Client Registration. "
|
"when the OIDC provider does not support Dynamic Client Registration. "
|
||||||
f"Discovery URL: {discovery_url}"
|
f"Discovery URL: {discovery_url}"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -288,8 +288,8 @@ def get_settings() -> Settings:
|
|||||||
return Settings(
|
return Settings(
|
||||||
# OAuth/OIDC settings
|
# OAuth/OIDC settings
|
||||||
oidc_discovery_url=os.getenv("OIDC_DISCOVERY_URL"),
|
oidc_discovery_url=os.getenv("OIDC_DISCOVERY_URL"),
|
||||||
oidc_client_id=os.getenv("OIDC_CLIENT_ID"),
|
oidc_client_id=os.getenv("NEXTCLOUD_OIDC_CLIENT_ID"),
|
||||||
oidc_client_secret=os.getenv("OIDC_CLIENT_SECRET"),
|
oidc_client_secret=os.getenv("NEXTCLOUD_OIDC_CLIENT_SECRET"),
|
||||||
oidc_issuer=os.getenv("OIDC_ISSUER"),
|
oidc_issuer=os.getenv("OIDC_ISSUER"),
|
||||||
# Nextcloud settings
|
# Nextcloud settings
|
||||||
nextcloud_host=os.getenv("NEXTCLOUD_HOST"),
|
nextcloud_host=os.getenv("NEXTCLOUD_HOST"),
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.33.1"
|
version = "0.34.2"
|
||||||
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
||||||
|
|||||||
+51
-3
@@ -9,6 +9,7 @@ import pytest
|
|||||||
from httpx import HTTPStatusError
|
from httpx import HTTPStatusError
|
||||||
from mcp import ClientSession
|
from mcp import ClientSession
|
||||||
from mcp.client.session import RequestContext
|
from mcp.client.session import RequestContext
|
||||||
|
from mcp.client.sse import sse_client
|
||||||
from mcp.client.streamable_http import streamablehttp_client
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
from mcp.types import ElicitRequestParams, ElicitResult, ErrorData
|
from mcp.types import ElicitRequestParams, ElicitResult, ErrorData
|
||||||
|
|
||||||
@@ -165,6 +166,51 @@ async def create_mcp_client_session(
|
|||||||
logger.debug(f"{client_name} client session cleaned up successfully")
|
logger.debug(f"{client_name} client session cleaned up successfully")
|
||||||
|
|
||||||
|
|
||||||
|
async def create_mcp_client_session_sse(
|
||||||
|
url: str,
|
||||||
|
token: str | None = None,
|
||||||
|
client_name: str = "MCP",
|
||||||
|
elicitation_callback: Any = None,
|
||||||
|
) -> AsyncGenerator[ClientSession, Any]:
|
||||||
|
"""
|
||||||
|
Factory function to create an MCP client session using SSE transport.
|
||||||
|
|
||||||
|
Similar to create_mcp_client_session but uses SSE transport instead of streamable-http.
|
||||||
|
Uses native async context managers to ensure correct LIFO cleanup order.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: MCP server URL (e.g., "http://localhost:8000/sse")
|
||||||
|
token: Optional OAuth access token for Bearer authentication
|
||||||
|
client_name: Client name for logging (e.g., "Basic MCP (SSE)")
|
||||||
|
elicitation_callback: Optional callback for handling elicitation requests
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
Initialized MCP ClientSession
|
||||||
|
|
||||||
|
Note:
|
||||||
|
SSE transport is being deprecated in favor of streamable-http.
|
||||||
|
This function exists for compatibility testing only.
|
||||||
|
"""
|
||||||
|
logger.info(f"Creating SSE client for {client_name}")
|
||||||
|
|
||||||
|
# Prepare headers with OAuth token if provided
|
||||||
|
headers = {"Authorization": f"Bearer {token}"} if token else None
|
||||||
|
|
||||||
|
# Use native async with - Python ensures LIFO cleanup
|
||||||
|
# Cleanup order will be: ClientSession.__aexit__ -> sse_client.__aexit__
|
||||||
|
# Note: sse_client yields only (read_stream, write_stream), not 3 values like streamablehttp_client
|
||||||
|
async with sse_client(url, headers=headers) as (read_stream, write_stream):
|
||||||
|
async with ClientSession(
|
||||||
|
read_stream, write_stream, elicitation_callback=elicitation_callback
|
||||||
|
) as session:
|
||||||
|
await session.initialize()
|
||||||
|
logger.info(f"{client_name} client session initialized successfully")
|
||||||
|
yield session
|
||||||
|
|
||||||
|
# Cleanup happens automatically in LIFO order - no exception suppression needed
|
||||||
|
logger.debug(f"{client_name} client session cleaned up successfully")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def nc_client(anyio_backend) -> AsyncGenerator[NextcloudClient, Any]:
|
async def nc_client(anyio_backend) -> AsyncGenerator[NextcloudClient, Any]:
|
||||||
"""
|
"""
|
||||||
@@ -203,12 +249,14 @@ async def nc_client(anyio_backend) -> AsyncGenerator[NextcloudClient, Any]:
|
|||||||
@pytest.fixture(scope="session")
|
@pytest.fixture(scope="session")
|
||||||
async def nc_mcp_client(anyio_backend) -> AsyncGenerator[ClientSession, Any]:
|
async def nc_mcp_client(anyio_backend) -> AsyncGenerator[ClientSession, Any]:
|
||||||
"""
|
"""
|
||||||
Fixture to create an MCP client session for integration tests using streamable-http.
|
Fixture to create an MCP client session for integration tests using SSE transport.
|
||||||
|
|
||||||
Uses anyio pytest plugin for proper async fixture handling.
|
Uses anyio pytest plugin for proper async fixture handling.
|
||||||
|
|
||||||
|
Note: SSE transport is being deprecated. This fixture uses SSE for compatibility testing.
|
||||||
"""
|
"""
|
||||||
async for session in create_mcp_client_session(
|
async for session in create_mcp_client_session_sse(
|
||||||
url="http://localhost:8000/mcp", client_name="Basic MCP"
|
url="http://localhost:8000/sse", client_name="Basic MCP (SSE)"
|
||||||
):
|
):
|
||||||
yield session
|
yield session
|
||||||
|
|
||||||
|
|||||||
@@ -1053,7 +1053,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.33.1"
|
version = "0.34.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
|
|||||||
Reference in New Issue
Block a user