Skip to content

feat: support mcp 2.x (#92) - #93

Open
sleeyax wants to merge 2 commits into
ergut:mainfrom
sleeyax:feat/mcp-2-support
Open

feat: support mcp 2.x (#92)#93
sleeyax wants to merge 2 commits into
ergut:mainfrom
sleeyax:feat/mcp-2-support

Conversation

@sleeyax

@sleeyax sleeyax commented Aug 12, 2026

Copy link
Copy Markdown

Human: I tested the changes locally and so far this implementation works for me.

Let me know if anything needs to be changed. The rest of the PR body is written by my agent.

Fixes #92.

mcp 2.0.0 removed the low-level Server's @list_tools() / @call_tool() decorators in favour of constructor-based handler registration, so build_app() raised AttributeError at import and the server never served a request. Since mcp>=1.27 had no upper bound, every fresh install hit this.

This ports the server to the 2.x API rather than capping the dependency, and requires mcp>=2.0.

What changed

Handler registration. list_tools / call_tool are now plain (ctx, params) coroutines passed as on_list_tools / on_call_tool, returning ListToolsResult / CallToolResult.

Error reporting, now done by us. In 2.x an exception out of a handler becomes a JSON-RPC error scrubbed to a generic "Internal server error", so the model would no longer see why a call failed — an access denial, a missing page, an unreachable Logseq. call_tool catches and returns is_error results carrying the message instead. The strings clients see are unchanged from 1.8.0 (Error: …, Unknown tool: …, Input validation error: …).

Argument validation, now done by us. 1.x validated arguments against inputSchema by default; 2.x moved validation client-side. call_tool validates via jsonschema, which is added as an explicit dependency since it is now used directly rather than transitively.

Field spelling. Tool fields are snake_case for attribute access in 2.x, so tool descriptions and the test assertions that read .inputSchema move to .input_schema. The wire format is unaffected — the camelCase alias still works for construction and serialization.

Testing

  • Full suite passes against mcp 2.0.0 (636 tests). Pyright reports the same 9 pre-existing errors as before, none in the rewritten files.
  • New tests/integration/test_mcp_protocol.py drives a real ClientSession against the real Server over in-memory streams — initialize, tools/list, tools/call, error results, schema validation, and read-only tool filtering. This layer had no coverage previously, which is why the break only surfaced at a user's first connection.
  • Fresh install into a clean venv resolves mcp 2.0.0; driving the installed mcp-logseq binary over real stdio completes the handshake, lists 17 tools, and returns a tool-call failure as an in-band error result with the message intact.

Notes

  • The mcp>=2.0 floor is a breaking dependency change, so this probably wants a minor version bump rather than a patch.
  • There is no CI in the repo, so nothing enforces the new floor beyond uv.lock.

mcp 2.0.0 replaced the low-level Server's @list_tools() / @call_tool() decorators with constructor-based handler registration, so build_app() raised AttributeError at import and the server never served a request.
Handlers are now plain (ctx, params) coroutines passed as on_list_tools / on_call_tool, returning ListToolsResult / CallToolResult.

The 2.x low-level server drops two things its 1.x decorators did on a handler's behalf, both of which the client-visible contract depends on, so call_tool now does them itself:
a failed tool call is returned as an is_error result carrying the message (a raised exception would reach the client scrubbed to "Internal server error"), and arguments are validated against the tool's schema.
The resulting error strings are unchanged from 1.8.0.

Tool descriptions move to the snake_case field spelling (input_schema) that 2.x exposes for attribute access; the camelCase wire form is unaffected.

Adds an end-to-end test that drives a real ClientSession against the real Server over in-memory streams, covering initialize, tools/list and tools/call — the wiring this issue broke, which no test previously touched.
update_page in replace mode minted fresh uuids for every block it wrote.
Any ((uuid)) reference elsewhere in the graph was left dangling, and Logseq rewrote those refs as plain text in the referring files — content loss well outside the page being written.

Two paths dropped the uuids.
insertBatchBlock was called without keepUUID, so Logseq ignored the id:: it was handed.
The first block went through appendBlockInPage instead, which cannot carry a uuid at all; it now writes an empty anchor and routes every block through the batch.

keepUUID is only requested when every id in the batch parses as an RFC 4122 uuid.
Logseq answers a malformed one by discarding the entire batch and still reporting success, so a single bad id would empty the page.

On file graphs the anchor is kept when the page has properties, since Logseq stores those as `key:: value` lines in the first block.
Deleting it merged the page properties into the first block of real content.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Server fails to start with mcp 2.0.0: 'Server' object has no attribute 'list_tools'

1 participant