<?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: Eshaan Agrawal</title>
    <description>The latest articles on DEV Community by Eshaan Agrawal (@eshaanagrawal).</description>
    <link>https://dev.to/eshaanagrawal</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%2F4007341%2Fece0db01-e244-4b94-8dcf-a35878432bf2.jpg</url>
      <title>DEV Community: Eshaan Agrawal</title>
      <link>https://dev.to/eshaanagrawal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eshaanagrawal"/>
    <language>en</language>
    <item>
      <title>How I Found a Silent API Bug in a 3,548-Star MCP Server</title>
      <dc:creator>Eshaan Agrawal</dc:creator>
      <pubDate>Mon, 29 Jun 2026 05:44:47 +0000</pubDate>
      <link>https://dev.to/eshaanagrawal/how-i-found-a-silent-api-bug-in-a-3548-star-mcp-server-1b55</link>
      <guid>https://dev.to/eshaanagrawal/how-i-found-a-silent-api-bug-in-a-3548-star-mcp-server-1b55</guid>
      <description>&lt;h1&gt;
  
  
  How I Found a Silent API Bug in a 3,548-Star MCP Server
&lt;/h1&gt;

&lt;p&gt;The API accepted your request. Returned HTTP 200. And then quietly did nothing.&lt;/p&gt;

&lt;p&gt;No error. No stack trace. Just an empty result where your Cypher query should have run.&lt;/p&gt;

&lt;p&gt;This is the story of how I found that bug in &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext" rel="noopener noreferrer"&gt;CodeGraphContext&lt;/a&gt; — an MCP server that indexes local codebases into a graph database to give AI assistants real code context. 3,548 stars. Actively maintained. Used by developers building on top of Claude and other LLM toolchains.&lt;/p&gt;

&lt;p&gt;And it had a silent failure sitting in its public HTTP query endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is CodeGraphContext?
&lt;/h2&gt;

&lt;p&gt;Before the bug — quick context.&lt;/p&gt;

&lt;p&gt;CodeGraphContext is an MCP (Model Context Protocol) server. It lets you point it at a codebase, index it into a graph database (FalkorDB or Kuzu), and then query that graph using Cypher — the query language for graph databases. The idea is that AI assistants can use it to understand code structure, dependencies, and relationships at a depth that flat file reads don't give you.&lt;/p&gt;

&lt;p&gt;It exposes both an MCP interface and a standard HTTP API. The HTTP API is what's relevant here.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I Found It
&lt;/h2&gt;

&lt;p&gt;I wasn't hunting for bugs. I was reading the source.&lt;/p&gt;

&lt;p&gt;When I contribute to a new repo, I start by tracing a single request end-to-end through the codebase. Pick an endpoint, follow it from the route definition into the handler, into any downstream calls, and back out. It's the fastest way to actually understand what a codebase does.&lt;/p&gt;

&lt;p&gt;I started with &lt;code&gt;/api/v1/query&lt;/code&gt; — the endpoint that lets you send a Cypher query over HTTP and get results back.&lt;/p&gt;

&lt;p&gt;The route lives in &lt;code&gt;src/codegraphcontext/api/router.py&lt;/code&gt;. Here's what I found at line 89:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route was calling &lt;code&gt;handle_tool_call&lt;/code&gt; with the tool name &lt;code&gt;execute_cypher_query&lt;/code&gt; and passing the query as &lt;code&gt;"query"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then I went to the handler. &lt;code&gt;src/codegraphcontext/tools/handlers/query_handlers.py&lt;/code&gt;, line 16:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cypher_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;cypher_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cypher query cannot be empty.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The handler reads &lt;code&gt;cypher_query&lt;/code&gt;. Not &lt;code&gt;query&lt;/code&gt;. &lt;code&gt;cypher_query&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The route passes &lt;code&gt;query&lt;/code&gt;. The handler reads &lt;code&gt;cypher_query&lt;/code&gt;. They never meet.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;args.get("cypher_query")&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;. The handler hits the empty check. Returns &lt;code&gt;{"error": "Cypher query cannot be empty."}&lt;/code&gt;. HTTP 200.&lt;/p&gt;

&lt;p&gt;A valid request, carrying a real Cypher query, silently failed — not because the query was wrong, but because the key name didn't match across an internal function call boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Is Easy to Miss
&lt;/h2&gt;

&lt;p&gt;The failure mode is the problem.&lt;/p&gt;

&lt;p&gt;If this threw an exception, you'd see a 500. If it returned a 4xx, you'd know the API rejected your input. But it returned 200 with an error payload that looks like a validation error. The natural read is: "I sent a bad query." The actual problem is: the API layer and the tool handler were speaking different languages about what to call the same field.&lt;/p&gt;

