Skip to content

feat(mcp): register and execute custom tool profiles - #677

Open
vishal-bala wants to merge 1 commit into
feat/mcp-profile-configfrom
feat/mcp-profile-tools
Open

feat(mcp): register and execute custom tool profiles#677
vishal-bala wants to merge 1 commit into
feat/mcp-profile-configfrom
feat/mcp-profile-tools

Conversation

@vishal-bala

@vishal-bala vishal-bala commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Stack position: 3 of 3. Base is #676. This completes the feature — the point at which a custom_tools: entry becomes a real tool.

How locked arguments are actually unreachable

register_profile_tool builds each profile's wrapper signature dynamically and hands it to FastMCP, which derives the advertised input schema from that signature and marks it additionalProperties: false. That is what makes a locked or hidden argument genuinely unreachable rather than merely undocumented — the model cannot name an argument the schema does not contain.

I verified empirically that FastMCP derives its schema from a dynamic __signature__, since the whole design rests on it. The wrapper still re-checks exposure per call rather than trusting the schema alone.

The filter annotation is an object type, never a string, so a raw filter string is refused by the advertised schema; the wrapper refuses one too, because the schema is the client's contract and the wrapper is the server's. A limit cap is published as Field(le=cap) so the ceiling is visible to the model rather than only enforced on rejection.

Startup validation

Catches what config load could not, because it needs the inspected schema: a locked projection or filter naming a field the bound index does not have, a locked exists on a field without INDEXMISSING, or one on a vector field. It runs before registration so a bad profile fails startup instead of leaving a half-registered tool set behind.

Two operational hazards that would otherwise be silent

Tools register once per process, but a profile bakes its locked filter, projection, and signature in at registration time. A restart that reloads a changed config would therefore keep enforcing the old profiles. The dangerous direction is an operator tightening a lock and believing the restart applied it, so the server fingerprints the config its tools were built from and warns when that no longer matches.

The empty-surface warning from #668 now also names custom_tools as a possible cause.

Also included

Integration coverage against real Redis, the concept and how-to documentation, and unit tests for registration, execution, description building, and per-binding lock isolation.

Verification

  • MCP unit tests: 356 passing
  • make check-types: clean

After this merges

The integration branch holds #675 + #676 + this, and squash-merges to main as one "custom tool profiles" commit. Phase 2 (auth-claim tenant injection) and v1.1 (code tools) are separate follow-ups and not in this stack.

One thing recorded for phase 2: TokenEscaper does not escape |, so a scalar claim like acme|evil would render @tenant_id:{acme|evil} — a cross-tenant OR. Harmless here because the value is ANDed under the lock, but claim injection must validate claim characters, not just type.


Note

Medium Risk
Changes affect MCP tool contracts and filter composition (scoped AND semantics and injection defenses); mistakes could widen retrieval scope, but startup validation, schema narrowing, and extensive tests mitigate that.

Overview
Adds declarative custom tool profiles so YAML custom_tools entries become real MCP tools: curated names/descriptions over search-records with lock (frozen filter, return_fields, pinned index) and params (expose/hide args, limit.max).

Runtime: New register_profile_tools builds dynamic signatures for FastMCP (locked/hidden args omitted from the advertised schema; profiles accept object-only caller filters). Execution delegates to search_records with locked_filter AND-combined with caller filters and optional limit_cap. Startup validates profiles against inspected index schemas before registration; warns if custom_tools / builtin_tools change after tools were registered once per process.

Docs: Concept and how-to sections for profiles, builtin_tools interplay, and filter scoping semantics.

Tests: Broad unit coverage (signatures, limits, filter merge, multi-profile isolation) plus Redis integration tests for locks, schema, and injection-style filter values.

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

Completes the stack: the machine that honors the profile config models,
and the point at which a `custom_tools:` entry becomes a real tool.

`register_profile_tool` builds each profile's wrapper signature
dynamically and hands it to FastMCP, which derives the advertised input
schema from that signature and marks it `additionalProperties: false`.
That is what makes a locked or hidden argument genuinely unreachable
rather than merely undocumented -- the model cannot name an argument the
schema does not contain. The wrapper still re-checks exposure per call
rather than trusting the schema alone.

The `filter` annotation is an object type, never a string, so a raw filter
string is refused by the advertised schema; the wrapper refuses one too,
because the schema is the client's contract and the wrapper is the
server's. A `limit` cap is published as `Field(le=cap)` so the ceiling is
visible to the model rather than only enforced on rejection.

Startup validation catches what config load could not, since it needs the
inspected schema: a locked projection or filter naming a field the bound
index does not have, a locked `exists` on a field without INDEXMISSING, or
one on a vector field. It runs before registration so a bad profile fails
startup instead of leaving a half-registered tool set.

Two operational hazards get warnings rather than silence. Tools register
once per process, but a profile bakes its locked filter, projection, and
signature in at registration time -- so a restart that reloads a *changed*
config would keep enforcing the old profiles. The dangerous direction is
an operator tightening a lock and believing the restart applied it, so the
server fingerprints the config its tools were built from and warns when
that no longer matches. The empty-surface warning also now names
`custom_tools` as a possible cause.

Adds the integration coverage that exercises profiles against real Redis,
the concept and how-to documentation, and unit tests for registration,
execution, description building, and per-binding lock isolation. The
restart-warning path and `_register_tools` idempotency are covered here
too, since both only became load-bearing once profiles existed.
@vishal-bala
vishal-bala force-pushed the feat/mcp-profile-tools branch from 4132493 to e03783b Compare August 14, 2026 14:57
@vishal-bala
vishal-bala requested a review from nkanu17 August 20, 2026 07:48
@vishal-bala
vishal-bala marked this pull request as ready for review August 20, 2026 07:48
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