Compare commits

..

37 Commits

Author SHA1 Message Date
Chris Coutinho 7e3c2c9774 chore: Update README.md 2025-09-29 18:20:56 +02:00
Chris Coutinho 752c22147c Merge pull request #170 from cbcoutinho/renovate/nextcloud-31.0.9
chore(deps): update nextcloud:31.0.9 docker digest to 88fe398
2025-09-29 09:13:24 +02:00
Chris Coutinho 4c07ca9f0a Merge pull request #171 from cbcoutinho/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2025-09-29 06:51:10 +02:00
renovate-bot-cbcoutinho[bot] 55945c6c0f chore(deps): lock file maintenance 2025-09-29 04:12:15 +00:00
renovate-bot-cbcoutinho[bot] 3f8312e6f3 chore(deps): update nextcloud:31.0.9 docker digest to 88fe398 2025-09-28 22:05:34 +00:00
Chris Coutinho c39b69d08c Merge pull request #169 from cbcoutinho/renovate/nextcloud-31.0.9
chore(deps): update nextcloud:31.0.9 docker digest to 875511f
2025-09-27 13:27:12 +02:00
renovate-bot-cbcoutinho[bot] 290ad2edc2 chore(deps): update nextcloud:31.0.9 docker digest to 875511f 2025-09-27 10:05:24 +00:00
github-actions[bot] 144c08c339 bump: version 0.12.3 → 0.12.4 2025-09-25 16:17:59 +00:00
Chris Coutinho b461af8aa1 Merge pull request #156 from cbcoutinho/renovate/mcp-1.x
fix(deps): update dependency mcp to >=1.15,<1.16
2025-09-25 18:17:31 +02:00
renovate-bot-cbcoutinho[bot] 4bdf67b042 fix(deps): update dependency mcp to >=1.15,<1.16 2025-09-25 16:07:30 +00:00
github-actions[bot] 93b109e5b9 bump: version 0.12.2 → 0.12.3 2025-09-23 22:22:36 +00:00
Chris Coutinho 0c5ebd5d84 Merge pull request #168 from cbcoutinho/feature/tools
Add tools for all resources to enable tool-only workflows
2025-09-24 00:22:11 +02:00
Chris Coutinho 79e6250377 update deprecated log warnings 2025-09-24 00:17:57 +02:00
Chris Coutinho a5ec712b88 Merge pull request #167 from cbcoutinho/renovate/ghcr.io-astral-sh-uv-0.x
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.22
2025-09-24 00:15:08 +02:00
Chris Coutinho cc9650b077 refactor: Add tools for all resources to enable tool-only workflows 2025-09-24 00:13:24 +02:00
renovate-bot-cbcoutinho[bot] 1a37a6c1fe chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.22 2025-09-23 22:07:49 +00:00
Chris Coutinho 4572287870 Merge pull request #165 from cbcoutinho/renovate/ghcr.io-astral-sh-uv-0.x
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.20
2025-09-23 12:35:20 +02:00
renovate-bot-cbcoutinho[bot] 67617d7fcc chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.20 2025-09-23 04:07:43 +00:00
github-actions[bot] 22811f29f6 bump: version 0.12.1 → 0.12.2 2025-09-20 20:34:35 +00:00
Chris Coutinho 71da620099 refactor: Add http to --transport option 2025-09-20 22:23:13 +02:00
Chris Coutinho de7c848aa6 Merge pull request #164 from cbcoutinho/renovate/ghcr.io-astral-sh-uv-0.x
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.19
2025-09-20 11:44:35 +02:00
renovate-bot-cbcoutinho[bot] 8d4303a624 chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.19 2025-09-19 22:07:37 +00:00
Chris Coutinho 4c7880a4e5 Merge pull request #163 from cbcoutinho/renovate/ghcr.io-astral-sh-uv-0.x
chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.18
2025-09-18 11:49:09 +02:00
renovate-bot-cbcoutinho[bot] 0a307b87ae chore(deps): update ghcr.io/astral-sh/uv docker tag to v0.8.18 2025-09-17 22:06:28 +00:00
Chris Coutinho 48eced80fb Merge pull request #162 from cbcoutinho/renovate/nextcloud-31.0.9
chore(deps): update nextcloud:31.0.9 docker digest to 11f1580
2025-09-17 08:36:48 +02:00
renovate-bot-cbcoutinho[bot] aafac732c6 chore(deps): update nextcloud:31.0.9 docker digest to 11f1580 2025-09-17 04:04:06 +00:00
Chris Coutinho 12d48bb920 Merge pull request #161 from cbcoutinho/renovate/mariadb-lts
chore(deps): update mariadb:lts docker digest to 851a602
2025-09-16 08:42:21 +02:00
renovate-bot-cbcoutinho[bot] 0600cea87b chore(deps): update mariadb:lts docker digest to 851a602 2025-09-16 04:05:11 +00:00
Chris Coutinho 145141e1d8 Merge pull request #160 from cbcoutinho/renovate/nextcloud-31.x
chore(deps): update nextcloud docker tag to v31.0.9
2025-09-16 00:17:58 +02:00
renovate-bot-cbcoutinho[bot] 948e7a4d91 chore(deps): update nextcloud docker tag to v31.0.9 2025-09-15 22:07:01 +00:00
Chris Coutinho 39ff811d1a Merge pull request #159 from cbcoutinho/renovate/astral-sh-setup-uv-digest
chore(deps): update astral-sh/setup-uv digest to b75a909
2025-09-14 20:47:05 +02:00
Chris Coutinho cfd03a761b ci: pin 2025-09-14 20:42:14 +02:00
renovate-bot-cbcoutinho[bot] e7b37312a7 chore(deps): update astral-sh/setup-uv digest to b75a909 2025-09-14 16:03:58 +00:00
Chris Coutinho 4ad47b4fa3 Merge pull request #158 from cbcoutinho/renovate/lock-file-maintenance
chore(deps): lock file maintenance
2025-09-13 11:13:51 +02:00
renovate-bot-cbcoutinho[bot] ffbb86df57 chore(deps): lock file maintenance 2025-09-13 09:02:50 +00:00
Chris Coutinho 7a57247a9c Merge pull request #157 from cbcoutinho/renovate/nextcloud-31.0.8
chore(deps): update nextcloud:31.0.8 docker digest to 92bc503
2025-09-12 18:56:43 +02:00
renovate-bot-cbcoutinho[bot] 4ea6ce3477 chore(deps): update nextcloud:31.0.8 docker digest to 92bc503 2025-09-12 16:05:34 +00:00
13 changed files with 700 additions and 346 deletions
+1 -2
View File
@@ -1,8 +1,7 @@
*
!pyproject.toml
!poetry.lock
!README.md
!uv.lock
!nextcloud_mcp_server/
!nextcloud_mcp_server/**/*.py
+2 -2
View File
@@ -11,7 +11,7 @@ jobs:
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- 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
run: |
uv run --frozen ruff format --diff
@@ -31,7 +31,7 @@ jobs:
with:
compose-file: "./docker-compose.yml"
- 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
run: |
+18
View File
@@ -1,3 +1,21 @@
## 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
+2 -2
View File
@@ -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
@@ -6,4 +6,4 @@ COPY . .
RUN uv sync --locked --no-dev
ENTRYPOINT ["/app/.venv/bin/python", "-m", "nextcloud_mcp_server.app", "--host", "0.0.0.0"]
ENTRYPOINT ["/app/.venv/bin/nextcloud-mcp-server", "--host", "0.0.0.0"]
+4
View File
@@ -248,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).
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=cbcoutinho/nextcloud-mcp-server&type=Date)](https://www.star-history.com/#cbcoutinho/nextcloud-mcp-server&Date)
## License
This project is licensed under the AGPL-3.0 License. See the [LICENSE](./LICENSE) file for details.
+2 -2
View File
@@ -3,7 +3,7 @@ services:
# https://hub.docker.com/_/mariadb
db:
# 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
command: --transaction-isolation=READ-COMMITTED
volumes:
@@ -21,7 +21,7 @@ services:
restart: always
app:
image: nextcloud:31.0.8@sha256:c3329db9d0d0d79b1fe6433b54b81c28acaefecfe96a400be202b7da80f6b8ca
image: nextcloud:31.0.9@sha256:88fe398340a896eeebfe0a4ba847998ff2c8fbb3d72de354ac1f08bc7b44db18
#user: www-data:www-data
restart: always
#post_start:
+5 -18
View File
@@ -6,8 +6,7 @@ from contextlib import asynccontextmanager, AsyncExitStack
from dataclasses import dataclass
from starlette.applications import Starlette
from starlette.routing import Mount, Route
from starlette.responses import JSONResponse
from starlette.routing import Mount
from mcp.server.fastmcp import Context, FastMCP
@@ -45,11 +44,7 @@ async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]:
await client.close()
async def heartbeat(request):
return JSONResponse({"status": "ok"})
def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None = None):
def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
setup_logging()
# Create an MCP server
@@ -89,11 +84,9 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
)
if transport == "sse":
mcp.settings.sse_path = "/"
mcp_app = mcp.sse_app()
lifespan = None
else:
mcp.settings.streamable_http_path = "/"
elif transport in ("http", "streamable-http"):
mcp_app = mcp.streamable_http_app()
@asynccontextmanager
@@ -102,13 +95,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
await stack.enter_async_context(mcp.session_manager.run())
yield
app = Starlette(
routes=[
Route("/heartbeat", endpoint=heartbeat),
Mount("/sse" if transport == "sse" else "/mcp", app=mcp_app),
],
lifespan=lifespan,
)
app = Starlette(routes=[Mount("/", app=mcp_app)], lifespan=lifespan)
return app
@@ -130,7 +117,7 @@ def get_app(transport: str = "streamable-http", enabled_apps: list[str] | None =
"-t",
default="sse",
show_default=True,
type=click.Choice(["sse", "streamable-http"]),
type=click.Choice(["sse", "streamable-http", "http"]),
)
@click.option(
"--enable-app",
+91 -1
View File
@@ -5,6 +5,10 @@ from mcp.server.fastmcp import Context, FastMCP
from nextcloud_mcp_server.client import NextcloudClient
from nextcloud_mcp_server.models.deck import (
DeckBoard,
DeckStack,
DeckCard,
DeckLabel,
CreateBoardResponse,
CreateStackResponse,
StackOperationResponse,
@@ -25,6 +29,7 @@ def configure_deck_tools(mcp: FastMCP):
async def deck_boards_resource():
"""List all Nextcloud Deck boards"""
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
boards = await client.deck.get_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):
"""Get details of a specific Nextcloud Deck board"""
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
board = await client.deck.get_board(board_id)
return board.model_dump()
@@ -41,6 +49,9 @@ def configure_deck_tools(mcp: FastMCP):
async def deck_stacks_resource(board_id: int):
"""List all stacks in a Nextcloud Deck board"""
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
stacks = await client.deck.get_stacks(board_id)
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):
"""Get details of a specific Nextcloud Deck stack"""
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
stack = await client.deck.get_stack(board_id, stack_id)
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):
"""List all cards in a Nextcloud Deck stack"""
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
stack = await client.deck.get_stack(board_id, stack_id)
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):
"""Get details of a specific Nextcloud Deck card"""
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
card = await client.deck.get_card(board_id, stack_id, card_id)
return card.model_dump()
@@ -75,6 +95,9 @@ def configure_deck_tools(mcp: FastMCP):
async def deck_labels_resource(board_id: int):
"""List all labels in a Nextcloud Deck board"""
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
board = await client.deck.get_board(board_id)
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):
"""Get details of a specific Nextcloud Deck label"""
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
label = await client.deck.get_label(board_id, label_id)
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()
async def deck_create_board(
+63 -3
View File
@@ -32,7 +32,7 @@ def configure_notes_tools(mcp: FastMCP):
return NotesSettings(**settings_data)
@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"""
ctx: Context = mcp.get_context()
client: NextcloudClient = ctx.request_context.lifespan_context.client
@@ -53,7 +53,7 @@ def configure_notes_tools(mcp: FastMCP):
}
@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"""
ctx: Context = mcp.get_context()
@@ -129,7 +129,7 @@ def configure_notes_tools(mcp: FastMCP):
"""Update an existing note's title, content, or category.
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,
the update will fail with a 412 error."""
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()
async def nc_notes_delete_note(note_id: int, ctx: Context) -> DeleteNoteResponse:
"""Delete a note permanently"""
+2 -2
View File
@@ -1,6 +1,6 @@
[project]
name = "nextcloud-mcp-server"
version = "0.12.1"
version = "0.12.4"
description = ""
authors = [
{name = "Chris Coutinho",email = "chris@coutinho.io"}
@@ -8,7 +8,7 @@ authors = [
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
"mcp[cli] (>=1.13,<1.14)",
"mcp[cli] (>=1.15,<1.16)",
"httpx (>=0.28.1,<0.29.0)",
"pillow (>=11.2.1,<12.0.0)",
"icalendar (>=6.0.0,<7.0.0)",
+9 -6
View File
@@ -2,7 +2,6 @@
import logging
from mcp import ClientSession
from mcp.shared.exceptions import McpError
import pytest
@@ -10,11 +9,15 @@ logger = logging.getLogger(__name__)
@pytest.mark.integration
async def test_missing_note_resource_error(nc_mcp_client: ClientSession):
"""Test that accessing a non-existent note resource returns proper error."""
# Try to get a non-existent note via resource - should raise McpError with improved message
with pytest.raises(McpError, match=r"Note 999999 not found"):
await nc_mcp_client.read_resource("nc://Notes/999999")
async def test_missing_note_tool_error(nc_mcp_client: ClientSession):
"""Test that accessing a non-existent note via tool returns proper error."""
# Try to get a non-existent note via tool - should return error response
response = await nc_mcp_client.call_tool("nc_notes_get_note", {"note_id": 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
+7 -4
View File
@@ -68,7 +68,8 @@ async def test_mcp_connectivity(nc_mcp_client: ClientSession):
template_uris.append(template.uriTemplate)
# 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:
assert expected_template in template_uris, (
@@ -140,9 +141,11 @@ async def test_mcp_notes_crud_workflow(
# 3. Read note via MCP
logger.info(f"Reading note via MCP: {note_id}")
read_result = await nc_mcp_client.read_resource(f"nc://Notes/{note_id}")
assert len(read_result.contents) == 1, "Expected exactly one content item"
read_note_data = json.loads(read_result.contents[0].text)
read_result = await nc_mcp_client.call_tool(
"nc_notes_get_note", {"note_id": note_id}
)
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["content"] == test_content
Generated
+494 -304
View File
File diff suppressed because it is too large Load Diff