refactor: change OAuth scope separator from colon to dot for IDP compatibility

Many identity providers (AWS Cognito, Okta, Azure AD) reject or mishandle
colons in OAuth scope names. This migrates all custom scopes from
`resource:action` to `resource.action` format (e.g., `notes:read` →
`notes.read`), which is universally accepted and aligns with industry
conventions (Microsoft, Google).

Includes Alembic migration 004 for stored scope strings and ADR-024
documenting the rationale and RFC references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-04-07 10:07:02 +02:00
co-authored by Claude Opus 4.6
parent 899b9c7191
commit 29fd0486c9
44 changed files with 724 additions and 520 deletions
@@ -0,0 +1,65 @@
"""Migrate scope separator from colon to dot
Many identity providers reject ':' in OAuth scope names. This migration
updates stored scope strings from the old 'resource:action' format to
'resource.action' (e.g., 'notes:read' -> 'notes.read').
See ADR-023 for rationale.
Revision ID: 004
Revises: 003
Create Date: 2026-04-07 12:00:00.000000
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "004"
down_revision = "003"
branch_labels = None
depends_on = None
def upgrade() -> None:
"""Replace colon separator with dot in stored scope strings."""
# Update scopes in app_passwords (JSON array of scope strings)
# Only ':' characters in a JSON array like '["notes:read","calendar:write"]'
# are inside scope name strings, so REPLACE is safe here.
op.execute(
"""
UPDATE app_passwords
SET scopes = REPLACE(scopes, ':', '.')
WHERE scopes IS NOT NULL
"""
)
# Update requested_scopes in login_flow_sessions
op.execute(
"""
UPDATE login_flow_sessions
SET requested_scopes = REPLACE(requested_scopes, ':', '.')
WHERE requested_scopes IS NOT NULL
"""
)
def downgrade() -> None:
"""Revert dot separator back to colon in stored scope strings."""
op.execute(
"""
UPDATE app_passwords
SET scopes = REPLACE(scopes, '.', ':')
WHERE scopes IS NOT NULL
"""
)
op.execute(
"""
UPDATE login_flow_sessions
SET requested_scopes = REPLACE(requested_scopes, '.', ':')
WHERE requested_scopes IS NOT NULL
"""
)
+7 -7
View File
@@ -462,19 +462,19 @@ async def load_oauth_client_credentials(
# These must stay in sync — any scope a tool uses via @require_scopes must be listed here.
dcr_scopes = (
"openid profile email "
"notes:read notes:write calendar:read calendar:write todo:read todo:write "
"contacts:read contacts:write cookbook:read cookbook:write deck:read deck:write "
"tables:read tables:write files:read files:write sharing:read sharing:write "
"news:read news:write collectives:read collectives:write"
"notes.read notes.write calendar.read calendar.write todo.read todo.write "
"contacts.read contacts.write cookbook.read cookbook.write deck.read deck.write "
"tables.read tables.write files.read files.write sharing.read sharing.write "
"news.read news.write collectives.read collectives.write"
)
# Add conditional scopes based on server configuration
dcr_settings = get_settings()
# semantic:read gates MCP-server-level semantic search tools
# semantic.read gates MCP-server-level semantic search tools
if dcr_settings.vector_sync_enabled:
dcr_scopes = f"{dcr_scopes} semantic:read"
logger.info("✓ semantic:read scope enabled for semantic search tools")
dcr_scopes = f"{dcr_scopes} semantic.read"
logger.info("✓ semantic.read scope enabled for semantic search tools")
# offline_access enables refresh tokens for background operations
enable_offline_access = dcr_settings.enable_offline_access
@@ -86,7 +86,7 @@ async def oauth_login(request: Request) -> RedirectResponse | JSONResponse:
callback_uri = f"{mcp_server_url}/oauth/callback"
# Request only basic OIDC scopes for browser session
# Note: Nextcloud app scopes (notes:read, etc.) are for MCP client access tokens,
# Note: Nextcloud app scopes (notes.read, etc.) are for MCP client access tokens,
# not for the MCP server's own browser authentication
scopes = "openid profile email offline_access"
@@ -74,7 +74,7 @@ def require_scopes(*required_scopes: str):
users who lack the necessary scopes.
Args:
*required_scopes: Variable number of scope strings required (e.g., "notes:read", "notes:write")
*required_scopes: Variable number of scope strings required (e.g., "notes.read", "notes.write")
Returns:
Decorated function that checks scopes before execution
@@ -82,15 +82,15 @@ def require_scopes(*required_scopes: str):
Example:
```python
@mcp.tool()
@require_scopes("notes:read")
@require_scopes("notes.read")
async def nc_notes_get_note(ctx: Context, note_id: int):
# This tool requires the notes:read scope
# This tool requires the notes.read scope
...
@mcp.tool()
@require_scopes("notes:write")
@require_scopes("notes.write")
async def nc_notes_create_note(ctx: Context, ...):
# This tool requires the notes:write scope
# This tool requires the notes.write scope
...
```
@@ -203,12 +203,12 @@ def require_scopes(*required_scopes: str):
if any(
s.startswith(prefix)
for prefix in [
"notes:",
"calendar:",
"contacts:",
"files:",
"tables:",
"deck:",
"notes.",
"calendar.",
"contacts.",
"files.",
"tables.",
"deck.",
]
)
]
@@ -223,12 +223,12 @@ def require_scopes(*required_scopes: str):
s.startswith(prefix)
for s in token_scopes
for prefix in [
"notes:",
"calendar:",
"contacts:",
"files:",
"tables:",
"deck:",
"notes.",
"calendar.",
"contacts.",
"files.",
"tables.",
"deck.",
]
)
@@ -305,7 +305,7 @@ def check_scopes(ctx: Context, *required_scopes: str) -> tuple[bool, set[str]]:
Example:
```python
async def my_tool(ctx: Context):
has_scopes, missing = check_scopes(ctx, "notes:read", "notes:write")
has_scopes, missing = check_scopes(ctx, "notes.read", "notes.write")
if not has_scopes:
# Handle missing scopes
...
@@ -335,11 +335,11 @@ def get_required_scopes(func: Callable) -> list[str]:
Example:
```python
@require_scopes("notes:read", "notes:write")
@require_scopes("notes.read", "notes.write")
async def my_tool():
pass
scopes = get_required_scopes(my_tool) # ["notes:read", "notes:write"]
scopes = get_required_scopes(my_tool) # ["notes.read", "notes.write"]
```
"""
return getattr(func, "_required_scopes", [])
@@ -385,14 +385,14 @@ def has_required_scopes(func: Callable, user_scopes: set[str]) -> bool:
Example:
```python
@require_scopes("notes:write")
@require_scopes("notes.write")
async def create_note():
pass
user_scopes = {"notes:read", "notes:write"}
user_scopes = {"notes.read", "notes.write"}
can_see = has_required_scopes(create_note, user_scopes) # True
limited_user_scopes = {"notes:read"}
limited_user_scopes = {"notes.read"}
can_see = has_required_scopes(create_note, limited_user_scopes) # False
```
"""
@@ -431,17 +431,17 @@ def discover_all_scopes(mcp) -> list[str]:
mcp = FastMCP("My Server")
@mcp.tool()
@require_scopes("notes:read")
@require_scopes("notes.read")
async def get_notes():
pass
@mcp.tool()
@require_scopes("notes:write")
@require_scopes("notes.write")
async def create_note():
pass
scopes = discover_all_scopes(mcp)
# Returns: ["notes:read", "notes:write", "openid", "profile", "email"]
# Returns: ["notes.read", "notes.write", "openid", "profile", "email"]
```
Note:
+2 -2
View File
@@ -337,7 +337,7 @@ class TokenBrokerService:
data = {
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"scope": "openid profile email offline_access notes:read notes:write calendar:read calendar:write",
"scope": "openid profile email offline_access notes.read notes.write calendar.read calendar.write",
"client_id": self.client_id,
"client_secret": self.client_secret,
}
@@ -521,7 +521,7 @@ class TokenBrokerService:
data = {
"grant_type": "refresh_token",
"refresh_token": current_refresh_token,
"scope": "openid profile email offline_access notes:read notes:write calendar:read calendar:write",
"scope": "openid profile email offline_access notes.read notes.write calendar.read calendar.write",
}
response = await client.post(
+2 -2
View File
@@ -90,7 +90,7 @@ from .app import get_app
@click.option(
"--oauth-scopes",
envvar="NEXTCLOUD_OIDC_SCOPES",
default="openid profile email notes:read notes:write calendar:read calendar:write todo:read todo:write contacts:read contacts:write cookbook:read cookbook:write deck:read deck:write tables:read tables:write files:read files:write sharing:read sharing:write",
default="openid profile email notes.read notes.write calendar.read calendar.write todo.read todo.write contacts.read contacts.write cookbook.read cookbook.write deck.read deck.write tables.read tables.write files.read files.write sharing.read sharing.write",
show_default=True,
help="OAuth scopes to request during client registration. These define the maximum allowed scopes for the client. Note: Actual supported scopes are discovered dynamically from MCP tools at runtime. (can also use NEXTCLOUD_OIDC_SCOPES env var)",
)
@@ -153,7 +153,7 @@ def run(
# OAuth mode with custom scopes and JWT tokens
$ nextcloud-mcp-server --nextcloud-host=https://cloud.example.com --oauth \\
--oauth-scopes="openid notes:read notes:write" --oauth-token-type=jwt
--oauth-scopes="openid notes.read notes.write" --oauth-token-type=jwt
# OAuth with public issuer URL (for Docker/proxy setups)
$ nextcloud-mcp-server --nextcloud-host=http://app --oauth \\
+22 -22
View File
@@ -54,27 +54,27 @@ class UpdateScopesResponse(BaseResponse):
# All supported application-level scopes (frozenset for O(1) membership tests)
ALL_SUPPORTED_SCOPES: frozenset[str] = frozenset(
{
"notes:read",
"notes:write",
"calendar:read",
"calendar:write",
"todo:read",
"todo:write",
"contacts:read",
"contacts:write",
"files:read",
"files:write",
"tables:read",
"tables:write",
"deck:read",
"deck:write",
"cookbook:read",
"cookbook:write",
"sharing:read",
"sharing:write",
"news:read",
"news:write",
"collectives:read",
"collectives:write",
"notes.read",
"notes.write",
"calendar.read",
"calendar.write",
"todo.read",
"todo.write",
"contacts.read",
"contacts.write",
"files.read",
"files.write",
"tables.read",
"tables.write",
"deck.read",
"deck.write",
"cookbook.read",
"cookbook.write",
"sharing.read",
"sharing.write",
"news.read",
"news.write",
"collectives.read",
"collectives.write",
}
)
@@ -410,7 +410,7 @@ def instrument_tool(func):
Usage:
@mcp.tool()
@require_scopes("notes:write")
@require_scopes("notes.write")
@instrument_tool
async def nc_notes_create_note(...):
...
+1 -1
View File
@@ -56,7 +56,7 @@ def register_auth_tools(mcp: FastMCP) -> None:
Args:
ctx: MCP context
scopes: Requested application scopes (e.g. ["notes:read", "calendar:write"]).
scopes: Requested application scopes (e.g. ["notes.read", "calendar.write"]).
If not specified, all available scopes are requested.
Returns:
+16 -16
View File
@@ -55,7 +55,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="List Calendars",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@require_scopes("calendar.read")
@instrument_tool
async def nc_calendar_list_calendars(ctx: Context) -> ListCalendarsResponse:
"""List all available calendars for the user"""
@@ -69,7 +69,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Create Calendar Event",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@require_scopes("calendar.write")
@instrument_tool
async def nc_calendar_create_event(
calendar_name: str,
@@ -149,7 +149,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="List Calendar Events",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@require_scopes("calendar.read")
@instrument_tool
async def nc_calendar_list_events(
calendar_name: str,
@@ -269,7 +269,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Get Calendar Event",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@require_scopes("calendar.read")
@instrument_tool
async def nc_calendar_get_event(
calendar_name: str,
@@ -285,7 +285,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Update Calendar Event",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@require_scopes("calendar.write")
@instrument_tool
async def nc_calendar_update_event(
calendar_name: str,
@@ -364,7 +364,7 @@ def configure_calendar_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("calendar:write")
@require_scopes("calendar.write")
@instrument_tool
async def nc_calendar_delete_event(
calendar_name: str,
@@ -379,7 +379,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Create Meeting",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@require_scopes("calendar.write")
@instrument_tool
async def nc_calendar_create_meeting(
title: str,
@@ -449,7 +449,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Get Upcoming Events",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@require_scopes("calendar.read")
@instrument_tool
async def nc_calendar_get_upcoming_events(
ctx: Context,
@@ -512,7 +512,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Find Availability",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("calendar:read")
@require_scopes("calendar.read")
@instrument_tool
async def nc_calendar_find_availability(
duration_minutes: int,
@@ -596,7 +596,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Bulk Calendar Operations",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@require_scopes("calendar.write")
@instrument_tool
async def nc_calendar_bulk_operations(
operation: str, # "update", "delete", "move"
@@ -849,7 +849,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Manage Calendar",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("calendar:write")
@require_scopes("calendar.write")
@instrument_tool
async def nc_calendar_manage_calendar(
action: str, # "create", "delete", "update", "list"
@@ -922,7 +922,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="List Todo Tasks",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("todo:read", "calendar:read")
@require_scopes("todo.read", "calendar.read")
@instrument_tool
async def nc_calendar_list_todos(
calendar_name: str,
@@ -971,7 +971,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Create Todo Task",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("todo:write", "calendar:read")
@require_scopes("todo.write", "calendar.read")
@instrument_tool
async def nc_calendar_create_todo(
calendar_name: str,
@@ -1018,7 +1018,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Update Todo Task",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("todo:write", "calendar:read")
@require_scopes("todo.write", "calendar.read")
@instrument_tool
async def nc_calendar_update_todo(
calendar_name: str,
@@ -1084,7 +1084,7 @@ def configure_calendar_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("todo:write", "calendar:read")
@require_scopes("todo.write", "calendar.read")
@instrument_tool
async def nc_calendar_delete_todo(
calendar_name: str,
@@ -1108,7 +1108,7 @@ def configure_calendar_tools(mcp: FastMCP):
title="Search Todo Tasks",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("todo:read", "calendar:read")
@require_scopes("todo.read", "calendar.read")
@instrument_tool
async def nc_calendar_search_todos(
ctx: Context,
+20 -20
View File
@@ -48,7 +48,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="List Collectives",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_get_collectives(
ctx: Context,
@@ -66,7 +66,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="List Collective Pages",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_get_pages(
ctx: Context, collective_id: int
@@ -90,7 +90,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Get Collective Page",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_get_page(
ctx: Context, collective_id: int, page_id: int
@@ -138,7 +138,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Search Collective Pages",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_search_pages(
ctx: Context, collective_id: int, query: str
@@ -166,7 +166,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="List Collective Tags",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_get_tags(
ctx: Context, collective_id: int
@@ -188,7 +188,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="List Trashed Collective Pages",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_get_trashed_pages(
ctx: Context, collective_id: int
@@ -212,7 +212,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="List Trashed Collectives",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("collectives:read")
@require_scopes("collectives.read")
@instrument_tool
async def collectives_get_trashed_collectives(
ctx: Context,
@@ -238,7 +238,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Create Collective",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_create_collective(
ctx: Context, name: str, emoji: str | None = None
@@ -263,7 +263,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Set Collective Emoji",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_set_collective_emoji(
ctx: Context, collective_id: int, emoji: str | None = None
@@ -295,7 +295,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Trash Collective",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_trash_collective(
ctx: Context, collective_id: int
@@ -324,7 +324,7 @@ def configure_collectives_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=False, openWorldHint=True
),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_delete_collective(
ctx: Context, collective_id: int
@@ -353,7 +353,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Restore Collective",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_restore_collective(
ctx: Context, collective_id: int
@@ -379,7 +379,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Create Collective Page",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_create_page(
ctx: Context, collective_id: int, parent_id: int, title: str
@@ -412,7 +412,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Move Collective Page",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_move_page(
ctx: Context,
@@ -453,7 +453,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Trash Collective Page",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_trash_page(
ctx: Context, collective_id: int, page_id: int
@@ -484,7 +484,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Restore Collective Page",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_restore_page(
ctx: Context, collective_id: int, page_id: int
@@ -512,7 +512,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Set Collective Page Emoji",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_set_page_emoji(
ctx: Context,
@@ -544,7 +544,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Create Collective Tag",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_create_tag(
ctx: Context, collective_id: int, name: str, color: str
@@ -568,7 +568,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Assign Tag to Collective Page",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_assign_tag(
ctx: Context, collective_id: int, page_id: int, tag_id: int
@@ -596,7 +596,7 @@ def configure_collectives_tools(mcp: FastMCP):
title="Remove Tag from Collective Page",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("collectives:write")
@require_scopes("collectives.write")
@instrument_tool
async def collectives_remove_tag(
ctx: Context, collective_id: int, page_id: int, tag_id: int
+7 -7
View File
@@ -99,7 +99,7 @@ def configure_contacts_tools(mcp: FastMCP):
title="List Address Books",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("contacts:read")
@require_scopes("contacts.read")
@instrument_tool
async def nc_contacts_list_addressbooks(ctx: Context) -> ListAddressBooksResponse:
"""List all addressbooks for the user."""
@@ -123,7 +123,7 @@ def configure_contacts_tools(mcp: FastMCP):
title="List Contacts",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("contacts:read")
@require_scopes("contacts.read")
@instrument_tool
async def nc_contacts_list_contacts(
ctx: Context, *, addressbook: str
@@ -146,7 +146,7 @@ def configure_contacts_tools(mcp: FastMCP):
title="Create Address Book",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("contacts:write")
@require_scopes("contacts.write")
@instrument_tool
async def nc_contacts_create_addressbook(
ctx: Context, *, name: str, display_name: str
@@ -168,7 +168,7 @@ def configure_contacts_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("contacts:write")
@require_scopes("contacts.write")
@instrument_tool
async def nc_contacts_delete_addressbook(ctx: Context, *, name: str):
"""Delete an addressbook."""
@@ -179,7 +179,7 @@ def configure_contacts_tools(mcp: FastMCP):
title="Create Contact",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("contacts:write")
@require_scopes("contacts.write")
@instrument_tool
async def nc_contacts_create_contact(
ctx: Context, *, addressbook: str, uid: str, contact_data: dict
@@ -204,7 +204,7 @@ def configure_contacts_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("contacts:write")
@require_scopes("contacts.write")
@instrument_tool
async def nc_contacts_delete_contact(ctx: Context, *, addressbook: str, uid: str):
"""Delete a contact.
@@ -222,7 +222,7 @@ def configure_contacts_tools(mcp: FastMCP):
title="Update Contact",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("contacts:write")
@require_scopes("contacts.write")
@instrument_tool
async def nc_contacts_update_contact(
ctx: Context, *, addressbook: str, uid: str, contact_data: dict, etag: str = ""
+13 -13
View File
@@ -75,7 +75,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Import Recipe from URL",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@require_scopes("cookbook.write")
@instrument_tool
async def nc_cookbook_import_recipe(url: str, ctx: Context) -> ImportRecipeResponse:
"""Import a recipe from a URL using schema.org metadata.
@@ -136,7 +136,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="List Recipes",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_list_recipes(ctx: Context) -> ListRecipesResponse:
"""Get all recipes in the database"""
@@ -165,7 +165,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Get Recipe",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_get_recipe(recipe_id: int, ctx: Context) -> Recipe:
"""Get a specific recipe by its ID"""
@@ -194,7 +194,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Create Recipe",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@require_scopes("cookbook.write")
@instrument_tool
async def nc_cookbook_create_recipe(
name: str,
@@ -277,7 +277,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Update Recipe",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@require_scopes("cookbook.write")
@instrument_tool
async def nc_cookbook_update_recipe(
recipe_id: int,
@@ -372,7 +372,7 @@ def configure_cookbook_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("cookbook:write")
@require_scopes("cookbook.write")
@instrument_tool
async def nc_cookbook_delete_recipe(
recipe_id: int, ctx: Context
@@ -411,7 +411,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Search Recipes",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_search_recipes(
query: str, ctx: Context
@@ -451,7 +451,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="List Recipe Categories",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_list_categories(ctx: Context) -> ListCategoriesResponse:
"""Get all known categories.
@@ -482,7 +482,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Get Recipes in Category",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_get_recipes_in_category(
category: str, ctx: Context
@@ -522,7 +522,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="List Recipe Keywords",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_list_keywords(ctx: Context) -> ListKeywordsResponse:
"""Get all known keywords/tags"""
@@ -551,7 +551,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Get Recipes with Keywords",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("cookbook:read")
@require_scopes("cookbook.read")
@instrument_tool
async def nc_cookbook_get_recipes_with_keywords(
keywords: list[str], ctx: Context
@@ -589,7 +589,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Set Cookbook Configuration",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@require_scopes("cookbook.write")
@instrument_tool
async def nc_cookbook_set_config(
folder: str | None = None,
@@ -636,7 +636,7 @@ def configure_cookbook_tools(mcp: FastMCP):
title="Reindex Recipes",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("cookbook:write")
@require_scopes("cookbook.write")
@instrument_tool
async def nc_cookbook_reindex(ctx: Context) -> ReindexResponse:
"""Trigger a rescan of all recipes into the caching database.
+25 -25
View File
@@ -126,7 +126,7 @@ def configure_deck_tools(mcp: FastMCP):
title="List Deck Boards",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_boards(ctx: Context) -> ListBoardsResponse:
"""Get all Nextcloud Deck boards"""
@@ -138,7 +138,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Get Deck Board",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_board(ctx: Context, board_id: int) -> DeckBoard:
"""Get details of a specific Nextcloud Deck board"""
@@ -150,7 +150,7 @@ def configure_deck_tools(mcp: FastMCP):
title="List Deck Stacks",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_stacks(ctx: Context, board_id: int) -> ListStacksResponse:
"""Get all stacks in a Nextcloud Deck board"""
@@ -162,7 +162,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Get Deck Stack",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_stack(ctx: Context, board_id: int, stack_id: int) -> DeckStack:
"""Get details of a specific Nextcloud Deck stack"""
@@ -174,7 +174,7 @@ def configure_deck_tools(mcp: FastMCP):
title="List Deck Cards",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_cards(
ctx: Context, board_id: int, stack_id: int
@@ -189,7 +189,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Get Deck Card",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_card(
ctx: Context, board_id: int, stack_id: int, card_id: int
@@ -203,7 +203,7 @@ def configure_deck_tools(mcp: FastMCP):
title="List Deck Labels",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_labels(ctx: Context, board_id: int) -> ListLabelsResponse:
"""Get all labels in a Nextcloud Deck board"""
@@ -216,7 +216,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Get Deck Label",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("deck:read")
@require_scopes("deck.read")
@instrument_tool
async def deck_get_label(ctx: Context, board_id: int, label_id: int) -> DeckLabel:
"""Get details of a specific Nextcloud Deck label"""
@@ -230,7 +230,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Create Deck Board",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_create_board(
ctx: Context, title: str, color: str
@@ -251,7 +251,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Create Deck Stack",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_create_stack(
ctx: Context, board_id: int, title: str, order: int
@@ -271,7 +271,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Update Deck Stack",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_update_stack(
ctx: Context,
@@ -303,7 +303,7 @@ def configure_deck_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_delete_stack(
ctx: Context, board_id: int, stack_id: int
@@ -328,7 +328,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Create Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_create_card(
ctx: Context,
@@ -366,7 +366,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Update Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_update_card(
ctx: Context,
@@ -425,7 +425,7 @@ def configure_deck_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_delete_card(
ctx: Context, board_id: int, stack_id: int, card_id: int
@@ -451,7 +451,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Archive Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_archive_card(
ctx: Context, board_id: int, stack_id: int, card_id: int
@@ -477,7 +477,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Unarchive Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_unarchive_card(
ctx: Context, board_id: int, stack_id: int, card_id: int
@@ -503,7 +503,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Reorder/Move Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_reorder_card(
ctx: Context,
@@ -539,7 +539,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Create Deck Label",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_create_label(
ctx: Context, board_id: int, title: str, color: str
@@ -559,7 +559,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Update Deck Label",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_update_label(
ctx: Context,
@@ -591,7 +591,7 @@ def configure_deck_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_delete_label(
ctx: Context, board_id: int, label_id: int
@@ -616,7 +616,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Assign Label to Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_assign_label_to_card(
ctx: Context, board_id: int, stack_id: int, card_id: int, label_id: int
@@ -643,7 +643,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Remove Label from Deck Card",
annotations=ToolAnnotations(idempotentHint=True, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_remove_label_from_card(
ctx: Context, board_id: int, stack_id: int, card_id: int, label_id: int
@@ -671,7 +671,7 @@ def configure_deck_tools(mcp: FastMCP):
title="Assign User to Deck Card",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_assign_user_to_card(
ctx: Context, board_id: int, stack_id: int, card_id: int, user_id: str
@@ -700,7 +700,7 @@ def configure_deck_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("deck:write")
@require_scopes("deck.write")
@instrument_tool
async def deck_unassign_user_from_card(
ctx: Context, board_id: int, stack_id: int, card_id: int, user_id: str
+16 -16
View File
@@ -34,10 +34,10 @@ def configure_news_tools(mcp: FastMCP):
title="List News Folders",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_list_folders(ctx: Context) -> ListFoldersResponse:
"""List all News folders (requires news:read scope)."""
"""List all News folders (requires news.read scope)."""
client = await get_client(ctx)
try:
folders_data = await client.news.get_folders()
@@ -59,10 +59,10 @@ def configure_news_tools(mcp: FastMCP):
title="List News Feeds",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_list_feeds(ctx: Context) -> ListFeedsResponse:
"""List all News feeds with metadata (requires news:read scope).
"""List all News feeds with metadata (requires news.read scope).
Returns feeds with unread counts, error status, and overall starred count.
"""
@@ -92,7 +92,7 @@ def configure_news_tools(mcp: FastMCP):
title="List News Items",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_list_items(
ctx: Context,
@@ -103,7 +103,7 @@ def configure_news_tools(mcp: FastMCP):
limit: int = 50,
offset: int = 0,
) -> ListItemsResponse:
"""List News items (articles) with optional filtering (requires news:read scope).
"""List News items (articles) with optional filtering (requires news.read scope).
Args:
feed_id: Filter by specific feed ID
@@ -166,10 +166,10 @@ def configure_news_tools(mcp: FastMCP):
title="Get News Item",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_get_item(item_id: int, ctx: Context) -> GetItemResponse:
"""Get a specific News item by ID with full content (requires news:read scope).
"""Get a specific News item by ID with full content (requires news.read scope).
Args:
item_id: Item ID
@@ -204,12 +204,12 @@ def configure_news_tools(mcp: FastMCP):
title="Get Starred News Items",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_get_starred_items(
ctx: Context, limit: int = 50, offset: int = 0
) -> ListItemsResponse:
"""Get starred (favorited) News items (requires news:read scope).
"""Get starred (favorited) News items (requires news.read scope).
Convenience method for retrieving user's starred articles.
@@ -257,12 +257,12 @@ def configure_news_tools(mcp: FastMCP):
title="Get Unread News Items",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_get_unread_items(
ctx: Context, limit: int = 50, offset: int = 0
) -> ListItemsResponse:
"""Get unread News items (requires news:read scope).
"""Get unread News items (requires news.read scope).
Convenience method for retrieving unread articles across all feeds.
@@ -310,10 +310,10 @@ def configure_news_tools(mcp: FastMCP):
title="Get News Feed Health",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_get_feed_health(feed_id: int, ctx: Context) -> FeedHealthResponse:
"""Get health status for a specific feed (requires news:read scope).
"""Get health status for a specific feed (requires news.read scope).
Returns error count and last error message if the feed has update issues.
@@ -357,10 +357,10 @@ def configure_news_tools(mcp: FastMCP):
title="Get News App Status",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("news:read")
@require_scopes("news.read")
@instrument_tool
async def nc_news_get_status(ctx: Context) -> GetStatusResponse:
"""Get News app status and version (requires news:read scope).
"""Get News app status and version (requires news.read scope).
Returns version information and any configuration warnings.
"""
+11 -11
View File
@@ -92,12 +92,12 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:write")
@require_scopes("notes.write")
@instrument_tool
async def nc_notes_create_note(
title: str, content: str, category: str, ctx: Context
) -> CreateNoteResponse:
"""Create a new note (requires notes:write scope)"""
"""Create a new note (requires notes.write scope)"""
client = await get_client(ctx)
try:
note_data = await client.notes.create_note(
@@ -145,7 +145,7 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:write")
@require_scopes("notes.write")
@instrument_tool
async def nc_notes_update_note(
note_id: int,
@@ -155,7 +155,7 @@ def configure_notes_tools(mcp: FastMCP):
category: str | None,
ctx: Context,
) -> UpdateNoteResponse:
"""Update an existing note's title, content, or category (requires notes:write scope).
"""Update an existing note's title, content, or category (requires notes.write scope).
REQUIRED: etag parameter must be provided to prevent overwriting concurrent changes.
Get the current ETag by first retrieving the note using nc_notes_get_note tool.
@@ -217,7 +217,7 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:write")
@require_scopes("notes.write")
@instrument_tool
async def nc_notes_append_content(
note_id: int, content: str, ctx: Context
@@ -274,10 +274,10 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:read")
@require_scopes("notes.read")
@instrument_tool
async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse:
"""Search notes by title or content, returning only id, title, and category (requires notes:read scope)."""
"""Search notes by title or content, returning only id, title, and category (requires notes.read scope)."""
client = await get_client(ctx)
try:
search_results_raw = await client.notes_search_notes(query=query)
@@ -327,10 +327,10 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:read")
@require_scopes("notes.read")
@instrument_tool
async def nc_notes_get_note(note_id: int, ctx: Context) -> Note:
"""Get a specific note by its ID (requires notes:read scope)"""
"""Get a specific note by its ID (requires notes.read scope)"""
client = await get_client(ctx)
try:
note_data = await client.notes.get_note(note_id)
@@ -363,7 +363,7 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:read")
@require_scopes("notes.read")
@instrument_tool
async def nc_notes_get_attachment(
note_id: int, attachment_filename: str, ctx: Context
@@ -417,7 +417,7 @@ def configure_notes_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("notes:write")
@require_scopes("notes.write")
@instrument_tool
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
"""Delete a note permanently"""
+8 -8
View File
@@ -473,14 +473,14 @@ async def check_logged_in(ctx: Context, user_id: Optional[str] = None) -> str:
"profile",
"email",
"offline_access", # Critical for background operations
"notes:read",
"notes:write",
"calendar:read",
"calendar:write",
"contacts:read",
"contacts:write",
"files:read",
"files:write",
"notes.read",
"notes.write",
"calendar.read",
"calendar.write",
"contacts.read",
"contacts.write",
"files.read",
"files.write",
]
# Generate authorization URL
+3 -3
View File
@@ -48,7 +48,7 @@ def configure_semantic_tools(mcp: FastMCP):
openWorldHint=True, # Queries external Nextcloud service
),
)
@require_scopes("semantic:read")
@require_scopes("semantic.read")
@instrument_tool
async def nc_semantic_search(
query: str,
@@ -303,7 +303,7 @@ def configure_semantic_tools(mcp: FastMCP):
openWorldHint=False, # Searches only indexed Nextcloud data
),
)
@require_scopes("semantic:read")
@require_scopes("semantic.read")
@instrument_tool
async def nc_semantic_search_answer(
query: str,
@@ -645,7 +645,7 @@ def configure_semantic_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("semantic:read")
@require_scopes("semantic.read")
@instrument_tool
async def nc_get_vector_sync_status(ctx: Context) -> VectorSyncStatusResponse:
"""Get the current vector sync status.
+5 -5
View File
@@ -21,7 +21,7 @@ def configure_sharing_tools(mcp: FastMCP):
title="Create Share",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("sharing:write")
@require_scopes("sharing.write")
@instrument_tool
async def nc_share_create(
path: str,
@@ -66,7 +66,7 @@ def configure_sharing_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("sharing:write")
@require_scopes("sharing.write")
@instrument_tool
async def nc_share_delete(share_id: int, ctx: Context) -> str:
"""Delete a share by its ID.
@@ -89,7 +89,7 @@ def configure_sharing_tools(mcp: FastMCP):
title="Get Share Details",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("sharing:write")
@require_scopes("sharing.write")
@instrument_tool
async def nc_share_get(share_id: int, ctx: Context) -> str:
"""Get information about a specific share.
@@ -111,7 +111,7 @@ def configure_sharing_tools(mcp: FastMCP):
title="List Shares",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("sharing:write")
@require_scopes("sharing.write")
@instrument_tool
async def nc_share_list(
ctx: Context, path: str | None = None, shared_with_me: bool = False
@@ -136,7 +136,7 @@ def configure_sharing_tools(mcp: FastMCP):
title="Update Share",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("sharing:write")
@require_scopes("sharing.write")
@instrument_tool
async def nc_share_update(share_id: int, permissions: int, ctx: Context) -> str:
"""Update the permissions of an existing share.
+6 -6
View File
@@ -17,7 +17,7 @@ def configure_tables_tools(mcp: FastMCP):
title="List Tables",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("tables:read")
@require_scopes("tables.read")
@instrument_tool
async def nc_tables_list_tables(ctx: Context) -> ListTablesResponse:
"""List all tables available to the user"""
@@ -30,7 +30,7 @@ def configure_tables_tools(mcp: FastMCP):
title="Get Table Schema",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("tables:read")
@require_scopes("tables.read")
@instrument_tool
async def nc_tables_get_schema(table_id: int, ctx: Context):
"""Get the schema/structure of a specific table including columns and views"""
@@ -41,7 +41,7 @@ def configure_tables_tools(mcp: FastMCP):
title="Read Table Rows",
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
)
@require_scopes("tables:read")
@require_scopes("tables.read")
@instrument_tool
async def nc_tables_read_table(
table_id: int,
@@ -57,7 +57,7 @@ def configure_tables_tools(mcp: FastMCP):
title="Insert Table Row",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("tables:write")
@require_scopes("tables.write")
@instrument_tool
async def nc_tables_insert_row(table_id: int, data: dict, ctx: Context):
"""Insert a new row into a table.
@@ -71,7 +71,7 @@ def configure_tables_tools(mcp: FastMCP):
title="Update Table Row",
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
)
@require_scopes("tables:write")
@require_scopes("tables.write")
@instrument_tool
async def nc_tables_update_row(row_id: int, data: dict, ctx: Context):
"""Update an existing row in a table.
@@ -87,7 +87,7 @@ def configure_tables_tools(mcp: FastMCP):
destructiveHint=True, idempotentHint=True, openWorldHint=True
),
)
@require_scopes("tables:write")
@require_scopes("tables.write")
@instrument_tool
async def nc_tables_delete_row(row_id: int, ctx: Context):
"""Delete a row from a table"""
+11 -11
View File
@@ -25,7 +25,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:read")
@require_scopes("files.read")
@instrument_tool
async def nc_webdav_list_directory(
ctx: Context, path: str = ""
@@ -65,7 +65,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:read")
@require_scopes("files.read")
@instrument_tool
async def nc_webdav_read_file(path: str, ctx: Context):
"""Read the content of a file from NextCloud.
@@ -137,7 +137,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:write")
@require_scopes("files.write")
@instrument_tool
async def nc_webdav_write_file(
path: str, content: str, ctx: Context, content_type: str | None = None
@@ -170,7 +170,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:write")
@require_scopes("files.write")
@instrument_tool
async def nc_webdav_create_directory(path: str, ctx: Context):
"""Create a directory in NextCloud.
@@ -192,7 +192,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:write")
@require_scopes("files.write")
@instrument_tool
async def nc_webdav_delete_resource(path: str, ctx: Context):
"""Delete a file or directory in NextCloud.
@@ -213,7 +213,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:write")
@require_scopes("files.write")
@instrument_tool
async def nc_webdav_move_resource(
source_path: str, destination_path: str, ctx: Context, overwrite: bool = False
@@ -240,7 +240,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:write")
@require_scopes("files.write")
@instrument_tool
async def nc_webdav_copy_resource(
source_path: str, destination_path: str, ctx: Context, overwrite: bool = False
@@ -267,7 +267,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:read")
@require_scopes("files.read")
@instrument_tool
async def nc_webdav_search_files(
ctx: Context,
@@ -390,7 +390,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:read")
@require_scopes("files.read")
@instrument_tool
async def nc_webdav_find_by_name(
pattern: str, ctx: Context, scope: str = "", limit: int | None = None
@@ -424,7 +424,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:read")
@require_scopes("files.read")
@instrument_tool
async def nc_webdav_find_by_type(
mime_type: str, ctx: Context, scope: str = "", limit: int | None = None
@@ -458,7 +458,7 @@ def configure_webdav_tools(mcp: FastMCP):
openWorldHint=True,
),
)
@require_scopes("files:read")
@require_scopes("files.read")
@instrument_tool
async def nc_webdav_list_favorites(
ctx: Context, scope: str = "", limit: int | None = None
+4 -4
View File
@@ -46,10 +46,10 @@ logger = logging.getLogger(__name__)
# Scopes required for vector sync operations
VECTOR_SYNC_SCOPES = [
"notes:read",
"files:read",
"deck:read",
# "news:read", # News app may not be installed
"notes.read",
"files.read",
"deck.read",
# "news.read", # News app may not be installed
]