Repository navigation
feat(mcp): register code tools from Python with server.code_tool - #760
Draft
vishal-bala wants to merge 6 commits into
Draft
vishal-bala wants to merge 6 commits into
vishal-bala wants to merge 6 commits into
Conversation
server.custom_tool(...) registers an async function as an MCP tool on a server started from the caller's own process. At startup each tool is paired with its binding and refused when it cannot hold: a missing or unknown index, a name another custom tool uses, writes on a read-only binding or server, or any binding over a Redis index that a profile scopes with lock.inject, since nothing shows a code tool applies that scope. Code tools register after profiles and count towards the tool-surface fingerprint. ToolContext is now exported from redisvl.mcp, and the concepts and how-to guides document both.
…-code-tools/03-server-custom-tool
…de tools server.custom_tool becomes server.code_tool, matching allow_code_tools and the docs, and no longer reading as a way to register a profile, which custom_tools already means. A restart that would pair the registered code tools with different bindings now fails instead of warning, since the old pairing would stay live. A writing tool on a read-only server says so rather than blaming the index. The docs describe the id-only writes and the SQLQuery refusal, replace an example that assumed the record carried its own id, and name code tools in the tool surface.
vishal-bala
added this pull request to stack #762
October 14, 2026 09:01
…-code-tools/03-server-code-tool
…-code-tools/03-server-code-tool
This was referenced Oct 20, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
This PR makes code tools usable from a server started in the caller's own Python process. It builds on the tool wrapper from the previous PR:
The model sees
count-open-tickets, takingproduct, and never the context or the index.Changes
Registration
server.code_tool(...)checks the function at decoration and returns it unchanged. Registering after the first startup raisesRuntimeError, because tools register once per process. Code tools register after profiles.The method is named
code_toolrather thancustom_tool, which the proposal used.custom_tools:already means profiles in the config, and every message and setting calls these code tools.Startup checks
Each tool is paired with its binding once the config is loaded, before any binding connects. Startup fails, naming the tool, when:
indexis missing with several bindings, or names no bindinglock.injectredis_name, so a sibling binding over the same documents counts.The last check extends
_verify_no_unscoped_route_to_injected_indexesrather than duplicating its grouping, so the error lists a code tool alongside any other unscoped route.A restart that would pair the registered code tools with different bindings now fails, rather than taking the warning path other tool-surface changes take. The old pairing would stay live, and for operator code that is not something a log line should report.
Docs and exports
ToolContext,IndexHandleandWritableIndexHandleare exported fromredisvl.mcp; the guide shows thecasta type checker needs to see a writing tool's methods. The concepts page gains a Code Tools section covering what the context exposes and withholds, the rails applied to every call, and what fails at startup. The how-to guide gains "Code Tools Registered From Python", with a read tool and a guarded delete, plus a troubleshooting entry. The tool-surface and tenant-scoping sections now mention code tools, and a line saying custom tools never involve Python now says that of profiles specifically.Notes
Integration tests run against Redis 8:
Release Notes
Register an async Python function as an MCP tool with
server.code_tool(...)when you run the server from your own process. It reaches Redis only through aToolContextwhose handle stays inside the bound index, bounds every call, and offers writes only to tools declared withwrites=True.Next Steps
The final PR in the stack lets
rvl mcpload code tools from modules named in the config, behind an explicit opt-in.Review record
Reviewed at
392ccdeby Claude Code (claude-opus-5-5) withpre-pr-review; fixes inbeb1803.Lenses: correctness (always); security (startup checks guarding tenant-scoped indexes); design (new public
server.code_tooland MCP surface); tests (adds unit and integration tests); docs (two guide pages and docstrings); Redis behaviour (the guides state how keys map to an index). The lenses ran over the whole stack, and findings were attributed to the PR that introduced the line.tickets.triagewithout saying scopes apply only with authentication (concepts/mcp.md:312,how_to_guides/mcp.md:456,server.py:246); the how-to said a tool "cannot touch another index's keys", which overlapping or empty prefixes break (how_to_guides/mcp.md:461); the documented writing tool failed mypy because the handle types were not exported (__init__.py, with acastnote in the guide); anInjectSpecdocstring promised code tools the injection guarantees they never get (auth.py:250); a read-tool integration test duplicated the concurrency-cap test (test_code_tools.py:121, deleted).RuntimeErrorexplains itself; the index-pinning rule repeated from profile validation (about 12 lines, no failure); "already used by another custom tool" wording for two code tools; decorator docstrings without a Raises line; duplicate async-only tests (each pins a different entry point);test_custom_tool_*test names; the tenant-route argument covered only by an integration test that CI runs; nodocs/apipage forredisvl.mcp(predates the stack).Verified:
pytest tests/unit/test_mcp(566 passed);pytest -n 1 tests/integration/test_mcp(83 passed, 2 skipped, Redis 8);mypy redisvl/mcp(no issues); mypy on the guide's writing tool (clean with thecast,attr-definedwithout it); Sphinx build at the #761 tip (no warnings from these sections; one broken Transport Security anchor predates the stack).Docs consulted: https://redis.io/docs/latest/commands/ft.create/; https://redis.io/docs/latest/commands/ft.info/.
Not checked: aliases and overlapping prefixes stay outside the tenant-index check by design; FastMCP 2.x (tested on 3.1.0 and 3.3.1).