Skip to main content
Glama
test_server.py34.6 kB
# test_server.py # # Copyright (c) 2025 Junpei Kawamoto # # This software is released under the MIT License. # # http://opensource.org/licenses/mit-license.php import asyncio import json import random from asyncio.subprocess import Process from pathlib import Path from typing import Generator, Tuple, AsyncGenerator, Any from unittest.mock import patch, MagicMock from urllib.parse import urlparse, parse_qs, urlencode, quote import pytest from mcp.server import FastMCP from mcp.server.fastmcp import Context from mcp.server.fastmcp.exceptions import ToolError from mcp.shared.context import RequestContext from mcp_bear import server, AppContext, BASE_URL, Note, NoteID, NoteInfo, ModifiedNote from mcp_bear.cli import generate_file_suffix BEAR_TOKEN = "abcdefg" def _encode_tags(obj: dict) -> dict: if "tags" in obj: obj["tags"] = json.dumps(obj["tags"]) return obj @pytest.fixture def temp_socket() -> Generator[Path, None, None]: while True: uds = Path("/tmp").joinpath(f"mcp-bear-{generate_file_suffix()}.sock") if not uds.exists(): break yield uds @pytest.fixture async def mcp_server(temp_socket: Path) -> AsyncGenerator[Tuple[FastMCP, Context[Any, AppContext]], None]: s = server(BEAR_TOKEN, temp_socket) async with s._mcp_server.lifespan(s) as lifespan_context: # type: ignore # noinspection PyTypeChecker ctx = Context( request_context=RequestContext( # type: ignore request_id=random.randint(1, 100), meta=None, session=None, lifespan_context=lifespan_context, request=None, ) ) yield s, ctx @pytest.fixture def mock_create_subprocess_exec() -> Generator[MagicMock, None, None]: original_exec = asyncio.create_subprocess_exec with patch("asyncio.create_subprocess_exec") as mock_exec: async def side_effect(cmd: str, *args: str, **_kwargs: Any) -> Process: queries = parse_qs(urlparse(args[2]).query) callback_url = (queries.get("x-success") or [""])[0] return await original_exec( cmd, args[0], args[1], f"{callback_url}?{urlencode(mock_exec.stubbed_queries, quote_via=quote)}" ) mock_exec.side_effect = side_effect yield mock_exec @pytest.fixture def mock_create_subprocess_exec_error() -> Generator[MagicMock, None, None]: original_exec = asyncio.create_subprocess_exec with patch("asyncio.create_subprocess_exec") as mock_exec: async def side_effect(cmd: str, *args: str, **_kwargs: Any) -> Process: queries = parse_qs(urlparse(args[2]).query) callback_url = (queries.get("x-error") or [""])[0] params = {"error-Code": "499", "errorMessage": "test error message"} return await original_exec(cmd, args[0], args[1], f"{callback_url}?{urlencode(params, quote_via=quote)}") mock_exec.side_effect = side_effect yield mock_exec @pytest.fixture def mock_requests_get() -> Generator[MagicMock, None, None]: with patch("requests.get") as mock_get: mock_response = MagicMock() mock_response.status_code = 200 mock_response.content = b"mocked http request" mock_get.return_value = mock_response yield mock_get @pytest.mark.anyio @pytest.mark.parametrize( "arguments", [{"id": "1234567890"}, {"title": "test note"}, {"id": "1234567890", "title": "test note"}] ) async def test_open_note( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server expect = Note( note="test note" * 16 * 1024, # > 16KB identifier="1234567890", title="test note", tags=["a", "b"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ) mock_create_subprocess_exec.stubbed_queries = _encode_tags(expect.model_dump()) res = await s._tool_manager.call_tool("open_note", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "new_window": "no", "float": "no", "show_window": "no", "open_note": "no", "selected": "no", "edit": "no", **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/open-note?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_open_note_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("open_note", arguments={"id": "1234567890", "title": "test note"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "arguments,expect_req_params", [ ({"text": "test note"}, {"text": "test note"}), ({"title": "test title", "text": "test note"}, {"title": "test title", "text": "test note"}), ({"title": "test title", "text": "# test title\ntest note"}, {"title": "test title", "text": "\ntest note"}), ( {"title": "test title", "text": "test note", "tags": ["a", "b"]}, {"title": "test title", "text": "test note", "tags": "a,b"}, ), ( {"title": "test title", "text": "test note", "timestamp": True}, {"title": "test title", "text": "test note", "timestamp": "yes"}, ), ], ) async def test_create( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, expect_req_params: dict, ) -> None: s, ctx = mcp_server expect = NoteID(identifier="1234567890", title="test title") mock_create_subprocess_exec.stubbed_queries = expect.model_dump() res = await s._tool_manager.call_tool("create", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "open_note": "no", "new_window": "no", "float": "no", "show_window": "no", **expect_req_params, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/create?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_create_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("create", arguments={"text": "test note"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "arguments,expect_req_params", [ ({"id": "123456"}, {"id": "123456"}), ({"text": "test note"}, {"text": "test note"}), ( {"title": "test title", "text": "test note"}, { "text": "test note", "title": "test title", }, ), ( {"title": "test title", "text": "test note", "tags": ["a", "b"]}, {"text": "test note", "title": "test title", "tags": "a,b"}, ), ( {"title": "test title", "text": "test note", "timestamp": True}, {"text": "test note", "title": "test title", "timestamp": "yes"}, ), ], ) async def test_replace_note( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, expect_req_params: dict, ) -> None: s, ctx = mcp_server expect = ModifiedNote(note="updated note", title="test title") mock_create_subprocess_exec.stubbed_queries = expect.model_dump() res = await s._tool_manager.call_tool("replace_note", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "mode": "replace_all" if "title" in arguments else "replace", "open_note": "no", "new_window": "no", "show_window": "no", "edit": "no", **expect_req_params, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/add-text?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_replace_note_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("replace_note", arguments={"id": "123456", "text": "new text"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "argument,expect", [ ("title", "# title"), ("# title", "# title"), ], ) async def test_add_title( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, argument: str, expect: str, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("add_title", arguments={"id": "123", "title": argument}, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "id": "123", "text": expect, "mode": "prepend", "open_note": "no", "new_window": "no", "show_window": "no", "edit": "no", "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/add-text?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_add_title_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("add_title", arguments={"id": "123456", "title": "new title"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "arguments,expect_req_params", [ ( {"id": "123456", "file": "dGVzdA==", "filename": "test.txt"}, {"file": "dGVzdA==", "filename": "test.txt", "id": "123456"}, ), ( {"title": "sample note", "file": "dGVzdA==", "filename": "test.txt"}, { "file": "dGVzdA==", "filename": "test.txt", "title": "sample note", }, ), ( {"id": "123456", "file": "dGVzdA==", "filename": "test.txt", "header": "supplement"}, { "file": "dGVzdA==", "filename": "test.txt", "id": "123456", "header": "supplement", }, ), ( {"id": "123456", "file": "dGVzdA==", "filename": "test.txt", "mode": "prepend"}, { "file": "dGVzdA==", "filename": "test.txt", "id": "123456", "mode": "prepend", }, ), ], ) async def test_add_file( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, expect_req_params: dict, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("add_file", arguments=arguments, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "selected": "no", "open_note": "no", "new_window": "no", "show_window": "no", "edit": "no", **expect_req_params, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/add-file?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio @pytest.mark.parametrize( "arguments,expect_req_params", [ ( {"id": "123456", "file": "http://example.com", "filename": "test.txt"}, { "file": "bW9ja2VkIGh0dHAgcmVxdWVzdA==", "filename": "test.txt", "id": "123456", }, ), ( {"title": "sample note", "file": "https://example.com", "filename": "test.txt"}, { "file": "bW9ja2VkIGh0dHAgcmVxdWVzdA==", "filename": "test.txt", "title": "sample note", }, ), ], ) async def test_add_file_http_request( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, mock_requests_get: MagicMock, arguments: dict, expect_req_params: dict, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("add_file", arguments=arguments, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "selected": "no", "open_note": "no", "new_window": "no", "show_window": "no", "edit": "no", **expect_req_params, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/add-file?{urlencode(req_params, quote_via=quote)}" ) mock_requests_get.assert_called_once_with(arguments["file"]) @pytest.mark.anyio async def test_add_file_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("add_file", arguments={"file": "dGVzdA==", "filename": "test.txt"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio async def test_tags( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = { "tags": json.dumps( [ {"name": "a"}, {"name": "b"}, {"name": "c"}, ] ) } res = await s._tool_manager.call_tool("tags", arguments={}, context=ctx) assert res == ["a", "b", "c"] assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "token": BEAR_TOKEN, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/tags?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_tags_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("tags", arguments={}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio async def test_open_tag( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, ) -> None: s, ctx = mcp_server expect = [ NoteInfo( title="note a", identifier="1", tags=["test", "data"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ), NoteInfo( title="note b", identifier="2", modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z" ), ] mock_create_subprocess_exec.stubbed_queries = { "notes": json.dumps([info.model_dump() for info in expect]), } res = await s._tool_manager.call_tool("open_tag", arguments={"name": "test_tag"}, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "name": "test_tag", "token": BEAR_TOKEN, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/open-tag?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_open_tag_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("open_tag", arguments={"name": "a"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio async def test_rename_tag( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("rename_tag", arguments={"name": "old name", "new_name": "new name"}, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "name": "old name", "new_name": "new name", "show_window": "no", "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/rename-tag?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_rename_tag_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool( "rename_tag", arguments={"name": "old name", "new_name": "new name"}, context=ctx ) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio async def test_delete_tag( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("delete_tag", arguments={"name": "tag name"}, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "name": "tag name", "show_window": "no", "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/delete-tag?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_delete_tag_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("delete_tag", arguments={"name": "tag name"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "arguments", [{"id": "1234567890"}, {"search": "test note"}, {"id": "1234567890", "search": "test note"}] ) async def test_trash( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("trash", arguments=arguments, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/trash?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_trash_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("trash", arguments={"search": "tag name"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "arguments", [{"id": "1234567890"}, {"search": "test note"}, {"id": "1234567890", "search": "test note"}] ) async def test_archive( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server mock_create_subprocess_exec.stubbed_queries = {} await s._tool_manager.call_tool("archive", arguments=arguments, context=ctx) assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/archive?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_archive_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("archive", arguments={"search": "tag name"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize("arguments", [{}, {"search": "keyword"}]) async def test_untagged( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server expect = [ NoteInfo( title="note a", identifier="1", tags=["test", "data"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ), NoteInfo( title="note b", identifier="2", modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z" ), ] mock_create_subprocess_exec.stubbed_queries = {"notes": json.dumps([info.model_dump() for info in expect])} res = await s._tool_manager.call_tool("untagged", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", "token": BEAR_TOKEN, **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/untagged?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_untagged_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("untagged", arguments={}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize("arguments", [{}, {"search": "keyword"}]) async def test_todo( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server expect = [ NoteInfo( title="note a", identifier="1", tags=["test", "data"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ), NoteInfo( title="note b", identifier="2", modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z" ), ] mock_create_subprocess_exec.stubbed_queries = {"notes": json.dumps([info.model_dump() for info in expect])} res = await s._tool_manager.call_tool("todo", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", "token": BEAR_TOKEN, **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/todo?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_todo_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("todo", arguments={}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize("arguments", [{}, {"search": "keyword"}]) async def test_today( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server expect = [ NoteInfo( title="note a", identifier="1", tags=["test", "data"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ), NoteInfo( title="note b", identifier="2", modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z" ), ] mock_create_subprocess_exec.stubbed_queries = {"notes": json.dumps([info.model_dump() for info in expect])} res = await s._tool_manager.call_tool("today", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", "token": BEAR_TOKEN, **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/today?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_today_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("today", arguments={}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize("arguments", [{}, {"search": "keyword"}]) async def test_locked( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server expect = [ NoteInfo( title="note a", identifier="1", tags=["test", "data"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ), NoteInfo( title="note b", identifier="2", modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z" ), ] mock_create_subprocess_exec.stubbed_queries = {"notes": json.dumps([info.model_dump() for info in expect])} res = await s._tool_manager.call_tool("locked", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", "token": BEAR_TOKEN, **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/locked?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_locked_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("locked", arguments={}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize( "arguments", [{"term": "1234567890"}, {"tag": "test note"}, {"term": "1234567890", "tag": "test note"}] ) async def test_search( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, arguments: dict, ) -> None: s, ctx = mcp_server expect = [ NoteInfo( title="note a", identifier="1", tags=["test", "data"], modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z", ), NoteInfo( title="note b", identifier="2", modificationDate="2023-01-01T00:00:00Z", creationDate="2023-01-01T00:00:00Z" ), ] mock_create_subprocess_exec.stubbed_queries = {"notes": json.dumps([info.model_dump() for info in expect])} res = await s._tool_manager.call_tool("search", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 req_params = { "show_window": "no", "token": BEAR_TOKEN, **arguments, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/search?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_search_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("search", arguments={"tags": "tag"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0 @pytest.mark.anyio @pytest.mark.parametrize("tags", [None, ["tag 1"], ["tag 1", "tag 2"]]) async def test_grab_url( temp_socket: Path, mcp_server: Tuple[FastMCP, Context], mock_create_subprocess_exec: MagicMock, tags: list[str] | None, ) -> None: s, ctx = mcp_server expect = NoteID(identifier="1234567890", title="test title") mock_create_subprocess_exec.stubbed_queries = expect.model_dump() arguments: dict[str, Any] = {"url": "https://bear.app"} arguments.update({"tags": tags} if tags else {}) res = await s._tool_manager.call_tool("grab_url", arguments=arguments, context=ctx) assert res == expect assert len(ctx.request_context.lifespan_context.futures) == 0 additional_params = {"tags": ",".join(tags)} if tags else {} req_params = { "url": "https://bear.app", **additional_params, "x-success": f"xfwder://{temp_socket.stem}/{ctx.request_id}/success", "x-error": f"xfwder://{temp_socket.stem}/{ctx.request_id}/error", } mock_create_subprocess_exec.assert_called_once_with( "open", "-g", "-j", f"{BASE_URL}/grab-url?{urlencode(req_params, quote_via=quote)}" ) @pytest.mark.anyio async def test_grab_url_failed( mcp_server: Tuple[FastMCP, Context[Any, AppContext]], mock_create_subprocess_exec_error: MagicMock ) -> None: s, ctx = mcp_server with pytest.raises(ToolError) as excinfo: await s._tool_manager.call_tool("grab_url", arguments={"url": "https://bear.app"}, context=ctx) assert "test error message" in str(excinfo.value) assert len(ctx.request_context.lifespan_context.futures) == 0

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jkawamoto/mcp-bear'

If you have feedback or need assistance with the MCP directory API, please join our Discord server