Add MCP output_format json mode across memory tools

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-02-18 19:30:49 -06:00
parent 0a36256f8a
commit 46b372c3e1
21 changed files with 1202 additions and 242 deletions
@@ -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
)