Merge remote-tracking branch 'origin/master' into docs/adr-024-dynaconf-config-management

This commit is contained in:
Chris Coutinho
2026-04-07 14:17:57 +02:00
48 changed files with 733 additions and 525 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
]