&lt;p&gt;Here's the repro from the issue I filed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/api/v1/query &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"query":"MATCH (n) RETURN count(n) AS count","params":{}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Valid Cypher. Valid JSON. Valid HTTP request. But the tool handler sees no &lt;code&gt;cypher_query&lt;/code&gt; key — because the route called it &lt;code&gt;query&lt;/code&gt; — and returns empty.&lt;/p&gt;

&lt;p&gt;The request body looks correct at the API boundary. The failure happens after the internal translation. That's what makes it a confusing debugging path. You'd inspect the request, see a valid payload, and have no obvious reason to look at the key name being passed between the route and the handler.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix
&lt;/h2&gt;

&lt;p&gt;One line change in &lt;code&gt;router.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Before
&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# After
&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;execute_cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change the key from &lt;code&gt;"query"&lt;/code&gt; to &lt;code&gt;"cypher_query"&lt;/code&gt; so it matches what the handler expects.&lt;/p&gt;

&lt;p&gt;I kept the fix narrow — just aligning the key name, no refactoring, no schema changes. Then added a regression test in &lt;code&gt;tests/unit/api/test_query_router.py&lt;/code&gt; that verifies a POST to &lt;code&gt;/api/v1/query&lt;/code&gt; reaches &lt;code&gt;execute_cypher_query&lt;/code&gt; with the correct key. So this can't silently break again.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Added: The Regression Test
&lt;/h2&gt;

&lt;p&gt;This is the part I'd push anyone fixing a bug like this to do.&lt;/p&gt;

&lt;p&gt;The fix itself is obvious once you see it. What's not obvious is that without a test, the exact same mismatch can come back — someone refactors the handler to read a different key, or the route gets updated without checking what the handler expects, and you're back to square one.&lt;/p&gt;

&lt;p&gt;The test mocks &lt;code&gt;server.handle_tool_call&lt;/code&gt; and asserts that after a valid POST to &lt;code&gt;/api/v1/query&lt;/code&gt;, the mock is called with &lt;code&gt;"cypher_query"&lt;/code&gt; in the arguments dict. Small test, precise assertion, catches exactly this class of regression.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_query_route_passes_cypher_query_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mock_server&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/v1/query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MATCH (n) RETURN count(n)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;params&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;call_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mock_server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;handle_tool_call&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_args&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cypher_query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;call_args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Broader Pattern
&lt;/h2&gt;

&lt;p&gt;This is a specific instance of a bug pattern that shows up everywhere in layered systems: &lt;strong&gt;argument name drift&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You have a public interface (the HTTP route) that translates external input into an internal call. The public interface uses one name (&lt;code&gt;query&lt;/code&gt;). The internal interface expects another (&lt;code&gt;cypher_query&lt;/code&gt;). Both sides are internally consistent. The mismatch only exists at the translation boundary — and unless someone traces the full call path, it's invisible.&lt;/p&gt;

&lt;p&gt;It's especially common when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An internal tool or handler is defined separately from the route that calls it&lt;/li&gt;
&lt;li&gt;The field names are semantically similar but not identical (&lt;code&gt;query&lt;/code&gt; vs &lt;code&gt;cypher_query&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The failure mode is a validation error rather than an exception (so it looks like bad input, not a broken translation)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix is always the same: trace the call, align the names, add a test that pins the contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PR #1025: &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext/pull/1025" rel="noopener noreferrer"&gt;fix(api): pass cypher query argument to query handler&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Issue #1023: &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext/issues/1023" rel="noopener noreferrer"&gt;bug: HTTP query route passes wrong argument to execute_cypher_query&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/CodeGraphContext/CodeGraphContext" rel="noopener noreferrer"&gt;CodeGraphContext/CodeGraphContext&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;I also filed and opened PR #1026 on the same repo — the MCP SSE module was importing &lt;code&gt;mcp.server&lt;/code&gt; and &lt;code&gt;mcp.server.sse&lt;/code&gt; without declaring &lt;code&gt;mcp&lt;/code&gt; as a runtime dependency in &lt;code&gt;pyproject.toml&lt;/code&gt;. Clean installs could fail at import time with a &lt;code&gt;ModuleNotFoundError&lt;/code&gt; on a code path the project advertises. That one's a writeup for another day.&lt;/p&gt;

&lt;p&gt;If you're building on MCP or contributing to open source Python projects — read the source end-to-end before you start. One pass through a real request flow teaches you more about a codebase than reading the README three times.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This fix was part of my GSSoC 2026 contributions. I'm currently ranked #50 globally (S Tier, top 1% of 43,587 contributors) across 14 repos. Writing about the bugs that actually taught me something.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>mcp</category>
    </item>
  </channel>
</rss>
