Skip to content

Commit d4b5a06

Browse files
vishal-balaclaude
andcommitted
feat(mcp): support multiple active index bindings (RAAE-1604)
Treat the MCP `indexes` config as a real multi-binding map instead of enforcing exactly one logical binding. This is the foundation for multi-index MCP support (RAAE-1603); single-index configs and callers behave identically. Config: - Drop the "exactly one configured index binding" validator; require at least one binding with non-blank ids. - Add per-binding `description` and `read_only` fields. - Move schema inspection / runtime-mapping / search validation methods onto MCPIndexBindingConfig and remove the single-binding convenience accessors from MCPConfig. Runtime/server: - Introduce an immutable BindingRuntime bundling a binding's config, index, effective schema, vectorizer, native-hybrid support, and effective read-only flag. - Replace single-resource server state with a dict of BindingRuntime; startup inspects/validates/initializes every binding independently (one client per binding) with all-or-nothing teardown. - Resolve native-hybrid support eagerly per binding. - Size the concurrency semaphore from the max binding limit and pass a per-binding request timeout into run_guarded. - Add resolve_binding(index_id): defaults to the sole binding, raises invalid_request for omitted-on-multi and unknown ids. - Compute effective_read_only as global read-only OR per-binding read_only. Tools: - Re-thread search/upsert onto a resolved BindingRuntime instead of single-binding server accessors. Tests follow TDD: multi-binding config/startup coverage, resolve_binding semantics, semaphore sizing, per-binding teardown, and backward-compatible single-index behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f2e43d6 commit d4b5a06

11 files changed

Lines changed: 732 additions & 335 deletions

File tree

redisvl/mcp/config.py

Lines changed: 34 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,16 @@ class MCPSchemaOverrides(BaseModel):
282282

283283

284284
class MCPIndexBindingConfig(BaseModel):
285-
"""The sole configured v1 index binding."""
285+
"""A single configured logical index binding.
286+
287+
A server can configure one or many of these under ``indexes.<id>``. Each
288+
binding inspects and serves one existing Redis index independently, and
289+
owns its own schema inspection, runtime mapping, and search validation.
290+
"""
286291

287292
redis_name: str = Field(..., min_length=1)
293+
description: str | None = Field(default=None, min_length=1)
294+
read_only: bool = False
288295
vectorizer: MCPVectorizerConfig | None = None
289296
search: MCPIndexSearchConfig
290297
runtime: MCPRuntimeConfig
@@ -355,83 +362,9 @@ def _validate_capability_requirements(self) -> "MCPIndexBindingConfig":
355362

356363
return self
357364

