Skip to content

feat(mcp): surface MCP tool behavior hints in the tools table - #14885

Open
thesaadmirza wants to merge 6 commits into
langflow-ai:mainfrom
thesaadmirza:feat/mcp-tool-access-hints
Open

feat(mcp): surface MCP tool behavior hints in the tools table#14885
thesaadmirza wants to merge 6 commits into
langflow-ai:mainfrom
thesaadmirza:feat/mcp-tool-access-hints

Conversation

@thesaadmirza

@thesaadmirza thesaadmirza commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Langflow can already require approval on a single tool, but nothing tells you which tools are worth gating. On a server with 40 tools you read descriptions and guess. MCP servers already answer this: the spec puts readOnlyHint and destructiveHint on every tool and the Python SDK parses them into Tool.annotations. Langflow reads outputSchema off that same object and drops the hints on the floor. This surfaces them in the Actions table. While testing it I hit a separate data-loss bug in the same table and fixed it here too.

Example: connect an MCP server and the table now shows search_repositories as Read-only, create_issue as Write, and delete_repository as Destructive, right next to the Requires Approval switch. A tool the server says nothing about gets no badge rather than a misleading default.

Changes

  • lfx/base/mcp/util.py: _tool_access_hint derives one of read_only / write / destructive from the server's ToolAnnotations, and update_tools puts it on the tool metadata beside output_schema.
  • lfx/custom/custom_component/component.py: _build_tool_data carries access_hint into the tools_metadata row.
  • frontend: new AccessHintBadge, an Access column in the tools table, and labels in all seven locales.
  • frontend: the fix below.

Fix: turning on Requires Approval silently disabled every other tool

The tools grid is configured with suppressRowClickSelection={true}, but the effect that applies selection restored the option to false instead of true after running. That effect fires on every modal open, so row-click selection was live from then on. In ag-grid 32, a plain click on an already-selected row then takes the setSelectedParams({newValue: true, clearSelection: true}) path, meaning "select only this row". Every other tool's checkbox cleared, which wrote status: false and dropped those tools from the toolset, so gating a delete tool quietly removed the search tool from the agent.

Both setGridOption calls are removed rather than corrected: suppressRowClickSelection is read in exactly one place in ag-grid (onRowClick), so the programmatic node.setSelected() this effect uses never consulted it and the guard protected nothing.

Same click, before and after:

before:  checked [false, false, true, false]   three tools disabled
after:   checked [true,  true,  true, true ]   nothing else touched

Note that the stopPropagation on the approval Switch could never have prevented this. React attaches synthetic listeners at the root container, ag-grid's mouse listeners are native and attached inside it, so ag-grid handles the click before React invokes the Switch's onClick. ag-grid's own opt-out is the __ag_Grid_Stop_Propagation flag it checks at the top of RowCtrl.onRowEvent and CellMouseListenerFeature.onMouseEvent.

Design notes

The hint is display-only. The MCP spec is explicit that a client must not make tool-use decisions from annotations it does not trust, so nothing gates, exempts, or runs a tool based on it, and approval_actions is never touched. A server that sent no annotations gets no badge: the spec defaults destructiveHint to true, and applying that to an un-annotated server would mark every one of its tools destructive. Once either hint is present the defaults do apply, so readOnlyHint: false alone reads as destructive.

The Access column is dropped entirely when no tool carries a hint, since only MCP servers declare them and every other component's tool table would otherwise show a permanently blank column.

access_hint stores a derived string rather than the raw annotations. That keeps the spec's default rules in one place, at the cost of not persisting idempotentHint and openWorldHint. Since the field lands in saved flow JSON, happy to switch to storing the raw hints instead if you would rather keep that door open.

Verified end to end against a real stdio MCP server exposing one tool per annotation case, including the persisted tools_metadata in the saved flow.

Summary by CodeRabbit

  • New Features
    • Added an Access column to the tools table when access information is available.
    • Displays read-only, write, and destructive access badges with explanatory tooltips.
    • Access levels are shown as server-provided hints, not guarantees.
  • Localization
    • Added translations for access labels and guidance in German, English, Spanish, French, Japanese, Portuguese, and Simplified Chinese.
  • Bug Fixes
    • Improved row selection behavior so selecting tools remains consistent when the table refreshes.

Gating a tool for approval is already possible per tool, but nothing told the
author which tools were worth gating, so on a server with 40 tools you read
descriptions and guessed. MCP servers already answer this: the spec puts
readOnlyHint and destructiveHint on every tool and the Python SDK parses them
into Tool.annotations, which Langflow discarded while reading outputSchema off
the same object.

