Skip to content

feat(mcp): load code tools from modules named in the config - #761

Open
vishal-bala wants to merge 9 commits into
feat/mcp-code-tools/03-server-code-toolfrom
feat/mcp-code-tools/04-module-loader
Open

vishal-bala wants to merge 9 commits into
feat/mcp-code-tools/03-server-code-toolfrom
feat/mcp-code-tools/04-module-loader

Conversation

@vishal-bala

@vishal-bala vishal-bala commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Motivation

server.code_tool(...) from the previous PR only helps when the caller's own code constructs the server. The common deployment is rvl mcp --config, which imports no user code. This PR lets the config name modules for the server to import. Because importing a module runs its code with the server's privileges, nothing is imported unless the operator opts in separately.

Changes

@code_tool(...), exported from redisvl.mcp, takes the same arguments as server.code_tool. It checks the function and attaches its spec as an attribute, without registering it anywhere:

# myapp/support_tools.py
from redisvl.mcp import ToolContext, code_tool


@code_tool(index="tickets", namespace="support")
async def count_open_tickets(ctx: ToolContext, product: str) -> dict:
    """Count open support tickets for a product."""
    ...
server:
  redis_url: redis://localhost:6379
  allow_code_tools: true
  code_tool_modules: [myapp.support_tools]

At startup, the server imports each listed module before any binding connects. Every function marked with @code_tool is validated and registered alongside tools from server.code_tool, under the same startup checks.

Startup refuses, naming the module or setting:

Refused when Why
code_tool_modules is set without allow_code_tools: true (default false) The import runs operator code; the opt-in keeps a profiles-only config from running any Python. Checked at config load.
A module entry is blank, padded or listed twice Almost certainly a typo.
A module cannot be imported, or raises while importing Reported by module name, with the cause chained.
A module defines no @code_tool function A misspelled module name should not quietly produce a server with fewer tools.
A decorator is applied over @code_tool functools.wraps copies the marker while the spec still calls the inner function, so the outer decorator would silently never run.

The marker lives on the function, not in a module-level registry. Importing a module therefore registers nothing anywhere, and two servers in one process stay independent, as do tests that import a tool module. The loader considers only functions and reads the marker from each function's own __dict__, so a module attribute with a permissive __getattr__, such as a lazy module proxy, is never consulted. A tool one listed module re-exports from another registers once.

The docs gain "Code Tools Loaded From Modules" and a troubleshooting entry. They state that rvl does not put the working directory on the import path: modules must be installed or on PYTHONPATH, and under uvx installed in the environment it builds.

Notes

The loader does not edit sys.path. Adding the config's directory would make import resolution depend on where the config file happens to live.

I verified the end-to-end flow manually with rvl mcp --transport streamable-http, a scratch module on PYTHONPATH and the server started from outside the project directory. A FastMCP client listed support-count-open-tickets with only product in its schema, and a call returned the expected count.

Release Notes

rvl mcp can load code tools from Python modules. Mark async functions with @code_tool from redisvl.mcp, list their modules under server.code_tool_modules, and set server.allow_code_tools: true, which defaults to false because importing a module runs its code with the server's privileges. Modules must be importable by the server process.

Next Steps

  • Update the custom-tools proposal to record where v1.1 departs from it: the code_tool names, no ctx.guarded (the handle applies the limits itself), id-only writes and no filter-wide bulk writes, no code tools on tenant-scoped indexes, async tools only, and the whole-call timeout.

Review record

Reviewed at d72b479 by Claude Code (claude-opus-5-5) with pre-pr-review; fixes in 9ef37cc.
Lenses: correctness (always); security (imports a module named in config); design (new public @code_tool and config keys); tests (adds unit and integration tests); docs (loader guide and docstrings); Redis behaviour (stack-wide pass, nothing in this PR). The lenses ran over the whole stack, and findings were attributed to the PR that introduced the line.

  • 3 findings raised here, 2 kept after challenge.
  • Fixed: a comment named a tool decorator that does not exist (tools/code.py:31); test_code_tool_modules_are_off_by_default duplicated the not-allowed case of the config-load test (test_config.py:1098, deleted).
  • Rejected: the tool_module fixture is copied between the unit and integration tests (about 15 lines; sharing it would mean the Docker-backed root conftest).
  • Open for the reviewer: none.

Verified: pytest tests/unit/test_mcp (581 passed); pytest -n 1 tests/integration/test_mcp (84 passed, 2 skipped, Redis 8); make check-types (no issues in 122 files); Sphinx build (no warnings from the new sections).
Not checked: FastMCP 2.x (tested on 3.1.0 and 3.3.1); importing a module under uvx (the guide's claim follows from how uvx builds its environment, not from a run).


Note

Medium Risk
Adds config-driven Python module import at startup; misconfiguration or untrusted modules could run arbitrary code in the server process, though allow_code_tools defaults off and pairs with strict load-time checks.

Overview
rvl mcp can now register code tools from Python modules, not only via server.code_tool() in an embedder process. Operators mark async functions with @code_tool (exported from redisvl.mcp, same options as the server decorator), list modules under server.code_tool_modules, and must set server.allow_code_tools: true (defaults to false) because importing runs that code with server privileges.

At startup the server imports listed modules before Redis connects, discovers functions tagged with __redisvl_mcp_tool__, and registers them alongside programmatically registered tools under the same validation (unique names, index binding, read-only/write rules, tenant-scoped index checks). Config load rejects modules without the opt-in, blank/duplicate module names, failed imports, modules with no @code_tool functions, and @code_tool not outermost when another decorator wraps it. Docs add a “loaded from modules” guide and troubleshooting for import path / PYTHONPATH ( rvl does not add the working directory ).

Reviewed by Cursor Bugbot for commit fb074d1. Bugbot is set up for automated code reviews on this repo. Configure here.

@tool marks an async function as a code tool, with the same arguments as
server.custom_tool. server.custom_tool_modules lists modules for the server to
import at startup, and every function in them marked with @tool registers
alongside tools from server.custom_tool, under the same startup checks.
Importing a module runs its code with the server's privileges, so the list is
refused unless server.allow_code_tools is true; it defaults to false. A module
that cannot be imported or defines no tools fails startup by name.
…p-code-tools/04-module-loader

# Conflicts:
#	redisvl/mcp/tools/code.py
#	tests/integration/test_mcp/test_code_tools.py
#	tests/unit/test_mcp/test_code_tools_unit.py
#	tests/unit/test_mcp/test_server_unit.py
@tool becomes @code_tool and server.custom_tool_modules becomes
server.code_tool_modules, matching allow_code_tools and server.code_tool. A
decorator applied over @code_tool copies the marker through functools.wraps
while the spec still calls the inner function, so the outer decorator would
never run; the loader now refuses it by name. The docs note that uvx needs the
module installed in its environment, and that the import check has to run
outside the project directory to mirror rvl.
@vishal-bala
vishal-bala added this pull request to stack #762 October 14, 2026 09:01
@vishal-bala vishal-bala added the auto:minor Increment the minor version when merged label Oct 15, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T13:49:30.849987Z 9ef37cc Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@vishal-bala
vishal-bala requested a review from limjoobin October 27, 2026 13:48

@limjoobin limjoobin left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, this PR adds the @code_tool decorator and config-based module loading, allowing rvl mcp to register custom code tools.

Just needs a rebase and we are good to go!

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

auto:minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants