Mapping MCP tool dependencies
May 24, 2026 · 5m read
When you wire up an MCP server, you get a flat list of tools. What you don't get is a map of which tools need to run before which other tools — the dependency graph that an agent runtime actually needs to plan efficiently.
I built MCP Atlas to generate that graph automatically.
The core idea: an edge A → B means that to execute tool B, you may first need to run tool A — either because B requires an identifier that A produces (a direct edge), or because the user supplies a friendly name that A resolves into the value B actually needs (a semantic edge).
Two design choices that matter
Edges attach to a specific parameter, not just the consumer tool. The same producer might satisfy one of a consumer's three required params and not the other two. Conflating tool → tool loses that signal and makes the graph less useful for agent planning.
Slugs are namespaced <server>::<tool>. Every MCP server tends to have its own list, read, and search tools. Without namespacing, identically-named tools across servers collide and the cross-server graph becomes undefined. The namespace also lets the visualization use server identity for color grouping without re-deriving it from the graph structure.
Three extraction layers
Heuristic
The heuristic pass (src/heuristics.ts) looks at each consumer's required params for <noun>_id / <noun>Id shapes, then scans the catalog for producer tools whose slug contains the noun and a producer verb (LIST / GET / SEARCH / FETCH / CREATE / READ / …). Case-insensitive, deterministic, and fast.
The caveat: most MCP servers use snake_case like read_file, list_directory — the param is path, not <noun>_id — so the slug heuristic is near-useless for MCP-native catalogs. It shines on Composio-style SCREAMING_SNAKE_CASE tools (GITHUB_GET_AN_ISSUE → issue_id).
Semantic (LLM)
The semantic pass (src/semantic.ts) makes one LLM call per consumer, supplying the consumer's params, the heuristic edges already found (to avoid duplicates), and the full cross-server catalog. It returns JSON edges. Each returned edge is validated: the from slug must exist in the catalog, the consumes must be a real required param, and duplicates of heuristic edges are dropped.
Provider is auto-detected from the environment — Anthropic, OpenAI, Gemini, or OpenRouter. OpenRouter falls back through :free-tier models so the project runs end-to-end without paid credits.
Bogus-edge filter
The merge stage (src/merge.ts) drops two classes of LLM hallucination:
- Mutation producers. DELETE / UPDATE / PATCH / REMOVE / CLOSE / WRITE / SEND tools sometimes get suggested as producers because their response payload echoes inputs. An agent doesn't delete a resource to discover its ID.
- Self-requiring producers — but only for
<noun>_id/<noun>Idshaped params. For generic MCP param names likepath/query/url, this filter is wrong:list_directorytakes onepathand legitimately returns many distinctpaths. Applying the filter there would nuke most of the chains inside a typical MCP server.
The narrow scope keeps the Composio-style protection while letting MCP-native tools work.
Cross-server edges
The headline result from the default demo (filesystem + fetch servers):
fetch::fetch → filesystem::write_file [content]— "An agent commonly fetches content from the internet and then saves it to a local file."
This is the kind of edge a heuristic can't find (neither tool has an <noun>_id param) but the semantic pass surfaces reliably. Cross-server edges are what make the graph useful beyond a single-server scope.
The default demo produces 15 nodes · 44 edges (0 direct, 44 semantic) with 1 of 18 required params being user-supplied.
The cartographic visualization
The graph is rendered as plain SVG drawn by hand, with dagre used only to compute the layout. Nodes are colored by server identity, edges by type (direct vs semantic vs manual). The visual metaphor is a scientific atlas — each server is a territory, edges are routes between them.
The layout runs client-side with a pre-computed graph.json (the canonical output of the build pipeline), so the page works as a static file without a server.
Caching and reproducibility
Per-consumer LLM responses are cached to data/cache/semantic/<slug>.json. Re-running the semantic pass after a network failure picks up exactly where it left off. The cache is keyed by consumer slug — change the prompt or the catalog and clear it.
Manual edges (data/edges.manual.json, optional) can assert specific chains the LLM consistently misses, treated like semantic edges but exempt from the bogus-edge filter.
What I'd do with more time
The current graph is one-hop. Precomputing multi-hop chains ("shortest path from any producer to slot X") would speed up agent planning. The heuristic could also be upgraded to output-schema-based direct matching if MCP's tools/list ever includes an output schema — right now it doesn't.
HTTP/SSE transport is also missing; only stdio is wired. The MCP SDK ships both; adding the second is mechanical but was out of scope for the initial release.
The live demo is at the MCP Atlas CreateOS deployment and the source is on GitHub.