A server that sent no annotations gets no badge rather than a default, because
the spec defaults destructiveHint to true and applying that to an un-annotated
server would mark every one of its tools destructive. The hint stays display
only: the MCP spec is explicit that a client must not make tool-use decisions
from annotations it does not trust, so nothing gates, exempts, or runs a tool
based on it and approval_actions is never touched.

Also fixes a data-loss bug in the same table: the selection effect restored
suppressRowClickSelection to false instead of the configured true, leaving
row-click selection live for the rest of the modal. A plain click on an
already-selected row then took ag-grid's clearSelection path, so gating one
tool cleared every other row, wrote status:false, and silently dropped those
tools from the toolset. The option is read only by ag-grid's onRowClick and
never by the programmatic node.setSelected this effect uses, so both calls are
removed rather than corrected.
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 4d8def8b-958a-41f7-9372-43267ad4aa81

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 80f69ea5-4990-4713-94f8-504f871b5ee1

📥 Commits

Reviewing files that changed from the base of the PR and between 9e978f5 and 988b964.

📒 Files selected for processing (15)
  • src/frontend/src/locales/de.json
  • src/frontend/src/locales/en.json
  • src/frontend/src/locales/es.json
  • src/frontend/src/locales/fr.json
  • src/frontend/src/locales/ja.json
  • src/frontend/src/locales/pt.json
  • src/frontend/src/locales/zh-Hans.json
  • src/frontend/src/modals/toolsModal/components/toolsTable/AccessHintBadge.tsx
  • src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/accessHintBadge.test.tsx
  • src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/toolsTable-accessColumn.test.tsx
  • src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/toolsTable-clickSelection.test.tsx
  • src/frontend/src/modals/toolsModal/components/toolsTable/index.tsx
  • src/lfx/src/lfx/base/mcp/util.py
  • src/lfx/src/lfx/custom/custom_component/component.py
  • src/lfx/tests/unit/mcp/test_mcp_tool_annotations.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


Walkthrough

MCP annotations now produce access hints that flow into tool metadata and the tools table. The table displays localized access badges when hints exist. Selection synchronization preserves AG Grid row-click suppression.

Changes

MCP access hint display

