<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: yash161004</title>
    <description>The latest articles on DEV Community by yash161004 (@yash161004).</description>
    <link>https://dev.to/yash161004</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4053798%2F1d4f6889-0f60-44f2-a9bf-0fea14b98e24.jpg</url>
      <title>DEV Community: yash161004</title>
      <link>https://dev.to/yash161004</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yash161004"/>
    <language>en</language>
    <item>
      <title>Why Cosine Similarity Fails to Catch Confusable MCP Tools</title>
      <dc:creator>yash161004</dc:creator>
      <pubDate>Wed, 29 Jul 2026 19:08:34 +0000</pubDate>
      <link>https://dev.to/yash161004/why-cosine-similarity-fails-to-catch-confusable-mcp-tools-23ge</link>
      <guid>https://dev.to/yash161004/why-cosine-similarity-fails-to-catch-confusable-mcp-tools-23ge</guid>
      <description>&lt;p&gt;MCP (Model Context Protocol) servers expose tool definitions — names,&lt;br&gt;
descriptions, JSON schemas — that an LLM agent reads at runtime to decide what&lt;br&gt;
to call. When a server declares several tools with overlapping capabilities&lt;br&gt;
(&lt;code&gt;read_file&lt;/code&gt;, &lt;code&gt;read_text_file&lt;/code&gt;, &lt;code&gt;read_media_file&lt;/code&gt;), agents frequently pick the&lt;br&gt;
wrong one or construct invalid arguments. As tool counts grow, catching&lt;br&gt;
confusable definitions before deployment becomes a real quality and safety&lt;br&gt;
problem.&lt;/p&gt;

&lt;p&gt;The obvious fix is text similarity: vectorize the descriptions, compute&lt;br&gt;
cosine similarity, flag anything above a threshold. I ran that against real&lt;br&gt;
data and it doesn't work — here's the empirical case for why, and what&lt;br&gt;
worked instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The naive approach
&lt;/h2&gt;

&lt;p&gt;TF-IDF vectorize each tool description, compute pairwise cosine similarity,&lt;br&gt;
flag pairs above ~0.85.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it fails
&lt;/h2&gt;

&lt;p&gt;Tested against the 14 tools of the official&lt;br&gt;
&lt;code&gt;@modelcontextprotocol/server-filesystem&lt;/code&gt; server — 91 distinct pairs.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero detections at the standard threshold.&lt;/strong&gt; At 0.85 cosine similarity,
TF-IDF flagged 0 of 91 pairs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No threshold works.&lt;/strong&gt; Lowering it doesn't help — confusable pairs
(&lt;code&gt;read_file&lt;/code&gt; vs. &lt;code&gt;read_text_file&lt;/code&gt;) and clearly distinct pairs (&lt;code&gt;read_file&lt;/code&gt;
vs. &lt;code&gt;write_file&lt;/code&gt;) produce overlapping similarity distributions. There's no
cut point that separates them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two reasons this collapses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Shared domain vocabulary dominates.&lt;/strong&gt; Tools on the same server repeat
the same nouns (&lt;code&gt;path&lt;/code&gt;, &lt;code&gt;directory&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;permissions&lt;/code&gt;) regardless of
whether they're confusable. High term overlap reflects the server's
domain, not tool ambiguity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Opposing verbs barely move the needle.&lt;/strong&gt; &lt;code&gt;read_file&lt;/code&gt; and &lt;code&gt;write_file&lt;/code&gt;
share 80%+ of their tokens. Cosine similarity treats &lt;code&gt;read&lt;/code&gt; vs. &lt;code&gt;write&lt;/code&gt; as
one differing word among many — even though it's the whole difference
that matters.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What worked: gate on schema substitutability first
&lt;/h2&gt;

&lt;p&gt;Instead of scoring text first, gate on structure first: &lt;strong&gt;can one tool's&lt;br&gt;
arguments satisfy the other tool's schema?&lt;/strong&gt; Two schemas are substitutable if&lt;br&gt;
every required property in A exists in B with a compatible type, and neither&lt;br&gt;
has a required parameter that would immediately break the other. If the&lt;br&gt;
schemas aren't substitutable, an agent literally can't confuse the two calls&lt;br&gt;
— so those pairs are discarded before any text comparison happens.&lt;/p&gt;

&lt;p&gt;On the filesystem server, this structural gate removes 63 of 91 pairs before&lt;br&gt;
scoring starts.&lt;/p&gt;

&lt;p&gt;For the remaining 28, score on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;name affinity (edit distance, shared prefix/suffix)&lt;/li&gt;
&lt;li&gt;description similarity (non-domain token overlap)&lt;/li&gt;
&lt;li&gt;a hard veto if names or descriptions contain opposing verbs
(&lt;code&gt;read&lt;/code&gt;/&lt;code&gt;write&lt;/code&gt;, &lt;code&gt;create&lt;/code&gt;/&lt;code&gt;delete&lt;/code&gt;, &lt;code&gt;encrypt&lt;/code&gt;/&lt;code&gt;decrypt&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Four pairs flagged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;read_file&lt;/code&gt; / &lt;code&gt;read_text_file&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read_file&lt;/code&gt; / &lt;code&gt;read_media_file&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read_text_file&lt;/code&gt; / &lt;code&gt;read_media_file&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;list_directory&lt;/code&gt; / &lt;code&gt;list_directory_with_sizes&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their scores sit between 0.42–0.48, cleanly separated from every non-flagged&lt;br&gt;
pair (0.00–0.32) — a real 0.33–0.50 gap between the two classes, which cosine&lt;br&gt;
similarity alone never produced.&lt;/p&gt;

&lt;p&gt;The full 91-pair dataset — raw cosine scores, substitutability decisions,&lt;br&gt;
affinity scores, veto flags — is public:&lt;br&gt;
&lt;a href="https://github.com/yash161004/mcplock/blob/master/docs/data/filesystem-server-ambiguity-scan.json" rel="noopener noreferrer"&gt;&lt;code&gt;docs/data/filesystem-server-ambiguity-scan.json&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single server.&lt;/strong&gt; This evaluation covers one server, 14 tools, 91 pairs.
Whether this generalizes to other MCP servers is untested.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The verb veto is a heuristic, not a proof.&lt;/strong&gt; It's tuned to common CRUD
antonyms and will miss scope-based ambiguity that isn't verb-opposed —
e.g., &lt;code&gt;read_file&lt;/code&gt; vs. a hypothetical &lt;code&gt;read_all_files&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This catches documentation gaps, not runtime bugs.&lt;/strong&gt; A flagged pair might
be implemented perfectly safely; an unflagged pair might still have bugs.
This tool audits what the agent is told, not what the server actually does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;This is implemented in &lt;a href="https://github.com/yash161004/mcplock" rel="noopener noreferrer"&gt;&lt;code&gt;mcplock&lt;/code&gt;&lt;/a&gt;, an&lt;br&gt;
open-source CLI (&lt;code&gt;pip install mcplock&lt;/code&gt;) that runs this as a lint check&lt;br&gt;
against any MCP server, plus a separate hash-based baseline/diff mode for&lt;br&gt;
catching drift after the fact. Feedback on the substitutability approach —&lt;br&gt;
especially against other MCP servers — is genuinely useful; that's the main&lt;br&gt;
open question right now.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>machinelearning</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
