Compare commits

...

14 Commits

Author SHA1 Message Date
Chris Coutinho 3485b55e2d ci: Update oidc app 2025-11-05 15:58:40 +01:00
Chris Coutinho 4adb9de5f0 chore: fix typo 2025-11-05 15:36:50 +01:00
Chris Coutinho bfa944d0e8 ci: Rename pre-commit hook [skip ci] 2025-11-05 15:31:52 +01:00
Chris Coutinho 01569497d7 ci: Add pre-commit hook for ty [skip ci] 2025-11-05 15:26:00 +01:00
Chris Coutinho 6cccd92b3b build: Add type checking 2025-11-05 15:19:55 +01:00
Chris Coutinho 9dcda0cd6a test: Update config 2025-11-05 09:53:23 +01:00
Chris Coutinho 7c2f39930a ci: Update oidc app config 2025-11-05 07:13:46 +01:00
Chris Coutinho 205c3b013c build: Update oidc submodule 2025-11-05 06:57:12 +01:00
github-actions[bot] f587a4e31f bump: version 0.23.0 → 0.24.0 2025-11-04 10:27:39 +00:00
Chris Coutinho 6e95447272 Merge pull request #256 from cbcoutinho/feature/keycloak
feature/keycloak
2025-11-04 11:27:09 +01:00
Chris Coutinho 96789db29d Merge pull request #258 from cbcoutinho/renovate/docker.io-library-redis-alpine
chore(deps): update docker.io/library/redis:alpine docker digest to 28c9c4d
2025-11-04 01:15:51 +01:00
renovate-bot-cbcoutinho[bot] 615f345928 chore(deps): update docker.io/library/redis:alpine docker digest to 28c9c4d 2025-11-03 23:11:28 +00:00
Chris Coutinho 9adfc72612 Merge pull request #257 from cbcoutinho/renovate/pin-dependencies
chore(deps): pin quay.io/keycloak/keycloak docker tag to 3617b09
2025-11-03 08:22:12 +01:00
renovate-bot-cbcoutinho[bot] 6c3997b24c chore(deps): pin quay.io/keycloak/keycloak docker tag to 3617b09 2025-11-03 05:12:12 +00:00
23 changed files with 158 additions and 55 deletions
+3
View File
@@ -18,6 +18,9 @@ jobs:
- name: Linting - name: Linting
run: | run: |
uv run --frozen ruff check uv run --frozen ruff check
- name: Linting
run: |
uv run --frozen ty check -- nextcloud_mcp_server
integration-test: integration-test:
+6
View File
@@ -18,3 +18,9 @@ repos:
entry: uv run ruff format entry: uv run ruff format
language: system language: system
types: [python] types: [python]
- id: ty-check
name: ty-check
language: system
types: [python]
exclude: tests/.*
entry: uv run ty check
+38
View File
@@ -1,3 +1,41 @@
## v0.24.0 (2025-11-04)
### Feat
- add scope protection to OAuth provisioning tools
- enable authorization services for token exchange in Keycloak
- implement scope-based audience mapping and RFC 9728 support
- integrate token exchange into MCP server application
- implement RFC 8693 Standard Token Exchange for Keycloak
- Add userinfo route/page
- add browser-based user info page with separate OAuth flow
- Implement ADR-004 Progressive Consent foundation (partial)
- Complete ADR-004 Progressive Consent OAuth flows implementation
- Implement ADR-004 Progressive Consent foundation components
- Implement ADR-004 Hybrid Flow with comprehensive integration tests
### Fix
- add missing await for get_nextcloud_client in capabilities resource
- use valid Fernet encryption keys in token exchange tests
- accept resource URL in token audience for Nextcloud JWT tokens
- remove token-exchange-nextcloud scope and accept tokens without audience
- move audience mapper from scope to nextcloud-mcp-server client
- move token-exchange-nextcloud from default to optional scopes
- restructure routes to prevent SessionAuthBackend from interfering with FastMCP OAuth
- allow OAuth Bearer tokens on /mcp endpoint by excluding from session auth
- correct OAuth token audience validation using RFC 8707 resource parameter
- remove remaining references to deleted oauth_callback and oauth_token
- remove Hybrid Flow, make Progressive Consent default (ADR-004)
- browser OAuth userinfo endpoint and refresh token rotation
- make ENABLE_PROGRESSIVE_CONSENT consistently opt-in (default false)
- make provisioning checks opt-in (default false)
- Disable Progressive Consent for mcp-oauth to enable Hybrid Flow tests
### Refactor
- integrate token exchange into unified get_client() pattern
## v0.23.0 (2025-11-03) ## v0.23.0 (2025-11-03)
### Feat ### Feat
@@ -31,8 +31,9 @@ else
fi fi
# Configure OIDC Identity Provider with dynamic client registration enabled # Configure OIDC Identity Provider with dynamic client registration enabled
php /var/www/html/occ config:app:set oidc dynamic_client_registration --value='true' php /var/www/html/occ config:app:set oidc dynamic_client_registration --value='true' # NOTE: String
php /var/www/html/occ config:app:set oidc proof_key_for_code_exchange --value=true --type=boolean php /var/www/html/occ config:app:set oidc proof_key_for_code_exchange --value=true --type=boolean
php /var/www/html/occ config:app:set oidc allow_user_settings --value='enabled'
php /var/www/html/occ config:app:set oidc default_token_type --value='jwt' php /var/www/html/occ config:app:set oidc default_token_type --value='jwt'
echo "OIDC app installed and configured successfully" echo "OIDC app installed and configured successfully"
+2 -2
View File
@@ -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.23.0 version: 0.24.0
appVersion: "0.23.0" appVersion: "0.24.0"
keywords: keywords:
- nextcloud - nextcloud
- mcp - mcp
+3 -2
View File
@@ -17,7 +17,7 @@ services:
# Note: Redis is an external service. You can find more information about the configuration here: # Note: Redis is an external service. You can find more information about the configuration here:
# https://hub.docker.com/_/redis # https://hub.docker.com/_/redis
redis: redis:
image: docker.io/library/redis:alpine@sha256:59b6e694653476de2c992937ebe1c64182af4728e54bb49e9b7a6c26614d8933 image: docker.io/library/redis:alpine@sha256:28c9c4d7596949a24b183eaaab6455f8e5d55ecbf72d02ff5e2c17fe72671d31
restart: always restart: always
app: app:
@@ -28,6 +28,7 @@ services:
depends_on: depends_on:
- redis - redis
- db - db
- keycloak
volumes: volumes:
- nextcloud:/var/www/html - nextcloud:/var/www/html
- ./app-hooks:/docker-entrypoint-hooks.d:ro - ./app-hooks:/docker-entrypoint-hooks.d:ro
@@ -114,7 +115,7 @@ services:
- oauth-tokens:/app/data - oauth-tokens:/app/data
keycloak: keycloak:
image: quay.io/keycloak/keycloak:26.4.2 image: quay.io/keycloak/keycloak:26.4.2@sha256:3617b09bb4b7510a8d8d9b9fc5707399e2d70688dbcc2f8fb013a144829be1b9
command: command:
- "start-dev" - "start-dev"
- "--import-realm" - "--import-realm"
+1 -1
View File
@@ -818,7 +818,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
return allowed_tools return allowed_tools
# Replace the tool manager's list_tools method # Replace the tool manager's list_tools method
mcp._tool_manager.list_tools = list_tools_filtered mcp._tool_manager.list_tools = list_tools_filtered # type: ignore[method-assign]
logger.info( logger.info(
"Dynamic tool filtering enabled for OAuth mode (JWT and Bearer tokens)" "Dynamic tool filtering enabled for OAuth mode (JWT and Bearer tokens)"
) )
@@ -34,7 +34,7 @@ class ProgressiveConsentTokenVerifier:
def __init__( def __init__(
self, self,
token_storage: RefreshTokenStorage, token_storage: RefreshTokenStorage | None,
token_broker: Optional[TokenBrokerService] = None, token_broker: Optional[TokenBrokerService] = None,
oidc_discovery_url: Optional[str] = None, oidc_discovery_url: Optional[str] = None,
nextcloud_host: Optional[str] = None, nextcloud_host: Optional[str] = None,
@@ -80,7 +80,7 @@ class ProgressiveConsentTokenVerifier:
# Create token broker if not provided # Create token broker if not provided
if token_broker: if token_broker:
self.token_broker = token_broker self.token_broker = token_broker
elif self.encryption_key: elif self.encryption_key and token_storage and self.nextcloud_host:
self.token_broker = TokenBrokerService( self.token_broker = TokenBrokerService(
storage=token_storage, storage=token_storage,
oidc_discovery_url=self.oidc_discovery_url, oidc_discovery_url=self.oidc_discovery_url,
@@ -89,7 +89,12 @@ class ProgressiveConsentTokenVerifier:
) )
else: else:
self.token_broker = None self.token_broker = None
logger.warning("Token broker not available - encryption key missing") if not self.encryption_key:
logger.warning("Token broker not available - encryption key missing")
elif not token_storage:
logger.warning("Token broker not available - token storage missing")
elif not self.nextcloud_host:
logger.warning("Token broker not available - nextcloud host missing")
async def verify_token(self, token: str) -> Optional[AccessToken]: async def verify_token(self, token: str) -> Optional[AccessToken]:
""" """
@@ -25,7 +25,7 @@ import logging
import os import os
import time import time
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Any, Optional
import aiosqlite import aiosqlite
from cryptography.fernet import Fernet from cryptography.fernet import Fernet
@@ -283,7 +283,7 @@ class RefreshTokenStorage:
) )
async def store_user_profile( async def store_user_profile(
self, user_id: str, profile_data: dict[str, any] self, user_id: str, profile_data: dict[str, Any]
) -> None: ) -> None:
""" """
Store user profile data (cached from IdP userinfo endpoint). Store user profile data (cached from IdP userinfo endpoint).
@@ -314,7 +314,7 @@ class RefreshTokenStorage:
logger.debug(f"Cached user profile for {user_id}") logger.debug(f"Cached user profile for {user_id}")
async def get_user_profile(self, user_id: str) -> Optional[dict[str, any]]: async def get_user_profile(self, user_id: str) -> Optional[dict[str, Any]]:
""" """
Retrieve cached user profile data. Retrieve cached user profile data.
@@ -3,7 +3,7 @@
import logging import logging
import os import os
from functools import wraps from functools import wraps
from typing import Callable from typing import Any, Callable
from mcp.server.auth.middleware.auth_context import get_access_token from mcp.server.auth.middleware.auth_context import get_access_token
from mcp.server.auth.provider import AccessToken from mcp.server.auth.provider import AccessToken
@@ -88,15 +88,18 @@ def require_scopes(*required_scopes: str):
ScopeAuthorizationError: If required scopes are not present in the access token ScopeAuthorizationError: If required scopes are not present in the access token
""" """
def decorator(func: Callable): def decorator(func: Callable) -> Callable:
# Store scope requirements as function metadata for dynamic filtering # Store scope requirements as function metadata for dynamic filtering
func._required_scopes = list(required_scopes) # type: ignore func._required_scopes = list(required_scopes) # type: ignore[attr-defined]
# Get function name for logging (works for any callable)
func_name = getattr(func, "__name__", repr(func))
# Find which parameter receives the Context (FastMCP injects it by name) # Find which parameter receives the Context (FastMCP injects it by name)
context_param_name = find_context_parameter(func) context_param_name = find_context_parameter(func)
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args: Any, **kwargs: Any) -> Any:
# Extract context from kwargs (where FastMCP injected it) # Extract context from kwargs (where FastMCP injected it)
ctx: Context | None = ( ctx: Context | None = (
kwargs.get(context_param_name) if context_param_name else None kwargs.get(context_param_name) if context_param_name else None
@@ -106,7 +109,7 @@ def require_scopes(*required_scopes: str):
# No context parameter found - likely BasicAuth mode # No context parameter found - likely BasicAuth mode
# In BasicAuth mode, all operations are allowed # In BasicAuth mode, all operations are allowed
logger.debug( logger.debug(
f"No context parameter for {func.__name__} - allowing (BasicAuth mode)" f"No context parameter for {func_name} - allowing (BasicAuth mode)"
) )
return await func(*args, **kwargs) return await func(*args, **kwargs)
@@ -119,7 +122,7 @@ def require_scopes(*required_scopes: str):
# Not in OAuth mode (BasicAuth or no auth) # Not in OAuth mode (BasicAuth or no auth)
# In BasicAuth mode, all operations are allowed # In BasicAuth mode, all operations are allowed
logger.debug( logger.debug(
f"No access token present for {func.__name__} - allowing (BasicAuth mode)" f"No access token present for {func_name} - allowing (BasicAuth mode)"
) )
return await func(*args, **kwargs) return await func(*args, **kwargs)
@@ -172,7 +175,7 @@ def require_scopes(*required_scopes: str):
if not has_nextcloud_scopes: if not has_nextcloud_scopes:
error_msg = ( error_msg = (
f"Access denied to {func.__name__}: " f"Access denied to {func_name}: "
f"Nextcloud resource access not provisioned. " f"Nextcloud resource access not provisioned. "
f"Please run the 'provision_nextcloud_access' tool first." f"Please run the 'provision_nextcloud_access' tool first."
) )
@@ -183,7 +186,7 @@ def require_scopes(*required_scopes: str):
missing_scopes = required_scopes_set - token_scopes missing_scopes = required_scopes_set - token_scopes
if missing_scopes: if missing_scopes:
error_msg = ( error_msg = (
f"Access denied to {func.__name__}: " f"Access denied to {func_name}: "
f"Missing required scopes: {', '.join(sorted(missing_scopes))}. " f"Missing required scopes: {', '.join(sorted(missing_scopes))}. "
f"Token has scopes: {', '.join(sorted(token_scopes)) if token_scopes else 'none'}" f"Token has scopes: {', '.join(sorted(token_scopes)) if token_scopes else 'none'}"
) )
@@ -192,7 +195,7 @@ def require_scopes(*required_scopes: str):
# All required scopes present - allow execution # All required scopes present - allow execution
logger.debug( logger.debug(
f"Scope authorization passed for {func.__name__}: {required_scopes}" f"Scope authorization passed for {func_name}: {required_scopes}"
) )
return await func(*args, **kwargs) return await func(*args, **kwargs)
+1 -1
View File
@@ -68,7 +68,7 @@ class TokenCache:
logger.debug(f"Using cached token for user {user_id}") logger.debug(f"Using cached token for user {user_id}")
return token return token
async def set(self, user_id: str, token: str, expires_in: int = None): async def set(self, user_id: str, token: str, expires_in: int | None = None):
"""Store token in cache.""" """Store token in cache."""
async with self._lock: async with self._lock:
# Use provided expiry or default TTL # Use provided expiry or default TTL
+4 -1
View File
@@ -114,7 +114,8 @@ class TokenExchangeService:
if not self.oidc_discovery_url: if not self.oidc_discovery_url:
# Fallback to Nextcloud OIDC if no discovery URL # Fallback to Nextcloud OIDC if no discovery URL
self.oidc_discovery_url = urljoin( self.oidc_discovery_url = urljoin(
self.nextcloud_host, "/.well-known/openid-configuration" self.nextcloud_host, # type: ignore[arg-type]
"/.well-known/openid-configuration",
) )
try: try:
@@ -363,6 +364,7 @@ class TokenExchangeService:
True if provisioned, False otherwise True if provisioned, False otherwise
""" """
await self._ensure_storage() await self._ensure_storage()
assert self.storage is not None # _ensure_storage() ensures this
token_data = await self.storage.get_refresh_token(user_id) token_data = await self.storage.get_refresh_token(user_id)
return token_data is not None return token_data is not None
@@ -376,6 +378,7 @@ class TokenExchangeService:
Refresh token if found, None otherwise Refresh token if found, None otherwise
""" """
await self._ensure_storage() await self._ensure_storage()
assert self.storage is not None # _ensure_storage() ensures this
token_data = await self.storage.get_refresh_token(user_id) token_data = await self.storage.get_refresh_token(user_id)
if token_data: if token_data:
return token_data.get("refresh_token") return token_data.get("refresh_token")
+2 -1
View File
@@ -165,6 +165,7 @@ class NextcloudTokenVerifier(TokenVerifier):
""" """
try: try:
# Get signing key from JWKS # Get signing key from JWKS
assert self._jwks_client is not None # Caller should check before calling
signing_key = self._jwks_client.get_signing_key_from_jwt(token) signing_key = self._jwks_client.get_signing_key_from_jwt(token)
# Verify and decode JWT # Verify and decode JWT
@@ -257,7 +258,7 @@ class NextcloudTokenVerifier(TokenVerifier):
try: try:
# Introspection requires client authentication # Introspection requires client authentication
response = await self._client.post( response = await self._client.post(
self.introspection_uri, self.introspection_uri, # type: ignore
data={"token": token}, data={"token": token},
auth=(self.client_id, self.client_secret), auth=(self.client_id, self.client_secret),
) )
+18 -12
View File
@@ -100,7 +100,7 @@ class CalendarClient:
# Use custom PROPFIND with CalendarServer namespace (cs:) for calendar-color. # Use custom PROPFIND with CalendarServer namespace (cs:) for calendar-color.
# caldav library's nsmap lacks "CS" namespace, and its CalendarColor uses # caldav library's nsmap lacks "CS" namespace, and its CalendarColor uses
# Apple iCal namespace which Nextcloud doesn't recognize. # Apple iCal namespace which Nextcloud doesn't recognize.
from lxml import etree from lxml import etree # type: ignore[import-untyped]
propfind_body = """<?xml version="1.0" encoding="utf-8"?> propfind_body = """<?xml version="1.0" encoding="utf-8"?>
<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav"> <d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav">
@@ -261,11 +261,12 @@ class CalendarClient:
result = [] result = []
for event in events: for event in events:
await event.load(only_if_unloaded=True) await event.load(only_if_unloaded=True)
event_dict = self._parse_ical_event(event.data) if event.data:
if event_dict: event_dict = self._parse_ical_event(event.data)
event_dict["href"] = str(event.url) if event_dict:
event_dict["etag"] = "" event_dict["href"] = str(event.url)
result.append(event_dict) event_dict["etag"] = ""
result.append(event_dict)
if len(result) >= limit: if len(result) >= limit:
break break
@@ -314,8 +315,8 @@ class CalendarClient:
await event.load(only_if_unloaded=True) await event.load(only_if_unloaded=True)
# Merge updates into existing iCal data # Merge updates into existing iCal data
updated_ical = self._merge_ical_properties(event.data, event_data, event_uid) updated_ical = self._merge_ical_properties(event.data, event_data, event_uid) # type: ignore[arg-type]
event.data = updated_ical event.data = updated_ical # type: ignore[misc]
await event.save() await event.save()
@@ -349,7 +350,7 @@ class CalendarClient:
event = await calendar.event_by_uid(event_uid) event = await calendar.event_by_uid(event_uid)
await event.load(only_if_unloaded=True) await event.load(only_if_unloaded=True)
event_data = self._parse_ical_event(event.data) event_data = self._parse_ical_event(event.data) if event.data else None # type: ignore[arg-type]
if not event_data: if not event_data:
raise ValueError(f"Failed to parse event data for {event_uid}") raise ValueError(f"Failed to parse event data for {event_uid}")
@@ -416,7 +417,10 @@ class CalendarClient:
# Only load if data not already present from REPORT response # Only load if data not already present from REPORT response
# This avoids 404 errors for virtual calendars (e.g., Deck boards) # This avoids 404 errors for virtual calendars (e.g., Deck boards)
await todo.load(only_if_unloaded=True) await todo.load(only_if_unloaded=True)
todo_dict = self._parse_ical_todo(todo.data) if todo.data:
todo_dict = self._parse_ical_todo(todo.data) # type: ignore[arg-type]
else:
continue
if todo_dict: if todo_dict:
todo_dict["href"] = str(todo.url) todo_dict["href"] = str(todo.url)
todo_dict["etag"] = "" todo_dict["etag"] = ""
@@ -470,12 +474,14 @@ class CalendarClient:
await todo.load(only_if_unloaded=True) await todo.load(only_if_unloaded=True)
logger.debug( logger.debug(
f"Loaded todo {todo_uid}, current data length: {len(todo.data)}" f"Loaded todo {todo_uid}, current data length: {len(todo.data)}" # type: ignore
) )
# Merge updates into existing iCal data # Merge updates into existing iCal data
updated_ical = self._merge_ical_todo_properties( updated_ical = self._merge_ical_todo_properties(
todo.data, todo_data, todo_uid todo.data, # type: ignore[arg-type]
todo_data,
todo_uid,
) )
logger.debug(f"Merged iCal data length: {len(updated_ical)}") logger.debug(f"Merged iCal data length: {len(updated_ical)}")
logger.debug(f"Updated iCal content:\n{updated_ical}") logger.debug(f"Updated iCal content:\n{updated_ical}")
+2 -2
View File
@@ -124,7 +124,7 @@ class ContactsClient(BaseNextcloudClient):
carddav_path = self._get_carddav_base_path() carddav_path = self._get_carddav_base_path()
url = f"{carddav_path}/{addressbook}/{uid}.vcf" url = f"{carddav_path}/{addressbook}/{uid}.vcf"
contact = Contact(fn=contact_data.get("fn"), uid=uid) contact = Contact(fn=contact_data.get("fn"), uid=uid) # type: ignore
if "email" in contact_data: if "email" in contact_data:
contact.email = [{"value": contact_data["email"], "type": ["HOME"]}] contact.email = [{"value": contact_data["email"], "type": ["HOME"]}]
if "tel" in contact_data: if "tel" in contact_data:
@@ -174,7 +174,7 @@ class ContactsClient(BaseNextcloudClient):
) )
else: else:
# Fallback to creating new vCard if we couldn't get existing # Fallback to creating new vCard if we couldn't get existing
contact = Contact(fn=contact_data.get("fn"), uid=uid) contact = Contact(fn=contact_data.get("fn"), uid=uid) # type: ignore
if "email" in contact_data: if "email" in contact_data:
contact.email = [{"value": contact_data["email"], "type": ["HOME"]}] contact.email = [{"value": contact_data["email"], "type": ["HOME"]}]
if "tel" in contact_data: if "tel" in contact_data:
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
try: try:
import io import io
import pytesseract import pytesseract # type: ignore
from PIL import Image from PIL import Image
TESSERACT_AVAILABLE = True TESSERACT_AVAILABLE = True
@@ -112,10 +112,10 @@ class UnstructuredProcessor(DocumentProcessor):
f"Processing document with unstructured... ({elapsed}s elapsed)" f"Processing document with unstructured... ({elapsed}s elapsed)"
) )
try: try:
await progress_callback( await progress_callback( # type: ignore
progress=float(elapsed), progress=float(elapsed), # type: ignore
total=None, # Unknown total duration total=None, # Unknown total duration # type: ignore
message=message, message=message, # type: ignore
) )
logger.debug(f"Progress update sent: {elapsed}s elapsed") logger.debug(f"Progress update sent: {elapsed}s elapsed")
except Exception as e: except Exception as e:
@@ -293,7 +293,7 @@ class UnstructuredProcessor(DocumentProcessor):
self._run_progress_poller, stop_event, progress_callback, start_time self._run_progress_poller, stop_event, progress_callback, start_time
) )
return result return result # type: ignore
async def health_check(self) -> bool: async def health_check(self) -> bool:
"""Check if Unstructured API is available. """Check if Unstructured API is available.
+3 -3
View File
@@ -191,7 +191,7 @@ def configure_cookbook_tools(mcp: FastMCP):
recipe_yield: int | None = None, recipe_yield: int | None = None,
category: str | None = None, category: str | None = None,
keywords: str | None = None, keywords: str | None = None,
ctx: Context = None, ctx: Context = None, # type: ignore
) -> CreateRecipeResponse: ) -> CreateRecipeResponse:
"""Create a new recipe. """Create a new recipe.
@@ -271,7 +271,7 @@ def configure_cookbook_tools(mcp: FastMCP):
recipe_yield: int | None = None, recipe_yield: int | None = None,
category: str | None = None, category: str | None = None,
keywords: str | None = None, keywords: str | None = None,
ctx: Context = None, ctx: Context = None, # type: ignore
) -> UpdateRecipeResponse: ) -> UpdateRecipeResponse:
"""Update an existing recipe. """Update an existing recipe.
@@ -544,7 +544,7 @@ def configure_cookbook_tools(mcp: FastMCP):
folder: str | None = None, folder: str | None = None,
update_interval: int | None = None, update_interval: int | None = None,
print_image: bool | None = None, print_image: bool | None = None,
ctx: Context = None, ctx: Context = None, # type: ignore
) -> ReindexResponse: ) -> ReindexResponse:
"""Set Cookbook app configuration. """Set Cookbook app configuration.
+1 -1
View File
@@ -331,7 +331,7 @@ def configure_notes_tools(mcp: FastMCP):
content, mime_type = await client.webdav.get_note_attachment( content, mime_type = await client.webdav.get_note_attachment(
note_id=note_id, filename=attachment_filename note_id=note_id, filename=attachment_filename
) )
return { return { # type: ignore
"uri": f"nc://Notes/{note_id}/attachments/{attachment_filename}", "uri": f"nc://Notes/{note_id}/attachments/{attachment_filename}",
"mimeType": mime_type, "mimeType": mime_type,
"data": content, "data": content,
+4 -4
View File
@@ -163,7 +163,7 @@ async def provision_nextcloud_access(
if not user_id: if not user_id:
# Get the authorization token from context # Get the authorization token from context
if hasattr(ctx, "authorization") and ctx.authorization: if hasattr(ctx, "authorization") and ctx.authorization:
token = ctx.authorization.token token = ctx.authorization.token # type: ignore
# Decode token to get user info # Decode token to get user info
try: try:
import jwt import jwt
@@ -304,7 +304,7 @@ async def revoke_nextcloud_access(
# Get user ID from context if not provided # Get user ID from context if not provided
if not user_id: if not user_id:
user_id = ( user_id = (
ctx.context.get("user_id", "default_user") ctx.context.get("user_id", "default_user") # type: ignore
if hasattr(ctx, "context") if hasattr(ctx, "context")
else "default_user" else "default_user"
) )
@@ -334,7 +334,7 @@ async def revoke_nextcloud_access(
"OIDC_DISCOVERY_URL", "OIDC_DISCOVERY_URL",
f"{os.getenv('NEXTCLOUD_HOST')}/.well-known/openid-configuration", f"{os.getenv('NEXTCLOUD_HOST')}/.well-known/openid-configuration",
), ),
nextcloud_host=os.getenv("NEXTCLOUD_HOST"), nextcloud_host=os.getenv("NEXTCLOUD_HOST"), # type: ignore
encryption_key=encryption_key, encryption_key=encryption_key,
) )
@@ -382,7 +382,7 @@ async def check_provisioning_status(
# Get user ID from context if not provided # Get user ID from context if not provided
if not user_id: if not user_id:
user_id = ( user_id = (
ctx.context.get("user_id", "default_user") ctx.context.get("user_id", "default_user") # type: ignore
if hasattr(ctx, "context") if hasattr(ctx, "context")
else "default_user" else "default_user"
) )
+2 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "nextcloud-mcp-server" name = "nextcloud-mcp-server"
version = "0.23.0" version = "0.24.0"
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"}
@@ -102,6 +102,7 @@ dev = [
"pytest-timeout>=2.3.1", "pytest-timeout>=2.3.1",
"ruff>=0.11.13", "ruff>=0.11.13",
"reportlab>=4.0.0", "reportlab>=4.0.0",
"ty>=0.0.1a25",
] ]
[project.scripts] [project.scripts]
Generated
+36 -1
View File
@@ -501,6 +501,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" }, { url = "https://files.pythonhosted.org/packages/1f/8e/abdd3f14d735b2929290a018ecf133c901be4874b858dd1c604b9319f064/greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8", size = 587684, upload-time = "2025-08-07T13:18:25.164Z" },
{ url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" }, { url = "https://files.pythonhosted.org/packages/5d/65/deb2a69c3e5996439b0176f6651e0052542bb6c8f8ec2e3fba97c9768805/greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52", size = 1116647, upload-time = "2025-08-07T13:42:38.655Z" },
{ url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" }, { url = "https://files.pythonhosted.org/packages/3f/cc/b07000438a29ac5cfb2194bfc128151d52f333cee74dd7dfe3fb733fc16c/greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa", size = 1142073, upload-time = "2025-08-07T13:18:21.737Z" },
{ url = "https://files.pythonhosted.org/packages/67/24/28a5b2fa42d12b3d7e5614145f0bd89714c34c08be6aabe39c14dd52db34/greenlet-3.2.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c9c6de1940a7d828635fbd254d69db79e54619f165ee7ce32fda763a9cb6a58c", size = 1548385, upload-time = "2025-11-04T12:42:11.067Z" },
{ url = "https://files.pythonhosted.org/packages/6a/05/03f2f0bdd0b0ff9a4f7b99333d57b53a7709c27723ec8123056b084e69cd/greenlet-3.2.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03c5136e7be905045160b1b9fdca93dd6727b180feeafda6818e6496434ed8c5", size = 1613329, upload-time = "2025-11-04T12:42:12.928Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" }, { url = "https://files.pythonhosted.org/packages/d8/0f/30aef242fcab550b0b3520b8e3561156857c94288f0332a79928c31a52cf/greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9", size = 299100, upload-time = "2025-08-07T13:44:12.287Z" },
{ url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" }, { url = "https://files.pythonhosted.org/packages/44/69/9b804adb5fd0671f367781560eb5eb586c4d495277c93bde4307b9e28068/greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd", size = 274079, upload-time = "2025-08-07T13:15:45.033Z" },
{ url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" }, { url = "https://files.pythonhosted.org/packages/46/e9/d2a80c99f19a153eff70bc451ab78615583b8dac0754cfb942223d2c1a0d/greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb", size = 640997, upload-time = "2025-08-07T13:42:56.234Z" },
@@ -510,6 +512,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" }, { url = "https://files.pythonhosted.org/packages/19/0d/6660d55f7373b2ff8152401a83e02084956da23ae58cddbfb0b330978fe9/greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0", size = 607586, upload-time = "2025-08-07T13:18:28.544Z" },
{ url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" }, { url = "https://files.pythonhosted.org/packages/8e/1a/c953fdedd22d81ee4629afbb38d2f9d71e37d23caace44775a3a969147d4/greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0", size = 1123281, upload-time = "2025-08-07T13:42:39.858Z" },
{ url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" }, { url = "https://files.pythonhosted.org/packages/3f/c7/12381b18e21aef2c6bd3a636da1088b888b97b7a0362fac2e4de92405f97/greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f", size = 1151142, upload-time = "2025-08-07T13:18:22.981Z" },
{ url = "https://files.pythonhosted.org/packages/27/45/80935968b53cfd3f33cf99ea5f08227f2646e044568c9b1555b58ffd61c2/greenlet-3.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ee7a6ec486883397d70eec05059353b8e83eca9168b9f3f9a361971e77e0bcd0", size = 1564846, upload-time = "2025-11-04T12:42:15.191Z" },
{ url = "https://files.pythonhosted.org/packages/69/02/b7c30e5e04752cb4db6202a3858b149c0710e5453b71a3b2aec5d78a1aab/greenlet-3.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:326d234cbf337c9c3def0676412eb7040a35a768efc92504b947b3e9cfc7543d", size = 1633814, upload-time = "2025-11-04T12:42:17.175Z" },
{ url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" }, { url = "https://files.pythonhosted.org/packages/e9/08/b0814846b79399e585f974bbeebf5580fbe59e258ea7be64d9dfb253c84f/greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02", size = 299899, upload-time = "2025-08-07T13:38:53.448Z" },
{ url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" }, { url = "https://files.pythonhosted.org/packages/49/e8/58c7f85958bda41dafea50497cbd59738c5c43dbbea5ee83d651234398f4/greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31", size = 272814, upload-time = "2025-08-07T13:15:50.011Z" },
{ url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" }, { url = "https://files.pythonhosted.org/packages/62/dd/b9f59862e9e257a16e4e610480cfffd29e3fae018a68c2332090b53aac3d/greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945", size = 641073, upload-time = "2025-08-07T13:42:57.23Z" },
@@ -519,6 +523,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" }, { url = "https://files.pythonhosted.org/packages/ee/43/3cecdc0349359e1a527cbf2e3e28e5f8f06d3343aaf82ca13437a9aa290f/greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671", size = 610497, upload-time = "2025-08-07T13:18:31.636Z" },
{ url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" }, { url = "https://files.pythonhosted.org/packages/b8/19/06b6cf5d604e2c382a6f31cafafd6f33d5dea706f4db7bdab184bad2b21d/greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b", size = 1121662, upload-time = "2025-08-07T13:42:41.117Z" },
{ url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" }, { url = "https://files.pythonhosted.org/packages/a2/15/0d5e4e1a66fab130d98168fe984c509249c833c1a3c16806b90f253ce7b9/greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae", size = 1149210, upload-time = "2025-08-07T13:18:24.072Z" },
{ url = "https://files.pythonhosted.org/packages/1c/53/f9c440463b3057485b8594d7a638bed53ba531165ef0ca0e6c364b5cc807/greenlet-3.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e343822feb58ac4d0a1211bd9399de2b3a04963ddeec21530fc426cc121f19b", size = 1564759, upload-time = "2025-11-04T12:42:19.395Z" },
{ url = "https://files.pythonhosted.org/packages/47/e4/3bb4240abdd0a8d23f4f88adec746a3099f0d86bfedb623f063b2e3b4df0/greenlet-3.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca7f6f1f2649b89ce02f6f229d7c19f680a6238af656f61e0115b24857917929", size = 1634288, upload-time = "2025-11-04T12:42:21.174Z" },
{ url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" }, { url = "https://files.pythonhosted.org/packages/0b/55/2321e43595e6801e105fcfdee02b34c0f996eb71e6ddffca6b10b7e1d771/greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b", size = 299685, upload-time = "2025-08-07T13:24:38.824Z" },
{ url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" }, { url = "https://files.pythonhosted.org/packages/22/5c/85273fd7cc388285632b0498dbbab97596e04b154933dfe0f3e68156c68c/greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0", size = 273586, upload-time = "2025-08-07T13:16:08.004Z" },
{ url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" }, { url = "https://files.pythonhosted.org/packages/d1/75/10aeeaa3da9332c2e761e4c50d4c3556c21113ee3f0afa2cf5769946f7a3/greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f", size = 686346, upload-time = "2025-08-07T13:42:59.944Z" },
@@ -526,6 +532,8 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" }, { url = "https://files.pythonhosted.org/packages/dc/8b/29aae55436521f1d6f8ff4e12fb676f3400de7fcf27fccd1d4d17fd8fecd/greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1", size = 694659, upload-time = "2025-08-07T13:53:17.759Z" },
{ url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" }, { url = "https://files.pythonhosted.org/packages/92/2e/ea25914b1ebfde93b6fc4ff46d6864564fba59024e928bdc7de475affc25/greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735", size = 695355, upload-time = "2025-08-07T13:18:34.517Z" },
{ url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" }, { url = "https://files.pythonhosted.org/packages/72/60/fc56c62046ec17f6b0d3060564562c64c862948c9d4bc8aa807cf5bd74f4/greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337", size = 657512, upload-time = "2025-08-07T13:18:33.969Z" },
{ url = "https://files.pythonhosted.org/packages/23/6e/74407aed965a4ab6ddd93a7ded3180b730d281c77b765788419484cdfeef/greenlet-3.2.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2917bdf657f5859fbf3386b12d68ede4cf1f04c90c3a6bc1f013dd68a22e2269", size = 1612508, upload-time = "2025-11-04T12:42:23.427Z" },
{ url = "https://files.pythonhosted.org/packages/0d/da/343cd760ab2f92bac1845ca07ee3faea9fe52bee65f7bcb19f16ad7de08b/greenlet-3.2.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:015d48959d4add5d6c9f6c5210ee3803a830dce46356e3bc326d6776bde54681", size = 1680760, upload-time = "2025-11-04T12:42:25.341Z" },
{ url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" }, { url = "https://files.pythonhosted.org/packages/e3/a5/6ddab2b4c112be95601c13428db1d8b6608a8b6039816f2ba09c346c08fc/greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01", size = 303425, upload-time = "2025-08-07T13:32:27.59Z" },
] ]
@@ -966,7 +974,7 @@ wheels = [
[[package]] [[package]]
name = "nextcloud-mcp-server" name = "nextcloud-mcp-server"
version = "0.23.0" version = "0.24.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiosqlite" }, { name = "aiosqlite" },
@@ -994,6 +1002,7 @@ dev = [
{ name = "pytest-timeout" }, { name = "pytest-timeout" },
{ name = "reportlab" }, { name = "reportlab" },
{ name = "ruff" }, { name = "ruff" },
{ name = "ty" },
] ]
[package.metadata] [package.metadata]
@@ -1023,6 +1032,7 @@ dev = [
{ name = "pytest-timeout", specifier = ">=2.3.1" }, { name = "pytest-timeout", specifier = ">=2.3.1" },
{ name = "reportlab", specifier = ">=4.0.0" }, { name = "reportlab", specifier = ">=4.0.0" },
{ name = "ruff", specifier = ">=0.11.13" }, { name = "ruff", specifier = ">=0.11.13" },
{ name = "ty", specifier = ">=0.0.1a25" },
] ]
[[package]] [[package]]
@@ -1954,6 +1964,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
] ]
[[package]]
name = "ty"
version = "0.0.1a25"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/6b/e73bc3c1039ea72936158a08313155a49e5aa5e7db5205a149fe516a4660/ty-0.0.1a25.tar.gz", hash = "sha256:5550b24b9dd0e0f8b4b2c1f0fcc608a55d0421dd67b6c364bc7bf25762334511", size = 4403670, upload-time = "2025-10-29T19:40:23.647Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/3b/4457231238a2eeb04cba4ba7cc33d735be68ee46ca40a98ae30e187de864/ty-0.0.1a25-py3-none-linux_armv6l.whl", hash = "sha256:d35b2c1f94a014a22875d2745aa0432761d2a9a8eb7212630d5caf547daeef6d", size = 8878803, upload-time = "2025-10-29T19:39:42.243Z" },
{ url = "https://files.pythonhosted.org/packages/8a/fa/a328713dd310018fc7a381693d8588185baa2fdae913e01a6839187215df/ty-0.0.1a25-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:192edac94675a468bac7f6e04687a77a64698e4e1fe01f6a048bf9b6dde5b703", size = 8695667, upload-time = "2025-10-29T19:39:45.179Z" },
{ url = "https://files.pythonhosted.org/packages/22/e8/5707939118992ced2bf5385adc3ede7723c1b717b07ad14c495eea1e47b4/ty-0.0.1a25-py3-none-macosx_11_0_arm64.whl", hash = "sha256:949523621f336e01bc7d687b7bd08fe838edadbdb6563c2c057ed1d264e820cf", size = 8159012, upload-time = "2025-10-29T19:39:47.011Z" },
{ url = "https://files.pythonhosted.org/packages/eb/fb/ff313aa71602225cd78f1bce3017713d6d1b1c1e0fa8101ead4594a60d95/ty-0.0.1a25-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94f78f621458c05e59e890061021198197f29a7b51a33eda82bbb036e7ed73d7", size = 8433675, upload-time = "2025-10-29T19:39:48.443Z" },
{ url = "https://files.pythonhosted.org/packages/c0/8d/cc7e7fb57215a15b575a43ed042bdd92971871e0decec1b26d2e7d969465/ty-0.0.1a25-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d9656fca8062a2c6709c30d76d662c96d2e7dbfee8f70e55ec6b6afd67b5d447", size = 8668456, upload-time = "2025-10-29T19:39:50.412Z" },
{ url = "https://files.pythonhosted.org/packages/b8/6d/d7bf5909ed2dcdcbc1e2ca7eea80929893e2d188d9c36b3fcb2b36532ff6/ty-0.0.1a25-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9f3bbf523b49935bbd76e230408d858dce0d614f44f5807bbbd0954f64e0f01", size = 9023543, upload-time = "2025-10-29T19:39:52.292Z" },
{ url = "https://files.pythonhosted.org/packages/b4/b8/72bcefb4be32e5a84f0b21de2552f16cdb4cae3eb271ac891c8199c26b1a/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f13ea9815f4a54a0a303ca7bf411b0650e3c2a24fc6c7889ffba2c94f5e97a6a", size = 9700013, upload-time = "2025-10-29T19:39:57.283Z" },
{ url = "https://files.pythonhosted.org/packages/90/0d/cf7e794b840cf6b0bbecb022e593c543f85abad27a582241cf2095048cb1/ty-0.0.1a25-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eab6e33ebe202a71a50c3d5a5580e3bc1a85cda3ffcdc48cec3f1c693b7a873b", size = 9372574, upload-time = "2025-10-29T19:40:04.532Z" },
{ url = "https://files.pythonhosted.org/packages/1e/71/2d35e7d51b48eabd330e2f7b7e0bce541cbd95950c4d2f780e85f3366af1/ty-0.0.1a25-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6b9a31da43424cdab483703a54a561b93aabba84630788505329fc5294a9c62", size = 9535726, upload-time = "2025-10-29T19:40:06.548Z" },
{ url = "https://files.pythonhosted.org/packages/57/d3/01ecc23bbd8f3e0dfbcf9172d06d84e88155c5f416f1491137e8066fd859/ty-0.0.1a25-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a90d897a7c1a5ae9b41a4c7b0a42262a06361476ad88d783dbedd7913edadbc", size = 9003380, upload-time = "2025-10-29T19:40:08.683Z" },
{ url = "https://files.pythonhosted.org/packages/de/f9/cde9380d8a1a6ca61baeb9aecb12cbec90d489aa929be55cd78ad5c2ccd9/ty-0.0.1a25-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:93c7e7ab2859af0f866d34d27f4ae70dd4fb95b847387f082de1197f9f34e068", size = 8401833, upload-time = "2025-10-29T19:40:10.627Z" },
{ url = "https://files.pythonhosted.org/packages/0b/39/0acf3625b0c495011795a391016b572f97a812aca1d67f7a76621fdb9ebf/ty-0.0.1a25-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4a247061bd32bae3865a236d7f8b6c9916c80995db30ae1600999010f90623a9", size = 8706761, upload-time = "2025-10-29T19:40:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/25/73/7de1648f3563dd9d416d36ab5f1649bfd7b47a179135027f31d44b89a246/ty-0.0.1a25-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1711dd587eccf04fd50c494dc39babe38f4cb345bc3901bf1d8149cac570e979", size = 8792426, upload-time = "2025-10-29T19:40:14.553Z" },
{ url = "https://files.pythonhosted.org/packages/7d/8a/b6e761a65eac7acd10b2e452f49b2d8ae0ea163ca36bb6b18b2dadae251b/ty-0.0.1a25-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f4c9b0cf7995e2e3de9bab4d066063dea92019f2f62673b7574e3612643dd35", size = 9103991, upload-time = "2025-10-29T19:40:16.332Z" },
{ url = "https://files.pythonhosted.org/packages/e4/25/9324ae947fcc4322470326cf8276a3fc2f08dc82adec1de79d963fdf7af5/ty-0.0.1a25-py3-none-win32.whl", hash = "sha256:168fc8aee396d617451acc44cd28baffa47359777342836060c27aa6f37e2445", size = 8387095, upload-time = "2025-10-29T19:40:18.368Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2b/cb12cbc7db1ba310aa7b1de9b4e018576f653105993736c086ee67d2ec02/ty-0.0.1a25-py3-none-win_amd64.whl", hash = "sha256:a2fad3d8e92bb4d57a8872a6f56b1aef54539d36f23ebb01abe88ac4338efafb", size = 9059225, upload-time = "2025-10-29T19:40:20.278Z" },
{ url = "https://files.pythonhosted.org/packages/2f/c1/f6be8cdd0bf387c1d8ee9d14bb299b7b5d2c0532f550a6693216a32ec0c5/ty-0.0.1a25-py3-none-win_arm64.whl", hash = "sha256:dde2962d448ed87c48736e9a4bb13715a4cced705525e732b1c0dac1d4c66e3d", size = 8536832, upload-time = "2025-10-29T19:40:22.014Z" },
]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.19.2" version = "0.19.2"