Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/build_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def _format_context_markdown(graph: GraphContext, project: str) -> str:


@mcp.tool(
title="Build Context",
description="""Build context from a memory:// URI to continue conversations naturally.

Use this to follow up on previous discussions or explore related topics.
Expand All @@ -131,6 +132,7 @@ def _format_context_markdown(graph: GraphContext, project: str) -> str:
- "json" (default): Structured JSON with internal fields excluded
- "text": Compact markdown text for LLM consumption
""",
tags={"navigation", "notes"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def build_context(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@


@mcp.tool(
title="Create Canvas",
description="Create an Obsidian canvas file to visualize concepts and connections.",
tags={"canvas", "notes"},
annotations={"destructiveHint": False, "idempotentHint": True, "openWorldHint": False},
)
async def canvas(
Expand Down
4 changes: 4 additions & 0 deletions src/basic_memory/mcp/tools/chatgpt_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ def _format_document_for_chatgpt(


@mcp.tool(
title="Search Knowledge Base",
description="Search for content across the knowledge base",
tags={"search"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def search(
Expand Down Expand Up @@ -167,7 +169,9 @@ async def search(


@mcp.tool(
title="Fetch Document",
description="Fetch the full contents of a search result document",
tags={"search", "notes"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def fetch(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/cloud_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

@mcp.tool(
"cloud_info",
title="Cloud Info",
tags={"cloud"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
def cloud_info() -> str:
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/delete_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ def _directory_path_for_delete(


@mcp.tool(
title="Delete Note",
description="Delete a note or directory by title, permalink, or path",
tags={"notes"},
annotations={"destructiveHint": True, "openWorldHint": False},
)
async def delete_note(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/edit_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ def _format_error_response(


@mcp.tool(
title="Edit Note",
description="Edit an existing markdown note using various operations like append, prepend, find_replace, replace_section, insert_before_section, or insert_after_section.",
tags={"notes"},
annotations={"destructiveHint": False, "openWorldHint": False},
)
async def edit_note(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/list_directory.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@


@mcp.tool(
title="List Directory",
description="List directory contents with filtering and depth control.",
tags={"navigation", "notes"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def list_directory(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/move_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,9 @@ def _format_move_error_response(error_message: str, identifier: str, destination


@mcp.tool(
title="Move Note",
description="Move a note or directory to a new location, updating database and maintaining links.",
tags={"notes"},
annotations={"destructiveHint": False, "openWorldHint": False},
)
async def move_note(
Expand Down
6 changes: 6 additions & 0 deletions src/basic_memory/mcp/tools/project_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@ def _format_project_list_json(

@mcp.tool(
"list_memory_projects",
title="List Memory Projects",
tags={"projects"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def list_memory_projects(
Expand Down Expand Up @@ -495,6 +497,8 @@ async def _resolve_workspace_routing(

@mcp.tool(
"create_memory_project",
title="Create Memory Project",
tags={"projects"},
annotations={"destructiveHint": False, "openWorldHint": False},
)
async def create_memory_project(
Expand Down Expand Up @@ -636,6 +640,8 @@ async def create_memory_project(


@mcp.tool(
title="Delete Project",
tags={"projects"},
annotations={"destructiveHint": True, "openWorldHint": False},
)
async def delete_project(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/read_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,9 @@ def optimize_image(img, content_length, max_output_bytes=350000):


@mcp.tool(
title="Read Content",
description="Read a file's raw content by path or permalink",
tags={"notes"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def read_content(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/read_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ def _parse_opening_frontmatter(content: str) -> tuple[str, dict | None]:


@mcp.tool(
title="Read Note",
description="Read a markdown note by title or permalink.",
tags={"notes"},
# TODO: re-enable once MCP client rendering is working
# meta={"ui/resourceUri": "ui://basic-memory/note-preview"},
annotations={"readOnlyHint": True, "openWorldHint": False},
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/recent_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@


@mcp.tool(
title="Recent Activity",
description="""Get recent activity for a project or across all projects.

Timeframe supports natural language formats like:
Expand All @@ -36,6 +37,7 @@
- "3 weeks ago"
Or standard formats like "7d"
""",
tags={"navigation", "notes"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def recent_activity(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/release_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

@mcp.tool(
"release_notes",
title="Release Notes",
tags={"cloud"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
def release_notes() -> str:
Expand Down
6 changes: 6 additions & 0 deletions src/basic_memory/mcp/tools/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,9 @@ def _no_schema_guidance(note_type: str, tool_name: str) -> str:


@mcp.tool(
title="Validate Schema",
description="Validate notes against their Picoschema definitions.",
tags={"schema"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def schema_validate(
Expand Down Expand Up @@ -317,7 +319,9 @@ async def schema_validate(


@mcp.tool(
title="Infer Schema",
description="Analyze existing notes and suggest a Picoschema definition.",
tags={"schema"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def schema_infer(
Expand Down Expand Up @@ -438,7 +442,9 @@ async def schema_infer(


@mcp.tool(
title="Schema Diff",
description="Detect drift between a schema definition and actual note usage.",
tags={"schema"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def schema_diff(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,9 @@ async def _search_all_projects(


@mcp.tool(
title="Search Notes",
description="Search across all content in the knowledge base with advanced syntax support.",
tags={"search"},
# TODO: re-enable once MCP client rendering is working
# meta={"ui/resourceUri": "ui://basic-memory/search-results"},
annotations={"readOnlyHint": True, "openWorldHint": False},
Expand Down
4 changes: 4 additions & 0 deletions src/basic_memory/mcp/tools/ui_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def _text_block(message: str) -> List[ContentBlock]:


@mcp.tool(
title="Search Notes (UI)",
description="Search notes and return an embedded MCP-UI resource (raw HTML).",
tags={"search", "ui"},
output_schema=None,
annotations={"readOnlyHint": True, "openWorldHint": False},
)
Expand Down Expand Up @@ -92,7 +94,9 @@ async def search_notes_ui(


@mcp.tool(
title="Read Note (UI)",
description="Read a note and return an embedded MCP-UI resource (raw HTML).",
tags={"notes", "ui"},
output_schema=None,
annotations={"readOnlyHint": True, "openWorldHint": False},
)
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/view_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@


@mcp.tool(
title="View Note",
description="View a note as a formatted artifact for better readability.",
tags={"notes"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def view_note(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/workspaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ def _workspace_list_response(workspaces: list[WorkspaceInfo]) -> WorkspaceListRe


@mcp.tool(
title="List Workspaces",
description="List available cloud workspaces (tenant_id, type, role, and name).",
tags={"cloud", "projects"},
annotations={"readOnlyHint": True, "openWorldHint": False},
)
async def list_workspaces(
Expand Down
2 changes: 2 additions & 0 deletions src/basic_memory/mcp/tools/write_note.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@


@mcp.tool(
title="Write Note",
description="Create a markdown note. If the note already exists, returns an error by default — pass overwrite=True to replace.",
tags={"notes"},
annotations={"destructiveHint": True, "idempotentHint": False, "openWorldHint": False},
)
async def write_note(
Expand Down
16 changes: 16 additions & 0 deletions tests/mcp/test_tool_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
from collections.abc import Callable
from typing import Any, cast

import pytest

from basic_memory.mcp import tools
from basic_memory.mcp.server import mcp


EXPECTED_TOOL_SIGNATURES: dict[str, list[str]] = {
Expand Down Expand Up @@ -157,3 +160,16 @@ def test_mcp_tool_signatures_are_stable():

for tool_name, tool_obj in TOOL_FUNCTIONS.items():
assert _signature_params(tool_obj) == EXPECTED_TOOL_SIGNATURES[tool_name]


@pytest.mark.asyncio
async def test_mcp_tools_have_title_and_tags():
"""Every registered MCP tool must declare a human-readable title and at least one tag.

This guards against regressions where a new tool is added without the Phase 1
FastMCP metadata (title + tags) required by issue #826.
"""
tool_list = await mcp.list_tools()
for tool in tool_list:
assert tool.title, f"Tool '{tool.name}' is missing a 'title' annotation"
assert tool.tags, f"Tool '{tool.name}' is missing 'tags' annotation"
Loading