Skip to content

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

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

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

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.

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.
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