Skip to content

feat(sdk): standalone local MCP server over stdio (#540) - #551

Open
abhayKashyap03 wants to merge 5 commits into
VectifyAI:mainfrom
abhayKashyap03:feat/local-mcp-server
Open

abhayKashyap03 wants to merge 5 commits into
VectifyAI:mainfrom
abhayKashyap03:feat/local-mcp-server

Conversation

@abhayKashyap03

Copy link
Copy Markdown

Closes #540.

What changes

A local library can now be used from MCP clients outside the Python process, such as Claude Desktop, Cursor and Cline. Until now, local mode reached agent tools only in-process, through as_openai_tools(), as_claude_mcp() and as_anthropic_tools().

pageindex-mcp --storage-path /path/to/index-store [--management]
  • New pageindex/local_mcp_server.py with LocalMcpServer, a low-level MCP server built on _tool_specs() for a PageIndexLocalClient.
    • Tool names, descriptions, schemas and annotations are the same as the in-process adapters, and agent_instructions() is served at initialize.
    • isError on tool results passes through unchanged.
    • Tool calls run in a worker thread through asyncio.to_thread, so they don't block the event loop.
  • Read-only by default. --management adds remove_document. When management is off, a direct call to remove_document gets an MCP tool error and deletes nothing.
  • stdio transport only. serve_stdio() points stdout at stderr while it serves, and the protocol writes to a private copy of the original stdout. Stray output from a tool, C extension or child process can't corrupt the JSON-RPC stream this way. MCP 2.x's stdio_server already does this; 1.x does not.
  • New pageindex-mcp console entry point ([tool.poetry.scripts]). It exits with a usage error when --storage-path is missing, isn't a directory or isn't readable. It warns on stderr when the path has no manifest.json, because the store would otherwise serve an empty library without saying so. Ctrl+C exits with code 130 and prints no traceback.
  • Works with the whole declared range, mcp>=1.19.0,<3. 1.x registers handlers with decorators and 2.x passes them to the constructor, so the server picks the right path at construction time. On 1.x it registers call_tool with validate_input=False. Otherwise the SDK's schema check rejects string booleans such as "false" before the tool layer can convert them, which cloud does.
  • local_mcp_server is added to pageindex._SUBMODULES.
  • README: a new "Local MCP server" section with the command and an mcpServers config. The "MCP server" row in the Local column now links to that section.

No new dependencies.

CI

  • pip install --no-deps -e . was added after the requirements install, so the console script exists for the executable tests.
  • A new matrix leg runs Python 3.10, no agent frameworks, pdfium 5 and mcp==1.19.0. It is the only leg that runs the 1.x code path, because the other legs install the latest mcp. Job names now include the mcp version. gate is unchanged.

Tests

tests/test_local_mcp.py, 13 tests. No LLM calls: fixtures seed a temporary store with seed_doc.

  • In-process MCP sessions:
    • initialize returns the agent instructions.
    • tools/list returns the right names, schemas and readOnlyHint annotations.
    • Each read tool returns a successful result.
    • A missing document returns NOT_FOUND. An unknown tool, and a call with missing arguments, both return errors.
    • The management gate is checked in both directions.
    • String booleans reach the tool layer.
    • Invokers run off the event-loop thread.
  • stdio subprocess:
    • A round trip completes.
    • A tool that prints to stdout without a newline doesn't corrupt the stream. This test fails on 1.x without serve_stdio().
  • Installed pageindex-mcp, run from an unrelated working directory:
    • Round trips with and without --management, including whether deletion is allowed.
    • --help output and invalid arguments.
    • An unreadable store is rejected.
    • An empty store produces the warning and exits cleanly when stdin closes.
    • The executable tests skip when the command isn't installed, and fail instead when CI is set.

Local runs:

  • mcp 2.3.0 with agent frameworks: full suite 734 passed, 100 skipped.
  • mcp 1.19.0 without frameworks and with CI=true (the new leg): test_local_mcp.py 13 passed. Before the latest two executable tests were added, the full suite on this leg was 614 passed, 218 skipped.
  • By hand, pageindex-mcp driven over raw JSON-RPC pipes with a minimal PATH and cwd=/, the way desktop hosts launch it: every stdout line is valid JSON-RPC, and the process exits 0 when stdin closes.

Not in this PR

  • Streamable HTTP transport. stdio covers the desktop and IDE clients from the issue.
  • MCP prompts and resources, such as the cloud's cited_answer prompt.
  • Concurrent tool calls against one store aren't covered by a test. This matters with --management, when remove_document runs alongside reads.
  • A corrupt manifest.json still loads as an empty library without error. That behaviour is in local_store and is unchanged here.
  • serve_stdio() uses os.dup/os.dup2, which exist on Windows, but CI runs only on Ubuntu, so Windows is untested.

- Skip the SDK's input validation so string booleans reach the tool layer.
- Add serve_stdio(), which redirects stray stdout to stderr so it can't corrupt the JSON-RPC stream.
- Add tests for both bugs and use serve_stdio() in the docs launcher.
@abhayKashyap03
abhayKashyap03 marked this pull request as ready for review October 3, 2026 08:08
@abhayKashyap03

Copy link
Copy Markdown
Author

@rejojer @zmtomorrow would appreciate a review, thanks.

This branch has not been deployed

No deployments
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.

[Feature] Standalone local MCP server so Claude Desktop / Cursor / any MCP client can use a local library

1 participant