feat: add MCP tool annotations for enhanced UX
Add ToolAnnotations to all 105+ MCP tools across 13 modules to enable better client-side UX with human-readable titles and behavioral hints. Changes: - Add title and ToolAnnotations to all @mcp.tool() decorators - Apply correct idempotency classification per ADR-017 - Add destructiveHint for delete operations - Set openWorldHint=False for semantic search (internal data only) Modules updated: - OAuth (4 tools): Authentication and provisioning - Notes (7 tools): Note management - WebDAV (11 tools): File operations - Semantic (3 tools): Semantic search and RAG - Calendar (16 tools): Events and todos - Contacts (7 tools): Address book management - Sharing (5 tools): File/folder sharing - Tables (6 tools): Structured data - Deck (25 tools): Kanban board management - Cookbook (13 tools): Recipe management - News (8 tools): RSS feed reader Annotation patterns: - Read operations: readOnlyHint=True, openWorldHint=True - Create operations: idempotentHint=False, openWorldHint=True - Update operations: idempotentHint=False, openWorldHint=True - Delete operations: destructiveHint=True, idempotentHint=True, openWorldHint=True See docs/ADR-017-mcp-tool-annotations.md for rationale and implementation details. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,492 @@
|
||||
# ADR-017: Add MCP Tool Annotations for Enhanced Client UX
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
The MCP Python SDK supports tool annotations that provide behavioral hints and improved UX to MCP clients. Currently, our 101 tools across 10 modules lack these annotations, resulting in:
|
||||
|
||||
- Snake_case function names displayed to users (e.g., "nc_notes_create_note" instead of "Create Note")
|
||||
- No behavioral hints for clients about read-only, destructive, or idempotent operations
|
||||
- Missing parameter descriptions for better auto-completion and inline help
|
||||
- Clients cannot optimize caching, warn before destructive operations, or retry safely
|
||||
|
||||
### Available MCP Annotations
|
||||
|
||||
The MCP SDK provides three types of annotations:
|
||||
|
||||
#### 1. Tool Decorator Parameters
|
||||
```python
|
||||
@mcp.tool(
|
||||
title="Human-Readable Name",
|
||||
description="Tool description", # Can also come from docstring
|
||||
annotations=ToolAnnotations(...),
|
||||
icons=[Icon(...)] # Optional visual icons
|
||||
)
|
||||
```
|
||||
|
||||
#### 2. ToolAnnotations Behavioral Hints
|
||||
```python
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
ToolAnnotations(
|
||||
title="Alternative Title", # Decorator title takes precedence
|
||||
readOnlyHint=True, # Tool doesn't modify data
|
||||
destructiveHint=True, # Tool may delete/overwrite data
|
||||
idempotentHint=True, # Repeated calls with same args are safe
|
||||
openWorldHint=True # Interacts with external entities
|
||||
)
|
||||
```
|
||||
|
||||
#### 3. Parameter Descriptions
|
||||
```python
|
||||
from pydantic import Field
|
||||
|
||||
async def tool(
|
||||
param: str = Field(description="What this parameter does"),
|
||||
ctx: Context
|
||||
):
|
||||
```
|
||||
|
||||
### Idempotency Analysis
|
||||
|
||||
**Important**: Idempotency means calling with **the same inputs** produces the same result.
|
||||
|
||||
**NOT Idempotent** (different inputs each call):
|
||||
- **Updates with etag**: `update_note(id=1, title="X", etag="abc")` → etag changes to "def"
|
||||
- Second call: `update_note(id=1, title="X", etag="abc")` → fails (etag mismatch)
|
||||
- Different input (stale etag) → different result (error)
|
||||
- **Creates**: `create_note(title="X")` → creates note 1
|
||||
- Second call → creates note 2 (different result)
|
||||
- **Append operations**: `append_content(id=1, text="X")` → adds X once
|
||||
- Second call → adds X again (different result)
|
||||
|
||||
**Idempotent**:
|
||||
- **Deletes**: `delete_note(id=1)` → note deleted
|
||||
- Second call → 404 or success (same end state: note doesn't exist)
|
||||
- Note: May return different status code, but end state is identical
|
||||
- **Full resource PUT without version control**: Would be idempotent (we don't have these)
|
||||
- **Set operations**: `set_property(id=1, value="X")` → property = X
|
||||
- Second call → property still = X (same result)
|
||||
- Note: Nextcloud updates use etags, so not applicable
|
||||
|
||||
**Read-Only** (always idempotent, never destructive):
|
||||
- All list, search, get operations
|
||||
|
||||
## Decision
|
||||
|
||||
Add annotations to all 101 tools in three phases:
|
||||
|
||||
### Phase 1: Titles (Quick Win)
|
||||
Add human-readable titles to all tools:
|
||||
|
||||
```python
|
||||
@mcp.tool(title="Create Note")
|
||||
async def nc_notes_create_note(...):
|
||||
```
|
||||
|
||||
**Effort**: 2-3 hours
|
||||
**Impact**: Immediate UX improvement
|
||||
|
||||
### Phase 2: ToolAnnotations (Behavioral Hints)
|
||||
Add annotations based on corrected categorization:
|
||||
|
||||
```python
|
||||
# Read-only tools
|
||||
@mcp.tool(
|
||||
title="Search Notes",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True # Nextcloud is external to MCP server
|
||||
)
|
||||
)
|
||||
|
||||
# Delete tools (idempotent: same end state)
|
||||
@mcp.tool(
|
||||
title="Delete Note",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True,
|
||||
idempotentHint=True, # Deleting deleted item = same end state
|
||||
openWorldHint=True
|
||||
)
|
||||
)
|
||||
|
||||
# Create tools (not idempotent: creates multiple items)
|
||||
@mcp.tool(
|
||||
title="Create Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False,
|
||||
openWorldHint=True
|
||||
)
|
||||
)
|
||||
|
||||
# Update tools with etag (not idempotent: etag changes)
|
||||
@mcp.tool(
|
||||
title="Update Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Etag required = different inputs each time
|
||||
openWorldHint=True
|
||||
)
|
||||
)
|
||||
|
||||
# Append operations (not idempotent: adds content each time)
|
||||
@mcp.tool(
|
||||
title="Append to Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False,
|
||||
openWorldHint=True
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
**Effort**: 4-6 hours
|
||||
**Impact**: Better client behavior (caching, warnings, retry logic)
|
||||
|
||||
### Phase 3: Parameter Descriptions
|
||||
Add Field() descriptions to parameters:
|
||||
|
||||
```python
|
||||
from pydantic import Field
|
||||
|
||||
@mcp.tool(title="Create Note", annotations=ToolAnnotations(idempotentHint=False))
|
||||
async def nc_notes_create_note(
|
||||
title: str = Field(description="The title of the note"),
|
||||
content: str = Field(description="Markdown content of the note"),
|
||||
category: str = Field(description="Category or folder name for organizing"),
|
||||
ctx: Context
|
||||
) -> CreateNoteResponse:
|
||||
```
|
||||
|
||||
**Effort**: 6-8 hours
|
||||
**Impact**: Better auto-completion and inline help
|
||||
|
||||
## Tool Categorization
|
||||
|
||||
### Read-Only Tools (~40 tools)
|
||||
**Pattern**: List, search, get operations
|
||||
**Annotations**: `readOnlyHint=True`, `openWorldHint=True`
|
||||
|
||||
Examples:
|
||||
- `nc_notes_search_notes` → "Search Notes"
|
||||
- `nc_webdav_list_directory` → "List Files and Directories"
|
||||
- `nc_calendar_list_calendars` → "List Calendars"
|
||||
- `nc_contacts_get_contact` → "Get Contact"
|
||||
- `nc_semantic_search` → "Semantic Search"
|
||||
- `check_logged_in` → "Check Server Login Status"
|
||||
|
||||
### Create Tools (~20 tools)
|
||||
**Pattern**: Create new resources
|
||||
**Annotations**: `idempotentHint=False`, `openWorldHint=True`
|
||||
|
||||
Examples:
|
||||
- `nc_notes_create_note` → "Create Note"
|
||||
- `nc_calendar_create_event` → "Create Calendar Event"
|
||||
- `nc_contacts_create_contact` → "Create Contact"
|
||||
- `deck_create_card` → "Create Kanban Card"
|
||||
- `nc_tables_create_row` → "Create Table Row"
|
||||
|
||||
### Update Tools (~25 tools)
|
||||
**Pattern**: Modify existing resources with etag
|
||||
**Annotations**: `idempotentHint=False` (etag changes), `openWorldHint=True`
|
||||
|
||||
Examples:
|
||||
- `nc_notes_update_note` → "Update Note"
|
||||
- `nc_calendar_update_event` → "Update Calendar Event"
|
||||
- `nc_contacts_update_contact` → "Update Contact"
|
||||
- `deck_update_card` → "Update Kanban Card"
|
||||
|
||||
**Rationale**: Updates require etag, which changes after each update. Same parameters on second call will fail due to stale etag = NOT idempotent.
|
||||
|
||||
### Append/Accumulate Tools (~5 tools)
|
||||
**Pattern**: Add content without replacing
|
||||
**Annotations**: `idempotentHint=False`, `openWorldHint=True`
|
||||
|
||||
Examples:
|
||||
- `nc_notes_append_content` → "Append to Note"
|
||||
|
||||
**Rationale**: Each call adds content, changing the result = NOT idempotent.
|
||||
|
||||
### Delete Tools (~10 tools)
|
||||
**Pattern**: Remove resources
|
||||
**Annotations**: `destructiveHint=True`, `idempotentHint=True`, `openWorldHint=True`
|
||||
|
||||
Examples:
|
||||
- `nc_notes_delete_note` → "Delete Note"
|
||||
- `nc_webdav_delete_resource` → "Delete File or Directory"
|
||||
- `nc_calendar_delete_event` → "Delete Calendar Event"
|
||||
- `nc_contacts_delete_contact` → "Delete Contact"
|
||||
|
||||
**Rationale**: Deleting already-deleted item results in same end state (item doesn't exist) = idempotent. Status code may differ, but outcome is identical.
|
||||
|
||||
### Special Cases
|
||||
|
||||
#### OAuth Provisioning Tools
|
||||
```python
|
||||
# Not read-only but requires user interaction
|
||||
@mcp.tool(
|
||||
title="Grant Server Access to Nextcloud",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=False,
|
||||
idempotentHint=False, # Creates new OAuth session each time
|
||||
openWorldHint=True
|
||||
)
|
||||
)
|
||||
async def provision_nextcloud_access(ctx: Context):
|
||||
```
|
||||
|
||||
#### Semantic Search (Closed World)
|
||||
```python
|
||||
@mcp.tool(
|
||||
title="Semantic Search",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=False # Searches only indexed Nextcloud data
|
||||
)
|
||||
)
|
||||
async def nc_semantic_search(query: str, ctx: Context):
|
||||
```
|
||||
|
||||
**Rationale**: Semantic search only queries pre-indexed Nextcloud content, not the "open world" like web search would.
|
||||
|
||||
## Tool Priority Matrix
|
||||
|
||||
### Critical Priority (~2 tools)
|
||||
OAuth tools required for server functionality:
|
||||
- `provision_nextcloud_access` → "Grant Server Access to Nextcloud"
|
||||
- `check_logged_in` → "Check Server Login Status"
|
||||
|
||||
### High Priority (~50 tools)
|
||||
Most commonly used modules:
|
||||
- **Notes** (14 tools): Create, read, update, delete notes
|
||||
- **WebDAV** (13 tools): File operations
|
||||
- **Calendar** (15 tools): Events and todos
|
||||
- **Semantic Search** (6 tools): AI-powered search
|
||||
- **Contacts** (9 tools): Address book operations
|
||||
|
||||
### Medium Priority (~35 tools)
|
||||
Secondary functionality:
|
||||
- **Deck** (9 tools): Kanban boards
|
||||
- **Tables** (7 tools): Structured data
|
||||
- **Sharing** (5 tools): File sharing
|
||||
|
||||
### Low Priority (~14 tools)
|
||||
Less frequently used:
|
||||
- **Cookbook** (8 tools): Recipe management
|
||||
- **News** (6 tools): RSS feeds
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Week 1: Phase 1 - Titles
|
||||
- Add human-readable titles to all 101 tools
|
||||
- Update tool name mapping in documentation
|
||||
- Manual test in MCP inspector
|
||||
|
||||
### Week 2: Phase 2 - ToolAnnotations (High Priority)
|
||||
- Add annotations to Critical and High priority tools (~52 tools)
|
||||
- Focus on Notes, WebDAV, Calendar, Semantic, OAuth
|
||||
- Add unit tests validating annotation presence
|
||||
|
||||
### Week 3: Phase 2 - ToolAnnotations (Medium/Low Priority)
|
||||
- Complete remaining tools (~49 tools)
|
||||
- Deck, Tables, Contacts, Cookbook, News
|
||||
- Update tool listings in README
|
||||
|
||||
### Week 4: Phase 3 - Parameter Descriptions
|
||||
- Add Field() descriptions to Critical/High priority tools
|
||||
- Start with OAuth, Notes, WebDAV modules
|
||||
- Incremental completion over time
|
||||
|
||||
## Benefits
|
||||
|
||||
### For Users
|
||||
- **Clearer UI**: "Create Note" vs "nc_notes_create_note"
|
||||
- **Safety**: Warnings before destructive operations
|
||||
- **Better help**: Parameter descriptions in auto-completion
|
||||
- **Confidence**: Know which operations are safe to retry
|
||||
|
||||
### For MCP Clients
|
||||
- **Caching**: Cache results from read-only tools
|
||||
- **Safety prompts**: Warn before destructiveHint=true
|
||||
- **Retry logic**: Safely retry idempotent operations
|
||||
- **UI organization**: Group by behavior (reads vs writes vs deletes)
|
||||
- **Performance**: Optimize based on hints
|
||||
|
||||
### For Developers
|
||||
- **Self-documenting**: Behavior is explicit
|
||||
- **Consistency**: Standard patterns across codebase
|
||||
- **Testing**: Validate annotations match implementation
|
||||
- **Maintenance**: Clear expectations for new tools
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Immediate UX improvement with minimal effort
|
||||
- Clients can make smarter decisions
|
||||
- Self-documenting code
|
||||
- Follows MCP best practices
|
||||
|
||||
### Negative
|
||||
- Initial effort to add annotations (12-15 hours total)
|
||||
- Must maintain annotations when adding new tools
|
||||
- Risk of incorrect annotations misleading clients
|
||||
|
||||
### Neutral
|
||||
- Annotations are hints, not guarantees
|
||||
- Clients may ignore annotations
|
||||
- Backward compatible (additive change)
|
||||
|
||||
### Mitigations
|
||||
- **Incorrect annotations**: Add tests validating behavior matches hints
|
||||
- **Maintenance burden**: Add to code review checklist and tool template
|
||||
- **Documentation**: Update CLAUDE.md with annotation guidelines
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Annotated Tool (Delete)
|
||||
|
||||
```python
|
||||
from mcp.types import ToolAnnotations
|
||||
from pydantic import Field
|
||||
|
||||
@mcp.tool(
|
||||
title="Delete Note",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, # Deletes data permanently
|
||||
idempotentHint=True, # Same end state (note doesn't exist)
|
||||
openWorldHint=True # Nextcloud is external
|
||||
)
|
||||
)
|
||||
@require_scopes("notes:write")
|
||||
@instrument_tool
|
||||
async def nc_notes_delete_note(
|
||||
note_id: int = Field(description="The ID of the note to delete permanently"),
|
||||
ctx: Context
|
||||
) -> DeleteNoteResponse:
|
||||
"""Delete a note permanently (requires notes:write scope)"""
|
||||
client = await get_client(ctx)
|
||||
# ... implementation ...
|
||||
```
|
||||
|
||||
### Complete Annotated Tool (Update)
|
||||
|
||||
```python
|
||||
@mcp.tool(
|
||||
title="Update Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # NOT idempotent: etag changes each update
|
||||
openWorldHint=True
|
||||
)
|
||||
)
|
||||
@require_scopes("notes:write")
|
||||
@instrument_tool
|
||||
async def nc_notes_update_note(
|
||||
note_id: int = Field(description="The ID of the note to update"),
|
||||
title: str | None = Field(
|
||||
default=None,
|
||||
description="New title (omit to keep current)"
|
||||
),
|
||||
content: str | None = Field(
|
||||
default=None,
|
||||
description="New markdown content (omit to keep current)"
|
||||
),
|
||||
category: str | None = Field(
|
||||
default=None,
|
||||
description="New category/folder (omit to keep current)"
|
||||
),
|
||||
etag: str = Field(
|
||||
description="ETag from get_note (prevents concurrent modification)"
|
||||
),
|
||||
ctx: Context
|
||||
) -> UpdateNoteResponse:
|
||||
"""Update an existing note's title, content, or category.
|
||||
|
||||
The etag parameter is required to prevent overwriting concurrent changes.
|
||||
Get the current ETag by first calling nc_notes_get_note.
|
||||
If the note has been modified since you retrieved it, the update will fail.
|
||||
"""
|
||||
client = await get_client(ctx)
|
||||
# ... implementation ...
|
||||
```
|
||||
|
||||
### Complete Annotated Tool (Read-Only)
|
||||
|
||||
```python
|
||||
@mcp.tool(
|
||||
title="Search Notes",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Doesn't modify data
|
||||
openWorldHint=True # Queries Nextcloud
|
||||
)
|
||||
)
|
||||
@require_scopes("notes:read")
|
||||
@instrument_tool
|
||||
async def nc_notes_search_notes(
|
||||
query: str = Field(description="Search term to match in note titles or content"),
|
||||
ctx: Context
|
||||
) -> SearchNotesResponse:
|
||||
"""Search notes by title or content, returning id, title, and category.
|
||||
|
||||
This is a read-only operation that searches across all user notes.
|
||||
Use nc_notes_get_note to retrieve the full content of matching notes.
|
||||
"""
|
||||
client = await get_client(ctx)
|
||||
# ... implementation ...
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
Add tests validating annotation presence and correctness:
|
||||
|
||||
```python
|
||||
def test_notes_tools_have_annotations():
|
||||
"""Verify all notes tools have appropriate annotations."""
|
||||
tools = get_registered_tools(mcp)
|
||||
|
||||
# Check create tool
|
||||
create_tool = tools["nc_notes_create_note"]
|
||||
assert create_tool.title == "Create Note"
|
||||
assert create_tool.annotations.idempotentHint is False
|
||||
|
||||
# Check delete tool
|
||||
delete_tool = tools["nc_notes_delete_note"]
|
||||
assert delete_tool.title == "Delete Note"
|
||||
assert delete_tool.annotations.destructiveHint is True
|
||||
assert delete_tool.annotations.idempotentHint is True
|
||||
|
||||
# Check read-only tool
|
||||
search_tool = tools["nc_notes_search_notes"]
|
||||
assert search_tool.title == "Search Notes"
|
||||
assert search_tool.annotations.readOnlyHint is True
|
||||
```
|
||||
|
||||
### Integration Tests
|
||||
- Verify existing tests pass with annotations
|
||||
- Manual testing in MCP inspector/client
|
||||
|
||||
### Documentation Updates
|
||||
- Update README tool listings with new titles
|
||||
- Add annotation guidelines to CLAUDE.md
|
||||
- Include examples in developer documentation
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Icons**: Should we add visual icons for tools? (Requires design work)
|
||||
2. **Semantic search openWorldHint**: False (internal data only) or True (Nextcloud is external)?
|
||||
3. **Read-only with side effects**: Should tools that log analytics still be readOnlyHint=true?
|
||||
|
||||
## References
|
||||
|
||||
- MCP Python SDK: `/home/chris/Software/python-sdk/`
|
||||
- ToolAnnotations spec: `src/mcp/types.py:1247`
|
||||
- FastMCP decorator: `src/mcp/server/fastmcp/server.py:444`
|
||||
- Examples: `examples/fastmcp/parameter_descriptions.py`, `examples/fastmcp/icons_demo.py`
|
||||
|
||||
## Decision Timeline
|
||||
|
||||
- **Proposed**: 2025-12-11
|
||||
- **Reviewed**: TBD
|
||||
- **Accepted**: TBD
|
||||
- **Implemented**: TBD
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -19,7 +20,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def configure_calendar_tools(mcp: FastMCP):
|
||||
# Calendar tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Calendars",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_list_calendars(ctx: Context) -> ListCalendarsResponse:
|
||||
@@ -30,7 +34,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
calendars = [Calendar(**cal_data) for cal_data in calendars_data]
|
||||
return ListCalendarsResponse(calendars=calendars, total_count=len(calendars))
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Calendar Event",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:write")
|
||||
@instrument_tool
|
||||
async def nc_calendar_create_event(
|
||||
@@ -107,7 +114,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
|
||||
return await client.calendar.create_event(calendar_name, event_data)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Calendar Events",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_list_events(
|
||||
@@ -210,7 +220,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
|
||||
return events
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Calendar Event",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_get_event(
|
||||
@@ -223,7 +236,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
event_data, etag = await client.calendar.get_event(calendar_name, event_uid)
|
||||
return event_data
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Calendar Event",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:write")
|
||||
@instrument_tool
|
||||
async def nc_calendar_update_event(
|
||||
@@ -297,7 +313,12 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
calendar_name, event_uid, event_data, etag
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Calendar Event",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("calendar:write")
|
||||
@instrument_tool
|
||||
async def nc_calendar_delete_event(
|
||||
@@ -309,7 +330,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.calendar.delete_event(calendar_name, event_uid)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Meeting",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:write")
|
||||
@instrument_tool
|
||||
async def nc_calendar_create_meeting(
|
||||
@@ -376,7 +400,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
|
||||
return await client.calendar.create_event(calendar_name, event_data)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Upcoming Events",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_get_upcoming_events(
|
||||
@@ -427,7 +454,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
all_events.sort(key=lambda x: x.get("start_datetime", ""))
|
||||
return all_events[:limit]
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Find Availability",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_find_availability(
|
||||
@@ -508,7 +538,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
constraints=constraints,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Bulk Calendar Operations",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:write")
|
||||
@instrument_tool
|
||||
async def nc_calendar_bulk_operations(
|
||||
@@ -758,7 +791,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
"results": results,
|
||||
}
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Manage Calendar",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("calendar:write")
|
||||
@instrument_tool
|
||||
async def nc_calendar_manage_calendar(
|
||||
@@ -828,7 +864,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
|
||||
# ============= Todo/Task Tools =============
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Todo Tasks",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("todo:read", "calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_list_todos(
|
||||
@@ -874,7 +913,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
todos=todos, calendar_name=calendar_name, total_count=len(todos)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Todo Task",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("todo:write", "calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_create_todo(
|
||||
@@ -918,7 +960,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
|
||||
return await client.calendar.create_todo(calendar_name, todo_data)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Todo Task",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("todo:write", "calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_update_todo(
|
||||
@@ -979,7 +1024,12 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
|
||||
return await client.calendar.update_todo(calendar_name, todo_uid, todo_data)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Todo Task",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("todo:write", "calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_delete_todo(
|
||||
@@ -1000,7 +1050,10 @@ def configure_calendar_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.calendar.delete_todo(calendar_name, todo_uid)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Search Todo Tasks",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("todo:read", "calendar:read")
|
||||
@instrument_tool
|
||||
async def nc_calendar_search_todos(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -11,7 +12,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def configure_contacts_tools(mcp: FastMCP):
|
||||
# Contacts tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Address Books",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("contacts:read")
|
||||
@instrument_tool
|
||||
async def nc_contacts_list_addressbooks(ctx: Context):
|
||||
@@ -19,7 +23,10 @@ def configure_contacts_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.contacts.list_addressbooks()
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Contacts",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("contacts:read")
|
||||
@instrument_tool
|
||||
async def nc_contacts_list_contacts(ctx: Context, *, addressbook: str):
|
||||
@@ -27,7 +34,10 @@ def configure_contacts_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.contacts.list_contacts(addressbook=addressbook)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Address Book",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("contacts:write")
|
||||
@instrument_tool
|
||||
async def nc_contacts_create_addressbook(
|
||||
@@ -44,7 +54,12 @@ def configure_contacts_tools(mcp: FastMCP):
|
||||
name=name, display_name=display_name
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Address Book",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("contacts:write")
|
||||
@instrument_tool
|
||||
async def nc_contacts_delete_addressbook(ctx: Context, *, name: str):
|
||||
@@ -52,7 +67,10 @@ def configure_contacts_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.contacts.delete_addressbook(name=name)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Contact",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("contacts:write")
|
||||
@instrument_tool
|
||||
async def nc_contacts_create_contact(
|
||||
@@ -70,7 +88,12 @@ def configure_contacts_tools(mcp: FastMCP):
|
||||
addressbook=addressbook, uid=uid, contact_data=contact_data
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Contact",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("contacts:write")
|
||||
@instrument_tool
|
||||
async def nc_contacts_delete_contact(ctx: Context, *, addressbook: str, uid: str):
|
||||
@@ -78,7 +101,10 @@ def configure_contacts_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.contacts.delete_contact(addressbook=addressbook, uid=uid)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Contact",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("contacts:write")
|
||||
@instrument_tool
|
||||
async def nc_contacts_update_contact(
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from httpx import HTTPStatusError, RequestError
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import ErrorData
|
||||
from mcp.types import ErrorData, ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -71,7 +71,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Import Recipe from URL",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:write")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_import_recipe(url: str, ctx: Context) -> ImportRecipeResponse:
|
||||
@@ -129,7 +132,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Recipes",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_list_recipes(ctx: Context) -> ListRecipesResponse:
|
||||
@@ -155,7 +161,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Recipe",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_get_recipe(recipe_id: int, ctx: Context) -> Recipe:
|
||||
@@ -181,7 +190,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Recipe",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:write")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_create_recipe(
|
||||
@@ -261,7 +273,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Recipe",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:write")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_update_recipe(
|
||||
@@ -351,7 +366,12 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Recipe",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("cookbook:write")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_delete_recipe(
|
||||
@@ -387,7 +407,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Search Recipes",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_search_recipes(
|
||||
@@ -424,7 +447,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Recipe Categories",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_list_categories(ctx: Context) -> ListCategoriesResponse:
|
||||
@@ -452,7 +478,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Recipes in Category",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_get_recipes_in_category(
|
||||
@@ -489,7 +518,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Recipe Keywords",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_list_keywords(ctx: Context) -> ListKeywordsResponse:
|
||||
@@ -515,7 +547,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Recipes with Keywords",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:read")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_get_recipes_with_keywords(
|
||||
@@ -550,7 +585,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Set Cookbook Configuration",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:write")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_set_config(
|
||||
@@ -594,7 +632,10 @@ def configure_cookbook_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Reindex Recipes",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("cookbook:write")
|
||||
@instrument_tool
|
||||
async def nc_cookbook_reindex(ctx: Context) -> ReindexResponse:
|
||||
|
||||
@@ -2,6 +2,7 @@ import logging
|
||||
from typing import Optional
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -117,7 +118,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
|
||||
# Read Tools (converted from resources)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Deck Boards",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_boards(ctx: Context) -> list[DeckBoard]:
|
||||
@@ -126,7 +130,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
boards = await client.deck.get_boards()
|
||||
return boards
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Deck Board",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_board(ctx: Context, board_id: int) -> DeckBoard:
|
||||
@@ -135,7 +142,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board = await client.deck.get_board(board_id)
|
||||
return board
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Deck Stacks",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_stacks(ctx: Context, board_id: int) -> list[DeckStack]:
|
||||
@@ -144,7 +154,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
stacks = await client.deck.get_stacks(board_id)
|
||||
return stacks
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Deck Stack",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_stack(ctx: Context, board_id: int, stack_id: int) -> DeckStack:
|
||||
@@ -153,7 +166,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
stack = await client.deck.get_stack(board_id, stack_id)
|
||||
return stack
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Deck Cards",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_cards(
|
||||
@@ -166,7 +182,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
return stack.cards
|
||||
return []
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Deck Card",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_card(
|
||||
@@ -177,7 +196,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
card = await client.deck.get_card(board_id, stack_id, card_id)
|
||||
return card
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Deck Labels",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_labels(ctx: Context, board_id: int) -> list[DeckLabel]:
|
||||
@@ -186,7 +208,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board = await client.deck.get_board(board_id)
|
||||
return board.labels
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Deck Label",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:read")
|
||||
@instrument_tool
|
||||
async def deck_get_label(ctx: Context, board_id: int, label_id: int) -> DeckLabel:
|
||||
@@ -197,7 +222,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
|
||||
# Create/Update/Delete Tools
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Deck Board",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_create_board(
|
||||
@@ -215,7 +243,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
|
||||
# Stack Tools
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Deck Stack",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_create_stack(
|
||||
@@ -232,7 +263,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
stack = await client.deck.create_stack(board_id, title, order)
|
||||
return CreateStackResponse(id=stack.id, title=stack.title, order=stack.order)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Deck Stack",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_update_stack(
|
||||
@@ -259,7 +293,12 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Deck Stack",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_delete_stack(
|
||||
@@ -281,7 +320,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
)
|
||||
|
||||
# Card Tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_create_card(
|
||||
@@ -316,7 +358,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
stackId=card.stackId,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_update_card(
|
||||
@@ -370,7 +415,12 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Deck Card",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_delete_card(
|
||||
@@ -393,7 +443,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Archive Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_archive_card(
|
||||
@@ -416,7 +469,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Unarchive Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_unarchive_card(
|
||||
@@ -439,7 +495,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Reorder/Move Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_reorder_card(
|
||||
@@ -472,7 +531,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
)
|
||||
|
||||
# Label Tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Deck Label",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_create_label(
|
||||
@@ -489,7 +551,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
label = await client.deck.create_label(board_id, title, color)
|
||||
return CreateLabelResponse(id=label.id, title=label.title, color=label.color)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Deck Label",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_update_label(
|
||||
@@ -516,7 +581,12 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Deck Label",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_delete_label(
|
||||
@@ -538,7 +608,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
)
|
||||
|
||||
# Card-Label Assignment Tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Assign Label to Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_assign_label_to_card(
|
||||
@@ -562,7 +635,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Remove Label from Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_remove_label_from_card(
|
||||
@@ -587,7 +663,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
)
|
||||
|
||||
# Card-User Assignment Tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Assign User to Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_assign_user_to_card(
|
||||
@@ -611,7 +690,10 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board_id=board_id,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Unassign User from Deck Card",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("deck:write")
|
||||
@instrument_tool
|
||||
async def deck_unassign_user_from_card(
|
||||
|
||||
@@ -5,7 +5,7 @@ import logging
|
||||
from httpx import HTTPStatusError, RequestError
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import ErrorData
|
||||
from mcp.types import ErrorData, ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.client.news import NewsItemType
|
||||
@@ -30,7 +30,10 @@ logger = logging.getLogger(__name__)
|
||||
def configure_news_tools(mcp: FastMCP):
|
||||
"""Configure News app MCP tools."""
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List News Folders",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_list_folders(ctx: Context) -> ListFoldersResponse:
|
||||
@@ -52,7 +55,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List News Feeds",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_list_feeds(ctx: Context) -> ListFeedsResponse:
|
||||
@@ -82,7 +88,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List News Items",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_list_items(
|
||||
@@ -153,7 +162,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get News Item",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_get_item(item_id: int, ctx: Context) -> GetItemResponse:
|
||||
@@ -188,7 +200,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Starred News Items",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_get_starred_items(
|
||||
@@ -238,7 +253,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Unread News Items",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_get_unread_items(
|
||||
@@ -288,7 +306,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get News Feed Health",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_get_feed_health(feed_id: int, ctx: Context) -> FeedHealthResponse:
|
||||
@@ -332,7 +353,10 @@ def configure_news_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get News App Status",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("news:read")
|
||||
@instrument_tool
|
||||
async def nc_news_get_status(ctx: Context) -> GetStatusResponse:
|
||||
|
||||
@@ -3,7 +3,7 @@ import logging
|
||||
from httpx import HTTPStatusError, RequestError
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.shared.exceptions import McpError
|
||||
from mcp.types import ErrorData
|
||||
from mcp.types import ErrorData, ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -85,7 +85,13 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Multiple calls create multiple notes
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:write")
|
||||
@instrument_tool
|
||||
async def nc_notes_create_note(
|
||||
@@ -132,7 +138,13 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Requires etag which changes = not idempotent
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:write")
|
||||
@instrument_tool
|
||||
async def nc_notes_update_note(
|
||||
@@ -198,7 +210,13 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Append to Note",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Each call adds content = not idempotent
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:write")
|
||||
@instrument_tool
|
||||
async def nc_notes_append_content(
|
||||
@@ -249,7 +267,13 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Search Notes",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Search doesn't modify data
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:read")
|
||||
@instrument_tool
|
||||
async def nc_notes_search_notes(query: str, ctx: Context) -> SearchNotesResponse:
|
||||
@@ -296,7 +320,13 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Note",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Read operation only
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:read")
|
||||
@instrument_tool
|
||||
async def nc_notes_get_note(note_id: int, ctx: Context) -> Note:
|
||||
@@ -326,7 +356,13 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Note Attachment",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Read operation only
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:read")
|
||||
@instrument_tool
|
||||
async def nc_notes_get_attachment(
|
||||
@@ -373,7 +409,14 @@ def configure_notes_tools(mcp: FastMCP):
|
||||
)
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Note",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, # Permanently deletes data
|
||||
idempotentHint=True, # Deleting deleted note = same end state
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("notes:write")
|
||||
@instrument_tool
|
||||
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
|
||||
|
||||
@@ -15,6 +15,7 @@ import httpx
|
||||
from mcp.server.auth.middleware.auth_context import get_access_token
|
||||
from mcp.server.auth.provider import AccessToken
|
||||
from mcp.server.fastmcp import Context
|
||||
from mcp.types import ToolAnnotations
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
@@ -684,11 +685,16 @@ def register_oauth_tools(mcp):
|
||||
|
||||
@mcp.tool(
|
||||
name="provision_nextcloud_access",
|
||||
title="Grant Server Access to Nextcloud",
|
||||
description=(
|
||||
"Provision offline access to Nextcloud resources. "
|
||||
"This is required before using Nextcloud tools. "
|
||||
"You'll need to complete an OAuth authorization in your browser."
|
||||
),
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Creates new OAuth session each time
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("openid")
|
||||
async def tool_provision_access(
|
||||
@@ -699,7 +705,13 @@ def register_oauth_tools(mcp):
|
||||
|
||||
@mcp.tool(
|
||||
name="revoke_nextcloud_access",
|
||||
title="Revoke Server Access to Nextcloud",
|
||||
description="Revoke offline access to Nextcloud resources.",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, # Removes stored access tokens
|
||||
idempotentHint=True, # Revoking revoked access = same end state
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("openid")
|
||||
async def tool_revoke_access(
|
||||
@@ -709,7 +721,12 @@ def register_oauth_tools(mcp):
|
||||
|
||||
@mcp.tool(
|
||||
name="check_provisioning_status",
|
||||
title="Check Provisioning Status",
|
||||
description="Check whether Nextcloud access is provisioned.",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Only checks status, doesn't modify
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("openid")
|
||||
async def tool_check_status(
|
||||
@@ -719,10 +736,15 @@ def register_oauth_tools(mcp):
|
||||
|
||||
@mcp.tool(
|
||||
name="check_logged_in",
|
||||
title="Check Server Login Status",
|
||||
description=(
|
||||
"Check if you are logged in to Nextcloud. "
|
||||
"If not logged in, this tool will prompt you to complete the login flow."
|
||||
),
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Checking status doesn't modify state
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("openid")
|
||||
async def tool_check_logged_in(ctx: Context, user_id: Optional[str] = None) -> str:
|
||||
|
||||
@@ -12,6 +12,7 @@ from mcp.types import (
|
||||
ModelPreferences,
|
||||
SamplingMessage,
|
||||
TextContent,
|
||||
ToolAnnotations,
|
||||
)
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
@@ -34,7 +35,13 @@ logger = logging.getLogger(__name__)
|
||||
def configure_semantic_tools(mcp: FastMCP):
|
||||
"""Configure semantic search tools for MCP server."""
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Semantic Search",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Search doesn't modify data
|
||||
openWorldHint=False, # Searches only indexed Nextcloud data
|
||||
),
|
||||
)
|
||||
@require_scopes("semantic:read")
|
||||
@instrument_tool
|
||||
async def nc_semantic_search(
|
||||
@@ -285,7 +292,13 @@ def configure_semantic_tools(mcp: FastMCP):
|
||||
logger.error(f"Search error: {e}", exc_info=True)
|
||||
raise McpError(ErrorData(code=-1, message=f"Search failed: {str(e)}"))
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Search with AI-Generated Answer",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Search doesn't modify data
|
||||
openWorldHint=False, # Searches only indexed Nextcloud data
|
||||
),
|
||||
)
|
||||
@require_scopes("semantic:read")
|
||||
@instrument_tool
|
||||
async def nc_semantic_search_answer(
|
||||
@@ -623,7 +636,13 @@ def configure_semantic_tools(mcp: FastMCP):
|
||||
success=True,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Check Indexing Status",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True, # Only checks status
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("semantic:read")
|
||||
@instrument_tool
|
||||
async def nc_get_vector_sync_status(ctx: Context) -> VectorSyncStatusResponse:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -16,7 +17,10 @@ def configure_sharing_tools(mcp: FastMCP):
|
||||
mcp: FastMCP server instance
|
||||
"""
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Share",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("sharing:write")
|
||||
@instrument_tool
|
||||
async def nc_share_create(
|
||||
@@ -56,7 +60,12 @@ def configure_sharing_tools(mcp: FastMCP):
|
||||
)
|
||||
return json.dumps(share_data, indent=2)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Share",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("sharing:write")
|
||||
@instrument_tool
|
||||
async def nc_share_delete(share_id: int, ctx: Context) -> str:
|
||||
@@ -76,7 +85,10 @@ def configure_sharing_tools(mcp: FastMCP):
|
||||
{"success": True, "message": f"Share {share_id} deleted"}, indent=2
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Share Details",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("sharing:write")
|
||||
@instrument_tool
|
||||
async def nc_share_get(share_id: int, ctx: Context) -> str:
|
||||
@@ -95,7 +107,10 @@ def configure_sharing_tools(mcp: FastMCP):
|
||||
share_data = await client.sharing.get_share(share_id)
|
||||
return json.dumps(share_data, indent=2)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Shares",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("sharing:write")
|
||||
@instrument_tool
|
||||
async def nc_share_list(
|
||||
@@ -117,7 +132,10 @@ def configure_sharing_tools(mcp: FastMCP):
|
||||
)
|
||||
return json.dumps(shares, indent=2)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Share",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("sharing:write")
|
||||
@instrument_tool
|
||||
async def nc_share_update(share_id: int, permissions: int, ctx: Context) -> str:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -11,7 +12,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def configure_tables_tools(mcp: FastMCP):
|
||||
# Tables tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Tables",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("tables:read")
|
||||
@instrument_tool
|
||||
async def nc_tables_list_tables(ctx: Context):
|
||||
@@ -19,7 +23,10 @@ def configure_tables_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.tables.list_tables()
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Get Table Schema",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("tables:read")
|
||||
@instrument_tool
|
||||
async def nc_tables_get_schema(table_id: int, ctx: Context):
|
||||
@@ -27,7 +34,10 @@ def configure_tables_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.tables.get_table_schema(table_id)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Read Table Rows",
|
||||
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("tables:read")
|
||||
@instrument_tool
|
||||
async def nc_tables_read_table(
|
||||
@@ -40,7 +50,10 @@ def configure_tables_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.tables.get_table_rows(table_id, limit, offset)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Insert Table Row",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("tables:write")
|
||||
@instrument_tool
|
||||
async def nc_tables_insert_row(table_id: int, data: dict, ctx: Context):
|
||||
@@ -51,7 +64,10 @@ def configure_tables_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.tables.create_row(table_id, data)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Update Table Row",
|
||||
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
||||
)
|
||||
@require_scopes("tables:write")
|
||||
@instrument_tool
|
||||
async def nc_tables_update_row(row_id: int, data: dict, ctx: Context):
|
||||
@@ -62,7 +78,12 @@ def configure_tables_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.tables.update_row(row_id, data)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete Table Row",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
||||
),
|
||||
)
|
||||
@require_scopes("tables:write")
|
||||
@instrument_tool
|
||||
async def nc_tables_delete_row(row_id: int, ctx: Context):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
from mcp.types import ToolAnnotations
|
||||
|
||||
from nextcloud_mcp_server.auth import require_scopes
|
||||
from nextcloud_mcp_server.context import get_client
|
||||
@@ -16,7 +17,13 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
def configure_webdav_tools(mcp: FastMCP):
|
||||
# WebDAV file system tools
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Files and Directories",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:read")
|
||||
@instrument_tool
|
||||
async def nc_webdav_list_directory(
|
||||
@@ -50,7 +57,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
total_size=total_size,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Read File",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:read")
|
||||
@instrument_tool
|
||||
async def nc_webdav_read_file(path: str, ctx: Context):
|
||||
@@ -117,7 +130,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
"encoding": "base64",
|
||||
}
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Write File",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # No etag/version control = not truly idempotent
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:write")
|
||||
@instrument_tool
|
||||
async def nc_webdav_write_file(
|
||||
@@ -146,7 +165,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
|
||||
return await client.webdav.write_file(path, content_bytes, content_type)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Create Directory",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=True, # Creating existing dir returns 405 = same end state
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:write")
|
||||
@instrument_tool
|
||||
async def nc_webdav_create_directory(path: str, ctx: Context):
|
||||
@@ -161,7 +186,14 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.webdav.create_directory(path)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Delete File or Directory",
|
||||
annotations=ToolAnnotations(
|
||||
destructiveHint=True, # Permanently deletes data
|
||||
idempotentHint=True, # Deleting deleted resource = same end state
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:write")
|
||||
@instrument_tool
|
||||
async def nc_webdav_delete_resource(path: str, ctx: Context):
|
||||
@@ -176,7 +208,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
client = await get_client(ctx)
|
||||
return await client.webdav.delete_resource(path)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Move or Rename File",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Moving changes source and dest
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:write")
|
||||
@instrument_tool
|
||||
async def nc_webdav_move_resource(
|
||||
@@ -197,7 +235,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
source_path, destination_path, overwrite
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Copy File or Directory",
|
||||
annotations=ToolAnnotations(
|
||||
idempotentHint=False, # Creates new resource each time
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:write")
|
||||
@instrument_tool
|
||||
async def nc_webdav_copy_resource(
|
||||
@@ -218,7 +262,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
source_path, destination_path, overwrite
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Search Files",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:read")
|
||||
@instrument_tool
|
||||
async def nc_webdav_search_files(
|
||||
@@ -335,7 +385,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
filters_applied=filters if filters else None,
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Find Files by Name",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:read")
|
||||
@instrument_tool
|
||||
async def nc_webdav_find_by_name(
|
||||
@@ -363,7 +419,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
filters_applied={"name_pattern": pattern},
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="Find Files by Type",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:read")
|
||||
@instrument_tool
|
||||
async def nc_webdav_find_by_type(
|
||||
@@ -391,7 +453,13 @@ def configure_webdav_tools(mcp: FastMCP):
|
||||
filters_applied={"mime_type": mime_type},
|
||||
)
|
||||
|
||||
@mcp.tool()
|
||||
@mcp.tool(
|
||||
title="List Favorite Files",
|
||||
annotations=ToolAnnotations(
|
||||
readOnlyHint=True,
|
||||
openWorldHint=True,
|
||||
),
|
||||
)
|
||||
@require_scopes("files:read")
|
||||
@instrument_tool
|
||||
async def nc_webdav_list_favorites(
|
||||
|
||||
Reference in New Issue
Block a user