Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e3c2c9774 | |||
| 752c22147c | |||
| 4c07ca9f0a | |||
| 55945c6c0f | |||
| 3f8312e6f3 | |||
| c39b69d08c | |||
| 290ad2edc2 | |||
| 144c08c339 | |||
| b461af8aa1 | |||
| 4bdf67b042 | |||
| 93b109e5b9 | |||
| 0c5ebd5d84 | |||
| 79e6250377 | |||
| a5ec712b88 | |||
| cc9650b077 | |||
| 1a37a6c1fe | |||
| 4572287870 | |||
| 67617d7fcc | |||
| 22811f29f6 | |||
| 71da620099 | |||
| de7c848aa6 | |||
| 8d4303a624 | |||
| 4c7880a4e5 | |||
| 0a307b87ae | |||
| 48eced80fb | |||
| aafac732c6 | |||
| 12d48bb920 | |||
| 0600cea87b | |||
| 145141e1d8 | |||
| 948e7a4d91 | |||
| 39ff811d1a | |||
| cfd03a761b | |||
| e7b37312a7 | |||
| 4ad47b4fa3 | |||
| ffbb86df57 | |||
| 7a57247a9c | |||
| 4ea6ce3477 | |||
| fad2cd8dcb | |||
| 06042357f8 | |||
| 5bdf840098 | |||
| 9711d1d161 | |||
| 2d802483e5 | |||
| b3cd2ace34 | |||
| 2cd91ceee7 | |||
| 84106a059e | |||
| c1c5a61952 | |||
| e7c4eb0842 | |||
| 2f60dec90d | |||
| 59633017b0 | |||
| 6fa59621bf | |||
| c2284298ce |
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
*
|
*
|
||||||
|
|
||||||
!pyproject.toml
|
!pyproject.toml
|
||||||
!poetry.lock
|
|
||||||
!README.md
|
!README.md
|
||||||
!uv.lock
|
!uv.lock
|
||||||
|
|
||||||
!nextcloud_mcp_server/
|
!nextcloud_mcp_server/**/*.py
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6
|
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||||
- name: Check format
|
- name: Check format
|
||||||
run: |
|
run: |
|
||||||
uv run --frozen ruff format --diff
|
uv run --frozen ruff format --diff
|
||||||
@@ -31,7 +31,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
compose-file: "./docker-compose.yml"
|
compose-file: "./docker-compose.yml"
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@557e51de59eb14aaaba2ed9621916900a91d50c6 # v6
|
uses: astral-sh/setup-uv@b75a909f75acd358c2196fb9a5f1299a9a8868a4 # v6.7.0
|
||||||
|
|
||||||
- name: Wait for service to be ready
|
- name: Wait for service to be ready
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -1,3 +1,46 @@
|
|||||||
|
## v0.12.4 (2025-09-25)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **deps**: update dependency mcp to >=1.15,<1.16
|
||||||
|
|
||||||
|
## v0.12.3 (2025-09-23)
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Add tools for all resources to enable tool-only workflows
|
||||||
|
|
||||||
|
## v0.12.2 (2025-09-20)
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Add `http` to --transport option
|
||||||
|
|
||||||
|
## v0.12.1 (2025-09-11)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **docker**: Provide --host 0.0.0.0 in default docker image
|
||||||
|
|
||||||
|
## v0.12.0 (2025-09-11)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- **server**: Add support for `streamable-http` transport type
|
||||||
|
|
||||||
|
## v0.11.1 (2025-09-11)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- **deps**: update dependency mcp to >=1.13,<1.14
|
||||||
|
|
||||||
|
## v0.11.0 (2025-09-11)
|
||||||
|
|
||||||
|
### Feat
|
||||||
|
|
||||||
|
- **deck**: Add support for stack, cards, labels
|
||||||
|
- **deck**: Initialize Deck app client/server
|
||||||
|
|
||||||
## v0.10.0 (2025-09-10)
|
## v0.10.0 (2025-09-10)
|
||||||
|
|
||||||
### Feat
|
### Feat
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM ghcr.io/astral-sh/uv:0.8.17-python3.11-alpine@sha256:2a2cae80b7d3b3b3c7f94ec3ed91e9b3ca2524a7a429824fbbadd9954fa5d6b6
|
FROM ghcr.io/astral-sh/uv:0.8.22-python3.11-alpine@sha256:a8d5f7079a3223380ec060fefe48afe45b4c4622d631ce0e495593ac9a38f546
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -6,4 +6,4 @@ COPY . .
|
|||||||
|
|
||||||
RUN uv sync --locked --no-dev
|
RUN uv sync --locked --no-dev
|
||||||
|
|
||||||
ENTRYPOINT ["/app/.venv/bin/python", "-m", "nextcloud_mcp_server.app"]
|
ENTRYPOINT ["/app/.venv/bin/nextcloud-mcp-server", "--host", "0.0.0.0"]
|
||||||
|
|||||||
@@ -23,94 +23,7 @@ The server provides integration with multiple Nextcloud apps, enabling LLMs to i
|
|||||||
Is there a Nextcloud app not present in this list that you'd like to be
|
Is there a Nextcloud app not present in this list that you'd like to be
|
||||||
included? Feel free to open an issue, or contribute via a pull-request.
|
included? Feel free to open an issue, or contribute via a pull-request.
|
||||||
|
|
||||||
## Available Tools
|
## Available Tools & Resources
|
||||||
|
|
||||||
### Notes Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `nc_get_note` | Get a specific note by ID |
|
|
||||||
| `nc_notes_create_note` | Create a new note with title, content, and category |
|
|
||||||
| `nc_notes_update_note` | Update an existing note by ID |
|
|
||||||
| `nc_notes_append_content` | Append content to an existing note with a clear separator |
|
|
||||||
| `nc_notes_delete_note` | Delete a note by ID |
|
|
||||||
| `nc_notes_search_notes` | Search notes by title or content |
|
|
||||||
|
|
||||||
### Calendar Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `nc_calendar_list_calendars` | List all available calendars for the user |
|
|
||||||
| `nc_calendar_create_event` | Create a comprehensive calendar event with full feature support (recurring, reminders, attendees, etc.) |
|
|
||||||
| `nc_calendar_list_events` | **Enhanced:** List events with advanced filtering (min attendees, duration, categories, status, search across all calendars) |
|
|
||||||
| `nc_calendar_get_event` | Get detailed information about a specific event |
|
|
||||||
| `nc_calendar_update_event` | Update any aspect of an existing event |
|
|
||||||
| `nc_calendar_delete_event` | Delete a calendar event |
|
|
||||||
| `nc_calendar_create_meeting` | Quick meeting creation with smart defaults |
|
|
||||||
| `nc_calendar_get_upcoming_events` | Get upcoming events in the next N days |
|
|
||||||
| `nc_calendar_find_availability` | **New:** Intelligent availability finder - find free time slots for meetings with attendee conflict detection |
|
|
||||||
| `nc_calendar_bulk_operations` | **New:** Bulk update, delete, or move events matching filter criteria |
|
|
||||||
| `nc_calendar_manage_calendar` | **New:** Create, delete, and manage calendar properties |
|
|
||||||
|
|
||||||
### Contacts Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `nc_contacts_list_addressbooks` | List all available addressbooks for the user |
|
|
||||||
| `nc_contacts_list_contacts` | List all contacts in a specific addressbook |
|
|
||||||
| `nc_contacts_create_addressbook` | Create a new addressbook |
|
|
||||||
| `nc_contacts_delete_addressbook` | Delete an addressbook |
|
|
||||||
| `nc_contacts_create_contact` | Create a new contact in an addressbook |
|
|
||||||
| `nc_contacts_delete_contact` | Delete a contact from an addressbook |
|
|
||||||
|
|
||||||
### Deck Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `deck_list_boards` | List all Nextcloud Deck boards with optional details and filtering |
|
|
||||||
| `deck_create_board` | Create a new Deck board with title and color |
|
|
||||||
| `deck_list_stacks` | List all stacks in a board |
|
|
||||||
| `deck_create_stack` | Create a new stack in a board |
|
|
||||||
| `deck_update_stack` | Update stack title and order |
|
|
||||||
| `deck_delete_stack` | Delete a stack and all its cards |
|
|
||||||
| `deck_create_card` | Create a new card in a stack with full options (title, description, due date, etc.) |
|
|
||||||
| `deck_update_card` | Update any aspect of a card (title, description, owner, order, etc.) |
|
|
||||||
| `deck_delete_card` | Delete a card |
|
|
||||||
| `deck_archive_card` | Archive a card |
|
|
||||||
| `deck_unarchive_card` | Unarchive a card |
|
|
||||||
| `deck_reorder_card` | Move/reorder cards within or between stacks |
|
|
||||||
| `deck_create_label` | Create a new label in a board |
|
|
||||||
| `deck_update_label` | Update label title and color |
|
|
||||||
| `deck_delete_label` | Delete a label |
|
|
||||||
| `deck_assign_label_to_card` | Assign a label to a card |
|
|
||||||
| `deck_remove_label_from_card` | Remove a label from a card |
|
|
||||||
| `deck_assign_user_to_card` | Assign a user to a card |
|
|
||||||
| `deck_unassign_user_from_card` | Remove a user assignment from a card |
|
|
||||||
|
|
||||||
### Tables Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `nc_tables_list_tables` | List all tables available to the user |
|
|
||||||
| `nc_tables_get_schema` | Get the schema/structure of a specific table including columns and views |
|
|
||||||
| `nc_tables_read_table` | Read rows from a table with optional pagination |
|
|
||||||
| `nc_tables_insert_row` | Insert a new row into a table |
|
|
||||||
| `nc_tables_update_row` | Update an existing row in a table |
|
|
||||||
| `nc_tables_delete_row` | Delete a row from a table |
|
|
||||||
|
|
||||||
### WebDAV File System Tools
|
|
||||||
|
|
||||||
| Tool | Description |
|
|
||||||
|------|-------------|
|
|
||||||
| `nc_webdav_list_directory` | List files and directories in any NextCloud path |
|
|
||||||
| `nc_webdav_read_file` | Read file content (text files decoded, binary as base64) |
|
|
||||||
| `nc_webdav_write_file` | Create or update files in NextCloud |
|
|
||||||
| `nc_webdav_create_directory` | Create new directories |
|
|
||||||
| `nc_webdav_delete_resource` | Delete files or directories |
|
|
||||||
| `nc_webdav_move_resource` | Move or rename files and directories |
|
|
||||||
| `nc_webdav_copy_resource` | Copy files and directories |
|
|
||||||
|
|
||||||
## Available Resources
|
|
||||||
|
|
||||||
Resources provide read-only access to data for browsing and discovery. Unlike tools, resources are automatically listed by MCP clients and enable LLMs to explore your Nextcloud data structure.
|
Resources provide read-only access to data for browsing and discovery. Unlike tools, resources are automatically listed by MCP clients and enable LLMs to explore your Nextcloud data structure.
|
||||||
|
|
||||||
@@ -121,17 +34,6 @@ Resources provide read-only access to data for browsing and discovery. Unlike to
|
|||||||
| `notes://settings` | Access Notes app settings |
|
| `notes://settings` | Access Notes app settings |
|
||||||
| `nc://Notes/{note_id}/attachments/{attachment_filename}` | Access attachments for notes |
|
| `nc://Notes/{note_id}/attachments/{attachment_filename}` | Access attachments for notes |
|
||||||
|
|
||||||
### Deck Resources
|
|
||||||
| Resource | Description |
|
|
||||||
|----------|-------------|
|
|
||||||
| `nc://Deck/boards` | List all deck boards |
|
|
||||||
| `nc://Deck/boards/{board_id}` | Get details of a specific board |
|
|
||||||
| `nc://Deck/boards/{board_id}/stacks` | List all stacks in a board |
|
|
||||||
| `nc://Deck/boards/{board_id}/stacks/{stack_id}` | Get details of a specific stack |
|
|
||||||
| `nc://Deck/boards/{board_id}/stacks/{stack_id}/cards` | List all cards in a stack |
|
|
||||||
| `nc://Deck/boards/{board_id}/stacks/{stack_id}/cards/{card_id}` | Get details of a specific card |
|
|
||||||
| `nc://Deck/boards/{board_id}/labels` | List all labels in a board |
|
|
||||||
| `nc://Deck/boards/{board_id}/labels/{label_id}` | Get details of a specific label |
|
|
||||||
|
|
||||||
### Tools vs Resources
|
### Tools vs Resources
|
||||||
|
|
||||||
@@ -147,231 +49,12 @@ Resources provide read-only access to data for browsing and discovery. Unlike to
|
|||||||
- Raw data format for exploration
|
- Raw data format for exploration
|
||||||
- Examples: `nc://Deck/boards/{board_id}`, `nc://Deck/boards/{board_id}/stacks`
|
- Examples: `nc://Deck/boards/{board_id}`, `nc://Deck/boards/{board_id}/stacks`
|
||||||
|
|
||||||
### WebDAV File System Access
|
|
||||||
|
|
||||||
The server provides complete file system access to your NextCloud instance, enabling you to:
|
|
||||||
|
|
||||||
- Browse any directory structure
|
|
||||||
- Read and write files of any type
|
|
||||||
- Create and delete directories
|
|
||||||
- Manage your NextCloud files directly through LLM interactions
|
|
||||||
|
|
||||||
**Usage Examples:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
# List files in root directory
|
|
||||||
await nc_webdav_list_directory("")
|
|
||||||
|
|
||||||
# Browse a specific folder
|
|
||||||
await nc_webdav_list_directory("Documents/Projects")
|
|
||||||
|
|
||||||
# Read a text file
|
|
||||||
content = await nc_webdav_read_file("Documents/readme.txt")
|
|
||||||
|
|
||||||
# Create a new directory
|
|
||||||
await nc_webdav_create_directory("NewProject/docs")
|
|
||||||
|
|
||||||
# Write content to a file
|
|
||||||
await nc_webdav_write_file("NewProject/docs/notes.md", "# My Notes\n\nContent here...")
|
|
||||||
|
|
||||||
# Delete a file or directory
|
|
||||||
await nc_webdav_delete_resource("old_file.txt")
|
|
||||||
|
|
||||||
# Move or rename a file
|
|
||||||
await nc_webdav_move_resource("document.txt", "new_name.txt")
|
|
||||||
|
|
||||||
# Move a file to another directory
|
|
||||||
await nc_webdav_move_resource("document.txt", "Archive/document.txt")
|
|
||||||
|
|
||||||
# Move a directory
|
|
||||||
await nc_webdav_move_resource("Projects/OldProject", "Projects/NewProject")
|
|
||||||
|
|
||||||
# Copy a file
|
|
||||||
await nc_webdav_copy_resource("document.txt", "document_copy.txt")
|
|
||||||
|
|
||||||
# Copy a file to another directory
|
|
||||||
await nc_webdav_copy_resource("document.txt", "Backup/document.txt")
|
|
||||||
|
|
||||||
# Copy a directory
|
|
||||||
await nc_webdav_copy_resource("Projects/ProjectA", "Projects/ProjectA_Backup")
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deck Project Management
|
|
||||||
|
|
||||||
The server provides complete Nextcloud Deck integration, enabling you to manage projects, tasks, and workflows:
|
|
||||||
|
|
||||||
- Create and manage boards, stacks, and cards
|
|
||||||
- Organize tasks with labels and user assignments
|
|
||||||
- Archive/unarchive cards and reorder within or between stacks
|
|
||||||
- Full CRUD operations on all Deck entities
|
|
||||||
- Browse project structure through hierarchical resources
|
|
||||||
|
|
||||||
**Usage Examples:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
# Create a new project board
|
|
||||||
await deck_create_board(title="Website Redesign", color="1976D2")
|
|
||||||
|
|
||||||
# Create workflow stacks
|
|
||||||
await deck_create_stack(board_id=1, title="To Do", order=1)
|
|
||||||
await deck_create_stack(board_id=1, title="In Progress", order=2)
|
|
||||||
await deck_create_stack(board_id=1, title="Done", order=3)
|
|
||||||
|
|
||||||
# Create task cards with details
|
|
||||||
await deck_create_card(
|
|
||||||
board_id=1,
|
|
||||||
stack_id=1,
|
|
||||||
title="Design new homepage",
|
|
||||||
description="Create mockups for the new homepage layout",
|
|
||||||
type="plain",
|
|
||||||
order=1,
|
|
||||||
duedate="2025-08-15T17:00:00"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create and assign labels for organization
|
|
||||||
await deck_create_label(board_id=1, title="High Priority", color="F44336")
|
|
||||||
await deck_create_label(board_id=1, title="UI/UX", color="9C27B0")
|
|
||||||
|
|
||||||
# Assign labels and users to cards
|
|
||||||
await deck_assign_label_to_card(board_id=1, stack_id=1, card_id=1, label_id=1)
|
|
||||||
await deck_assign_user_to_card(board_id=1, stack_id=1, card_id=1, user_id="designer")
|
|
||||||
|
|
||||||
# Move cards through workflow
|
|
||||||
await deck_reorder_card(
|
|
||||||
board_id=1,
|
|
||||||
stack_id=1, # From "To Do"
|
|
||||||
card_id=1,
|
|
||||||
order=1,
|
|
||||||
target_stack_id=2 # To "In Progress"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update task progress
|
|
||||||
await deck_update_card(
|
|
||||||
board_id=1,
|
|
||||||
stack_id=2,
|
|
||||||
card_id=1,
|
|
||||||
description="Homepage mockups completed, starting development",
|
|
||||||
order=1
|
|
||||||
)
|
|
||||||
|
|
||||||
# Complete tasks
|
|
||||||
await deck_reorder_card(
|
|
||||||
board_id=1,
|
|
||||||
stack_id=2, # From "In Progress"
|
|
||||||
card_id=1,
|
|
||||||
order=1,
|
|
||||||
target_stack_id=3 # To "Done"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Archive completed cards
|
|
||||||
await deck_archive_card(board_id=1, stack_id=3, card_id=1)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Calendar Integration
|
|
||||||
|
|
||||||
The server provides comprehensive calendar integration through CalDAV, enabling you to:
|
|
||||||
|
|
||||||
- List all available calendars
|
|
||||||
- Create, read, update, and delete calendar events
|
|
||||||
- Handle recurring events with RRULE support
|
|
||||||
- Manage event reminders and notifications
|
|
||||||
- Support all-day and timed events
|
|
||||||
- Handle attendees and meeting invitations
|
|
||||||
- Organize events with categories and priorities
|
|
||||||
|
|
||||||
**Usage Examples:**
|
|
||||||
|
|
||||||
```python
|
|
||||||
# List available calendars
|
|
||||||
calendars = await nc_calendar_list_calendars()
|
|
||||||
|
|
||||||
# Create a simple event
|
|
||||||
await nc_calendar_create_event(
|
|
||||||
calendar_name="personal",
|
|
||||||
title="Team Meeting",
|
|
||||||
start_datetime="2025-07-28T14:00:00",
|
|
||||||
end_datetime="2025-07-28T15:00:00",
|
|
||||||
description="Weekly team sync",
|
|
||||||
location="Conference Room A"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a recurring weekly meeting
|
|
||||||
await nc_calendar_create_event(
|
|
||||||
calendar_name="work",
|
|
||||||
title="Weekly Standup",
|
|
||||||
start_datetime="2025-07-28T09:00:00",
|
|
||||||
end_datetime="2025-07-28T09:30:00",
|
|
||||||
recurring=True,
|
|
||||||
recurrence_rule="FREQ=WEEKLY;BYDAY=MO"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Quick meeting creation
|
|
||||||
await nc_calendar_create_meeting(
|
|
||||||
title="Client Call",
|
|
||||||
date="2025-07-28",
|
|
||||||
time="15:00",
|
|
||||||
duration_minutes=60,
|
|
||||||
attendees="client@example.com,colleague@company.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get upcoming events
|
|
||||||
events = await nc_calendar_get_upcoming_events(days_ahead=7)
|
|
||||||
|
|
||||||
# Advanced search - find all meetings with 5+ attendees lasting 2+ hours
|
|
||||||
long_meetings = await nc_calendar_list_events(
|
|
||||||
calendar_name="", # Search all calendars
|
|
||||||
search_all_calendars=True,
|
|
||||||
start_date="2025-07-01",
|
|
||||||
end_date="2025-07-31",
|
|
||||||
min_attendees=5,
|
|
||||||
min_duration_minutes=120,
|
|
||||||
title_contains="meeting"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Find availability for a 1-hour meeting with specific attendees
|
|
||||||
availability = await nc_calendar_find_availability(
|
|
||||||
duration_minutes=60,
|
|
||||||
attendees="sarah@company.com,mike@company.com",
|
|
||||||
date_range_start="2025-07-28",
|
|
||||||
date_range_end="2025-08-04",
|
|
||||||
business_hours_only=True,
|
|
||||||
exclude_weekends=True,
|
|
||||||
preferred_times="09:00-12:00,14:00-17:00"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Bulk update all team meetings to new location
|
|
||||||
bulk_result = await nc_calendar_bulk_operations(
|
|
||||||
operation="update",
|
|
||||||
title_contains="team meeting",
|
|
||||||
start_date="2025-08-01",
|
|
||||||
end_date="2025-08-31",
|
|
||||||
new_location="Conference Room B",
|
|
||||||
new_reminder_minutes=15
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create a new project calendar
|
|
||||||
new_calendar = await nc_calendar_manage_calendar(
|
|
||||||
action="create",
|
|
||||||
calendar_name="project-alpha",
|
|
||||||
display_name="Project Alpha Calendar",
|
|
||||||
description="Calendar for Project Alpha team",
|
|
||||||
color="#FF5722"
|
|
||||||
)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Note Attachments
|
|
||||||
|
|
||||||
This server supports adding and retrieving note attachments via WebDAV. Please note the following behavior regarding attachments:
|
|
||||||
|
|
||||||
* When a note is deleted, its attachments remain in the system. This matches the behavior of the official Nextcloud Notes app.
|
|
||||||
* Orphaned attachments (attachments whose parent notes have been deleted) may accumulate over time.
|
|
||||||
* WebDAV permissions must be properly configured for attachment operations to work correctly.
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
* Python 3.8+
|
* Python 3.11+
|
||||||
* Access to a Nextcloud instance
|
* Access to a Nextcloud instance
|
||||||
|
|
||||||
### Local Installation
|
### Local Installation
|
||||||
@@ -386,6 +69,27 @@ This server supports adding and retrieving note attachments via WebDAV. Please n
|
|||||||
uv sync
|
uv sync
|
||||||
```
|
```
|
||||||
|
|
||||||
|
3. Run the CLI --help command to see all available options
|
||||||
|
```bash
|
||||||
|
$ uv run python -m nextcloud_mcp_server.app --help
|
||||||
|
Usage: python -m nextcloud_mcp_server.app [OPTIONS]
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-h, --host TEXT [default: 127.0.0.1]
|
||||||
|
-p, --port INTEGER [default: 8000]
|
||||||
|
-w, --workers INTEGER
|
||||||
|
-r, --reload
|
||||||
|
-l, --log-level [critical|error|warning|info|debug|trace]
|
||||||
|
[default: info]
|
||||||
|
-t, --transport [sse|streamable-http]
|
||||||
|
[default: sse]
|
||||||
|
-e, --enable-app [notes|tables|webdav|calendar|contacts|deck]
|
||||||
|
Enable specific Nextcloud app APIs. Can be
|
||||||
|
specified multiple times. If not specified,
|
||||||
|
all apps are enabled.
|
||||||
|
--help Show this message and exit.
|
||||||
|
```
|
||||||
|
|
||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
A pre-built Docker image is available: `ghcr.io/cbcoutinho/nextcloud-mcp-server`
|
A pre-built Docker image is available: `ghcr.io/cbcoutinho/nextcloud-mcp-server`
|
||||||
@@ -405,6 +109,40 @@ NEXTCLOUD_PASSWORD=your_nextcloud_app_password_or_login_password
|
|||||||
* `NEXTCLOUD_USERNAME`: Your Nextcloud username.
|
* `NEXTCLOUD_USERNAME`: Your Nextcloud username.
|
||||||
* `NEXTCLOUD_PASSWORD`: **Important:** It is highly recommended to use a dedicated Nextcloud App Password for security. You can generate one in your Nextcloud Security settings. Alternatively, you can use your regular login password, but this is less secure.
|
* `NEXTCLOUD_PASSWORD`: **Important:** It is highly recommended to use a dedicated Nextcloud App Password for security. You can generate one in your Nextcloud Security settings. Alternatively, you can use your regular login password, but this is less secure.
|
||||||
|
|
||||||
|
## Transport Types
|
||||||
|
|
||||||
|
The server supports two transport types for MCP communication:
|
||||||
|
|
||||||
|
### Streamable HTTP (Recommended)
|
||||||
|
The `streamable-http` transport is the recommended and modern transport type that provides improved streaming capabilities:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use streamable-http transport (recommended)
|
||||||
|
uv run python -m nextcloud_mcp_server.app --transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSE (Server-Sent Events) - Deprecated
|
||||||
|
> [!WARNING]
|
||||||
|
> ⚠️ **Deprecated**: SSE transport is deprecated and will be removed in a future version of the MCP spec. SSE will be supported for the foreseable future, but users are encouraged to switch to the new transport type. Please migrate to `streamable-http`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# SSE transport (deprecated - for backwards compatibility only)
|
||||||
|
uv run python -m nextcloud_mcp_server.app --transport sse
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Usage with Transports
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Using SSE transport (default - deprecated)
|
||||||
|
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
|
||||||
|
|
||||||
|
# Using streamable-http transport (recommended)
|
||||||
|
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest \
|
||||||
|
--transport streamable-http
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** When using MCP clients, ensure your client supports the transport type you've configured on the server. Most modern MCP clients support streamable-http.
|
||||||
|
|
||||||
## Running the Server
|
## Running the Server
|
||||||
|
|
||||||
### Locally
|
### Locally
|
||||||
@@ -416,8 +154,8 @@ Ensure your environment variables are loaded, then run the server. You have seve
|
|||||||
# Load environment variables from your .env file
|
# Load environment variables from your .env file
|
||||||
export $(grep -v '^#' .env | xargs)
|
export $(grep -v '^#' .env | xargs)
|
||||||
|
|
||||||
# Or run the app module directly with custom options
|
# Run the app module directly with custom options
|
||||||
uv run python -m nextcloud_mcp_server.app --host 0.0.0.0 --port 8080 --log-level info --reload
|
uv run python -m nextcloud_mcp_server.app --host 0.0.0.0 --port 8080 --log-level info
|
||||||
|
|
||||||
# Enable only specific Nextcloud app APIs
|
# Enable only specific Nextcloud app APIs
|
||||||
uv run python -m nextcloud_mcp_server.app --enable-app notes --enable-app calendar
|
uv run python -m nextcloud_mcp_server.app --enable-app notes --enable-app calendar
|
||||||
@@ -479,16 +217,15 @@ Mount your environment file when running the container:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run with all apps enabled (default)
|
# Run with all apps enabled (default)
|
||||||
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest \
|
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest
|
||||||
--host 0.0.0.0
|
|
||||||
|
|
||||||
# Run with only specific apps enabled
|
# Run with only specific apps enabled
|
||||||
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest \
|
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest \
|
||||||
--host 0.0.0.0 --enable-app notes --enable-app calendar
|
--enable-app notes --enable-app calendar
|
||||||
|
|
||||||
# Run with only WebDAV
|
# Run with only WebDAV
|
||||||
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest \
|
docker run -p 127.0.0.1:8000:8000 --env-file .env --rm ghcr.io/cbcoutinho/nextcloud-mcp-server:latest \
|
||||||
--host 0.0.0.0 --enable-app webdav
|
--enable-app webdav
|
||||||
```
|
```
|
||||||
|
|
||||||
This will start the server and expose it on port 8000 of your local machine.
|
This will start the server and expose it on port 8000 of your local machine.
|
||||||
@@ -511,6 +248,10 @@ You can then connect to and interact with the server's tools and resources throu
|
|||||||
|
|
||||||
Contributions are welcome! Please feel free to submit issues or pull requests on the [GitHub repository](https://github.com/cbcoutinho/nextcloud-mcp-server).
|
Contributions are welcome! Please feel free to submit issues or pull requests on the [GitHub repository](https://github.com/cbcoutinho/nextcloud-mcp-server).
|
||||||
|
|
||||||
|
## Star History
|
||||||
|
|
||||||
|
[](https://www.star-history.com/#cbcoutinho/nextcloud-mcp-server&Date)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project is licensed under the AGPL-3.0 License. See the [LICENSE](./LICENSE) file for details.
|
This project is licensed under the AGPL-3.0 License. See the [LICENSE](./LICENSE) file for details.
|
||||||
|
|||||||
+5
-5
@@ -3,7 +3,7 @@ services:
|
|||||||
# https://hub.docker.com/_/mariadb
|
# https://hub.docker.com/_/mariadb
|
||||||
db:
|
db:
|
||||||
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
|
# Note: Check the recommend version here: https://docs.nextcloud.com/server/latest/admin_manual/installation/system_requirements.html#server
|
||||||
image: mariadb:lts@sha256:ec5d50f32359ff020b93cce6834f9bf89147c34aea0e90c952ccf556c94a4fb8
|
image: mariadb:lts@sha256:851a6020c97b9eae7736b6fb275800601d64635222054d3a1b1b3c4abdfa117a
|
||||||
restart: always
|
restart: always
|
||||||
command: --transaction-isolation=READ-COMMITTED
|
command: --transaction-isolation=READ-COMMITTED
|
||||||
volumes:
|
volumes:
|
||||||
@@ -21,14 +21,14 @@ services:
|
|||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: nextcloud:31.0.8@sha256:c3329db9d0d0d79b1fe6433b54b81c28acaefecfe96a400be202b7da80f6b8ca
|
image: nextcloud:31.0.9@sha256:88fe398340a896eeebfe0a4ba847998ff2c8fbb3d72de354ac1f08bc7b44db18
|
||||||
#user: www-data:www-data
|
#user: www-data:www-data
|
||||||
restart: always
|
restart: always
|
||||||
#post_start:
|
#post_start:
|
||||||
#- command: chown -R www-data:www-data /var/www/html && while ! nc -z db 3306; do sleep 1; echo sleeping; done
|
#- command: chown -R www-data:www-data /var/www/html && while ! nc -z db 3306; do sleep 1; echo sleeping; done
|
||||||
#user: root
|
#user: root
|
||||||
ports:
|
ports:
|
||||||
- 8080:80
|
- 127.0.0.1:8080:80
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- db
|
- db
|
||||||
@@ -46,9 +46,9 @@ services:
|
|||||||
|
|
||||||
mcp:
|
mcp:
|
||||||
build: .
|
build: .
|
||||||
command: ["--host", "0.0.0.0"]
|
command: ["--transport", "streamable-http"]
|
||||||
ports:
|
ports:
|
||||||
- 8000:8000
|
- 127.0.0.1:8000:8000
|
||||||
environment:
|
environment:
|
||||||
- NEXTCLOUD_HOST=http://app:80
|
- NEXTCLOUD_HOST=http://app:80
|
||||||
- NEXTCLOUD_USERNAME=admin
|
- NEXTCLOUD_USERNAME=admin
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Calendar App
|
||||||
|
|
||||||
|
### Calendar Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `nc_calendar_list_calendars` | List all available calendars for the user |
|
||||||
|
| `nc_calendar_create_event` | Create a comprehensive calendar event with full feature support (recurring, reminders, attendees, etc.) |
|
||||||
|
| `nc_calendar_list_events` | **Enhanced:** List events with advanced filtering (min attendees, duration, categories, status, search across all calendars) |
|
||||||
|
| `nc_calendar_get_event` | Get detailed information about a specific event |
|
||||||
|
| `nc_calendar_update_event` | Update any aspect of an existing event |
|
||||||
|
| `nc_calendar_delete_event` | Delete a calendar event |
|
||||||
|
| `nc_calendar_create_meeting` | Quick meeting creation with smart defaults |
|
||||||
|
| `nc_calendar_get_upcoming_events` | Get upcoming events in the next N days |
|
||||||
|
| `nc_calendar_find_availability` | **New:** Intelligent availability finder - find free time slots for meetings with attendee conflict detection |
|
||||||
|
| `nc_calendar_bulk_operations` | **New:** Bulk update, delete, or move events matching filter criteria |
|
||||||
|
| `nc_calendar_manage_calendar` | **New:** Create, delete, and manage calendar properties |
|
||||||
|
|
||||||
|
### Calendar Integration
|
||||||
|
|
||||||
|
The server provides comprehensive calendar integration through CalDAV, enabling you to:
|
||||||
|
|
||||||
|
- List all available calendars
|
||||||
|
- Create, read, update, and delete calendar events
|
||||||
|
- Handle recurring events with RRULE support
|
||||||
|
- Manage event reminders and notifications
|
||||||
|
- Support all-day and timed events
|
||||||
|
- Handle attendees and meeting invitations
|
||||||
|
- Organize events with categories and priorities
|
||||||
|
|
||||||
|
**Usage Examples:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# List available calendars
|
||||||
|
calendars = await nc_calendar_list_calendars()
|
||||||
|
|
||||||
|
# Create a simple event
|
||||||
|
await nc_calendar_create_event(
|
||||||
|
calendar_name="personal",
|
||||||
|
title="Team Meeting",
|
||||||
|
start_datetime="2025-07-28T14:00:00",
|
||||||
|
end_datetime="2025-07-28T15:00:00",
|
||||||
|
description="Weekly team sync",
|
||||||
|
location="Conference Room A"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a recurring weekly meeting
|
||||||
|
await nc_calendar_create_event(
|
||||||
|
calendar_name="work",
|
||||||
|
title="Weekly Standup",
|
||||||
|
start_datetime="2025-07-28T09:00:00",
|
||||||
|
end_datetime="2025-07-28T09:30:00",
|
||||||
|
recurring=True,
|
||||||
|
recurrence_rule="FREQ=WEEKLY;BYDAY=MO"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Quick meeting creation
|
||||||
|
await nc_calendar_create_meeting(
|
||||||
|
title="Client Call",
|
||||||
|
date="2025-07-28",
|
||||||
|
time="15:00",
|
||||||
|
duration_minutes=60,
|
||||||
|
attendees="client@example.com,colleague@company.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get upcoming events
|
||||||
|
events = await nc_calendar_get_upcoming_events(days_ahead=7)
|
||||||
|
|
||||||
|
# Advanced search - find all meetings with 5+ attendees lasting 2+ hours
|
||||||
|
long_meetings = await nc_calendar_list_events(
|
||||||
|
calendar_name="", # Search all calendars
|
||||||
|
search_all_calendars=True,
|
||||||
|
start_date="2025-07-01",
|
||||||
|
end_date="2025-07-31",
|
||||||
|
min_attendees=5,
|
||||||
|
min_duration_minutes=120,
|
||||||
|
title_contains="meeting"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Find availability for a 1-hour meeting with specific attendees
|
||||||
|
availability = await nc_calendar_find_availability(
|
||||||
|
duration_minutes=60,
|
||||||
|
attendees="sarah@company.com,mike@company.com",
|
||||||
|
date_range_start="2025-07-28",
|
||||||
|
date_range_end="2025-08-04",
|
||||||
|
business_hours_only=True,
|
||||||
|
exclude_weekends=True,
|
||||||
|
preferred_times="09:00-12:00,14:00-17:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Bulk update all team meetings to new location
|
||||||
|
bulk_result = await nc_calendar_bulk_operations(
|
||||||
|
operation="update",
|
||||||
|
title_contains="team meeting",
|
||||||
|
start_date="2025-08-01",
|
||||||
|
end_date="2025-08-31",
|
||||||
|
new_location="Conference Room B",
|
||||||
|
new_reminder_minutes=15
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a new project calendar
|
||||||
|
new_calendar = await nc_calendar_manage_calendar(
|
||||||
|
action="create",
|
||||||
|
calendar_name="project-alpha",
|
||||||
|
display_name="Project Alpha Calendar",
|
||||||
|
description="Calendar for Project Alpha team",
|
||||||
|
color="#FF5722"
|
||||||
|
)
|
||||||
|
```
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Contacts App
|
||||||
|
|
||||||
|
### Contacts Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `nc_contacts_list_addressbooks` | List all available addressbooks for the user |
|
||||||
|
| `nc_contacts_list_contacts` | List all contacts in a specific addressbook |
|
||||||
|
| `nc_contacts_create_addressbook` | Create a new addressbook |
|
||||||
|
| `nc_contacts_delete_addressbook` | Delete an addressbook |
|
||||||
|
| `nc_contacts_create_contact` | Create a new contact in an addressbook |
|
||||||
|
| `nc_contacts_delete_contact` | Delete a contact from an addressbook |
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
# Deck App
|
||||||
|
|
||||||
|
### Deck Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `deck_create_board` | Create a new Deck board with title and color |
|
||||||
|
| `deck_create_stack` | Create a new stack in a board |
|
||||||
|
| `deck_update_stack` | Update stack title and order |
|
||||||
|
| `deck_delete_stack` | Delete a stack and all its cards |
|
||||||
|
| `deck_create_card` | Create a new card in a stack with full options (title, description, due date, etc.) |
|
||||||
|
| `deck_update_card` | Update any aspect of a card (title, description, owner, order, etc.) |
|
||||||
|
| `deck_delete_card` | Delete a card |
|
||||||
|
| `deck_archive_card` | Archive a card |
|
||||||
|
| `deck_unarchive_card` | Unarchive a card |
|
||||||
|
| `deck_reorder_card` | Move/reorder cards within or between stacks |
|
||||||
|
| `deck_create_label` | Create a new label in a board |
|
||||||
|
| `deck_update_label` | Update label title and color |
|
||||||
|
| `deck_delete_label` | Delete a label |
|
||||||
|
| `deck_assign_label_to_card` | Assign a label to a card |
|
||||||
|
| `deck_remove_label_from_card` | Remove a label from a card |
|
||||||
|
| `deck_assign_user_to_card` | Assign a user to a card |
|
||||||
|
| `deck_unassign_user_from_card` | Remove a user assignment from a card |
|
||||||
|
|
||||||
|
### Deck Resources
|
||||||
|
| Resource | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `nc://Deck/boards` | List all deck boards |
|
||||||
|
| `nc://Deck/boards/{board_id}` | Get details of a specific board |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks` | List all stacks in a board |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks/{stack_id}` | Get details of a specific stack |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks/{stack_id}/cards` | List all cards in a stack |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks/{stack_id}/cards/{card_id}` | Get details of a specific card |
|
||||||
|
| `nc://Deck/boards/{board_id}/labels` | List all labels in a board |
|
||||||
|
| `nc://Deck/boards/{board_id}/labels/{label_id}` | Get details of a specific label |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Deck Project Management
|
||||||
|
|
||||||
|
The server provides complete Nextcloud Deck integration, enabling you to manage projects, tasks, and workflows:
|
||||||
|
|
||||||
|
- Create and manage boards, stacks, and cards
|
||||||
|
- Organize tasks with labels and user assignments
|
||||||
|
- Archive/unarchive cards and reorder within or between stacks
|
||||||
|
- Full CRUD operations on all Deck entities
|
||||||
|
- Browse project structure through hierarchical resources
|
||||||
|
|
||||||
|
**Usage Examples:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create a new project board
|
||||||
|
await deck_create_board(title="Website Redesign", color="1976D2")
|
||||||
|
|
||||||
|
# Create workflow stacks
|
||||||
|
await deck_create_stack(board_id=1, title="To Do", order=1)
|
||||||
|
await deck_create_stack(board_id=1, title="In Progress", order=2)
|
||||||
|
await deck_create_stack(board_id=1, title="Done", order=3)
|
||||||
|
|
||||||
|
# Create task cards with details
|
||||||
|
await deck_create_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=1,
|
||||||
|
title="Design new homepage",
|
||||||
|
description="Create mockups for the new homepage layout",
|
||||||
|
type="plain",
|
||||||
|
order=1,
|
||||||
|
duedate="2025-08-15T17:00:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and assign labels for organization
|
||||||
|
await deck_create_label(board_id=1, title="High Priority", color="F44336")
|
||||||
|
await deck_create_label(board_id=1, title="UI/UX", color="9C27B0")
|
||||||
|
|
||||||
|
# Assign labels and users to cards
|
||||||
|
await deck_assign_label_to_card(board_id=1, stack_id=1, card_id=1, label_id=1)
|
||||||
|
await deck_assign_user_to_card(board_id=1, stack_id=1, card_id=1, user_id="designer")
|
||||||
|
|
||||||
|
# Move cards through workflow
|
||||||
|
await deck_reorder_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=1, # From "To Do"
|
||||||
|
card_id=1,
|
||||||
|
order=1,
|
||||||
|
target_stack_id=2 # To "In Progress"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update task progress
|
||||||
|
await deck_update_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=2,
|
||||||
|
card_id=1,
|
||||||
|
description="Homepage mockups completed, starting development",
|
||||||
|
order=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Complete tasks
|
||||||
|
await deck_reorder_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=2, # From "In Progress"
|
||||||
|
card_id=1,
|
||||||
|
order=1,
|
||||||
|
target_stack_id=3 # To "Done"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Archive completed cards
|
||||||
|
await deck_archive_card(board_id=1, stack_id=3, card_id=1)
|
||||||
|
```
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Notes App
|
||||||
|
|
||||||
|
### Notes Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `nc_notes_create_note` | Create a new note with title, content, and category |
|
||||||
|
| `nc_notes_update_note` | Update an existing note by ID |
|
||||||
|
| `nc_notes_append_content` | Append content to an existing note with a clear separator |
|
||||||
|
| `nc_notes_delete_note` | Delete a note by ID |
|
||||||
|
| `nc_notes_search_notes` | Search notes by title or content |
|
||||||
|
|
||||||
|
### Note Attachments
|
||||||
|
|
||||||
|
This server supports adding and retrieving note attachments via WebDAV. Please note the following behavior regarding attachments:
|
||||||
|
|
||||||
|
* When a note is deleted, its attachments remain in the system. This matches the behavior of the official Nextcloud Notes app.
|
||||||
|
* Orphaned attachments (attachments whose parent notes have been deleted) may accumulate over time.
|
||||||
|
* WebDAV permissions must be properly configured for attachment operations to work correctly.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Tables App
|
||||||
|
|
||||||
|
### Tables Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `nc_tables_list_tables` | List all tables available to the user |
|
||||||
|
| `nc_tables_get_schema` | Get the schema/structure of a specific table including columns and views |
|
||||||
|
| `nc_tables_read_table` | Read rows from a table with optional pagination |
|
||||||
|
| `nc_tables_insert_row` | Insert a new row into a table |
|
||||||
|
| `nc_tables_update_row` | Update an existing row in a table |
|
||||||
|
| `nc_tables_delete_row` | Delete a row from a table |
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# WebDAV support
|
||||||
|
|
||||||
|
### WebDAV File System Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `nc_webdav_list_directory` | List files and directories in any NextCloud path |
|
||||||
|
| `nc_webdav_read_file` | Read file content (text files decoded, binary as base64) |
|
||||||
|
| `nc_webdav_write_file` | Create or update files in NextCloud |
|
||||||
|
| `nc_webdav_create_directory` | Create new directories |
|
||||||
|
| `nc_webdav_delete_resource` | Delete files or directories |
|
||||||
|
| `nc_webdav_move_resource` | Move or rename files and directories |
|
||||||
|
| `nc_webdav_copy_resource` | Copy files and directories |
|
||||||
|
|
||||||
|
### WebDAV File System Access
|
||||||
|
|
||||||
|
The server provides complete file system access to your NextCloud instance, enabling you to:
|
||||||
|
|
||||||
|
- Browse any directory structure
|
||||||
|
- Read and write files of any type
|
||||||
|
- Create and delete directories
|
||||||
|
- Manage your NextCloud files directly through LLM interactions
|
||||||
|
|
||||||
|
**Usage Examples:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# List files in root directory
|
||||||
|
await nc_webdav_list_directory("")
|
||||||
|
|
||||||
|
# Browse a specific folder
|
||||||
|
await nc_webdav_list_directory("Documents/Projects")
|
||||||
|
|
||||||
|
# Read a text file
|
||||||
|
content = await nc_webdav_read_file("Documents/readme.txt")
|
||||||
|
|
||||||
|
# Create a new directory
|
||||||
|
await nc_webdav_create_directory("NewProject/docs")
|
||||||
|
|
||||||
|
# Write content to a file
|
||||||
|
await nc_webdav_write_file("NewProject/docs/notes.md", "# My Notes\n\nContent here...")
|
||||||
|
|
||||||
|
# Delete a file or directory
|
||||||
|
await nc_webdav_delete_resource("old_file.txt")
|
||||||
|
|
||||||
|
# Move or rename a file
|
||||||
|
await nc_webdav_move_resource("document.txt", "new_name.txt")
|
||||||
|
|
||||||
|
# Move a file to another directory
|
||||||
|
await nc_webdav_move_resource("document.txt", "Archive/document.txt")
|
||||||
|
|
||||||
|
# Move a directory
|
||||||
|
await nc_webdav_move_resource("Projects/OldProject", "Projects/NewProject")
|
||||||
|
|
||||||
|
# Copy a file
|
||||||
|
await nc_webdav_copy_resource("document.txt", "document_copy.txt")
|
||||||
|
|
||||||
|
# Copy a file to another directory
|
||||||
|
await nc_webdav_copy_resource("document.txt", "Backup/document.txt")
|
||||||
|
|
||||||
|
# Copy a directory
|
||||||
|
await nc_webdav_copy_resource("Projects/ProjectA", "Projects/ProjectA_Backup")
|
||||||
|
```
|
||||||
@@ -2,7 +2,7 @@ import click
|
|||||||
import logging
|
import logging
|
||||||
import uvicorn
|
import uvicorn
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager, AsyncExitStack
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from starlette.applications import Starlette
|
from starlette.applications import Starlette
|
||||||
@@ -83,24 +83,41 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
|
|||||||
f"Unknown app: {app_name}. Available apps: {list(available_apps.keys())}"
|
f"Unknown app: {app_name}. Available apps: {list(available_apps.keys())}"
|
||||||
)
|
)
|
||||||
|
|
||||||
mcp_app = mcp.sse_app() if transport == "sse" else mcp.streamable_http_app()
|
if transport == "sse":
|
||||||
|
mcp_app = mcp.sse_app()
|
||||||
|
lifespan = None
|
||||||
|
elif transport in ("http", "streamable-http"):
|
||||||
|
mcp_app = mcp.streamable_http_app()
|
||||||
|
|
||||||
app = Starlette(routes=[Mount("/", app=mcp_app)])
|
@asynccontextmanager
|
||||||
|
async def lifespan(app: Starlette):
|
||||||
|
async with AsyncExitStack() as stack:
|
||||||
|
await stack.enter_async_context(mcp.session_manager.run())
|
||||||
|
yield
|
||||||
|
|
||||||
|
app = Starlette(routes=[Mount("/", app=mcp_app)], lifespan=lifespan)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option("--host", "-h", default="127.0.0.1")
|
@click.option("--host", "-h", default="127.0.0.1", show_default=True)
|
||||||
@click.option("--port", "-p", type=int, default=8000)
|
@click.option("--port", "-p", type=int, default=8000, show_default=True)
|
||||||
@click.option("--workers", "-w", type=int, default=None)
|
@click.option("--workers", "-w", type=int, default=None)
|
||||||
@click.option("--reload", "-r", is_flag=True)
|
@click.option("--reload", "-r", is_flag=True)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--log-level",
|
"--log-level",
|
||||||
|
"-l",
|
||||||
|
default="info",
|
||||||
|
show_default=True,
|
||||||
type=click.Choice(["critical", "error", "warning", "info", "debug", "trace"]),
|
type=click.Choice(["critical", "error", "warning", "info", "debug", "trace"]),
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--transport", "-t", default="sse", type=click.Choice(["sse", "streamable-http"])
|
"--transport",
|
||||||
|
"-t",
|
||||||
|
default="sse",
|
||||||
|
show_default=True,
|
||||||
|
type=click.Choice(["sse", "streamable-http", "http"]),
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
"--enable-app",
|
"--enable-app",
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ from mcp.server.fastmcp import Context, FastMCP
|
|||||||
|
|
||||||
from nextcloud_mcp_server.client import NextcloudClient
|
from nextcloud_mcp_server.client import NextcloudClient
|
||||||
from nextcloud_mcp_server.models.deck import (
|
from nextcloud_mcp_server.models.deck import (
|
||||||
|
DeckBoard,
|
||||||
|
DeckStack,
|
||||||
|
DeckCard,
|
||||||
|
DeckLabel,
|
||||||
CreateBoardResponse,
|
CreateBoardResponse,
|
||||||
CreateStackResponse,
|
CreateStackResponse,
|
||||||
StackOperationResponse,
|
StackOperationResponse,
|
||||||
@@ -25,6 +29,7 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_boards_resource():
|
async def deck_boards_resource():
|
||||||
"""List all Nextcloud Deck boards"""
|
"""List all Nextcloud Deck boards"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning("This message is deprecated, use the deck_get_board instead")
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
boards = await client.deck.get_boards()
|
boards = await client.deck.get_boards()
|
||||||
return [board.model_dump() for board in boards]
|
return [board.model_dump() for board in boards]
|
||||||
@@ -33,6 +38,9 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_board_resource(board_id: int):
|
async def deck_board_resource(board_id: int):
|
||||||
"""Get details of a specific Nextcloud Deck board"""
|
"""Get details of a specific Nextcloud Deck board"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_board tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
board = await client.deck.get_board(board_id)
|
board = await client.deck.get_board(board_id)
|
||||||
return board.model_dump()
|
return board.model_dump()
|
||||||
@@ -41,6 +49,9 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_stacks_resource(board_id: int):
|
async def deck_stacks_resource(board_id: int):
|
||||||
"""List all stacks in a Nextcloud Deck board"""
|
"""List all stacks in a Nextcloud Deck board"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_stacks tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
stacks = await client.deck.get_stacks(board_id)
|
stacks = await client.deck.get_stacks(board_id)
|
||||||
return [stack.model_dump() for stack in stacks]
|
return [stack.model_dump() for stack in stacks]
|
||||||
@@ -49,6 +60,9 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_stack_resource(board_id: int, stack_id: int):
|
async def deck_stack_resource(board_id: int, stack_id: int):
|
||||||
"""Get details of a specific Nextcloud Deck stack"""
|
"""Get details of a specific Nextcloud Deck stack"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_stack tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
stack = await client.deck.get_stack(board_id, stack_id)
|
stack = await client.deck.get_stack(board_id, stack_id)
|
||||||
return stack.model_dump()
|
return stack.model_dump()
|
||||||
@@ -57,6 +71,9 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_cards_resource(board_id: int, stack_id: int):
|
async def deck_cards_resource(board_id: int, stack_id: int):
|
||||||
"""List all cards in a Nextcloud Deck stack"""
|
"""List all cards in a Nextcloud Deck stack"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_cards tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
stack = await client.deck.get_stack(board_id, stack_id)
|
stack = await client.deck.get_stack(board_id, stack_id)
|
||||||
if stack.cards:
|
if stack.cards:
|
||||||
@@ -67,6 +84,9 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_card_resource(board_id: int, stack_id: int, card_id: int):
|
async def deck_card_resource(board_id: int, stack_id: int, card_id: int):
|
||||||
"""Get details of a specific Nextcloud Deck card"""
|
"""Get details of a specific Nextcloud Deck card"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_card tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
card = await client.deck.get_card(board_id, stack_id, card_id)
|
card = await client.deck.get_card(board_id, stack_id, card_id)
|
||||||
return card.model_dump()
|
return card.model_dump()
|
||||||
@@ -75,6 +95,9 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_labels_resource(board_id: int):
|
async def deck_labels_resource(board_id: int):
|
||||||
"""List all labels in a Nextcloud Deck board"""
|
"""List all labels in a Nextcloud Deck board"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_labels tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
board = await client.deck.get_board(board_id)
|
board = await client.deck.get_board(board_id)
|
||||||
return [label.model_dump() for label in board.labels]
|
return [label.model_dump() for label in board.labels]
|
||||||
@@ -83,11 +106,78 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
async def deck_label_resource(board_id: int, label_id: int):
|
async def deck_label_resource(board_id: int, label_id: int):
|
||||||
"""Get details of a specific Nextcloud Deck label"""
|
"""Get details of a specific Nextcloud Deck label"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
|
await ctx.warning(
|
||||||
|
"This resource is deprecated, use the deck_get_label tool instead"
|
||||||
|
)
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
label = await client.deck.get_label(board_id, label_id)
|
label = await client.deck.get_label(board_id, label_id)
|
||||||
return label.model_dump()
|
return label.model_dump()
|
||||||
|
|
||||||
# Tools
|
# Read Tools (converted from resources)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_boards(ctx: Context) -> list[DeckBoard]:
|
||||||
|
"""Get all Nextcloud Deck boards"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
boards = await client.deck.get_boards()
|
||||||
|
return boards
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_board(ctx: Context, board_id: int) -> DeckBoard:
|
||||||
|
"""Get details of a specific Nextcloud Deck board"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
board = await client.deck.get_board(board_id)
|
||||||
|
return board
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_stacks(ctx: Context, board_id: int) -> list[DeckStack]:
|
||||||
|
"""Get all stacks in a Nextcloud Deck board"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stacks = await client.deck.get_stacks(board_id)
|
||||||
|
return stacks
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_stack(ctx: Context, board_id: int, stack_id: int) -> DeckStack:
|
||||||
|
"""Get details of a specific Nextcloud Deck stack"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stack = await client.deck.get_stack(board_id, stack_id)
|
||||||
|
return stack
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_cards(
|
||||||
|
ctx: Context, board_id: int, stack_id: int
|
||||||
|
) -> list[DeckCard]:
|
||||||
|
"""Get all cards in a Nextcloud Deck stack"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stack = await client.deck.get_stack(board_id, stack_id)
|
||||||
|
if stack.cards:
|
||||||
|
return stack.cards
|
||||||
|
return []
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int
|
||||||
|
) -> DeckCard:
|
||||||
|
"""Get details of a specific Nextcloud Deck card"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
card = await client.deck.get_card(board_id, stack_id, card_id)
|
||||||
|
return card
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_labels(ctx: Context, board_id: int) -> list[DeckLabel]:
|
||||||
|
"""Get all labels in a Nextcloud Deck board"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
board = await client.deck.get_board(board_id)
|
||||||
|
return board.labels
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_get_label(ctx: Context, board_id: int, label_id: int) -> DeckLabel:
|
||||||
|
"""Get details of a specific Nextcloud Deck label"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
label = await client.deck.get_label(board_id, label_id)
|
||||||
|
return label
|
||||||
|
|
||||||
|
# Create/Update/Delete Tools
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def deck_create_board(
|
async def deck_create_board(
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
return NotesSettings(**settings_data)
|
return NotesSettings(**settings_data)
|
||||||
|
|
||||||
@mcp.resource("nc://Notes/{note_id}/attachments/{attachment_filename}")
|
@mcp.resource("nc://Notes/{note_id}/attachments/{attachment_filename}")
|
||||||
async def nc_notes_get_attachment(note_id: int, attachment_filename: str):
|
async def nc_notes_get_attachment_resource(note_id: int, attachment_filename: str):
|
||||||
"""Get a specific attachment from a note"""
|
"""Get a specific attachment from a note"""
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
@@ -53,7 +53,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
}
|
}
|
||||||
|
|
||||||
@mcp.resource("nc://Notes/{note_id}")
|
@mcp.resource("nc://Notes/{note_id}")
|
||||||
async def nc_get_note(note_id: int):
|
async def nc_get_note_resource(note_id: int):
|
||||||
"""Get user note using note id"""
|
"""Get user note using note id"""
|
||||||
|
|
||||||
ctx: Context = mcp.get_context()
|
ctx: Context = mcp.get_context()
|
||||||
@@ -129,7 +129,7 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
"""Update an existing note's title, content, or category.
|
"""Update an existing note's title, content, or category.
|
||||||
|
|
||||||
REQUIRED: etag parameter must be provided to prevent overwriting concurrent changes.
|
REQUIRED: etag parameter must be provided to prevent overwriting concurrent changes.
|
||||||
Get the current ETag by first retrieving the note using nc://Notes/{note_id} resource.
|
Get the current ETag by first retrieving the note using nc_notes_get_note tool.
|
||||||
If the note has been modified by someone else since you retrieved it,
|
If the note has been modified by someone else since you retrieved it,
|
||||||
the update will fail with a 412 error."""
|
the update will fail with a 412 error."""
|
||||||
logger.info("Updating note %s", note_id)
|
logger.info("Updating note %s", note_id)
|
||||||
@@ -258,6 +258,66 @@ def configure_notes_tools(mcp: FastMCP):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def nc_notes_get_note(note_id: int, ctx: Context) -> Note:
|
||||||
|
"""Get a specific note by its ID"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
try:
|
||||||
|
note_data = await client.notes.get_note(note_id)
|
||||||
|
return Note(**note_data)
|
||||||
|
except HTTPStatusError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
raise McpError(ErrorData(code=-1, message=f"Note {note_id} not found"))
|
||||||
|
elif e.response.status_code == 403:
|
||||||
|
raise McpError(
|
||||||
|
ErrorData(code=-1, message=f"Access denied to note {note_id}")
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise McpError(
|
||||||
|
ErrorData(
|
||||||
|
code=-1,
|
||||||
|
message=f"Failed to retrieve note {note_id}: {e.response.reason_phrase}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def nc_notes_get_attachment(
|
||||||
|
note_id: int, attachment_filename: str, ctx: Context
|
||||||
|
) -> dict[str, str]:
|
||||||
|
"""Get a specific attachment from a note"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
try:
|
||||||
|
content, mime_type = await client.webdav.get_note_attachment(
|
||||||
|
note_id=note_id, filename=attachment_filename
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"uri": f"nc://Notes/{note_id}/attachments/{attachment_filename}",
|
||||||
|
"mimeType": mime_type,
|
||||||
|
"data": content,
|
||||||
|
}
|
||||||
|
except HTTPStatusError as e:
|
||||||
|
if e.response.status_code == 404:
|
||||||
|
raise McpError(
|
||||||
|
ErrorData(
|
||||||
|
code=-1,
|
||||||
|
message=f"Attachment {attachment_filename} not found for note {note_id}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
elif e.response.status_code == 403:
|
||||||
|
raise McpError(
|
||||||
|
ErrorData(
|
||||||
|
code=-1,
|
||||||
|
message=f"Access denied to attachment {attachment_filename} for note {note_id}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise McpError(
|
||||||
|
ErrorData(
|
||||||
|
code=-1,
|
||||||
|
message=f"Failed to retrieve attachment: {e.response.reason_phrase}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
|
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
|
||||||
"""Delete a note permanently"""
|
"""Delete a note permanently"""
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.10.0"
|
version = "0.12.4"
|
||||||
description = ""
|
description = ""
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Chris Coutinho",email = "chris@coutinho.io"}
|
{name = "Chris Coutinho",email = "chris@coutinho.io"}
|
||||||
@@ -8,7 +8,7 @@ authors = [
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"mcp[cli] (>=1.10,<1.11)",
|
"mcp[cli] (>=1.15,<1.16)",
|
||||||
"httpx (>=0.28.1,<0.29.0)",
|
"httpx (>=0.28.1,<0.29.0)",
|
||||||
"pillow (>=11.2.1,<12.0.0)",
|
"pillow (>=11.2.1,<12.0.0)",
|
||||||
"icalendar (>=6.0.0,<7.0.0)",
|
"icalendar (>=6.0.0,<7.0.0)",
|
||||||
|
|||||||
+10
-10
@@ -6,7 +6,7 @@ from typing import Any, AsyncGenerator
|
|||||||
import pytest
|
import pytest
|
||||||
from httpx import HTTPStatusError
|
from httpx import HTTPStatusError
|
||||||
from mcp import ClientSession
|
from mcp import ClientSession
|
||||||
from mcp.client.sse import sse_client
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
|
|
||||||
from nextcloud_mcp_server.client import NextcloudClient
|
from nextcloud_mcp_server.client import NextcloudClient
|
||||||
|
|
||||||
@@ -39,18 +39,18 @@ async def nc_client() -> AsyncGenerator[NextcloudClient, Any]:
|
|||||||
await client.close()
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope="session")
|
||||||
async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
|
async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
|
||||||
"""
|
"""
|
||||||
Fixture to create an MCP client session for integration tests.
|
Fixture to create an MCP client session for integration tests using streamable-http.
|
||||||
"""
|
"""
|
||||||
logger.info("Creating SSE client")
|
logger.info("Creating Streamable HTTP client")
|
||||||
sse_context = sse_client(url="http://127.0.0.1:8000/sse")
|
streamable_context = streamablehttp_client("http://127.0.0.1:8000/mcp")
|
||||||
session_context = None
|
session_context = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
read, write = await sse_context.__aenter__()
|
read_stream, write_stream, _ = await streamable_context.__aenter__()
|
||||||
session_context = ClientSession(read, write)
|
session_context = ClientSession(read_stream, write_stream)
|
||||||
session = await session_context.__aenter__()
|
session = await session_context.__aenter__()
|
||||||
await session.initialize()
|
await session.initialize()
|
||||||
logger.info("MCP client session initialized successfully")
|
logger.info("MCP client session initialized successfully")
|
||||||
@@ -71,14 +71,14 @@ async def nc_mcp_client() -> AsyncGenerator[ClientSession, Any]:
|
|||||||
logger.warning(f"Error closing session: {e}")
|
logger.warning(f"Error closing session: {e}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await sse_context.__aexit__(None, None, None)
|
await streamable_context.__aexit__(None, None, None)
|
||||||
except RuntimeError as e:
|
except RuntimeError as e:
|
||||||
if "cancel scope" in str(e):
|
if "cancel scope" in str(e):
|
||||||
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
|
logger.debug(f"Ignoring cancel scope teardown issue: {e}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"Error closing SSE client: {e}")
|
logger.warning(f"Error closing streamable HTTP client: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"Error closing SSE client: {e}")
|
logger.warning(f"Error closing streamable HTTP client: {e}")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
from mcp import ClientSession
|
from mcp import ClientSession
|
||||||
from mcp.shared.exceptions import McpError
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -10,11 +9,15 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
async def test_missing_note_resource_error(nc_mcp_client: ClientSession):
|
async def test_missing_note_tool_error(nc_mcp_client: ClientSession):
|
||||||
"""Test that accessing a non-existent note resource returns proper error."""
|
"""Test that accessing a non-existent note via tool returns proper error."""
|
||||||
# Try to get a non-existent note via resource - should raise McpError with improved message
|
# Try to get a non-existent note via tool - should return error response
|
||||||
with pytest.raises(McpError, match=r"Note 999999 not found"):
|
response = await nc_mcp_client.call_tool("nc_notes_get_note", {"note_id": 999999})
|
||||||
await nc_mcp_client.read_resource("nc://Notes/999999")
|
|
||||||
|
# Should return error response (not raise exception) for tools
|
||||||
|
assert response is not None
|
||||||
|
assert response.isError is True
|
||||||
|
assert "Note 999999 not found" in response.content[0].text
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.integration
|
@pytest.mark.integration
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
|
|||||||
template_uris.append(template.uriTemplate)
|
template_uris.append(template.uriTemplate)
|
||||||
|
|
||||||
# Verify expected resource templates
|
# Verify expected resource templates
|
||||||
expected_templates = ["nc://Notes/{note_id}/attachments/{attachment_filename}"]
|
# Note: Notes attachments are now handled via tools, not resource templates
|
||||||
|
expected_templates = []
|
||||||
|
|
||||||
for expected_template in expected_templates:
|
for expected_template in expected_templates:
|
||||||
assert expected_template in template_uris, (
|
assert expected_template in template_uris, (
|
||||||
@@ -140,9 +141,11 @@ async def test_mcp_notes_crud_workflow(
|
|||||||
|
|
||||||
# 3. Read note via MCP
|
# 3. Read note via MCP
|
||||||
logger.info(f"Reading note via MCP: {note_id}")
|
logger.info(f"Reading note via MCP: {note_id}")
|
||||||
read_result = await nc_mcp_client.read_resource(f"nc://Notes/{note_id}")
|
read_result = await nc_mcp_client.call_tool(
|
||||||
assert len(read_result.contents) == 1, "Expected exactly one content item"
|
"nc_notes_get_note", {"note_id": note_id}
|
||||||
read_note_data = json.loads(read_result.contents[0].text)
|
)
|
||||||
|
read_note_data = read_result.content[0].text
|
||||||
|
read_note_data = json.loads(read_note_data)
|
||||||
|
|
||||||
assert read_note_data["title"] == test_title
|
assert read_note_data["title"] == test_title
|
||||||
assert read_note_data["content"] == test_content
|
assert read_note_data["content"] == test_content
|
||||||
|
|||||||
Reference in New Issue
Block a user