358-
359-
class MCPConfig(BaseModel):
360-
"""Validated MCP server configuration loaded from YAML."""
361-
362-
server: MCPServerConfig
363-
indexes: dict[str, MCPIndexBindingConfig]
364-
365-
@model_validator(mode="after")
366-
def _validate_bindings(self) -> "MCPConfig":
367-
"""Validate that there is exactly one configured logical binding."""
368-
if len(self.indexes) != 1:
369-
raise ValueError(
370-
"indexes must contain exactly one configured index binding"
371-
)
372-
373-
binding_id = next(iter(self.indexes))
374-
if not binding_id.strip():
375-
raise ValueError("indexes binding id must be non-blank")
376-
return self
377-
378-
@property
379-
def binding_id(self) -> str:
380-
"""Return the single logical binding identifier configured for v1."""
381-
return next(iter(self.indexes))
382-
383-
@property
384-
def binding(self) -> MCPIndexBindingConfig:
385-
"""Return the sole configured binding."""
386-
return self.indexes[self.binding_id]
387-
388-
@property
389-
def runtime(self) -> MCPRuntimeConfig:
390-
"""Expose the sole binding's runtime config for phase 1."""
391-
return self.binding.runtime
392-
393-
@property
394-
def vectorizer(self) -> MCPVectorizerConfig | None:
395-
"""Expose the sole binding's vectorizer config for phase 1."""
396-
return self.binding.vectorizer
397-
398-
@property
399-
def search(self) -> MCPIndexSearchConfig:
400-
"""Expose the sole binding's configured search behavior."""
401-
return self.binding.search
402-
403-
@property
404-
def uses_text_search(self) -> bool:
405-
"""Return whether configured search uses a text field."""
406-
return self.binding.uses_text_search
407-
408-
@property
409-
def uses_query_embedding(self) -> bool:
410-
"""Return whether configured search embeds user queries."""
411-
return self.binding.uses_query_embedding
412-
413-
@property
414-
def supports_vector_backed_upsert(self) -> bool:
415-
"""Return whether configured upserts manage a vector field."""
416-
return self.binding.supports_vector_backed_upsert
417-
418-
@property
419-
def supports_server_side_embedding(self) -> bool:
420-
"""Return whether configured upserts can generate embeddings."""
421-
return self.binding.supports_server_side_embedding
422-
423-
@property
424-
def requires_startup_vectorizer(self) -> bool:
425-
"""Return whether startup must initialize a vectorizer."""
426-
return self.binding.requires_startup_vectorizer
427-
428-
@property
429-
def redis_name(self) -> str:
430-
"""Return the existing Redis index name that must be inspected at startup."""
431-
return self.binding.redis_name
432-
365+
@staticmethod
433366
def inspected_schema_from_index_info(
434-
self, index_info: dict[str, Any]
367+
index_info: dict[str, Any],
435368
) -> dict[str, Any]:
436369
"""Build a schema dict from FT.INFO while preserving discovered field identity.
437370
@@ -478,7 +411,7 @@ def merge_schema_overrides(
478411
if isinstance(field, dict) and "name" in field
479412
}
480413

481-
for override in self.binding.schema_overrides.fields:
414+
for override in self.schema_overrides.fields:
482415
discovered = discovered_fields.get(override.name)
483416
if discovered is None:
484417
raise ValueError(
@@ -575,6 +508,29 @@ def validate_search(
575508
)
576509

577510

511+
class MCPConfig(BaseModel):
512+
"""Validated MCP server configuration loaded from YAML.
513+
514+
``indexes`` is the canonical multi-binding map: a server may configure one
515+
or many logical bindings. Single-index configs remain valid and unchanged;
516+
each binding owns its own inspection, runtime mapping, and search behavior.
517+
"""
518+
519+
server: MCPServerConfig
520+
indexes: dict[str, MCPIndexBindingConfig]
521+
522+
@model_validator(mode="after")
523+
def _validate_bindings(self) -> "MCPConfig":
524+
"""Require at least one binding and reject blank logical ids."""
525+
if not self.indexes:
526+
raise ValueError("indexes must contain at least one configured binding")
527+
528+
for binding_id in self.indexes:
529+
if not binding_id.strip():
530+
raise ValueError("indexes binding id must be non-blank")
531+
return self
532+
533+
578534
def _substitute_env(value: Any) -> Any:
579535
"""Recursively resolve `${VAR}` and `${VAR:-default}` placeholders."""
580536
if isinstance(value, dict):

redisvl/mcp/runtime.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from dataclasses import dataclass
2+
from typing import Any
3+
4+
from redisvl.index import AsyncSearchIndex
5+
from redisvl.mcp.config import MCPIndexBindingConfig
6+
from redisvl.schema import IndexSchema
7+
8+
9+
@dataclass(frozen=True)
10+
class BindingRuntime:
11+
"""Immutable per-binding runtime state assembled once at server startup.
12+
13+
Each configured logical index becomes one ``BindingRuntime`` bundling the
14+
binding config with the resources a tool call needs: the connected index,
15+
its effective (inspected + overridden) schema, an optional vectorizer, the
16+
resolved native-hybrid-search capability, and the effective write policy.
17+
18+
Tools resolve a binding once via ``server.resolve_binding(index)`` and then
19+
read these attributes directly instead of calling back into the server.
20+
"""
21+
22+
binding_id: str
23+
binding: MCPIndexBindingConfig
24+
index: AsyncSearchIndex
25+
schema: IndexSchema
26+
vectorizer: Any | None
27+
supports_native_hybrid_search: bool
28+
effective_read_only: bool

0 commit comments

Comments
 (0)