Skip to content

fix(mcp): skip non-dict models/columns/rels in list and describe#2547

Open
Bartok9 wants to merge 2 commits into
Canner:mainfrom
Bartok9:fix/mcp-skip-non-dict-models
Open

fix(mcp): skip non-dict models/columns/rels in list and describe#2547
Bartok9 wants to merge 2 commits into
Canner:mainfrom
Bartok9:fix/mcp-skip-non-dict-models

Conversation

@Bartok9

@Bartok9 Bartok9 commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • MCP list_models / describe_model called .get on every model/column/relationship without type checks.
  • Non-dict rows in context load output crash tool handlers with AttributeError.
  • Skip non-dict models/columns/relationships; treat non-list columns as empty.

Motivation

Defensive consistency with memory/schema_indexer guards on Apache-2.0 core/**. MCP tools should return partial valid inventories rather than fail closed on one corrupt row.

Verification

$ cd core/wren && .venv/bin/python -m pytest tests/unit/test_mcp_model_guards.py -q
2 passed

(Logic mirror of the mcp_server sequence which is nested inside factory registration.)

Apache-2.0

Touches only core/wren/**.

Summary by CodeRabbit

  • Bug Fixes
    • Improved “List Models” and “Describe Model” handling for unexpected or incomplete model, column, and relationship data.
    • Non-conforming entries are now ignored instead of causing failures.
    • Column counts and column listings now remain accurate when column data is missing or malformed.
  • Tests
    • Added unit coverage to ensure junk inputs are skipped for both model listing and model description, including malformed column entries.

@github-actions github-actions Bot added python Pull requests that update Python code core labels Jul 20, 2026
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 51427ded-9ec0-4570-af9c-23889a486c3d

📥 Commits

Reviewing files that changed from the base of the PR and between 14ad14f and fd107d3.

📒 Files selected for processing (1)
  • core/wren/src/wren/mcp_server.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/wren/src/wren/mcp_server.py

Walkthrough

The MCP model tools now filter malformed models, columns, and relationships, normalize invalid column collections, preserve description fallbacks, and include unit tests for junk entries and safe column handling.

Changes

MCP model data guards

Layer / File(s) Summary
Defensive model handling and regression coverage
core/wren/src/wren/mcp_server.py, core/wren/tests/unit/test_mcp_model_guards.py
list_models and describe_model validate model, column, and relationship entries before constructing responses; tests verify malformed entries are skipped and invalid column collections produce safe counts.

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

Possibly related PRs

  • Canner/WrenAI#2522: Adds similar defensive handling for malformed model data in related MCP tooling.

Poem

A bunny found some models out of line,
And skipped the junk with a hop divine.
Columns count safely, descriptions shine,
Relationships guard their little sign.
The MCP burrow is tidy and fine!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: making MCP list and describe skip malformed non-dict models, columns, and relationships.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/wren/src/wren/mcp_server.py`:
- Around line 267-278: Prevent malformed scalar YAML values from reaching
collection operations: in core/wren/src/wren/mcp_server.py lines 267-278,
require model.get("columns") to be a list and require col.get("properties") to
be a dict before reading description; in lines 233-238, require
model.get("properties") to be a dict before accessing description; in lines
280-285, require load_relationships(ctx.project) and rel.get("models") to be
lists before iteration or membership checks. Mirror the properties guard in
core/wren/tests/unit/test_mcp_model_guards.py lines 9-14 and the columns guard
in lines 26-30.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e5581b2e-6af5-4145-a445-18a7b903bea1

📥 Commits

Reviewing files that changed from the base of the PR and between 2b0b335 and 1beb238.

📒 Files selected for processing (2)
  • core/wren/src/wren/mcp_server.py
  • core/wren/tests/unit/test_mcp_model_guards.py

Comment on lines +267 to +278
columns = []
for col in model.get("columns") or []:
if not isinstance(col, dict):
continue
columns.append(
{
"name": col.get("name"),
"type": col.get("type"),
"description": col.get("description")
or (col.get("properties") or {}).get("description"),
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Incomplete guards against scalar YAML values.

Based on the PR objectives to prevent failures from malformed context output, the current type checks are insufficient because they do not protect against truthy scalar types. If malformed YAML values for columns, properties, or relationships parse as integers or booleans, or [] and or {} will fall back to those exact scalars, leading to TypeError during iteration/membership tests or AttributeError when chaining .get(). Furthermore, unvalidated strings can cause unintended substring matches during in checks.

  • core/wren/src/wren/mcp_server.py#L267-L278: Guard model.get("columns") as a list before iterating, and verify col.get("properties") is a dict before calling .get("description").
  • core/wren/src/wren/mcp_server.py#L233-L238: Verify model.get("properties") is a dict before calling .get("description").
  • core/wren/src/wren/mcp_server.py#L280-L285: Guard load_relationships(ctx.project) and rel.get("models") as lists before iterating or performing in checks.
  • core/wren/tests/unit/test_mcp_model_guards.py#L9-L14: Mirror the properties dict check in the test helper.
  • core/wren/tests/unit/test_mcp_model_guards.py#L26-L30: Mirror the columns list check in the test helper.
🛡️ Proposed fixes

core/wren/src/wren/mcp_server.py

# Lines 233-238 (list_models fix)
-            description = model.get("description")
-            if description is None:
-                description = (model.get("properties") or {}).get("description")
-            columns = model.get("columns") or []
-            if not isinstance(columns, list):
-                columns = []
+            description = model.get("description")
+            if description is None:
+                props = model.get("properties")
+                if isinstance(props, dict):
+                    description = props.get("description")
+            columns = model.get("columns")
+            if not isinstance(columns, list):
+                columns = []

# Lines 267-278 (describe_model columns fix)
-        columns = []
-        for col in model.get("columns") or []:
-            if not isinstance(col, dict):
-                continue
-            columns.append(
-                {
-                    "name": col.get("name"),
-                    "type": col.get("type"),
-                    "description": col.get("description")
-                    or (col.get("properties") or {}).get("description"),
-                }
-            )
+        columns = []
+        raw_columns = model.get("columns")
+        if isinstance(raw_columns, list):
+            for col in raw_columns:
+                if not isinstance(col, dict):
+                    continue
+                desc = col.get("description")
+                if not desc:
+                    props = col.get("properties")
+                    if isinstance(props, dict):
+                        desc = props.get("description")
+                columns.append(
+                    {
+                        "name": col.get("name"),
+                        "type": col.get("type"),
+                        "description": desc,
+                    }
+                )

# Lines 280-285 (describe_model relationships fix)
-        relationships = []
-        for rel in load_relationships(ctx.project):
-            if not isinstance(rel, dict):
-                continue
-            if name in (rel.get("models") or []):
-                relationships.append(rel)
+        relationships = []
+        raw_rels = load_relationships(ctx.project)
+        if isinstance(raw_rels, list):
+            for rel in raw_rels:
+                if not isinstance(rel, dict):
+                    continue
+                models_list = rel.get("models")
+                if isinstance(models_list, list) and name in models_list:
+                    relationships.append(rel)

core/wren/tests/unit/test_mcp_model_guards.py

# Lines 9-14 (test helper fix)
-        description = model.get("description")
-        if description is None:
-            description = (model.get("properties") or {}).get("description")
-        columns = model.get("columns") or []
-        if not isinstance(columns, list):
-            columns = []
+        description = model.get("description")
+        if description is None:
+            props = model.get("properties")
+            if isinstance(props, dict):
+                description = props.get("description")
+        columns = model.get("columns")
+        if not isinstance(columns, list):
+            columns = []

# Lines 26-30 (test helper fix)
-    columns = []
-    for col in model.get("columns") or []:
-        if not isinstance(col, dict):
-            continue
-        columns.append({"name": col.get("name")})
+    columns = []
+    raw_columns = model.get("columns")
+    if isinstance(raw_columns, list):
+        for col in raw_columns:
+            if not isinstance(col, dict):
+                continue
+            columns.append({"name": col.get("name")})
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
columns = []
for col in model.get("columns") or []:
if not isinstance(col, dict):
continue
columns.append(
{
"name": col.get("name"),
"type": col.get("type"),
"description": col.get("description")
or (col.get("properties") or {}).get("description"),
}
)
columns = []
raw_columns = model.get("columns")
if isinstance(raw_columns, list):
for col in raw_columns:
if not isinstance(col, dict):
continue
desc = col.get("description")
if not desc:
props = col.get("properties")
if isinstance(props, dict):
desc = props.get("description")
columns.append(
{
"name": col.get("name"),
"type": col.get("type"),
"description": desc,
}
)
📍 Affects 2 files
  • core/wren/src/wren/mcp_server.py#L267-L278 (this comment)
  • core/wren/src/wren/mcp_server.py#L233-L238
  • core/wren/src/wren/mcp_server.py#L280-L285
  • core/wren/tests/unit/test_mcp_model_guards.py#L9-L14
  • core/wren/tests/unit/test_mcp_model_guards.py#L26-L30
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/wren/src/wren/mcp_server.py` around lines 267 - 278, Prevent malformed
scalar YAML values from reaching collection operations: in
core/wren/src/wren/mcp_server.py lines 267-278, require model.get("columns") to
be a list and require col.get("properties") to be a dict before reading
description; in lines 233-238, require model.get("properties") to be a dict
before accessing description; in lines 280-285, require
load_relationships(ctx.project) and rel.get("models") to be lists before
iteration or membership checks. Mirror the properties guard in
core/wren/tests/unit/test_mcp_model_guards.py lines 9-14 and the columns guard
in lines 26-30.

list_models/describe_model assumed every MDL row was a dict. Malformed
context blobs then AttributeError mid-tool call. Skip junk rows.
@Bartok9
Bartok9 force-pushed the fix/mcp-skip-non-dict-models branch from 1beb238 to 14ad14f Compare July 20, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core python Pull requests that update Python code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant