<?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: Sravya Dangeti</title>
    <description>The latest articles on DEV Community by Sravya Dangeti (@sravya_dangeti).</description>
    <link>https://dev.to/sravya_dangeti</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%2F3270311%2Fb3744650-227f-4fe3-8207-ea92e54289ac.png</url>
      <title>DEV Community: Sravya Dangeti</title>
      <link>https://dev.to/sravya_dangeti</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sravya_dangeti"/>
    <language>en</language>
    <item>
      <title>Robot-Use Agents: Why the Harness Matters More Than the Model</title>
      <dc:creator>Sravya Dangeti</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:42:20 +0000</pubDate>
      <link>https://dev.to/sravya_dangeti/robot-use-agents-why-the-harness-matters-more-than-the-model-2j3c</link>
      <guid>https://dev.to/sravya_dangeti/robot-use-agents-why-the-harness-matters-more-than-the-model-2j3c</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;General-purpose LLM agents can now control robots with little or no robot-specific training. The hard engineering work is moving from the model to the harness around it: the tools it can call, the checks that run before it acts, and the evals that tell you whether a change helped.&lt;/p&gt;

&lt;p&gt;I watched YC's episode &lt;a href="https://www.youtube.com/watch?v=Jv5B5CEaPJI" rel="noopener noreferrer"&gt;Robot-Use Agents: Why General-Purpose Models May Win in Robotics&lt;/a&gt; with the founders of &lt;a href="https://www.waddlelabs.ai/" rel="noopener noreferrer"&gt;Waddle Labs&lt;/a&gt; and &lt;a href="https://robocurve.org/" rel="noopener noreferrer"&gt;RoboCurve&lt;/a&gt;. Below are the four ideas that stayed with me, then a small harness you can run locally. I have built an MCP based control layer that puts a quadruped and a humanoid behind one tool interface, so this maps closely to my own work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;The episode builds on MIT professor Phillip Isola's idea of robot-use agents: general-purpose models that control different robots, write policies, and learn new physical tasks with little or no robot-specific training.&lt;/p&gt;

&lt;p&gt;Earlier approaches such as &lt;a href="https://robotics-transformer2.github.io/" rel="noopener noreferrer"&gt;RT-2&lt;/a&gt; trained a vision-language model to output robot actions instead of text. The guests argue that today's models are strong enough that this kind of fine-tuning is often unnecessary. They can call tools and write code directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ideas worth taking away
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Code as policy
&lt;/h3&gt;

&lt;p&gt;Give the LLM a list of robot functions, such as picking up an object or moving to a pose, and let it write the plan. Google's &lt;a href="https://code-as-policies.github.io/" rel="noopener noreferrer"&gt;Code as Policies&lt;/a&gt; work (2022) showed this can work with only a few examples in the prompt, because the model has already seen so much code that sequences steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Compile what works into skills
&lt;/h3&gt;

&lt;p&gt;An LLM thinking at every step is slow. The guests' approach: when a task repeats, turn it into code or a reusable skill. Keep the model for the points of variation, such as recognising an unfamiliar object or deciding what to do after an action fails.&lt;/p&gt;

&lt;p&gt;They also said frontier LLM latency is improving about 2x per month. Treat that as their claim, not an established figure, but it explains why they expect real-time control to get closer.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Computer-use data may teach spatial reasoning
&lt;/h3&gt;

&lt;p&gt;Dragging a cursor to orbit a CAD model teaches top-down, left-right and near-far. The guests believe this kind of data helps newer models reason about space. One practical tip from the episode: design robot tools so they resemble computer-use tools, and the model does better on physical tasks.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Evals decide what ships
&lt;/h3&gt;

&lt;p&gt;One guest shared an experiment where in-context learning stopped improving after roughly 20 to 40 examples. So lessons from a deployment have to leave the context window: first as skills, eventually as weights. It also means you need evals, or you cannot tell whether a harness change helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small harness you can run
&lt;/h2&gt;

&lt;p&gt;The pattern: the model asks, the harness checks, the adapter acts. This uses &lt;a href="https://gofastmcp.com/" rel="noopener noreferrer"&gt;FastMCP&lt;/a&gt; and a simulated robot, so it runs without hardware.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;harness.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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastMCP&lt;/span&gt;

&lt;span class="n"&gt;mcp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastMCP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;robot-harness&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RobotAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;One adapter per platform. Transport details stay inside.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;move_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SimAdapter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stand-in robot so the harness runs without hardware.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;move_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[sim] moving to (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[sim] stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# Swap SimAdapter for a real adapter (WebRTC, DDS, ROS, and so on).
&lt;/span&gt;&lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RobotAdapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimAdapter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;WORKSPACE&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;x&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="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&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="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;z&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="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;  &lt;span class="c1"&gt;# meters
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;in_workspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;WORKSPACE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;move_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Move the end effector to a target in meters, in the robot base frame.&lt;/span&gt;&lt;span class="sh"&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="nf"&gt;in_workspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rejected: target is outside the allowed workspace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;move_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="nd"&gt;@mcp.tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Stop all motion immediately.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stopped&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;try_it.py&lt;/code&gt; calls the tools the same way an agent would, through a real MCP client:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastmcp&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;harness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;mcp&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&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;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;list_tools&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tools:&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="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;move_to&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;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inside workspace:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;call_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;move_to&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;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;y&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;z&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside workspace:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fastmcp
python try_it.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tools: ['move_to', 'stop']
[sim] moving to (0.2, 0.1, 0.3)
inside workspace: ok
outside workspace: rejected: target is outside the allowed workspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The model never sees the transport.&lt;/strong&gt; It only sees tools. A new robot means a new adapter, not a new agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The safety check lives in code.&lt;/strong&gt; The workspace limit does not depend on the model behaving well, and no prompt can talk its way past it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every tool returns a clear result.&lt;/strong&gt; Failures are visible, which is what you need for recovery steps and for evals.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I would add next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A task suite with pass or fail checks per tool, so every harness change is measured.&lt;/li&gt;
&lt;li&gt;Logging of every tool call and result, so failures can be replayed.&lt;/li&gt;
&lt;li&gt;A promotion path: when the same sequence of calls succeeds three times in a row, save it as a skill and stop asking the model to rediscover it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Physical AI engineering is starting to look a lot like agent engineering. Tool design, safety gates, and evals decide whether a good model becomes a useful robot.&lt;/p&gt;

&lt;p&gt;Which matters more right now, a better model or a better harness? I would like to hear your view in the comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=Jv5B5CEaPJI" rel="noopener noreferrer"&gt;YC: Robot-Use Agents: Why General-Purpose Models May Win in Robotics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.waddlelabs.ai/" rel="noopener noreferrer"&gt;Waddle Labs&lt;/a&gt; and &lt;a href="https://robocurve.org/" rel="noopener noreferrer"&gt;RoboCurve&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://code-as-policies.github.io/" rel="noopener noreferrer"&gt;Code as Policies: Language Model Programs for Embodied Control&lt;/a&gt; (Liang et al., 2022)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://robotics-transformer2.github.io/" rel="noopener noreferrer"&gt;RT-2: Vision-Language-Action Models&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gofastmcp.com/" rel="noopener noreferrer"&gt;FastMCP documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>robotics</category>
      <category>mcp</category>
      <category>agents</category>
    </item>
    <item>
      <title>Firecrawl vs Tavily for an AI agent: 97% vs 82% fact recall, and why retrieval breadth decides grounding</title>
      <dc:creator>Sravya Dangeti</dc:creator>
      <pubDate>Fri, 09 Oct 2026 14:54:33 +0000</pubDate>
      <link>https://dev.to/sravya_dangeti/firecrawl-vs-tavily-for-an-ai-agent-97-vs-82-fact-recall-and-why-retrieval-breadth-decides-2p45</link>
      <guid>https://dev.to/sravya_dangeti/firecrawl-vs-tavily-for-an-ai-agent-97-vs-82-fact-recall-and-why-retrieval-breadth-decides-2p45</guid>
      <description>&lt;p&gt;I run a competitor-research agent: give it a company URL and it finds and verifies that company's competitors. It is framework-free (a raw tool-calling loop on Gemini), and it has a deterministic fabrication check: every page the agent cites must be in the ledger of pages my code actually fetched.&lt;/p&gt;

&lt;p&gt;Retrieval was Tavily only. I added Firecrawl as a second backend behind a provider switch and benchmarked both. Here is what I found, including where Firecrawl lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;10 companies, 20 pages&lt;/strong&gt;, chosen for different site types: JS-heavy apps, docs-heavy developer tools, pricing pages, sparse early-stage startup sites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An answer key of facts checked against each official page&lt;/strong&gt; (pricing tiers, product names, taglines, YC batch). Every fact was audited against the raw HTML with a plain HTTP request, neither provider involved. Corrections are logged in the repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two experiments:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A, retrieval only:&lt;/strong&gt; fetch each page twice, no LLM involved, and score which facts made it into the content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B, end to end:&lt;/strong&gt; run the full agent with model, prompts and temperature held constant.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Experiment A: retrieval quality
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Tavily (basic, as deployed)&lt;/th&gt;
&lt;th&gt;Firecrawl&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fetch success&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fact recall&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;82%&lt;/strong&gt; (56/68)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;97%&lt;/strong&gt; (66/68)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency p50&lt;/td&gt;
&lt;td&gt;250 ms&lt;/td&gt;
&lt;td&gt;1,391 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Median content&lt;/td&gt;
&lt;td&gt;11,735 chars&lt;/td&gt;
&lt;td&gt;16,257 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Firecrawl found more of the facts that matter. It was about 5.6x slower, and by published pricing about 5x the credit cost per page.&lt;/p&gt;

&lt;p&gt;Tavily's advanced extract tier did not help: it returned byte-identical content to basic on 19 of 20 pages, and on one page (docs.firecrawl.dev/billing) it returned a 404 where basic succeeded. I reproduced that with a raw API call outside my code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The boilerplate filter has a real cost
&lt;/h2&gt;

&lt;p&gt;Firecrawl's &lt;code&gt;only_main_content=True&lt;/code&gt; strips navigation and footers. I measured it on and off:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Page&lt;/th&gt;
&lt;th&gt;Boilerplate (on / off)&lt;/th&gt;
&lt;th&gt;Facts found (on / off)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;brickanta.com&lt;/td&gt;
&lt;td&gt;12% / 19%&lt;/td&gt;
&lt;td&gt;1/1 / 1/1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;composio.dev&lt;/td&gt;
&lt;td&gt;10% / 16%&lt;/td&gt;
&lt;td&gt;2/2 / 2/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;composio.dev/pricing&lt;/td&gt;
&lt;td&gt;4% / 18%&lt;/td&gt;
&lt;td&gt;2/2 / 2/2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;notion.com&lt;/td&gt;
&lt;td&gt;13% / 45%&lt;/td&gt;
&lt;td&gt;0/1 / 1/1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It was free on three pages. On notion.com it removed the product names in the navigation menu, which was the one fact I needed. Getting it back meant 45% boilerplate. Usually free, occasionally expensive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Experiment B: retrieval breadth decides grounding
&lt;/h2&gt;

&lt;p&gt;The surprise came from the end-to-end runs. Pooling all modes by how many sources the agent consulted:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Sources consulted&lt;/th&gt;
&lt;th&gt;Runs&lt;/th&gt;
&lt;th&gt;Citations rejected&lt;/th&gt;
&lt;th&gt;Per run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10 or fewer&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;5.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;More than 10&lt;/td&gt;
&lt;td&gt;42&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;0.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every low-source run came from one configuration of mine: Firecrawl search with full-page scraping, capped at 3 results per search to save credits. With fewer sources, the agent searched more, hit its 9-call budget more often, and cited pages it had never retrieved.&lt;/p&gt;

&lt;p&gt;The fabrication check caught every one of those citations. None reached a final report.&lt;/p&gt;

&lt;p&gt;The lesson: starve an agent of sources and it cites pages it never read. That looks like a hallucination problem. It is a retrieval problem.&lt;/p&gt;

&lt;p&gt;One caveat I want to be clear about: all 9 low-source runs used that same 3-result configuration, so breadth and configuration cannot be separated in this data. It shows an association, not isolated causation, and it is a finding about my settings, not about Firecrawl's quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Firecrawl lost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;About 5.6x slower at p50.&lt;/li&gt;
&lt;li&gt;More credits per page and per end-to-end run.&lt;/li&gt;
&lt;li&gt;On brickanta.com it dropped the hero heading and paragraph while keeping YouTube embed chrome, reproducibly.&lt;/li&gt;
&lt;li&gt;Worse boilerplate outliers (23% max vs 15%).&lt;/li&gt;
&lt;li&gt;The Notion navigation trade-off above.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Also worth knowing
&lt;/h2&gt;

&lt;p&gt;Running Tavily search with Firecrawl scraping (my "hybrid" mode) cost the same as pure Tavily and gave no measurable benefit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone choosing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If fact coverage on messy real-world pages matters most, Firecrawl is worth the latency and cost.&lt;/li&gt;
&lt;li&gt;If you need speed and low cost, Tavily basic is strong.&lt;/li&gt;
&lt;li&gt;Either way, give your agent enough sources per search. Breadth mattered more for grounding than the vendor did.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;10 companies, 20 pages: a small sample.&lt;/li&gt;
&lt;li&gt;End-to-end runs span two days, so sites may have changed in between.&lt;/li&gt;
&lt;li&gt;Some runs were excluded because of Gemini free-tier quota limits. The exclusions are counted per mode in the results.&lt;/li&gt;
&lt;li&gt;Tavily's cost comparison uses published pricing, not measured usage.&lt;/li&gt;
&lt;li&gt;Answer-key wording was corrected after an audit against raw HTML, before the final run. Every change is logged.&lt;/li&gt;
&lt;li&gt;I scraped firecrawl.dev with Firecrawl itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;Method, raw results, answer key and change log are all in the repo:&lt;br&gt;
&lt;a href="https://github.com/sravya520/competitor-research-agent/blob/main/docs/firecrawl_vs_tavily.md" rel="noopener noreferrer"&gt;https://github.com/sravya520/competitor-research-agent/blob/main/docs/firecrawl_vs_tavily.md&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome, especially from anyone who has tuned Firecrawl or Tavily settings for agent workloads.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How I connected an AI agent to GitHub with Nango and MCP (without touching a single OAuth token) published: false tags: ai, mcp, python, tutorial</title>
      <dc:creator>Sravya Dangeti</dc:creator>
      <pubDate>Mon, 28 Sep 2026 16:16:28 +0000</pubDate>
      <link>https://dev.to/sravya_dangeti/how-i-connected-an-ai-agent-to-github-with-nango-and-mcp-without-touching-a-single-oauth-token-14j1</link>
      <guid>https://dev.to/sravya_dangeti/how-i-connected-an-ai-agent-to-github-with-nango-and-mcp-without-touching-a-single-oauth-token-14j1</guid>
      <description>&lt;p&gt;Every time you connect an AI agent to an external API, you inherit the boring, risky part: OAuth flows, token storage, token refresh, and making sure nothing leaks.&lt;/p&gt;

&lt;p&gt;In this tutorial I built a small MCP server in Python that exposes GitHub to any MCP client, where &lt;strong&gt;my code never sees a GitHub token&lt;/strong&gt;. Nango handles the auth. My server only knows a Nango secret key and a connection ID.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/sravya520/nango-github-mcp" rel="noopener noreferrer"&gt;https://github.com/sravya520/nango-github-mcp&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MCP client  -&amp;gt;  my MCP server (Python)  -&amp;gt;  Nango proxy (adds GitHub auth)  -&amp;gt;  GitHub API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The server exposes three tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_my_repos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists my repos, most recently updated first&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_open_issues&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists open issues in a repo (skips pull requests)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;create_issue&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Creates an issue in a repo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 1: Connect GitHub in Nango
&lt;/h2&gt;

&lt;p&gt;Nango's getting-started flow creates a GitHub integration (&lt;code&gt;github-getting-started&lt;/code&gt;) and has you authorize your GitHub account. That gives you a &lt;strong&gt;connection ID&lt;/strong&gt;. From then on, Nango stores and refreshes the GitHub token for that connection.&lt;/p&gt;

&lt;p&gt;Put your values in a &lt;code&gt;.env&lt;/code&gt; file and add &lt;code&gt;.env&lt;/code&gt; to &lt;code&gt;.gitignore&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NANGO_SECRET_KEY=your-secret-key
NANGO_CONNECTION_ID=your-connection-id
NANGO_PROVIDER_CONFIG_KEY=github-getting-started
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Step 2: Call GitHub through Nango's proxy
&lt;/h2&gt;

&lt;p&gt;Instead of calling &lt;code&gt;api.github.com&lt;/code&gt; with a token, you call Nango's proxy with three headers. Nango finds the connection, adds the GitHub token and forwards the request.&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;def nango_request(method, endpoint, params=None, json=None):
    headers = {
        "Authorization": f"Bearer {os.getenv('NANGO_SECRET_KEY')}",
        "Connection-Id": os.getenv("NANGO_CONNECTION_ID"),
        "Provider-Config-Key": os.getenv("NANGO_PROVIDER_CONFIG_KEY"),
    }
    response = httpx.request(
        method, f"https://api.nango.dev/proxy{endpoint}",
        headers=headers, params=params, json=json, timeout=20,
    )
    ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;A one-line smoke test (&lt;code&gt;GET /user&lt;/code&gt;) confirms the whole chain works:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckhbqoqm4m9mgrtguum6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fckhbqoqm4m9mgrtguum6.png" alt="Smoke test: connected to GitHub as sravya520" width="798" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Turn the calls into MCP tools
&lt;/h2&gt;

&lt;p&gt;I used the official MCP Python SDK. One thing that caught me out: in &lt;strong&gt;version 2.x, &lt;code&gt;FastMCP&lt;/code&gt; was renamed to &lt;code&gt;MCPServer&lt;/code&gt;&lt;/strong&gt;, so older tutorials fail on import.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from mcp.server.mcpserver import MCPServer

mcp = MCPServer("github-via-nango")

@mcp.tool(annotations=READ_ONLY)
def list_my_repos(limit: int = 10) -&amp;gt; list[dict]:
    """List the user's GitHub repositories, most recently updated first.
    Use this first to find a repo's full name (owner/name)."""
    repos = nango_request("GET", "/user/repos",
                          params={"sort": "updated", "per_page": _clamp(limit)})
    return [{"full_name": r["full_name"], "url": r["html_url"]} for r in repos]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Three small choices make agents behave better:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The docstring is the instruction.&lt;/strong&gt; The agent reads it to decide when to call the tool. "Use this first to find a repo's full name" helps it chain tools in the right order.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small results.&lt;/strong&gt; Lists are capped at 20 items so the agent's context stays small.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool annotations.&lt;/strong&gt; Read tools are marked read-only and &lt;code&gt;create_issue&lt;/code&gt; is not, so clients can treat writes more carefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4: Make errors readable for the agent
&lt;/h2&gt;

&lt;p&gt;This was my biggest lesson. In the MCP SDK, if a tool raises a normal Python exception, the agent only sees &lt;strong&gt;"Error executing tool"&lt;/strong&gt;. It has no idea what went wrong.&lt;/p&gt;

&lt;p&gt;Only errors raised as &lt;code&gt;ToolError&lt;/code&gt; pass their message through. So my error class extends it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from mcp.server.mcpserver.exceptions import ToolError

class NangoError(ToolError):
    """A readable error the agent can show to the user."""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Now a bad repo name produces an answer the agent can act on (see the screenshot in the next step). I wrote a test that locks this in, plus seven others covering the Nango headers and URL, filtering out pull requests, input validation and error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Test it over real MCP
&lt;/h2&gt;

&lt;p&gt;VS Code is an MCP host. When I added the server, it started it and showed it as Running, with all three tools:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftho2b5yv5xlokhp0rzj2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftho2b5yv5xlokhp0rzj2.png" alt="VS Code showing github-via-nango Running" width="767" height="100"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To test the protocol directly, I wrote a small Python client (&lt;code&gt;client_demo.py&lt;/code&gt; in the repo). It launches the server over stdio, lists the tools, and calls them the way an agent would:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xl7ggb1auxeuh1lbykk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0xl7ggb1auxeuh1lbykk.png" alt="client_demo.py: tools listed and repos returned" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And the error case, where the message comes through clearly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foxqaytatjkxbm0pd6jo4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foxqaytatjkxbm0pd6jo4.png" alt="client_demo.py: readable error for a bad repo name" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I tested, and what I didn't:&lt;/strong&gt; &lt;code&gt;list_my_repos&lt;/code&gt; ran live over MCP, through Nango to GitHub, and the bad-repo error came back over MCP too. &lt;code&gt;list_open_issues&lt;/code&gt; and &lt;code&gt;create_issue&lt;/code&gt; are covered by the unit tests, but I did not run them against my live account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problems I hit (so you don't have to)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;401 Unauthorized from Nango.&lt;/strong&gt; Nango has separate environments, each with its own secret key, so copy the key from the environment where your connection lives. Also check your &lt;code&gt;.env&lt;/code&gt; for stray lines: &lt;code&gt;python-dotenv&lt;/code&gt; warned me about a parse error on line 4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Command not found" in the VS Code MCP setup.&lt;/strong&gt; &lt;code&gt;command&lt;/code&gt; is the program that runs the server (Python). The server file goes in &lt;code&gt;args&lt;/code&gt;. I had typed the server's name into &lt;code&gt;command&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wiring an AI client is the fiddly part.&lt;/strong&gt; Copilot's agent mode was out of quota, and Claude Code's panel did not pick up my server even though &lt;code&gt;claude mcp list&lt;/code&gt; showed it connected. Rather than keep debugging, I tested with the script above. Because the server speaks standard MCP, it should work with any MCP client.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I like this approach
&lt;/h2&gt;

&lt;p&gt;The agent side stays simple: three small tools and clear errors. All the hard auth work (OAuth, storage, refresh) lives in Nango. Adding another API like Notion or Slack would mostly mean a new integration in Nango and a few more tools, not a new auth system.&lt;/p&gt;

&lt;p&gt;Code, tests and setup: &lt;a href="https://github.com/sravya520/nango-github-mcp" rel="noopener noreferrer"&gt;https://github.com/sravya520/nango-github-mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