Layer / File(s) Summary
Access hint classification and metadata
src/lfx/src/lfx/base/mcp/util.py, src/lfx/src/lfx/custom/custom_component/component.py, src/lfx/tests/unit/mcp/test_mcp_tool_annotations.py
MCP annotations classify tools as read-only, write, or destructive. The hint flows into generated metadata and rendered tool rows. Tests cover classification and propagation without changing approval actions.
Access hint display in ToolsTable
src/frontend/src/modals/toolsModal/components/toolsTable/*, src/frontend/src/locales/*.json
ToolsTable conditionally adds an Access column and renders localized badges with a tooltip. Tests cover column presence, ordering, and recognized or unsupported hints.
Selection synchronization behavior
src/frontend/src/modals/toolsModal/components/toolsTable/index.tsx, src/frontend/src/modals/toolsModal/components/toolsTable/__tests__/toolsTable-clickSelection.test.tsx
Selection synchronization no longer changes suppressRowClickSelection. Tests verify the grid keeps row-click selection suppression enabled.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 988b9

The PR adds optional, display-only MCP access hints and fixes tool-table selection behavior so enabling approval does not disable unrelated tools. No actionable merge-blocking risk remains at the current head.

Sequence Diagram(s)

sequenceDiagram
  participant MCPToolServer
  participant MCPMetadata
  participant Component
  participant ToolsTable
  participant AccessHintBadge
  MCPToolServer->>MCPMetadata: provide annotations
  MCPMetadata->>Component: attach access_hint
  Component->>ToolsTable: provide tool row
  ToolsTable->>AccessHintBadge: render access_hint
  AccessHintBadge-->>ToolsTable: display localized badge
Loading

Suggested reviewers: cristhianzl

🚥 Pre-merge checks | ✅ 6 | ❌ 3

❌ Failed checks (3 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 8 files. (7 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Quality And Coverage ⚠️ Warning The added tests cover the pure hint classifier and some table configuration, but they do not comprehensively test the changed behavior. The async update_tools path is never called by the new backend… Add a pytest async test that mocks an MCP client, awaits update_tools, and asserts access_hint on constructed tool metadata for read-only, write, destructive, and unannotated tools. Add Playwright coverage for the Actions table with ann…
Test File Naming And Structure ⚠️ Warning The backend test follows pytest conventions: src/lfx/tests/unit/mcp/test_mcp_tool_annotations.py matches test_*.py, and its test functions use descriptive test_* names. The frontend test names a… Rewrite the three new frontend tests as Playwright tests using @playwright/test or the repository's Playwright fixtures. Move them to the configured src/frontend/tests tree, use a Playwright-discoverable test suffix, and retain positive…
✅ Passed checks (6 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: displaying MCP tool behavior hints in the tools table.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Coverage For New Implementations ✅ Passed The PR includes tests for each new behavior. accessHintBadge.test.tsx covers all three hint values and null, absent, and unknown hints. toolsTable-accessColumn.test.tsx covers conditional column r…
Excessive Mock Usage Warning ✅ Passed PASS. The two ToolsTable tests use nine mocks each, but the mocks replace UI children, formatting helpers, and the AG Grid wrapper. The tests execute the real ToolsTable logic and inspect its column d…
Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 8 files. (7 skipped: 7 unsupported.)

Full details: Test Coverage For New Implementations

Explanation

The PR includes tests for each new behavior. accessHintBadge.test.tsx covers all three hint values and null, absent, and unknown hints. toolsTable-accessColumn.test.tsx covers conditional column rendering and column order. test_mcp_tool_annotations.py covers annotation classification, missing annotations, metadata propagation, and the display-only approval state. toolsTable-clickSelection.test.tsx is a regression test for the row-selection data-loss bug. The backend test follows test_*.py, and the frontend .test.tsx names match the repository Jest configuration and nearby React tests.

Full details: Test Quality And Coverage

Explanation

The added tests cover the pure hint classifier and some table configuration, but they do not comprehensively test the changed behavior. The async update_tools path is never called by the new backend tests; the metadata test injects access_hint directly into a fake tool, so removing the new update_tools metadata assignment would still pass. The frontend badge tests assert only a generated data-testid, not the translated label, badge variant, or tooltip. The access-column tests inspect only column fields and never invoke the cell renderer. The selection tests mock the grid and assert configuration calls, but do not reproduce a row click or verify that other tool statuses remain enabled. The new frontend tests use Jest/Testing Library, while this check requires Playwright for frontend tests, and no Playwright regression test was added.

Resolution

Add a pytest async test that mocks an MCP client, awaits update_tools, and asserts access_hint on constructed tool metadata for read-only, write, destructive, and unannotated tools. Add Playwright coverage for the Actions table with annotated and unannotated MCP tools. Assert the Access column, translated labels, badge tooltip, column placement, and absence of the column when no hint exists. In the same Playwright flow, enable approval and click an already-selected row, then verify that every other tool remains enabled. Strengthen component tests by asserting badge text, variant classes, and tooltip content, and by invoking the access-column cell renderer.

Full details: Test File Naming And Structure

Explanation

The backend test follows pytest conventions: src/lfx/tests/unit/mcp/test_mcp_tool_annotations.py matches test_*.py, and its test functions use descriptive test_* names. The frontend test names and cases are descriptive and include positive and negative scenarios. However, all three new frontend tests import @testing-library/react and use Jest. They do not use Playwright. The frontend Jest configuration discovers these files under src/**/__tests__/**, while Playwright is configured for src/frontend/tests. This directly violates the frontend test requirement.

Resolution

Rewrite the three new frontend tests as Playwright tests using @playwright/test or the repository's Playwright fixtures. Move them to the configured src/frontend/tests tree, use a Playwright-discoverable test suffix, and retain positive, negative, and edge-case assertions. Keep the backend pytest test in its current unit-test location.

Full details: Excessive Mock Usage Warning

Explanation

PASS. The two ToolsTable tests use nine mocks each, but the mocks replace UI children, formatting helpers, and the AG Grid wrapper. The tests execute the real ToolsTable logic and inspect its column definitions and grid API interactions. The AG Grid wrapper is an appropriate test double for the external grid boundary. This pattern already exists in the pre-existing toolsTable-pagination.test.tsx. AccessHintBadge and the Python annotation tests do not add local mocks. The changed tests therefore do not show excessive mocking that obscures the behavior under test.

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 1, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 1, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 1, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 1, 2026
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 1, 2026
Conflicts were all in generated or auto-formatted files that autofix.ci had
rewritten on this branch: the component index, four starter projects, and one
Playwright spec. Resolved to main's version in every case, the same way the
py_autofix workflow resolves component_index.json, so this branch carries only
the MCP access-hint change and the tools-table selection fix.
@github-actions github-actions Bot added enhancement New feature or request and removed enhancement New feature or request labels Sep 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant