Skip to content

feat(mcp): add index routing + per-index write policy to upsert-records (RAAE-1607)#632

Draft
vishal-bala wants to merge 1 commit into
feature/raae-1606-search-routingfrom
feature/raae-1607-upsert-routing
Draft

feat(mcp): add index routing + per-index write policy to upsert-records (RAAE-1607)#632
vishal-bala wants to merge 1 commit into
feature/raae-1606-search-routingfrom
feature/raae-1607-upsert-routing

Conversation

@vishal-bala

@vishal-bala vishal-bala commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Motivation

This completes the multi-index tool surface for the RedisVL MCP server. After RAAE-1606 taught search-records to route by logical index, upsert-records (RAAE-1607) needs the same explicit routing — but writes also carry a policy dimension that reads do not. A single server can now host a mix of writable and read-only bindings, and the tool must respect both the global --read-only override and each binding's own read_only flag while staying backwards-safe for existing single-index clients.

The design keeps single-index behavior identical: when one binding is configured and index is omitted, the write resolves to that binding exactly as before. Routing becomes mandatory only once multiple bindings exist. Write enforcement happens at two complementary levels so the contract is unambiguous: a server with no writable bindings should not advertise the tool at all, while a server with some writable bindings still needs to protect the read-only ones on a per-call basis.

Implemented changes

upsert-records gains an optional index argument naming the logical binding to write to, resolved through the shared resolve_binding routing introduced in RAAE-1604. An omitted index with one binding resolves to that binding; an omitted index with multiple bindings returns invalid_request; and an unknown id returns invalid_request. The resolved logical id is echoed back as the index field of the response, and the selected binding's embedding, runtime limits, and schema validation drive the rest of the write unchanged.

Write availability is enforced at two levels. The tool registration gate is refined from "global read-only is off" to "at least one binding is writable" — expressed via effective_read_only, which already folds in both global read-only mode and a binding's own read_only policy — so an all-read-only server does not expose upsert-records at all. When the tool is registered, a per-call guard rejects writes to any individual read-only binding with invalid_request before any embedding or backend write occurs, so a writable server can still protect specific indexes.

Minor additional changes:

  • The FastMCP wrapper exposes index as a tool parameter.
  • Unit coverage for default-to-sole-binding, named routing, unknown-id rejection, read-only-binding rejection, the wrapper param, and both registration-gate outcomes (any-writable exposes the tool; all-read-only hides it).
  • Integration coverage on a two-binding server (one writable vector index, one read-only fulltext index): routing to the writable binding, omitted-index rejection, unknown-id rejection, read-only-binding rejection, and single-binding echo.

Verification

  • make format (isort + black) and mypy clean on changed files.
  • Full MCP suite: 244 passed, 2 skipped (Redis-version-gated) across unit + integration.

Stacking

This PR targets feature/raae-1606-search-routing so its diff stays scoped to upsert routing + write policy. Review/merge bottom-up: #629#630#631 → this PR.

🤖 Generated with Claude Code

…ords (RAAE-1607)

Add an optional `index` argument to the upsert-records tool so a multi-binding
MCP server can target a specific logical index for writes. As with search, the
argument is optional on single-binding servers and required when multiple
bindings exist; resolution flows through the shared resolve_binding routing so
an omitted index on a multi-binding server and unknown ids both surface as
invalid_request. The resolved logical id is echoed back as the `index` field in
the response, and the selected binding's embedding, runtime limits, and schema
validation are used throughout.

Write availability is now enforced at two levels. The upsert tool is registered
only when at least one binding is writable, so an all-read-only server (whether
from global read-only mode or every binding's own read_only policy) does not
advertise the tool at all. When the tool is registered, a per-call check
rejects writes to any individual read-only binding with invalid_request before
any embedding or backend write occurs, so a writable server can still protect
specific indexes.

- Expose `index` on the FastMCP wrapper param list.
- Refine the registration gate from "global read-only off" to "any binding
  writable" using effective_read_only, which folds in both global and
  per-index read-only.
- Add unit + integration coverage for routing, omitted-index rejection,
  unknown ids, read-only rejection, the registration gate, and single-binding
  backward compatibility.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jit-ci

jit-ci Bot commented Jun 16, 2026

Copy link
Copy Markdown

🛡️ Jit Security Scan Results

CRITICAL HIGH MEDIUM

✅ No security findings were detected in this PR


Security scan by Jit

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.

1 participant