Skip to content

MCP: Fix get_declaration leaking internal Debug representation for Unresolved ancestors#810

Open
bnferguson wants to merge 1 commit into
Shopify:mainfrom
bnferguson:fix-unresolved-ancestor-leak
Open

MCP: Fix get_declaration leaking internal Debug representation for Unresolved ancestors#810
bnferguson wants to merge 1 commit into
Shopify:mainfrom
bnferguson:fix-unresolved-ancestor-leak

Conversation

@bnferguson
Copy link
Copy Markdown

@bnferguson bnferguson commented May 15, 2026

Background

While working with the MCP Claude was complaining about debug output in get_declaration, digging into it it seems that when you have a ancestor that is unindexed (for instance, something defined in a gem), rather than say what the ancestor is rubydex prints the debug info. For instance for a class like class Dog < ActiveRecord::Base; end

you get:

      "ancestors": [{
        "name": "Unresolved(Name { str: Id { value: 13424087348773472403, _marker: PhantomData<...> }, parent_scope: Some(Id { value: 13486303262390686460,
  _marker: PhantomData<...> }), nesting: None, ref_count: 1 })",
        "kind": "Unresolved"
      }]

instead of:

      "ancestors": [{
        "name": "ActiveRecord::Base",
        "kind": "Unresolved"
      }]

The latter seems to make a bit more sense to me but I don't know if perhaps there's reasoning I'm missing for the former. So feel free to close this (obvs) if this is way off base and not in fact a bug or a fix that makes total sense.

Approach

Went with a very simple/minimal fix by reusing Graph::build_concatenated_name_from_name, which already existed in stats/orphan_report.rs for the orphan report. It walks the parent_scope chain off the Name and concatenates each segment into a qualified constant path.

Buuuut to call it from the MCP crate, its visibility moves from pub(crate) to pub.

The architecture docs note that model/graph.rs is where all the Graph-y public bits are so it seems like this my make sense to be moved over there. But in the interest of keeping this PR for a bug that may not even be a bug I kept the fix narrow. Happy to move things around.

There's a slight behavior change with this approach too. Previously, if graph.names().get(name_id) returned None, the ? would short-circuit filter_map and the ancestor would silently vanish from the response. After this change, that case renders as "<unknown>" (the existing fallback inside build_concatenated_name_from_name) and the ancestor stays in the array. TLDR; a missing name is now visible rather than swallowed. That seems preferable to me at first glance but may not be desired.

I do believe old swallowing behavior could be maintained pretty easily.

Testing

Added a regression test:

cargo test -p rubydex-mcp get_declaration_unresolved_ancestor_renders_qualified_name

Also if you want to test manually, index a workspace whose classes inherit from gem-provided base classes (any ActiveRecord model should do), and call get_declaration. You should no longer see the debug output but instead see ActiveRecord::Base

Notes

(For the cross linking context sickos out there [that's me])

`get_declaration` formatted `Ancestor::Partial` entries with `{:?}`,
dumping the internal `Name` struct (`Id { value: ... }`, `PhantomData`,
`ref_count`) into the MCP response. This fires for any class whose
superclass or mixin lives outside the indexed workspace -- e.g. every
Rails base class inheriting from a gem.

Reuse `Graph::build_concatenated_name_from_name` (made `pub`) to walk
the `parent_scope` chain into a readable constant path like
`ActiveRecord::Base`. A missing name lookup now yields `<unknown>` in
the chain rather than silently dropping the ancestor.
@bnferguson bnferguson requested a review from a team as a code owner May 15, 2026 13:07
@bnferguson
Copy link
Copy Markdown
Author

I have signed the CLA!

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