mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
Add MCP output_format json mode across memory tools
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
@@ -1,78 +0,0 @@
|
||||
"""
|
||||
Integration tests for ASCII/ANSI output formats in MCP tools.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_notes_ascii_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "ASCII Note",
|
||||
"directory": "notes",
|
||||
"content": "# ASCII Note\n\nThis is a note for ASCII output.",
|
||||
"tags": "ascii,output",
|
||||
},
|
||||
)
|
||||
|
||||
search_result = await client.call_tool(
|
||||
"search_notes",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"query": "ASCII",
|
||||
"output_format": "ascii",
|
||||
},
|
||||
)
|
||||
|
||||
assert len(search_result.content) == 1
|
||||
assert search_result.content[0].type == "text"
|
||||
text = search_result.content[0].text
|
||||
assert "Search results" in text
|
||||
assert "ASCII Note" in text
|
||||
assert "+" in text
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_ascii_and_ansi_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "Color Note",
|
||||
"directory": "notes",
|
||||
"content": "# Color Note\n\nThis note is for ANSI output.",
|
||||
"tags": "ansi,output",
|
||||
},
|
||||
)
|
||||
|
||||
ascii_result = await client.call_tool(
|
||||
"read_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"identifier": "Color Note",
|
||||
"output_format": "ascii",
|
||||
},
|
||||
)
|
||||
|
||||
assert len(ascii_result.content) == 1
|
||||
ascii_text = ascii_result.content[0].text
|
||||
assert "Note preview" in ascii_text
|
||||
assert "# Color Note" in ascii_text
|
||||
|
||||
ansi_result = await client.call_tool(
|
||||
"read_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"identifier": "Color Note",
|
||||
"output_format": "ansi",
|
||||
},
|
||||
)
|
||||
|
||||
ansi_text = ansi_result.content[0].text
|
||||
assert "\x1b[" in ansi_text
|
||||
@@ -0,0 +1,300 @@
|
||||
"""Integration tests for MCP `output_format="json"` responses."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastmcp import Client
|
||||
|
||||
|
||||
def _json_content(tool_result) -> dict | list:
|
||||
"""Parse a FastMCP tool result content block into JSON."""
|
||||
assert len(tool_result.content) == 1
|
||||
assert tool_result.content[0].type == "text"
|
||||
return json.loads(tool_result.content[0].text) # pyright: ignore [reportAttributeAccessIssue]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_note_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Write",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Write\n\nBody",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert payload["title"] == "JSON Integration Write"
|
||||
assert payload["action"] in ("created", "updated")
|
||||
assert payload["permalink"]
|
||||
assert payload["file_path"]
|
||||
assert "checksum" in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_note_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Read",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Read\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"read_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"identifier": "json-int/json-integration-read",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert payload["title"] == "JSON Integration Read"
|
||||
assert payload["permalink"]
|
||||
assert payload["file_path"]
|
||||
assert isinstance(payload["content"], str)
|
||||
assert "frontmatter" in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_note_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Edit",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Edit\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"edit_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"identifier": "json-int/json-integration-edit",
|
||||
"operation": "append",
|
||||
"content": "\n\nAppended",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert payload["title"] == "JSON Integration Edit"
|
||||
assert payload["operation"] == "append"
|
||||
assert payload["permalink"]
|
||||
assert payload["file_path"]
|
||||
assert "checksum" in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_recent_activity_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Recent",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Recent\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"recent_activity",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"timeframe": "7d",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert isinstance(payload, list)
|
||||
assert any(item.get("title") == "JSON Integration Recent" for item in payload)
|
||||
for item in payload:
|
||||
assert set(["title", "permalink", "file_path", "created_at"]).issubset(item.keys())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_memory_projects_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
result = await client.call_tool(
|
||||
"list_memory_projects",
|
||||
{"output_format": "json"},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert isinstance(payload, dict)
|
||||
assert "projects" in payload
|
||||
assert any(project["name"] == test_project.name for project in payload["projects"])
|
||||
assert "default_project" in payload
|
||||
assert "constrained_project" in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_memory_project_json_output_is_idempotent(
|
||||
mcp_server, app, test_project, tmp_path
|
||||
):
|
||||
async with Client(mcp_server) as client:
|
||||
project_name = "json-int-created"
|
||||
project_path = str(tmp_path.parent / (tmp_path.name + "-projects") / "json-int-created")
|
||||
|
||||
first = await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"project_path": project_path,
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
first_payload = _json_content(first)
|
||||
assert first_payload["name"] == project_name
|
||||
assert first_payload["path"] == project_path
|
||||
assert first_payload["created"] is True
|
||||
assert first_payload["already_exists"] is False
|
||||
|
||||
second = await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": project_name,
|
||||
"project_path": str(
|
||||
tmp_path.parent / (tmp_path.name + "-projects") / "json-int-created-second"
|
||||
),
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
second_payload = _json_content(second)
|
||||
assert second_payload["name"] == project_name
|
||||
assert second_payload["created"] is False
|
||||
assert second_payload["already_exists"] is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_note_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Delete",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Delete\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"delete_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"identifier": "json-int/json-integration-delete",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert payload["deleted"] is True
|
||||
assert payload["title"] == "JSON Integration Delete"
|
||||
assert payload["permalink"]
|
||||
assert payload["file_path"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_move_note_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Move",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Move\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"move_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"identifier": "json-int/json-integration-move",
|
||||
"destination_path": "json-int/moved/json-integration-move.md",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert payload["moved"] is True
|
||||
assert payload["title"] == "JSON Integration Move"
|
||||
assert payload["source"] == "json-int/json-integration-move"
|
||||
assert payload["destination"] == "json-int/moved/json-integration-move.md"
|
||||
assert payload["permalink"]
|
||||
assert payload["file_path"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_build_context_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Context",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Context\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"build_context",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"url": "memory://json-int/json-integration-context",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert isinstance(payload, dict)
|
||||
assert "results" in payload
|
||||
assert "metadata" in payload
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_search_notes_json_output(mcp_server, app, test_project):
|
||||
async with Client(mcp_server) as client:
|
||||
await client.call_tool(
|
||||
"write_note",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"title": "JSON Integration Search",
|
||||
"directory": "json-int",
|
||||
"content": "# JSON Integration Search\n\nBody",
|
||||
},
|
||||
)
|
||||
|
||||
result = await client.call_tool(
|
||||
"search_notes",
|
||||
{
|
||||
"project": test_project.name,
|
||||
"query": "JSON Integration Search",
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
|
||||
payload = _json_content(result)
|
||||
assert isinstance(payload, dict)
|
||||
assert "results" in payload
|
||||
assert isinstance(payload["results"], list)
|
||||
assert any(item.get("title") == "JSON Integration Search" for item in payload["results"])
|
||||
@@ -121,7 +121,7 @@ async def test_create_project_with_default_flag(mcp_server, app, test_project, t
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_project_duplicate_name(mcp_server, app, test_project, tmp_path):
|
||||
"""Test creating a project with duplicate name shows error."""
|
||||
"""Test creating a project with duplicate name is idempotent."""
|
||||
|
||||
async with Client(mcp_server) as client:
|
||||
# First create a project
|
||||
@@ -135,25 +135,35 @@ async def test_create_project_duplicate_name(mcp_server, app, test_project, tmp_
|
||||
},
|
||||
)
|
||||
|
||||
# Try to create another project with same name
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": "duplicate-test",
|
||||
"project_path": str(
|
||||
tmp_path.parent / (tmp_path.name + "-projects") / "project-duplicate-test-2"
|
||||
),
|
||||
},
|
||||
)
|
||||
# Second create with same name should succeed idempotently
|
||||
second_result = await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": "duplicate-test",
|
||||
"project_path": str(
|
||||
tmp_path.parent / (tmp_path.name + "-projects") / "project-duplicate-test-2"
|
||||
),
|
||||
},
|
||||
)
|
||||
second_text = second_result.content[0].text # pyright: ignore [reportAttributeAccessIssue]
|
||||
assert "already exists" in second_text.lower()
|
||||
assert "duplicate-test" in second_text
|
||||
|
||||
# Should show error about duplicate name
|
||||
error_message = str(exc_info.value)
|
||||
assert "create_memory_project" in error_message
|
||||
# JSON mode should explicitly report already_exists=true
|
||||
second_json = await client.call_tool(
|
||||
"create_memory_project",
|
||||
{
|
||||
"project_name": "duplicate-test",
|
||||
"project_path": str(
|
||||
tmp_path.parent / (tmp_path.name + "-projects") / "project-duplicate-test-3"
|
||||
),
|
||||
"output_format": "json",
|
||||
},
|
||||
)
|
||||
second_json_text = second_json.content[0].text # pyright: ignore [reportAttributeAccessIssue]
|
||||
assert (
|
||||
"duplicate-test" in error_message
|
||||
or "already exists" in error_message
|
||||
or "Invalid request" in error_message
|
||||
'"already_exists":true' in second_json_text
|
||||
or '"already_exists": true' in second_json_text
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user