<?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: joshyfruit</title>
    <description>The latest articles on DEV Community by joshyfruit (@joshyfruit).</description>
    <link>https://dev.to/joshyfruit</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%2F3239425%2Fd6f67ac4-5c23-42b7-bf4f-7a26435dd76f.jpeg</url>
      <title>DEV Community: joshyfruit</title>
      <link>https://dev.to/joshyfruit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/joshyfruit"/>
    <language>en</language>
    <item>
      <title>Release Radar Part 2:Deploying to Bedrock AgentCore</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Tue, 08 Sep 2026 07:10:17 +0000</pubDate>
      <link>https://dev.to/joshyfruit/deploying-to-bedrock-agentcore-385d</link>
      <guid>https://dev.to/joshyfruit/deploying-to-bedrock-agentcore-385d</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/joshyfruit/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated-dependencies-eh0"&gt;Part 1&lt;/a&gt;, we have built &lt;strong&gt;&lt;a href="https://github.com/coozgan/release-radar.git" rel="noopener noreferrer"&gt;Release Radar&lt;/a&gt;&lt;/strong&gt;, an agent that audits pinned dependencies. It catches three failures that version numbers miss: renamed repos, archived projects, and release notes that say "This package/modeule is deprecated and will reach End-of-Life."&lt;/p&gt;

&lt;p&gt;Last time, we made it work on our laptop. This time, we will run it on our own infrastructure. So, we might think that we need to deploy this on Lambda or through EKS, but AWS made deployment easy through AgentCore.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/agentcore-get-started-cli.html" rel="noopener noreferrer"&gt;AWS Bedrock AgentCore&lt;/a&gt;&lt;/strong&gt; is a managed service that can deploy your local Agent code (Strands, LangGraph, or CrewAI) to scale in production without requiring custom backend plumbing. AgentCore handles hosting, scaling, IAM, and observability.&lt;/p&gt;

&lt;p&gt;So instead of configuring through our web console on a Lambda function and exposing it through an API Gateway, AgentCore manages it for us via the CLI.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 250MB fork
&lt;/h2&gt;

&lt;p&gt;Package size sets the pace of every deploy after the first one.&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;agentcore deploy&lt;/code&gt;, the CLI takes one of two paths:&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%2Fipykq137pombgyl15s99.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%2Fipykq137pombgyl15s99.png" alt="Deployment Diagram" width="800" height="151"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under 250MB, the CLI packages your code, pushes it to an S3 bucket, and points the runtime at the asset. Installation takes seconds; then it builds a container and manages the runtime.&lt;/p&gt;

&lt;p&gt;Release Radar has only two dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"release-radar"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="s"&gt;"bedrock-agentcore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s"&gt;"strands-agents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Strands also ships &lt;code&gt;strands-agents-tools&lt;/code&gt;, a companion package with a large collection of prebuilt tools. I left it out. The three tools in this project take forty lines of standard-library &lt;code&gt;urllib&lt;/code&gt;, and keeping the extra package out leaves Release Radar on the fast path.&lt;/p&gt;

&lt;p&gt;Native wheels tend to push packages over the limit: numpy, pandas, torch, and anything else with compiled extensions. Sometimes you need them. Just account for the slower deployment loop when you do.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust boundary
&lt;/h2&gt;

&lt;p&gt;Once deployed, a request follows this path:&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%2F8wohd3y5883px2i1u33b.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%2F8wohd3y5883px2i1u33b.png" alt="Agentcore Request flow" width="800" height="759"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Validation happens at the entry point, before the model receives anything:&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;bedrock_agentcore&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockAgentCoreApp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;repo_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest_release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version_gap&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&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;handler&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;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&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;prompt&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt must be a non-empty string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest_release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;],&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;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;

&lt;span class="n"&gt;app&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;That's four lines of validation. AWS's SDK docs give the same direct advice: validate agent input before handing it to a framework, require prompts to be strings, and send only the prompt text to the agent.&lt;/p&gt;

&lt;p&gt;Release Radar needs this boundary because its job is to read text written by other people. Repository descriptions and release notes are attacker-writable. Someone can publish release notes containing text that looks like instructions for the agent.&lt;/p&gt;

&lt;p&gt;Once the agent reads untrusted text, that text enters the prompt. Downstream checks cannot undo that, so the entrypoint accepts one validated string and nothing else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;stream_async&lt;/code&gt; then yields chunks as they arrive. An audit of ten dependencies can show results as the agent produces them instead of waiting for the entire run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploy
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore validate
agentcore deploy
agentcore status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first deployment provisions CDK infrastructure and takes a few minutes. Later deployments only replace the S3 asset, which is much faster.&lt;/p&gt;

&lt;p&gt;A shell export stays on your machine. If you set &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in your terminal during local testing, the deployed runtime cannot see it. Put the token in &lt;code&gt;agentcore/.env.local&lt;/code&gt; (which is gitignored), or register it properly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore add credentials    &lt;span class="c"&gt;# stored in Secrets Manager&lt;/span&gt;
agentcore deploy             &lt;span class="c"&gt;# ← the part people forget&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second command matters because &lt;code&gt;agentcore add&lt;/code&gt; and &lt;code&gt;agentcore remove&lt;/code&gt; change only the local configuration. AWS sees no change until you deploy again.&lt;/p&gt;

&lt;p&gt;Config drift between &lt;code&gt;agentcore.json&lt;/code&gt; and the deployed stack is the most common cause of "but it works locally."&lt;/p&gt;

&lt;h2&gt;
  
  
  Invoke, then read the trace
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke

agentcore logs              &lt;span class="c"&gt;# stream or search runtime logs&lt;/span&gt;
agentcore traces list       &lt;span class="c"&gt;# recent traces&lt;/span&gt;
agentcore traces get &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;   &lt;span class="c"&gt;# download one as JSON&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read the trace. An agent can produce an answer that looks right without reaching it the right way. The tool-call sequence tells you which one you got.&lt;/p&gt;

&lt;p&gt;For three dependencies, a healthy run makes six to nine tool calls: one &lt;code&gt;repo_status&lt;/code&gt; and one &lt;code&gt;latest_release&lt;/code&gt; per dependency, followed by &lt;code&gt;version_gap&lt;/code&gt; wherever a release exists.&lt;/p&gt;

&lt;p&gt;The trace catches problems that the final output hides:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If there are too few calls, the model probably answered from its training data instead of checking the repositories. The answer may still look plausible. A weak docstring often causes this.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;version_gap&lt;/code&gt; receives an empty tag, &lt;code&gt;latest_release&lt;/code&gt; probably reached a repository that publishes tags instead of releases. That is expected, and the error's &lt;code&gt;hint&lt;/code&gt; field explains it.&lt;/li&gt;
&lt;li&gt;If the calls happen in the wrong order, the model did not follow the system prompt. &lt;code&gt;repo_status&lt;/code&gt; goes first so a rename cannot disappear beneath version chatter. Tighten the prompt if it does.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What breaks
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;command not found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Old toolkit shadowing the new CLI&lt;/td&gt;
&lt;td&gt;Uninstall &lt;code&gt;bedrock-agentcore-starter-toolkit&lt;/code&gt;; &lt;code&gt;which -a agentcore&lt;/code&gt; must return one path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 403&lt;/td&gt;
&lt;td&gt;Anonymous GitHub limit is 60/hr; this spends ~3 per dependency&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; locally &lt;strong&gt;and&lt;/strong&gt; in the runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP 404&lt;/td&gt;
&lt;td&gt;Repo publishes tags not releases, or you queried a renamed path&lt;/td&gt;
&lt;td&gt;Expected — &lt;code&gt;repo_status&lt;/code&gt; still resolves renames&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AccessDenied&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bedrock model access not enabled in that region&lt;/td&gt;
&lt;td&gt;Enable Claude Sonnet in the Bedrock console for your &lt;code&gt;aws-targets.json&lt;/code&gt; region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploy got slow&lt;/td&gt;
&lt;td&gt;Package crossed 250MB&lt;/td&gt;
&lt;td&gt;Drop unused deps; keep native wheels out of &lt;code&gt;app/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools never called&lt;/td&gt;
&lt;td&gt;Docstrings lost in copy&lt;/td&gt;
&lt;td&gt;Every &lt;code&gt;@tool&lt;/code&gt; needs a docstring saying &lt;em&gt;when&lt;/em&gt; to call it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works local, fails deployed&lt;/td&gt;
&lt;td&gt;Env var never left your shell&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agentcore/.env.local&lt;/code&gt; or &lt;code&gt;agentcore add credentials&lt;/code&gt;, then redeploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You'll probably meet the 403 first. At roughly three API calls per dependency, one run over a twenty-dependency manifest uses the entire anonymous allowance of 60 requests per hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn it off
&lt;/h2&gt;

&lt;p&gt;A deployed runtime keeps billing while it exists.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore remove     &lt;span class="c"&gt;# pick resources to drop&lt;/span&gt;
agentcore deploy     &lt;span class="c"&gt;# applies the removal to AWS&lt;/span&gt;
agentcore status     &lt;span class="c"&gt;# confirm nothing is left&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked the full command surface. There is no &lt;code&gt;destroy&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, or &lt;code&gt;delete&lt;/code&gt; command, and &lt;code&gt;deploy&lt;/code&gt; has no destroy flag. The teardown path really is &lt;code&gt;remove&lt;/code&gt; followed by &lt;code&gt;deploy&lt;/code&gt;. To clear everything, use &lt;code&gt;remove all&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore remove all &lt;span class="nt"&gt;-y&lt;/span&gt;    &lt;span class="c"&gt;# "Reset all agentcore schemas to empty state"&lt;/span&gt;
agentcore deploy &lt;span class="nt"&gt;-y&lt;/span&gt;        &lt;span class="c"&gt;# log prints: ✓ Tear down stack&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A clean &lt;code&gt;status&lt;/code&gt; is not enough. Check from outside the CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="nt"&gt;--stack-name&lt;/span&gt; AgentCore-&amp;lt;project&amp;gt;-default
&lt;span class="c"&gt;# → ValidationError: stack does not exist&lt;/span&gt;

aws bedrock-agentcore-control list-agent-runtimes &lt;span class="nt"&gt;--region&lt;/span&gt; &amp;lt;region&amp;gt;
&lt;span class="c"&gt;# → empty&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both checks came back clean after my run. The generated IAM role also returned &lt;code&gt;NoSuchEntity&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I have left pricing figures out. They will age faster than the rest of this post, so check the current Bedrock and AgentCore Runtime rates before leaving resources running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four ways to extend it
&lt;/h2&gt;

&lt;p&gt;Each extension starts with one &lt;code&gt;add&lt;/code&gt; command and requires another &lt;code&gt;deploy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;agentcore add memory&lt;/code&gt; to give the agent last week's verdicts. This is the most useful addition of the four. Instead of reading every result as new, it can report changes such as "3 new BLOCKED since Monday." That produces a report you can skim and act on.&lt;/p&gt;

&lt;p&gt;A web-search gateway can find a migration guide after the agent flags a dependency:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agentcore add gateway-target --type connector --connector web-search --gateway &amp;lt;name&amp;gt; --name &amp;lt;target&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;--exclude-domains&lt;/code&gt; to keep content farms out.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agentcore add evaluator&lt;/code&gt; defines an LLM-as-a-judge, and &lt;code&gt;agentcore run eval&lt;/code&gt; scores it against real traces. Add this once verdicts start driving real decisions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agentcore add policy-engine&lt;/code&gt; applies Cedar policies before and after calls, including prompt-attack detection. That fits this project because the agent reads attacker-writable release notes.&lt;/p&gt;

&lt;p&gt;There is also &lt;code&gt;agentcore add harness&lt;/code&gt;. It puts the runtime, model, tools, memory, and observability into declarative configuration, with no agent code. If you outgrow that setup, &lt;code&gt;agentcore export harness&lt;/code&gt; converts it into a Strands agent. Read the generated &lt;code&gt;EXPORT_NOTES.md&lt;/code&gt; before deploying because it lists anything the exporter could not automate.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I wish I'd known
&lt;/h2&gt;

&lt;p&gt;Docstrings route tool calls. When an agent ignores a tool, a vague docstring is often the problem, not the model.&lt;/p&gt;

&lt;p&gt;The deterministic code still needs tests. I shipped a real version-comparison bug, and Part 1 has the autopsy. Five assertions found it in thirty seconds. A model may make part of the system fuzzy, but arithmetic is still arithmetic.&lt;/p&gt;

&lt;p&gt;Run the agent locally before deploying it. &lt;code&gt;agentcore dev&lt;/code&gt; uses the same entrypoint against real Bedrock on your machine, so a bad tool signature fails in seconds rather than after an upload.&lt;/p&gt;

&lt;p&gt;Part 1 originally said, "I have not run this end-to-end against live AWS." That is no longer true. I deployed Release Radar to a real account, invoked it, tore it down, and verified the teardown outside the CLI. The deployment details above came from that run.&lt;/p&gt;

&lt;p&gt;The run also forced four corrections. The most visible one is the scaffold from &lt;code&gt;agentcore create&lt;/code&gt;, which is much richer than the version shown in Part 1. It has a two-argument entrypoint, a different import path, a generated model loader, and five dependencies instead of two. Part 1 shows the version you would write by hand because it makes the shape easier to learn. When working from the scaffold, modify the generated file instead of replacing it.&lt;/p&gt;

&lt;p&gt;Two more corrections affect how you run it. Bedrock's on-demand model IDs do not invoke in every region, so you need the &lt;code&gt;global.&lt;/code&gt; inference profile. The CLI handles that for you. Automation also needs explicit flags because bare &lt;code&gt;agentcore deploy&lt;/code&gt; refuses to run without a TTY.&lt;/p&gt;

&lt;p&gt;Deployment enables transaction search automatically. It takes about ten minutes to index and adds a cost I had not accounted for.&lt;/p&gt;

&lt;p&gt;If you build this and find another mistake, I want to hear about it. Corrections are the point of this project.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>aws</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Release Radar Part 1: The tutorials moved faster than I did</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Thu, 03 Sep 2026 06:08:52 +0000</pubDate>
      <link>https://dev.to/joshyfruit/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated-dependencies-eh0</link>
      <guid>https://dev.to/joshyfruit/the-tutorials-moved-faster-than-i-did-building-an-agent-that-catches-deprecated-dependencies-eh0</guid>
      <description>&lt;p&gt;I set out to learn Amazon Bedrock AgentCore, AWS’s managed runtime for AI agents. So the plan is to find a few tutorials, follow one, and deploy something.&lt;/p&gt;

&lt;p&gt;Instead, I found the project I ended up building.&lt;/p&gt;

&lt;p&gt;Several walkthroughs teach &lt;code&gt;bedrock-agentcore-starter-toolkit&lt;/code&gt;. The package now describes itself this way:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Python CLI toolkit for Amazon Bedrock AgentCore &lt;strong&gt;(legacy)&lt;/strong&gt;. For new projects, use the AgentCore CLI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;No one did anything wrong. AWS released a replacement, &lt;code&gt;@aws/agentcore&lt;/code&gt;, and clearly marked the old package as legacy. The walkthroughs were accurate when their authors published them. The tooling progressed more quickly, which happens often in this ecosystem.&lt;/p&gt;

&lt;p&gt;Some current AWS guides teach the new CLI. The stale material includes the old toolkit’s own docs site, which is still online and easy to reach, plus third-party posts. No single source is wrong, so there is no single fix. You have to check what you are installing.&lt;/p&gt;

&lt;p&gt;So I found a use case. &lt;code&gt;strands-agents/sdk-python&lt;/code&gt;, the agent framework’s repository, had been renamed to &lt;code&gt;strands-agents/harness-sdk&lt;/code&gt;. &lt;em&gt;hmmmm! 🤔&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You can see the redirect here:&lt;/p&gt;

&lt;blockquote&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%2Fkb3xrhzchdfwjwjymh6u.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%2Fkb3xrhzchdfwjwjymh6u.png" alt="Terminal showing a curl request to the GitHub API for strands-agents/sdk-python returning HTTP status 301" width="657" height="143"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The response is &lt;strong&gt;301 Moved Permanently&lt;/strong&gt;. Follow it, and you land on &lt;code&gt;harness-sdk&lt;/code&gt;. Nothing breaks. Old links still resolve because that is what a 301 is for. &lt;code&gt;pip install strands-agents&lt;/code&gt; also works because the package name did not change, only the repository did. The PyPI listing already points to the new URL. GitHub and PyPI are both doing the helpful thing.&lt;/p&gt;

&lt;p&gt;That is why the rename is easy to miss. Every signal you would normally check looks fine.&lt;/p&gt;

&lt;p&gt;I caught both cases only because I checked the repositories instead of trusting what I had read. If I had followed a tutorial as written, I would have installed a legacy CLI and spent an hour wondering why a command didn't exist. That gave me something worth building.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gap this sits in
&lt;/h2&gt;

&lt;p&gt;Tools already exist for outdated dependencies: Dependabot, Renovate, &lt;code&gt;npm outdated&lt;/code&gt;, and &lt;code&gt;pip list --outdated&lt;/code&gt;. They compare the version you pinned with the latest version, then report the difference.&lt;/p&gt;

&lt;p&gt;That works well for version gaps, but it misses three other problems:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;What the version number shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The repo was &lt;strong&gt;renamed&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Nothing. Your old name still resolves.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The project was &lt;strong&gt;archived&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Nothing. The last version is still the latest version.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The notes say &lt;strong&gt;“deprecated, migrate to X”&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Nothing. That’s prose, in a release body.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Number comparison cannot detect any of these. The evidence is written in plain English on the repository page. In the third case, someone has written a paragraph asking you to stop using the dependency.&lt;/p&gt;

&lt;p&gt;Number comparison cannot read. An agent can. “Read this page and tell me if it says anything alarming” is a good task for a language model and a poor one for a regex.&lt;/p&gt;

&lt;p&gt;I called the project &lt;strong&gt;Release Radar&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;You give it a list of pinned dependencies. For each one, it finds the repository, reads its description and latest release notes, checks the version distance, and returns one of three verdicts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BLOCKED&lt;/strong&gt;: renamed, archived, or described as deprecated in the repository description or release notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BEHIND&lt;/strong&gt;: a real version gap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OK&lt;/strong&gt;: current or only trivially behind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole product. I kept it small enough to finish in an afternoon, but useful enough to catch a real problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;p&gt;Two parts of the stack are easy to confuse:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Strands Agents&lt;/strong&gt; is the framework, including the loop that decides which tool to use next. It runs anywhere, including your laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bedrock AgentCore&lt;/strong&gt; is AWS’s managed runtime. It is where the agent lives when you want it to run on a schedule instead of only when you remember to start it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just to let you know, this is a 2-part series;&lt;br&gt;
1️⃣ This activity (this post you are reading) builds the agent and checks its deterministic function logic locally. &lt;br&gt;
2️⃣ The next post moves the complete agent into AgentCore.&lt;/p&gt;

&lt;p&gt;First, remove the conflicting CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip uninstall bedrock-agentcore-starter-toolkit
&lt;span class="c"&gt;# Or depending on how you installed it: &lt;/span&gt;
&lt;span class="c"&gt;# pipx uninstall&lt;/span&gt;
&lt;span class="c"&gt;# uv tool uninstall&lt;/span&gt;

npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws/agentcore &lt;span class="c"&gt;#install agentcore&lt;/span&gt;

which &lt;span class="nt"&gt;-a&lt;/span&gt; agentcore  &lt;span class="c"&gt;# Must print exactly ONE path.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not skip &lt;code&gt;which -a&lt;/code&gt;. If it prints two paths, the old CLI is shadowing the new one. Every confusing error that follows will trace back to that conflict.&lt;/p&gt;

&lt;p&gt;Next, scaffold the project. The wizard asks for a framework and a model provider. Choose &lt;strong&gt;Strands Agents&lt;/strong&gt; and &lt;strong&gt;Amazon Bedrock&lt;/strong&gt;. Bedrock is the only provider in the wizard that does not require an API key because it uses your existing AWS credentials. Anthropic, Google Gemini, and OpenAI each require a key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore create  &lt;span class="c"&gt;# Name it: release-radar&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;release-radar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command creates two directories: configuration in &lt;code&gt;agentcore/&lt;/code&gt; and your code in &lt;code&gt;app/&lt;/code&gt;. You only need to edit &lt;code&gt;app/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the tools
&lt;/h2&gt;

&lt;p&gt;A Strands tool is a regular Python function with a decorator. Its docstring is routing logic, not ordinary documentation.&lt;/p&gt;

&lt;p&gt;The model reads the docstring to decide whether to call the function. If it is vague, the tool may never run, leaving you with a correct-looking agent that ignores half its capabilities. Write docstrings for the model rather than for a future maintainer.&lt;/p&gt;

&lt;p&gt;The second paragraph below does most of the routing work:&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;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.error&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;

&lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com&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;_get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;GET a GitHub API path. Never raises—errors come back as data.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;API&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&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;Accept&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;application/vnd.github+json&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;User-Agent&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;release-radar&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="c1"&gt;# Lifts the anonymous rate limit from 60/hr to 5000/hr.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&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;GITHUB_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&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;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="si"&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&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;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;


&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;repo_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Look up a GitHub repository&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s current identity and health.

    Returns the canonical full_name, which differs from the requested
    owner/repo when the project has been renamed. Also reports whether
    the repo is archived and when it was last pushed to. Call this
    FIRST for every dependency—a rename or archive matters more than
    any version gap.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;asked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&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;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get&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;/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&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;repo&lt;/span&gt;&lt;span class="si"&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;if&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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&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;requested&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asked&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="n"&gt;d&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="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;requested&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;canonical&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_name&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;renamed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full_name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;archived&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;archived&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;description&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;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;280&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_push&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pushed_at&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;stars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stargazers_count&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_get&lt;/code&gt; never raises. It returns errors as &lt;code&gt;{"_error": ...}&lt;/code&gt; data. When a tool throws, it kills the agent’s turn. When it returns an error string, the model can say “that one 404’d” and continue. The failure becomes data instead of control flow.&lt;/p&gt;

&lt;p&gt;So the rename check is one line: &lt;code&gt;d["full_name"].lower() != asked.lower()&lt;/code&gt;. GitHub’s API follows the redirect and returns the canonical name. Ask for &lt;code&gt;strands-agents/sdk-python&lt;/code&gt;, and it returns &lt;code&gt;strands-agents/harness-sdk&lt;/code&gt;. Comparing the two reveals the rename. That one line is why this project exists.&lt;/p&gt;

&lt;p&gt;The second tool fetches release notes. Together with the description returned by &lt;code&gt;repo_status&lt;/code&gt;, this gives the model prose to inspect for words such as &lt;em&gt;deprecated&lt;/em&gt;, &lt;em&gt;legacy&lt;/em&gt;, &lt;em&gt;superseded&lt;/em&gt;, and &lt;em&gt;migrate&lt;/em&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="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;latest_release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;owner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fetch the most recent published release for a repository.

    Returns the tag name, publish date, and the opening of the release
    notes. Read the notes for deprecation and breaking-change language.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_get&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;/repos/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&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;repo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/releases/latest&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="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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;d&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;repo&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&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;repo&lt;/span&gt;&lt;span class="si"&gt;}&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;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&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;hint&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;404 here usually means the repo publishes tags, not releases&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="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;repo&lt;/span&gt;&lt;span class="sh"&gt;"&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;owner&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;repo&lt;/span&gt;&lt;span class="si"&gt;}&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;tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tag_name&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;published&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;published_at&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;notes&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;d&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;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This field &lt;code&gt;hint&lt;/code&gt; is intentional because the model reads the error when the tool fails, so the error should explain what probably happened. Many repositories publish tags without releases. A bare &lt;strong&gt;404&lt;/strong&gt; would leave the agent guessing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitation
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;One limitation, GitHub defines “latest” at the repository level. In a monorepo with separate Python and TypeScript release streams, this endpoint may return the newest release for the wrong language. That does not affect a rename or archive verdict, but production code should filter releases by the dependency’s tag prefix.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug I shipped
&lt;/h2&gt;

&lt;p&gt;The third tool compares versions. Most of the code is parsing because version tags come in forms such as &lt;code&gt;1.2.3&lt;/code&gt;, &lt;code&gt;v1.2.3&lt;/code&gt;, and, in Strands’ case, &lt;code&gt;python/v1.54.0&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="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Compare a pinned version against the latest release tag.

    Handles common prefixes (v1.2.3, python/v1.2.3) and returns how many
    major, minor, and patch releases the pin is behind.
    &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;parts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;tuple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rsplit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vV&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;core&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;digits&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;level&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;major&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;minor&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;patch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;behind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;behind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="k"&gt;break&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;pinned&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pinned&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;behind_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;behind&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first attempt at the comparison loop was wrong. Because this function contains the project’s only substantial logic, I added asserts:&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;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="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&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;1.0.0&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;current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;v0.1.0&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;v0.28.1&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;behind_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.2.3&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;2.0.0&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;behind_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;major&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python/v1.53.0&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;python/v1.54.0&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;behind_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;version_gap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0.0&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;1.9.9&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;behind_by&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&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;version_gap ok&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 last assert failed.&lt;/p&gt;

&lt;p&gt;My first version found the earliest position where the latest version exceeded the pinned version:&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;behind&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try that with &lt;code&gt;2.0.0&lt;/code&gt; and &lt;code&gt;1.9.9&lt;/code&gt;. At the major position, &lt;code&gt;1 &amp;gt; 2&lt;/code&gt; is false. At the minor position, &lt;code&gt;9 &amp;gt; 0&lt;/code&gt; is true. The function therefore reports “minor behind” even though the dependency is a full major version ahead.&lt;/p&gt;

&lt;p&gt;The code scans all three positions independently, but version comparison must move from left to right and stop at the first difference. Later positions no longer matter. The corrected &lt;code&gt;for&lt;/code&gt; loop does that with a &lt;code&gt;break&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I could have written the one-liner, decided it looked right, and shipped it. You would only discover the bug when the agent confidently told you to upgrade a dependency you had already upgraded. At that point, you might blame the model instead of the arithmetic.&lt;/p&gt;

&lt;p&gt;Five asserts caught the bug in thirty seconds. The deterministic parts of an agent still need tests. A model in the loop makes some behavior fuzzy, but &lt;code&gt;version_gap&lt;/code&gt; is arithmetic, and arithmetic is easy to check.&lt;/p&gt;

&lt;p&gt;Run the checks before continuing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run python tools.py  &lt;span class="c"&gt;# → version_gap ok&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Wiring the agent
&lt;/h2&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;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;latest_release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repo_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version_gap&lt;/span&gt;

&lt;span class="n"&gt;SYSTEM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You audit pinned software dependencies.

For each `owner/repo@version` the user gives you:

 1. Call repo_status first. If `renamed` is true or `archived` is
    true, that is the headline; report it before anything else.
 2. Read the repository description and latest release notes for
    deprecated, legacy, superseded, migrate, or breaking.
 3. Call version_gap to measure the distance.

Then assign exactly one verdict per dependency:

 BLOCKED  renamed, archived, or description/notes say deprecated/legacy
 BEHIND   major or minor releases behind
 OK       current, or patch-behind only

Output one line per dependency: VERDICT  owner/repo  one-clause reason.

No preamble. If a tool returns an error, say so and move on.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SYSTEM&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;repo_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest_release&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version_gap&lt;/span&gt;&lt;span class="p"&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 prompt sets the tool order, verdict vocabulary, and output format. This will leave those choices open, and the model tends to return three paragraphs of friendly hedging. A line such as &lt;code&gt;BLOCKED aws/foo - archived&lt;/code&gt; is easy to &lt;code&gt;grep&lt;/code&gt; and act on later.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;agentcore create&lt;/code&gt; will generate a richer &lt;code&gt;main.py&lt;/code&gt; than the version above. It will include session caching, MCP wiring, and a two-argument &lt;code&gt;invoke(payload, context)&lt;/code&gt; entry point imported from &lt;code&gt;bedrock_agentcore.runtime&lt;/code&gt;. The shorter version is easier to learn from, but in practice you should modify the generated file rather than replace it. Add &lt;code&gt;tools.py&lt;/code&gt;, add one import, swap the system prompt, and replace the demo tool. That is three edits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Point it at the real thing
&lt;/h2&gt;

&lt;p&gt;Now test the three dependencies from the start of this post:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Audit these:

aws/bedrock-agentcore-starter-toolkit@0.1.0
strands-agents/sdk-python@1.0.0
aws/agentcore-cli@0.28.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last pin was current when I wrote this. Run &lt;code&gt;npm view @aws/agentcore version&lt;/code&gt; to get today’s version. If it has moved, you will get a live &lt;strong&gt;BEHIND&lt;/strong&gt; result alongside the two &lt;strong&gt;BLOCKED&lt;/strong&gt; results.&lt;/p&gt;

&lt;p&gt;This is what the agent returned:&lt;/p&gt;

&lt;blockquote&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%2Focexh0x8jxkagt52tpni.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%2Focexh0x8jxkagt52tpni.png" alt="Terminal output showing two BLOCKED verdicts for the legacy starter toolkit and the renamed Strands repo, and one OK for the current CLI" width="800" height="248"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The response may vary from time to time since LLMs are non-deterministic in nature. Across three runs, the third result said “pinned version is current,” “pinned version matches latest release,” and “current at v0.28.1.” What matters is that the first two results are &lt;strong&gt;BLOCKED&lt;/strong&gt; and their reasons identify the rename and legacy status. If either comes back &lt;strong&gt;OK&lt;/strong&gt;, &lt;code&gt;repo_status&lt;/code&gt; is probably not being called. Check that its docstring survived the copy.&lt;/p&gt;

&lt;p&gt;The agent finds the same problem that prompted me to build it. These are two live repositories, not synthetic fixtures, and either could cost someone an afternoon.&lt;/p&gt;

&lt;p&gt;That is my standard for a tutorial project. It should do more than return “hello” from a hello-world fixture. It should fail informatively when pointed at reality.&lt;/p&gt;

&lt;h2&gt;
  
  
  At this point
&lt;/h2&gt;

&lt;p&gt;The tools and agent wiring are ready. My 2nd post will cover deploying our Agent to AgentCore and will cover the issues that appear after it leaves your computer 🖥️:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The 250MB packaging fork&lt;/strong&gt; that determines whether your deployment loop takes 40 seconds or 6 minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The trust boundary&lt;/strong&gt; at the entry-point, and why validating input matters when the agent reads text written by strangers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reading traces&lt;/strong&gt; to distinguish a correct answer from a lucky one&lt;/li&gt;
&lt;li&gt;The failure table and teardown (We don't want that extra cost)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have run these examples against the live GitHub API, npm, and PyPI. I also deployed the agent to a real AgentCore runtime, invoked it three times, and tore it down. &lt;em&gt;The screenshot above is output from that deployed runtime, not a mocked fixture.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This post is a snapshot, and snapshots decay. If you are reading it much later, run the agent against its own examples. If &lt;code&gt;strands-agents/sdk-python&lt;/code&gt; no longer returns a 301, something moved again. That is more useful to know than whether this post still looks trustworthy.&lt;/p&gt;

&lt;p&gt;Here’s my &lt;a href="https://github.com/coozgan/release-radar.git" rel="noopener noreferrer"&gt;GitHub repo&lt;/a&gt; for the code used in this project.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>python</category>
    </item>
    <item>
      <title>Building an Agentic On-Call Copilot on AWS, in Public (Part 1: Why and Scope)</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Fri, 24 Jul 2026 02:10:37 +0000</pubDate>
      <link>https://dev.to/joshyfruit/building-an-agentic-on-call-copilot-on-aws-in-public-part-1-why-and-scope-2l47</link>
      <guid>https://dev.to/joshyfruit/building-an-agentic-on-call-copilot-on-aws-in-public-part-1-why-and-scope-2l47</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I build agents for fun and break them to find out where they crack. This is Project 1 of 3 I'm doing this year on Amazon Bedrock, AWS Strands, Bedrock AgentCore, and Kiro, and I'm posting the whole build, gotchas included.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Most "AI for DevOps" demos summarize a log and stop there. I want the opposite. Incident Copilot reads a real production alert, pulls the runbooks and past incidents that relate to it, reasons about a likely root cause, and recommends the next thing I should check. It asks me before it runs anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'm building
&lt;/h2&gt;

&lt;p&gt;Incident Copilot takes one production alert and runs an agent loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Classify and summarize the alert.&lt;/li&gt;
&lt;li&gt;Retrieve runbooks and historical postmortems with RAG.&lt;/li&gt;
&lt;li&gt;Propose a root-cause hypothesis.&lt;/li&gt;
&lt;li&gt;Suggest one or two read-only diagnostic actions, like checking recent deploys or querying the error-rate metric.&lt;/li&gt;
&lt;li&gt;Wait for me to approve.&lt;/li&gt;
&lt;li&gt;Run the approved tools and return a summary plus a recommended next step.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Version one does no auto-remediation. I chose that on purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision that makes or breaks it: cut the scope
&lt;/h2&gt;

&lt;p&gt;I have two to four hours a week for this. The thing that kills side projects isn't a model&lt;br&gt;
that reasons poorly. It's a scope so big that I quit in week three. So the MVP stays small:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One alert type, high API latency. Not "any incident."&lt;/li&gt;
&lt;li&gt;Read-only tools. The agent looks. It does not touch.&lt;/li&gt;
&lt;li&gt;I approve before any tool runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fun part is the safety rails, not despite them. An on-call agent that can run &lt;code&gt;kubectl delete&lt;/code&gt; without asking would be the kind of thing that ends up in a postmortem of its own. Scoping the agent and gating its actions is the puzzle I actually wanted to solve here.&lt;/p&gt;
&lt;h2&gt;
  
  
  The architecture
&lt;/h2&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%2F72rwps75gpe06yjfr6hf.jpeg" 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%2F72rwps75gpe06yjfr6hf.jpeg" alt="Incident Response Workflow" width="800" height="447"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alert -&amp;gt; Strands agent (Bedrock / Claude reasoning)
   1. classify and summarize the alert
   2. retrieve runbooks and past incidents (RAG)
   3. propose a root-cause hypothesis
   4. suggest read-only diagnostic calls -&amp;gt; [HUMAN APPROVAL GATE]
   5. run the approved tools
   -&amp;gt; incident summary plus a recommended next step
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AWS Strands runs the loop and the tool calls. Amazon Bedrock with Claude does the reasoning at steps 1, 3, and 4. RAG grounds step 2 so the hypothesis cites a real runbook. Bedrock AgentCore is where I deploy this in August, which gives me the production story.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned already
&lt;/h2&gt;

&lt;p&gt;Writing the scope down before I wrote any code killed three features I would have sunk a week into. The build log earns its keep twice over: every decision and gotcha I jot down feeds the weekly posts and the final Devpost writeup. The content comes from the build rather than competing with it for my evenings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next
&lt;/h2&gt;

&lt;p&gt;Week 2: the Strands agent skeleton and the first tool contract. I'll show the code.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
      <category>automation</category>
      <category>aiops</category>
    </item>
    <item>
      <title>From IT Manager to AI Engineer: Build a Cloud Infrastructure Agent with Cloud Run's Managed MCP Server</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Thu, 30 Apr 2026 00:57:46 +0000</pubDate>
      <link>https://dev.to/joshyfruit/from-it-manager-to-ai-engineer-build-a-cloud-infrastructure-agent-with-cloud-runs-managed-mcp-pan</link>
      <guid>https://dev.to/joshyfruit/from-it-manager-to-ai-engineer-build-a-cloud-infrastructure-agent-with-cloud-runs-managed-mcp-pan</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Google Cloud NEXT '26 dropped over 260 announcements.&lt;/strong&gt; Most headlines went to Gemini 3.1, TPU v8, and the Agentic Data Cloud. But buried in the Cloud Run section was something that made me stop scrolling — a &lt;strong&gt;fully managed remote MCP server&lt;/strong&gt;, now generally available. If you manage infrastructure AND build AI systems, this one's for you.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why This Hit Different for Me
&lt;/h2&gt;

&lt;p&gt;I wear two hats. One day I'm SSH-ing into VMs, reviewing Cloud Run deployments, and making sure services don't fall over at 2am. The next I'm wiring up LLM agents, building tool pipelines, and figuring out why my context window blew up. These two worlds have always felt weirdly disconnected.&lt;/p&gt;

&lt;p&gt;MCP (Model Context Protocol) on Cloud Run is the first thing I've seen that genuinely bridges them. Instead of hand-crafting API clients for every infra operation your agent needs to do, you point it at a managed MCP server — and suddenly your AI agent can deploy services, read logs, and inspect health metrics like a junior SRE who never sleeps.&lt;/p&gt;

&lt;p&gt;Let's build it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;By the end of this walkthrough you'll have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The built-in Cloud Run MCP server&lt;/strong&gt; wired up to Gemini CLI so you can manage deployments via natural language&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A custom MCP server&lt;/strong&gt; running on Cloud Run that exposes infrastructure health tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An ADK agent&lt;/strong&gt; that combines both to answer questions like &lt;em&gt;"Which of my services had errors in the last hour?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the full picture of what we're assembling:&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.amazonaws.com%2Fuploads%2Farticles%2F5jo4gpfuq5dh16ayrp0c.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.amazonaws.com%2Fuploads%2Farticles%2F5jo4gpfuq5dh16ayrp0c.png" alt="Architecture diagram showing the Cloud Run Managed MCP Server setup — Gemini CLI and ADK Agent connect through Model Armor and Cloud IAM to both the Google Managed MCP Server (run.googleapis.com/mcp) and a custom infra-health-mcp service on Cloud Run, which reads from Cloud Logging and Artifact Registry" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A Google Cloud project with billing enabled&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/sdk/docs/install" rel="noopener noreferrer"&gt;&lt;code&gt;gcloud&lt;/code&gt; CLI&lt;/a&gt; installed and authenticated&lt;/li&gt;
&lt;li&gt;Python 3.10+&lt;/li&gt;
&lt;li&gt;Docker (for building the custom server)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/google-gemini/gemini-cli" rel="noopener noreferrer"&gt;Gemini CLI&lt;/a&gt; installed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set your project up front so every command just works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"my-project-id"&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"us-central1"&lt;/span&gt;
gcloud config &lt;span class="nb"&gt;set &lt;/span&gt;project &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IAM roles you'll need on your account:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;roles/run.admin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roles/iam.serviceAccountUser&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;roles/artifactregistry.writer&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1 — Use the Built-in Cloud Run MCP Server
&lt;/h2&gt;

&lt;p&gt;Google now hosts a fully managed MCP server at &lt;code&gt;https://run.googleapis.com/mcp&lt;/code&gt;. It exposes tools like &lt;code&gt;list_services&lt;/code&gt;, &lt;code&gt;get_service&lt;/code&gt;, &lt;code&gt;deploy_service_from_image&lt;/code&gt;, and &lt;code&gt;deploy_service_from_archive&lt;/code&gt; — no setup required on your end.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Authenticate
&lt;/h3&gt;

&lt;p&gt;The managed endpoint uses your Google Cloud identity. Make sure your ADC (Application Default Credentials) are set:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud auth application-default login
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Wire it to Gemini CLI
&lt;/h3&gt;

&lt;p&gt;Open (or create) &lt;code&gt;~/.gemini/settings.json&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"cloud-run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://run.googleapis.com/mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"transport"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Talk to Your Infrastructure
&lt;/h3&gt;

&lt;p&gt;Fire up Gemini CLI and try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gemini

&amp;gt; List all my Cloud Run services in us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see it call &lt;code&gt;list_services&lt;/code&gt; under the hood and return a clean summary of every service, its URL, and status. No &lt;code&gt;gcloud run services list --region us-central1 --format=json | jq ...&lt;/code&gt; gymnastics required.&lt;/p&gt;

&lt;p&gt;Try something bolder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; Deploy the image us-docker.pkg.dev/cloudrun/container/hello to a new service
  called "hello-from-agent" in us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It calls &lt;code&gt;deploy_service_from_image&lt;/code&gt;, fills in the parameters, and your service is live. That's infrastructure-as-conversation, and honestly it feels a little magical the first time.&lt;/p&gt;

&lt;p&gt;Here's what a full agent session looks like — listing services, spotting errors, and triggering a hotfix deploy all from one prompt chain:&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.amazonaws.com%2Fuploads%2Farticles%2Fxipz598e5f3u0zhj4uv4.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.amazonaws.com%2Fuploads%2Farticles%2Fxipz598e5f3u0zhj4uv4.png" alt="Terminal screenshot showing a Gemini CLI session: the agent calls infra-health-mcp to list services, checks error rates in parallel across three services, surfaces a 5xx spike on api-gateway, then deploys a hotfix image via the Cloud Run managed MCP server" width="800" height="792"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;IT Specialist note:&lt;/strong&gt; The managed endpoint enforces Cloud IAM on every call. If your credentials don't have &lt;code&gt;run.services.create&lt;/code&gt;, the deploy fails cleanly with a permission error — not a hallucinated success. That's the kind of guardrail you need when agents touch production infra.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 2 — Build &amp;amp; Deploy Your Own Custom MCP Server
&lt;/h2&gt;

&lt;p&gt;The built-in server covers Cloud Run operations. But what about your custom health checks, log analysis, or cross-service diagnostics? That's where you roll your own.&lt;/p&gt;

&lt;p&gt;We'll build an &lt;strong&gt;Infra Health MCP Server&lt;/strong&gt; with three tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;list_services&lt;/code&gt; — wraps Cloud Run's Admin API&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;get_service_error_rate&lt;/code&gt; — queries Cloud Logging for 5xx errors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check_service_health&lt;/code&gt; — returns a simple green/yellow/red status&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 1: Create the Project
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;infra-health-mcp &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;infra-health-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[project]&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"infra-health-mcp"&lt;/span&gt;
&lt;span class="py"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.1.0"&lt;/span&gt;
&lt;span class="py"&gt;requires-python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.10&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="py"&gt;dependencies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="py"&gt;"fastmcp&amp;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="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"google-cloud-run&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;    &lt;span class="py"&gt;"google-cloud-logging&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="s"&gt;",&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Write the MCP Server
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;server.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;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.cloud&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;run_v2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cloud_logging&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[%(levelname)s]: %(message)s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&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;Infra Health MCP Server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;run_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_v2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ServicesClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;log_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cloud_logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Client&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;list_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;List all Cloud Run services with their status and URLs.

    Args:
        project_id: Google Cloud project ID
        region: GCP region (e.g. us-central1)

    Returns:
        JSON list of services with name, URL, and last deployment time
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Listing services in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&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;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&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;projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/locations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;run_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;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;uri&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;last_deployed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_time&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&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;ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal_condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&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;if&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal_condition&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&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;get_service_error_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&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;Get the 5xx error count for a Cloud Run service over a time window.

    Args:
        project_id: Google Cloud project ID
        region: GCP region
        service_name: Name of the Cloud Run service
        minutes: How many minutes back to look (default 60)

    Returns:
        JSON with total requests, error count, and error rate percentage
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Checking error rate for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; over last &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;since&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;filter_str&lt;/span&gt; &lt;span class="o"&gt;=&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;resource.type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cloud_run_revision&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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;resource.labels.service_name=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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;resource.labels.location=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&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;httpRequest.status&amp;gt;=500 &lt;/span&gt;&lt;span class="sh"&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;timestamp&amp;gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;since&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"'&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;error_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;log_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;list_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;filter_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filter_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;projects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;window_minutes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;minutes&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_count_5xx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;error_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checked_at&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&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;check_service_health&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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;Return a simple health status for a Cloud Run service.

    Args:
        project_id: Google Cloud project ID
        region: GCP region
        service_name: Name of the Cloud Run service

    Returns:
        JSON with status (green/yellow/red) and a human-readable reason
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Health check for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&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;projects/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/locations/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/services/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;svc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal_condition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&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;if&lt;/span&gt; &lt;span class="n"&gt;svc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal_condition&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&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;state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONDITION_SUCCEEDED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;green&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;Service is running and healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONDITION_FAILED&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;CONTAINER_FAILED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;red&lt;/span&gt;&lt;span class="sh"&gt;"&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;Service is in a failed state: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yellow&lt;/span&gt;&lt;span class="sh"&gt;"&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;Service state is uncertain: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="p"&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;port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&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;Infra Health MCP server starting on port &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;mcp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streamable-http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;port&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why Streamable HTTP?&lt;/strong&gt; Cloud Run is stateless and scales horizontally. The older SSE transport needed persistent connections — a terrible fit for serverless. Streamable HTTP uses plain POST/GET, so every request is independent. Your MCP server scales to zero between calls and you only pay when it's actually doing work.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 3: Containerize It
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.13-slim&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . /app&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; PYTHONUNBUFFERED=1&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;uv &lt;span class="nb"&gt;sync&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; $PORT&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["uv", "run", "server.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Create a Service Account
&lt;/h3&gt;

&lt;p&gt;Your MCP server needs permission to read Cloud Run and Cloud Logging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud iam service-accounts create infra-health-sa &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Infra Health MCP Server"&lt;/span&gt;

gcloud projects add-iam-policy-binding &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:infra-health-sa@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/run.viewer"&lt;/span&gt;

gcloud projects add-iam-policy-binding &lt;span class="nv"&gt;$PROJECT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:infra-health-sa@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iam.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/logging.viewer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5: Build &amp;amp; Deploy
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create Artifact Registry repo&lt;/span&gt;
gcloud artifacts repositories create mcp-servers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;docker &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;

&lt;span class="c"&gt;# Build and push&lt;/span&gt;
gcloud builds submit &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tag&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-docker.pkg.dev/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp-servers/infra-health:latest"&lt;/span&gt;

&lt;span class="c"&gt;# Deploy&lt;/span&gt;
gcloud run deploy infra-health-mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-docker.pkg.dev/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/mcp-servers/infra-health:latest"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;512Mi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--concurrency&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;120 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"infra-health-sa@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.iam.gserviceaccount.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cloud Run gives you a URL like &lt;code&gt;https://infra-health-mcp-&amp;lt;hash&amp;gt;-uc.a.run.app&lt;/code&gt;. Grab it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;MCP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud run services describe infra-health-mcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'value(status.url)'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 6: Test It Locally via the Cloud Run Proxy
&lt;/h3&gt;

&lt;p&gt;Don't expose your MCP server to the internet directly. Use the proxy to test with your local credentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud run services proxy infra-health-mcp &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now hit it at &lt;code&gt;http://localhost:3000&lt;/code&gt; — your credentials are injected automatically, no token management needed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — Wire Both Servers into an ADK Agent
&lt;/h2&gt;

&lt;p&gt;Now the fun part. We'll build an agent that uses &lt;strong&gt;both&lt;/strong&gt; MCP servers — the built-in Cloud Run one and your custom infra health server — to answer infrastructure questions like a seasoned SRE.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install ADK
&lt;/h3&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;google-adk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Agent
&lt;/h3&gt;

&lt;p&gt;Create &lt;code&gt;agent.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;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;google.adk.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;LlmAgent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;google.adk.tools.mcp_tool.mcp_toolset&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;MCPToolset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StreamableHTTPConnectionParams&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;MCP_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MCP_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# your infra-health-mcp URL
&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="c1"&gt;# Connect to both MCP servers
&lt;/span&gt;    &lt;span class="n"&gt;cloud_run_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPToolset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;StreamableHTTPConnectionParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://run.googleapis.com/mcp&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;infra_health_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MCPToolset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;StreamableHTTPConnectionParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MCP_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LlmAgent&lt;/span&gt;&lt;span class="p"&gt;(&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;infra-agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemini-2.0-flash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;instruction&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;You are an infrastructure operations assistant. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You have access to Cloud Run management tools and infrastructure health tools. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;When asked about service health or errors, always check both the service status &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;and recent error rates before answering. Be concise and actionable.&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cloud_run_tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;infra_health_tools&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Example queries — swap these for interactive input
&lt;/span&gt;    &lt;span class="n"&gt;queries&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;List all my Cloud Run services in us-central1&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;Which services had 5xx errors in the last hour?&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;Give me a health summary for all services&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&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="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;agent&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="n"&gt;query&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="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&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;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;&lt;span class="nv"&gt;MCP_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$MCP_URL&lt;/span&gt; python agent.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see the agent autonomously call &lt;code&gt;list_services&lt;/code&gt;, then loop over each service calling &lt;code&gt;get_service_error_rate&lt;/code&gt; and &lt;code&gt;check_service_health&lt;/code&gt; — building a full infra health picture without you writing a single orchestration loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;This is the moment it clicks.&lt;/strong&gt; You didn't write "for service in services: check health". The agent reasoned its way to that pattern. Your job was defining the tools. That's a genuine shift in how we build infra tooling.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Security: Don't Skip This Section
&lt;/h2&gt;

&lt;p&gt;Agents with infrastructure permissions need real guardrails. Here's what I'd put in place before letting this near production:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Scope service account permissions tightly.&lt;/strong&gt; The &lt;code&gt;infra-health-sa&lt;/code&gt; has read-only roles. If you want an agent that can also deploy, create a &lt;em&gt;separate&lt;/em&gt; service account for write operations and require explicit approval flows before those tools fire.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use IAM deny policies for the write MCP tools.&lt;/strong&gt; You can explicitly deny &lt;code&gt;run.services.create&lt;/code&gt; on specific service accounts at the project level — useful if you only want agents to have deploy access in staging, not prod.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Enable Model Armor.&lt;/strong&gt; Google's Model Armor sits in front of MCP calls and blocks prompt injection attempts, malicious URIs, and unsafe content before they reach your tools. Enable it in the Google Cloud console under AI Safety.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Cloud Audit Logs are your friend.&lt;/strong&gt; Every MCP tool call made through Google-managed servers is logged automatically. Set up a log-based alert for any &lt;code&gt;deploy_service_from_image&lt;/code&gt; calls from service accounts that shouldn't be deploying.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Example: alert on unexpected deploys&lt;/span&gt;
gcloud logging metrics create unexpected-agent-deploy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"MCP deploy calls from unexpected accounts"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'protoPayload.methodName="google.cloud.run.v2.Services.CreateService"'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  My Honest Take
&lt;/h2&gt;

&lt;p&gt;What impressed me most at NEXT '26 isn't any single feature — it's that Google is treating MCP as a first-class citizen across the entire platform. BigQuery has a managed MCP server. Cloud Logging has one. Cloud SQL is getting one. This is becoming the standard interface layer between AI agents and cloud services.&lt;/p&gt;

&lt;p&gt;For IT specialists and infrastructure engineers, this is actually exciting rather than threatening. The tedious parts of infra ops — writing one-off scripts to list resources, cross-referencing logs with deployment times, checking health across 20 services — are exactly what agents are good at. You shift from &lt;em&gt;doing&lt;/em&gt; the repetitive tasks to &lt;em&gt;designing the tools&lt;/em&gt; that do them.&lt;/p&gt;

&lt;p&gt;The rough edges? Auth setup for remote MCP servers is still fiddly, especially in multi-project setups. The ADK toolset documentation is still catching up to the pace of announcements. And "fully managed" doesn't yet mean "zero config" — you still need to wire up IAM carefully.&lt;/p&gt;

&lt;p&gt;But the direction is clear, and the foundation is solid. The infra engineer who learns to build good MCP servers is going to be unreasonably productive over the next few years.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Explore the &lt;a href="https://cloud.google.com/run/docs/use-cloud-run-mcp" rel="noopener noreferrer"&gt;official Cloud Run MCP docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check out the &lt;a href="https://codelabs.developers.google.com/codelabs/cloud-run/use-mcp-server-on-cloud-run-with-an-adk-agent" rel="noopener noreferrer"&gt;ADK MCP codelab&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try adding a &lt;code&gt;deploy_service&lt;/code&gt; tool to your custom server — and see how differently it feels when an agent handles rollback logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Drop a comment if you build something cool with this. I'm especially curious what domain-specific MCP servers people come up with.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built and tested as part of the Google Cloud NEXT '26 Writing Challenge. All code examples use placeholder project IDs — swap in your own before running.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloudnextchallenge</category>
    </item>
    <item>
      <title>From Prompts to Action: My Journey Through the Google &amp; Kaggle AI Agents Bootcamp</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Mon, 15 Dec 2025 02:03:16 +0000</pubDate>
      <link>https://dev.to/joshyfruit/from-prompts-to-action-my-journey-through-the-google-kaggle-ai-agents-bootcamp-2489</link>
      <guid>https://dev.to/joshyfruit/from-prompts-to-action-my-journey-through-the-google-kaggle-ai-agents-bootcamp-2489</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-kaggle-ai-agents-2025-11-10"&gt;Google AI Agents Writing Challenge&lt;/a&gt;: Learning Reflections&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  From Prompts to Action: My Journey Through the Google &amp;amp; Kaggle AI Agents Bootcamp
&lt;/h1&gt;

&lt;p&gt;As someone who has watched AI evolve from "magic black box" to "everyday tool," I often felt a barrier between &lt;em&gt;using&lt;/em&gt; AI and &lt;em&gt;building&lt;/em&gt; with it. Aside from a chatbot what else could AI do and how do I harness the power of AI? I thought building agents required a PhD in Machine Learning. This week, the &lt;strong&gt;5-Day AI Agents Intensive Course&lt;/strong&gt; by Google and Kaggle completely shattered that illusion.&lt;/p&gt;

&lt;p&gt;It turns out, if you can write a Python function, you can build an agent. Here is my deep dive into the code, the concepts, and the tools that made this journey accessible, featuring my capstone project: &lt;strong&gt;Jarbest&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Awakening: Hello, Agent
&lt;/h2&gt;

&lt;p&gt;As coming from non-developer background, I always imagined software as a "bricklayer"—rigidly following a blueprint. Day 1 introduced me to the &lt;strong&gt;Agent&lt;/strong&gt;: a system that acts more like a &lt;strong&gt;film director&lt;/strong&gt;. It doesn't just predict text; it has a "Brain" (the model), "Hands" (tools), and a "Nervous System" (orchestration) to autonomously perceive, reason, and act. We learned that an agent operates in a continuous loop—&lt;strong&gt;Mission, Scan, Think, Act, Observe&lt;/strong&gt;—constantly adapting its plan to solve problems. This framework demystified the magic: I wasn't just coding a chatbot; I was building a system with the agency to execute multi-step missions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Aha!" Moment: It's Just Python &amp;amp; The "USB Port" for AI
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fhclwvcfuxyc93hlc6ivx.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fhclwvcfuxyc93hlc6ivx.jpeg" alt="The " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Day 2 was a revelation. I learned that Models are just "Brains"—pattern predictors that cannot see or act. To be useful, they need &lt;strong&gt;Tools&lt;/strong&gt;: the "Eyes" and "Hands" that let them fetch data or execute actions.&lt;/p&gt;

&lt;p&gt;But connecting every tool to every model is a nightmare (the N x M problem). Enter the &lt;strong&gt;Model Context Protocol (MCP)&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Think of MCP as the &lt;strong&gt;USB port for AI&lt;/strong&gt;. Before USB, you needed a specific cable for every device. MCP lets you plug any tool into any agent using a standard connection.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Code: Giving the Agent "Hands"
&lt;/h3&gt;

&lt;p&gt;In my project, &lt;strong&gt;Jarbest&lt;/strong&gt; (an accessible personal companion), I needed an agent that could check bank account balances. instead of writing a custom connector, I used MCP to "plug in" a secure banking server.&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;# Finance Agent: Manages the banks transactions
&lt;/span&gt;&lt;span class="n"&gt;finance_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&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;finance_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;An agent that can help with banking operations like checking balances...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# Assuming; This toolset connects to a secure internal banking server
&lt;/span&gt;    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;MCPToolset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;connection_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;StreamableHTTPConnectionParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BANK_MCP_URL&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&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="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/mcp&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="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why this matters (and the danger):&lt;/strong&gt;&lt;br&gt;
The agent reads these tool definitions and knows exactly when to use them. If a user asks "Can I afford this pizza?", the agent inherently knows it must first call &lt;code&gt;check_balance&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;However, the notes warned us: &lt;strong&gt;Using MCP is like plugging in a random USB drive found on the street.&lt;/strong&gt; It could be a legitimate tool, or it could be a "Tool Shadow" (a malicious copy). That's why in Jarbest, I implemented a strict &lt;strong&gt;Application-Layer Gateway&lt;/strong&gt; (via hardcoded allowlists)—ensuring the agent can &lt;em&gt;only&lt;/em&gt; connect to my specific, internal MCP banking server, preventing it from ever "plugging in" to an untrusted source&lt;/p&gt;
&lt;h2&gt;
  
  
  Deep Dive: The Brain (Memory)
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2Fzn75ysl68gfa4mx36o3b.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2Fzn75ysl68gfa4mx36o3b.jpeg" alt="The Memory" width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Day 3 was where things got sophisticated. A chatbot forgets you the moment you close the tab. An &lt;em&gt;agent&lt;/em&gt; remembers.&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Jarbest&lt;/strong&gt;, which is designed for elderly users who value consistency, memory is critical. If "Grandma Jane" asks for her "usual order," the agent shouldn't ask "What is that?"; it should know.&lt;/p&gt;

&lt;p&gt;Here is how I implemented the "Brain" in my root agent:&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;root_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&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;root_agent&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    You are Jarbest...
    Memory: Use the load_memory tool to recall past conversations and preferences 
    (e.g., &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ordering the usual&lt;/span&gt;&lt;span class="sh"&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="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;load_memory&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;--- This single line gives the agent a "brain"
&lt;/span&gt;    &lt;span class="n"&gt;after_agent_callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;auto_save_to_memory&lt;/span&gt; &lt;span class="c1"&gt;# Auto-saves every interaction
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Non-Developer Perspective:&lt;/strong&gt; Think of &lt;code&gt;load_memory&lt;/code&gt; like giving the agent a filing cabinet. When Grandma Jane says "Order me some food," the agent thinks: &lt;em&gt;"I need to check if she has a preference,"&lt;/em&gt; opens the cabinet (&lt;code&gt;load_memory&lt;/code&gt;), finds "Likes Large Pepperoni Pizza," and acts on it. Watching this thought process in real-time was mind-blowing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The "Squeeze": Debugging the Black Box
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F9y170zectzkrr1qb3whi.jpeg" 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.amazonaws.com%2Fuploads%2Farticles%2F9y170zectzkrr1qb3whi.jpeg" alt="The " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Day 4 taught us that "it works" isn't enough. You need to know &lt;em&gt;why&lt;/em&gt; it works. When building a safety-focused agent like Jarbest, I couldn't afford "hallucinations."&lt;/p&gt;

&lt;p&gt;Exploring the &lt;strong&gt;Agent Observability&lt;/strong&gt; labs, I learned to trace the agent's reasoning steps. When my agent refused to order a pizza, I could look at the trace and see:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;User:&lt;/strong&gt; "Order a pizza."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Tool Call:&lt;/strong&gt; &lt;code&gt;check_balance&lt;/code&gt; -&amp;gt; returned $5.00.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Reasoning:&lt;/strong&gt; "Pizza costs $20. User has $5. Result: Unsafe."&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Response:&lt;/strong&gt; "I cannot complete this order because your balance is too low."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Seeing that raw reasoning log felt like looking into the matrix. It transformed the LLM from a mysterious oracle into a logical, debuggable software component. I realized I wasn't just "prompting" anymore; I was engineering logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ecosystem: Agents Talking to Agents (A2A)
&lt;/h2&gt;

&lt;p&gt;Day 5 introduced the &lt;strong&gt;Agent-to-Agent (A2A) Protocol&lt;/strong&gt;. This is where I moved from building a single assistant to building a &lt;em&gt;team&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;My "Purchaser Agent" doesn't know how to make pizza. Instead, it connects to a completely separate "Pizza Shop Agent" (simulating a 3rd party vendor).&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;# Creating a client-side proxy for a remote agent
&lt;/span&gt;&lt;span class="n"&gt;pizza_agent_proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RemoteA2aAgent&lt;/span&gt;&lt;span class="p"&gt;(&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;pizza_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# The "Agent Card" acts like a business card for discovery
&lt;/span&gt;    &lt;span class="n"&gt;agent_card&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:10000/.well-known/agent-card.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Remote pizza agent from external vendor...&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;purchaser_agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;(&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;purchaser_agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;instruction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Your goal is to help the user find and buy items.&lt;/span&gt;&lt;span class="sh"&gt;"&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="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;AgentTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pizza_agent_proxy&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="c1"&gt;# &amp;lt;--- Treating another agent as a tool
&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Cool Idea:&lt;/strong&gt; The "Agent Card" isn't just a technical manifest; it's a completely new way for businesses to interact.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;For SMBs (Small to Medium Businesses):&lt;/strong&gt; Instead of constantly maintaining and documenting complex APIs for developers to read, you simply publish an "Agent Card" (like a digital business card) that describes what your service does (e.g., "I sell pepperoni pizza").&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;For Developers:&lt;/strong&gt; It saves massive amounts of time. My support agent just reads this card and &lt;em&gt;instantly&lt;/em&gt; knows how to ask for a customized order.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Future:&lt;/strong&gt; This allows agents to communicate autonomously, representing the transaction of each individual without human friction. It’s like an API that reads itself.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Capstone Spotlight: Jarbest - Agents for Good
&lt;/h2&gt;

&lt;p&gt;Applying these concepts, I built &lt;strong&gt;Jarbest&lt;/strong&gt; for the "Agents for Good" track.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Problem:&lt;/strong&gt; The digital world is full of dark patterns and complex UIs that exploit vulnerable users, especially the elderly.&lt;br&gt;
&lt;strong&gt;The Solution:&lt;/strong&gt; A unified "Action Space" that replaces app sprawl.&lt;br&gt;
Jarbest eliminates the friction of switching between banking apps, delivery apps, and websites. Instead of forcing Grandma Jane to install and navigate ten different confusing interfaces, Jarbest uses &lt;strong&gt;Tools, MCP, and A2A&lt;/strong&gt; to communicate with these services directly on her behalf.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; Simplicity and Safety. By centralizing these actions into one verified conversation, we inherently protect the user. They no longer need to open browsers or install random apps where they might fall victim to phishing sites or fake download buttons. Jarbest acts as the safe, validated operational layer for their digital life.&lt;/p&gt;

&lt;p&gt;Jarbest uses a &lt;strong&gt;Hierarchical Architecture&lt;/strong&gt;:&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.amazonaws.com%2Fuploads%2Farticles%2Ffsa2lg0yowi0n2rc6uw8.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.amazonaws.com%2Fuploads%2Farticles%2Ffsa2lg0yowi0n2rc6uw8.png" alt="Design Architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Guardian (Root Agent):&lt;/strong&gt; The "Thinking" layer. It never touches money directly. It validates safety.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Auditor (Finance Agent):&lt;/strong&gt; The only agent with access to the MCP banking server (via MCP).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Doer (Purchaser Agent):&lt;/strong&gt; The logistics layer that talks to vendors (via A2A).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This separation of concerns ensures that even if the "Doer" gets confused, the "Guardian" prevents any financial mistakes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/coozgan/kaggle-5day-ai-agents-capstone" rel="noopener noreferrer"&gt;Check out the Code Repository Here&lt;/a&gt;&lt;br&gt;
&lt;a href="https://youtu.be/-NyenNXYFfY" rel="noopener noreferrer"&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.amazonaws.com%2Fuploads%2Farticles%2F34glvbwfhlgl8wo2ppzg.jpg" alt="Watch on YouTube" width="480" height="360"&gt;&lt;/a&gt;&lt;br&gt;
WATCH THIS VIDEO 👆👆👆&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Multi-Agent Architecture?
&lt;/h2&gt;

&lt;p&gt;Working with a single "god agent" that handles everything creates a bottleneck. It forces one model to juggle complex reasoning (safety checks, intent parsing) with mundane execution (API calls, order formatting), leading to context overflow and hallucinations.&lt;/p&gt;

&lt;p&gt;By breaking the system into specialized sub-agents, we achieve:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Reduced Cognitive Load:&lt;/strong&gt; The Root Agent focuses purely on orchestration and safety, while the Purchaser Agent focuses solely on logistics.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Efficiency:&lt;/strong&gt; We can route simple tasks to faster, cheaper models (Gemini 2.5 Flash) and reserve the powerful reasoning models (Gemini 3 Pro) for the Guardian role.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Scalability:&lt;/strong&gt; New vendors (e.g., a Pharmacy Agent) can be added as new tools for the Purchaser Agent without retraining or complicating the Root Agent's logic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Secret Weapon: NotebookLM
&lt;/h2&gt;

&lt;p&gt;The course came with dense whitepapers—goldmines of information on "Context Engineering" and "Agent Quality." But digesting 20-page PDFs can be daunting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My Workflow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Feed the Brain:&lt;/strong&gt; I downloaded the &lt;em&gt;Context Engineering&lt;/em&gt; whitepaper and uploaded it directly to &lt;strong&gt;NotebookLM&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Conversation:&lt;/strong&gt; Instead of reading linearly, I interrogated the text.

&lt;ul&gt;
&lt;li&gt;  &lt;em&gt;Me:&lt;/em&gt; "Explain the trade-offs between vector databases and keyword search for agent memory."&lt;/li&gt;
&lt;li&gt;  &lt;em&gt;NotebookLM:&lt;/em&gt; It synthesized the answer &lt;em&gt;specifically from the whitepaper&lt;/em&gt;, citing the exact page numbers.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Podcast:&lt;/strong&gt; I used the "Audio Overview" feature to generate a podcast of the whitepaper. I listened to two AI hosts debate the merits of "Session" vs. "Memory" while I cooked dinner. It turned homework into entertainment.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently (The Roadmap)
&lt;/h2&gt;

&lt;p&gt;Building &lt;strong&gt;Jarbest&lt;/strong&gt; in just 5 days was a sprint, and I left plenty of ideas on the cutting room floor. If I had another week, here is what I would tackle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Dynamic Tool Loading:&lt;/strong&gt; Instead of hardcoding tools, I'd want the agent to "discover" new MCP servers on the local network automatically.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Voice Interface:&lt;/strong&gt; Accessibility is key for my target audience (elderly users). Adding a voice layer on top of the text interface would be a game-changer.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Proactive Alerts:&lt;/strong&gt; Currently, the agent waits for input. I want to build a background loop where it can nudge the user: &lt;em&gt;"Hey, you usually order groceries on Tuesday. Should I do that?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This bootcamp didn’t just teach me syntax; it fundamentally shifted my mental model of software development. I went from viewing AI as a passive chatbot to seeing it as a dynamic, composable ecosystem of "Doers".&lt;/p&gt;

&lt;p&gt;The combination of accessible frameworks like the Google GenAI SDK, standardized protocols like MCP, and powerful reasoning models has truly democratized agency. You don't need a research lab or a PhD to build systems that perceive, reason, and act—you just need a clear mission and the curiosity to prompt it.&lt;/p&gt;

&lt;p&gt;If you've been on the fence about diving into AI Agents, now is the time to start. The tools are ready and the barrier to entry has never been lower. I can't wait to see what you build.&lt;/p&gt;

</description>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>agents</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>Let's Deploy n8n on ec2 instances 🚀🚀🚀</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Wed, 10 Dec 2025 13:05:04 +0000</pubDate>
      <link>https://dev.to/joshyfruit/lets-deploy-n8n-on-ec2-instances-4jdm</link>
      <guid>https://dev.to/joshyfruit/lets-deploy-n8n-on-ec2-instances-4jdm</guid>
      <description>&lt;h2&gt;
  
  
  I really love automating
&lt;/h2&gt;

&lt;p&gt;I’ve always loved automating things in my workflows.  &lt;/p&gt;

&lt;p&gt;When I stumbled on n8n, I was honestly in awe. Suddenly all the ideas I had before—like updating Google Sheets, posting to social media, hosting my own APIs, syncing Google Drive to an S3 bucket, and a lot more—became realistic without needing to learn a new framework or library for every single task.  &lt;/p&gt;

&lt;p&gt;n8n makes it much easier to build small automations that actually ship and help you in day‑to‑day work, and that’s what made me fall in love with it.  &lt;/p&gt;

&lt;p&gt;So we’ll walk through how to deploy n8n on an AWS EC2 instance so you can start running your own automations on your own infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Launching the Instance 🚀
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Launch an Instance
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Log in to your AWS Management Console.&lt;/li&gt;
&lt;li&gt; Navigate to the &lt;strong&gt;EC2 Dashboard&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click the &lt;strong&gt;Launch instance&lt;/strong&gt; button.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Name and OS Selection
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Name:&lt;/strong&gt; Give your instance a recognizable name, such as &lt;code&gt;n8n&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Application and OS Images (AMI):&lt;/strong&gt; Select &lt;strong&gt;Amazon Linux&lt;/strong&gt;. The default "Amazon Linux 2023 AMI" is a great choice and is eligible for the Free Tier.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Architecture:&lt;/strong&gt; Select &lt;code&gt;64-bit (ARM)&lt;/code&gt; for better cost efficiency on AWS (Graviton processors offer better performance-per-dollar).&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 3: Choose an Instance Type
&lt;/h3&gt;

&lt;p&gt;Select an instance type that suits your needs. For this tutorial, we are using &lt;strong&gt;t4g.medium&lt;/strong&gt; (ARM-based Graviton). Here's why t4g.medium is the most cost-efficient choice for n8n:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Better Performance-per-Dollar:&lt;/strong&gt; AWS Graviton2 processors offer 40% better price-to-performance than comparable x86 instances&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sufficient Resources:&lt;/strong&gt; 2 vCPUs and 4GB RAM handle n8n with multiple workers smoothly without over-provisioning&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burstable Performance:&lt;/strong&gt; T4g instances include CPU credits, allowing you to handle traffic spikes without constant high usage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lower Costs:&lt;/strong&gt; t4g.medium costs ~40% less than equivalent t3.medium with superior performance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-Ready:&lt;/strong&gt; Unlike t4g.micro (limited burstable capacity), t4g.medium can sustain moderate workflows continuously&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For comparison:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;t4g.micro:&lt;/strong&gt; Limited for production (Free Tier only, ~1 vCPU, 1GB RAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;t4g.medium:&lt;/strong&gt; Recommended for small-to-medium deployments (2 vCPUs, 4GB RAM)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;t4g.large:&lt;/strong&gt; For high-volume workflows (2 vCPUs, 8GB RAM)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step 4: Key Pair
&lt;/h3&gt;

&lt;p&gt;Under &lt;strong&gt;Key pair (login)&lt;/strong&gt;, select an existing key pair from the dropdown menu to ensure you can SSH into your server later. If you don't have one, click "Create new key pair".&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Network Settings (Security Groups)
&lt;/h3&gt;

&lt;p&gt;This is a crucial step to ensure your instance is accessible.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Under &lt;strong&gt;Network settings&lt;/strong&gt;, choose &lt;strong&gt;Create security group&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Ensure the following rules are checked/added:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule Type&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Protocol&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;Your IP (0.0.0.0/0 for testing only)&lt;/td&gt;
&lt;td&gt;Remote terminal access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTP&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;Web traffic (redirect to HTTPS)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTTPS&lt;/td&gt;
&lt;td&gt;443&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;td&gt;0.0.0.0/0&lt;/td&gt;
&lt;td&gt;Secure web traffic for n8n UI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fqw8naiglesky7klblzt9.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.amazonaws.com%2Fuploads%2Farticles%2Fqw8naiglesky7klblzt9.png" alt="n8n Essential Security Groups" width="799" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Configure Storage
&lt;/h3&gt;

&lt;p&gt;The default &lt;strong&gt;8 GiB&lt;/strong&gt; of gp3 storage is usually sufficient for a basic installation. You can leave this as is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Advanced Details - User Data (The Installation Script)
&lt;/h3&gt;

&lt;p&gt;This is the most important part of the automation. Instead of manually installing software after the server boots, we will provide a script to do it automatically.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Scroll down to the &lt;strong&gt;Advanced details&lt;/strong&gt; section.&lt;/li&gt;
&lt;li&gt; Scroll to the very bottom to find the &lt;strong&gt;User data&lt;/strong&gt; text field.&lt;/li&gt;
&lt;li&gt; Paste the following script. This script updates the system, installs Docker, sets up Docker Compose, and configures permissions.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
yum &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git docker

&lt;span class="c"&gt;# Install Docker Compose plugin (system-wide)&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /usr/local/lib/docker/cli-plugins
curl &lt;span class="nt"&gt;-SL&lt;/span&gt; &lt;span class="s2"&gt;"https://github.com/docker/compose/releases/latest/download/docker-compose-linux-&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/local/lib/docker/cli-plugins/docker-compose
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/lib/docker/cli-plugins/docker-compose

&lt;span class="c"&gt;# Enable and start Docker&lt;/span&gt;
systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker
systemctl start docker

&lt;span class="c"&gt;# Allow ec2-user to run docker without sudo&lt;/span&gt;
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker ec2-user
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Highlight:&lt;/strong&gt; The script above automates the installation of Git, Docker, and Docker Compose, saving you several manual command-line steps later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 8: Launch
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Review your settings in the &lt;strong&gt;Summary&lt;/strong&gt; panel on the right.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Launch instance&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Wait for the success message, then click the Instance ID to view your running server.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Connecting and Configuring 🔌
&lt;/h2&gt;

&lt;p&gt;Now that your instance is running, we need to connect to it and download the necessary n8n configuration files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Connect to your Instance
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Select your running instance from the list.&lt;/li&gt;
&lt;li&gt; Click the &lt;strong&gt;Connect&lt;/strong&gt; button at the top right of the console.&lt;/li&gt;
&lt;li&gt; Select the &lt;strong&gt;EC2 Instance Connect&lt;/strong&gt; tab.&lt;/li&gt;
&lt;li&gt; Leave the default username as &lt;code&gt;ec2-user&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt; Click the orange &lt;strong&gt;Connect&lt;/strong&gt; button. A new browser window will open with a terminal interface.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Step 2: Verify Installation
&lt;/h3&gt;

&lt;p&gt;Once the terminal loads, verify that the installation script from Part 1 ran successfully by checking the Docker Compose version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;If this command returns a version number, your environment is ready.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Download the n8n Setup
&lt;/h3&gt;

&lt;p&gt;We will use a pre-configured setup from GitHub to get n8n running quickly.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Clone the repository:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/coozgan/hosting-n8n-aws.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigate into the project directory:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Navigate into the project directory:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;hosting-n8n-aws
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Deploying n8n with Workers 🐳
&lt;/h2&gt;

&lt;p&gt;Now we'll set up a production-ready n8n deployment with distributed workers, Redis queuing, PostgreSQL persistence, and Caddy reverse proxy for SSL/TLS.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Review Configuration
&lt;/h3&gt;

&lt;p&gt;Review the architecture. This setup includes:flow triggers&lt;br&gt;
    - &lt;strong&gt;n8n Workers&lt;/strong&gt;: Execute workflows asynchronously from a job queue&lt;br&gt;
    - &lt;strong&gt;Redis&lt;/strong&gt;: Manages job distribution and retries&lt;br&gt;
    - &lt;strong&gt;PostgreSQL&lt;/strong&gt;: Stores workflows, executions, and user data&lt;br&gt;
    - &lt;strong&gt;Caddy&lt;/strong&gt;: Reverse proxy with automatic SSL/TLS certificates&lt;/p&gt;
&lt;h3&gt;
  
  
  Step 2: Configure Environment Variables
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create and edit the &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env-example .env
nano .env
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;a. Create a DuckDNS subdomain&lt;br&gt;
Go to &lt;a href="https://www.duckdns.org" rel="noopener noreferrer"&gt;https://www.duckdns.org&lt;/a&gt; and sign in with GitHub, Google, etc.&lt;/p&gt;

&lt;p&gt;b. On the main page, choose a subdomain name (for example, myn8n) and click add domain.&lt;/p&gt;

&lt;p&gt;c. &lt;strong&gt;Find your ip address&lt;/strong&gt; head back to instance connect and type the command, then copy the &lt;em&gt;IP Address&lt;/em&gt;;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl ifconfig.me
&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;d. paste the &lt;em&gt;IP Address&lt;/em&gt; duckdns.org website and add the &lt;em&gt;IP Address&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&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.amazonaws.com%2Fuploads%2Farticles%2Fj6m8u6ot9zp9l0318dug.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.amazonaws.com%2Fuploads%2Farticles%2Fj6m8u6ot9zp9l0318dug.png" alt="Paste IP Address on Duckdns.org" width="800" height="246"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Add the following essential variables:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Domain Configuration&lt;/span&gt;
&lt;span class="nv"&gt;DOMAIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-domain.com &lt;span class="c"&gt;#use localhost if you don't have domain&lt;/span&gt;

&lt;span class="c"&gt;# n8n Encryption (generate a strong random key)&lt;/span&gt;
&lt;span class="nv"&gt;N8N_ENCRYPTION_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; CHOOSE_YOUR_OWN_KEY

&lt;span class="c"&gt;# PostgreSQL Database&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;CHOOSE_YOUR_OWN_PASSWORD

&lt;span class="c"&gt;# Timezone&lt;/span&gt;
&lt;span class="nv"&gt;GENERIC_TIMEZONE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;America/New_York
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Press &lt;code&gt;Ctrl+X&lt;/code&gt; to save, then &lt;code&gt;Y&lt;/code&gt; and &lt;code&gt;Enter&lt;/code&gt; to exit nano.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Keep your &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; and &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; safe. If you lose them, you won't be able to recover your workflows or data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 3: Start the Services
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; Press &lt;code&gt;Ctrl+X&lt;/code&gt; to exit, then press &lt;code&gt;Y&lt;/code&gt; to confirm save, and &lt;code&gt;Enter&lt;/code&gt; to finalize.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Keep your &lt;code&gt;N8N_ENCRYPTION_KEY&lt;/code&gt; and &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; safe. If you lose them, you won't be able to recover your workflows or data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Step 3: Start the Services
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Verify all containers are running:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Check the logs to ensure everything started correctly:&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Step 4: Access Your n8n Instance
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Once services are healthy, navigate to your domain in a web browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you configured a domain: &lt;code&gt;https://your-domain.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;If testing without domain: &lt;code&gt;http://&amp;lt;instance_public_ipaddress&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You should see the n8n login screen. Create your first user account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Congratulations! You have yourself a n8n single‑instance.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&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.amazonaws.com%2Fuploads%2Farticles%2F1g0quu8j8hwz4thlecmh.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.amazonaws.com%2Fuploads%2Farticles%2F1g0quu8j8hwz4thlecmh.png" alt="Architecture Diagram" width="800" height="1132"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Scaling and Best Practices ⚡
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Scaling Horizontally (within the instance)
&lt;/h3&gt;

&lt;p&gt;To add more worker instances, update your &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;n8n-worker-2&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-worker&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-worker-2&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;#copy the rest of the code&lt;/span&gt;

&lt;span class="na"&gt;n8n-worker-3&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-worker&lt;/span&gt;
  &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;n8n-worker-3&lt;/span&gt;
&lt;span class="nn"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;#copy the rest of the code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  &lt;a href="https://github.com/coozgan/hosting-n8n-aws?tab=readme-ov-file#horizontal-worker-scaling" rel="noopener noreferrer"&gt;For more information follow this...&lt;/a&gt; 👈
&lt;/h4&gt;




&lt;h3&gt;
  
  
  Performance Optimization Tips
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Adjust Worker Count:&lt;/strong&gt; Start with 2-3 workers and monitor CPU/memory usage.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stats
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Database Tuning:&lt;/strong&gt; Prune old execution logs to keep PostgreSQL performant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The docker-compose includes &lt;code&gt;EXECUTIONS_DATA_PRUNE=true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;It keeps 7 days of history by default (&lt;code&gt;EXECUTIONS_DATA_MAX_AGE=168&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Redis Configuration:&lt;/strong&gt; The Redis instance has a 512MB memory limit with LRU eviction policy.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor with: &lt;code&gt;docker exec redis redis-cli info stats&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Security Best Practices
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enable User Management:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The setup has &lt;code&gt;N8N_USER_MANAGEMENT_DISABLED=false&lt;/code&gt; by default&lt;/li&gt;
&lt;li&gt;Create separate user accounts for team members&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Restrict SSH Access:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Update your security group to only allow SSH from your IP&lt;/li&gt;
&lt;li&gt;In AWS Console: Security Groups &amp;gt; Inbound Rules &amp;gt; Edit SSH rule&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Backup Strategy:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Regularly backup your PostgreSQL database:
&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;postgres pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; postgres postgres &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Backup your n8n data volume:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; n8n-with-workers_n8n-data:/data &lt;span class="se"&gt;\ &lt;/span&gt;
   &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;:/backup alpine &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/n8n-backup.tar.gz &lt;span class="se"&gt;\&lt;/span&gt;
   &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;SSL/TLS Certificates:&lt;/strong&gt;

&lt;ul&gt;
&lt;li&gt;Caddy automatically obtains and renews Let's Encrypt certificates&lt;/li&gt;
&lt;li&gt;Certificates are stored in the &lt;code&gt;caddy-data&lt;/code&gt; volume&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Issue: Cannot Connect to n8n solution
&lt;/h3&gt;

&lt;p&gt;1️⃣ Check if all services are running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose ps

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2️⃣ Check Caddy logs for SSL certificate issues:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose logs caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3️⃣ Ensure your security group allows HTTP (port 80) and HTTPS (port 443)&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue: Workers Not Processing Jobs Solution
&lt;/h3&gt;

&lt;p&gt;1️⃣ Verify Redis is healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec redis redis-cli ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2️⃣ Check worker logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose logs n8n-worker

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3️⃣ Ensure &lt;code&gt;EXECUTIONS_MODE=queue&lt;/code&gt; is set in your environment&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue: Database Connection Errors Solution
&lt;/h3&gt;

&lt;p&gt;1️⃣ Verify PostgreSQL is running and healthy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker compose logs postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2️⃣ Check database credentials match in &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker exec postgres psql -U postgres -d postgres -c "\dt"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3️⃣ Ensure &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; in your &lt;code&gt;.env&lt;/code&gt; is correct&lt;/p&gt;

&lt;h3&gt;
  
  
  Issue:Out of Disk Space Solution
&lt;/h3&gt;

&lt;p&gt;1️⃣ Check disk usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker system df
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2️⃣ Prune old data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker system prune -a
docker volume prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;3️⃣ Clean up old execution logs in n8n UI: &lt;code&gt;Settings → Executions → Delete&lt;/code&gt; old executions&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring and Maintenance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Monitor Container Health
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Real-time resource usage
docker stats

# View service logs
docker compose logs -f [service-name]

# Check specific service health
docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Regular Maintenance Tasks
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Weekly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Monitor disk usage: &lt;code&gt;df -h&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Check error logs: &lt;code&gt;docker compose logs --since 1w | grep -i error&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monthly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backup databases and volumes&lt;/li&gt;
&lt;li&gt;Review user access and remove inactive accounts&lt;/li&gt;
&lt;li&gt;Update Docker images: &lt;code&gt;docker compose pull &amp;amp;&amp;amp; docker compose up -d&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Quarterly:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security audit of your workflows&lt;/li&gt;
&lt;li&gt;Review and optimize your AWS security groups&lt;/li&gt;
&lt;li&gt;Test disaster recovery (restore from backup)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Cost Optimization on AWS
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Instance Type:&lt;/strong&gt; t4g.medium provides the best balance of cost and performance for n8n deployments

&lt;ul&gt;
&lt;li&gt;ARM-based Graviton2 processors (40% better price-to-performance than x86)&lt;/li&gt;
&lt;li&gt;Sufficient resources for multiple workers without over-provisioning&lt;/li&gt;
&lt;li&gt;Monthly cost: ~$9-12 (vs ~$15-18 for equivalent x86 instances)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data Transfer:&lt;/strong&gt; Minimize data egress by keeping compute and data in the same region&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS Alternative:&lt;/strong&gt; For high-volume deployments, consider AWS RDS for PostgreSQL instead of self-hosted&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CloudWatch:&lt;/strong&gt; Set up alarms for unusual activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-Scaling:&lt;/strong&gt; Use EC2 Auto Scaling Groups for multiple instances in production&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Next Steps:make it production-ready
&lt;/h2&gt;

&lt;p&gt;This guide focuses on getting n8n running quickly on EC2 so you can start experimenting. For a more production-ready setup, you’ll likely want to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use serverless deployment (ECS Fargate, Elasticache and RDS)&lt;/li&gt;
&lt;li&gt;Use a custom domain with Route 53 or another DNS provider&lt;/li&gt;
&lt;li&gt;Use Elastic Load Balancer and Automatic Scaling Group on ECS Clusters&lt;/li&gt;
&lt;li&gt;Configure environment variables for n8n (like WEBHOOK_URL, credentials, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those topics can be a follow-up post in this series, where the EC2 instance you just created becomes the base for a more secure, robust n8n deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion 🎉
&lt;/h2&gt;

&lt;p&gt;You now have a production-grade n8n automation platform running on AWS with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ Cost effective to small to medium automations&lt;/li&gt;
&lt;li&gt;✅ Scalable worker architecture on ARM-based Graviton processors&lt;/li&gt;
&lt;li&gt;✅ Persistent data storage with PostgreSQL&lt;/li&gt;
&lt;li&gt;✅ Automatic SSL/TLS encryption via Caddy&lt;/li&gt;
&lt;li&gt;✅ Job queue system with Redis for reliable executions&lt;/li&gt;
&lt;li&gt;✅ Easy horizontal scaling within your instance&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>architecture</category>
      <category>devops</category>
      <category>automation</category>
    </item>
    <item>
      <title>Host your first AI Agent on Agentcore</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Tue, 11 Nov 2025 04:04:06 +0000</pubDate>
      <link>https://dev.to/joshyfruit/host-your-first-ai-agent-on-agentcore-2o06</link>
      <guid>https://dev.to/joshyfruit/host-your-first-ai-agent-on-agentcore-2o06</guid>
      <description>&lt;p&gt;&lt;strong&gt;Exploring AgentCore: Effortless AI Agent Deployment with AWS Strands SDK&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I had a good time exploring AgentCore and I see what AWS is trying to do: eliminate the second guessing on how to deploy your AI Agent. With the release of the Strands SDK, AWS has introduced a truly streamlined way to get agents running quickly. In this post, I’ll walk you through how I hosted my own agent on AgentCore, with everything from setup to cloud deployment.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why AgentCore and Strands SDK?
&lt;/h3&gt;

&lt;p&gt;AWS AgentCore Runtime solves a major pain-point for developers: &lt;strong&gt;secure, reliable, and scalable deployment of AI agents without wrangling cloud infrastructure details&lt;/strong&gt;. The new Strands SDK tightly integrates with AgentCore, letting you focus on your agent logic while AWS handles scaling, session isolation, and production readiness.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step-by-Step: Deploying Your Agent
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS account and CLI configured (&lt;code&gt;aws configure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Python 3.10+, pip, Docker&lt;/li&gt;
&lt;li&gt;Install the AgentCore toolkit and Strands SDK:
&lt;/li&gt;
&lt;/ul&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;bedrock-agentcore bedrock-agentcore-starter-toolkit strands-agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Sample Agent (&lt;code&gt;my_agent.py&lt;/code&gt;):&lt;/strong&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;bedrock_agentcore&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BedrockAgentCoreApp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;strands&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Agent&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BedrockAgentCoreApp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Agent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nd"&gt;@app.entrypoint&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;invoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&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;prompt&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;Hello! How can I help you today?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_message&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;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&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;app&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;strong&gt;Local Test (optional):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python my_agent.py
&lt;span class="c"&gt;# In another terminal:&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/invocations &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;'{"prompt": "Hello!"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configure for Cloud Deployment:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore configure &lt;span class="nt"&gt;--entrypoint&lt;/span&gt; my_agent.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow prompts for runtime options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deploy to AgentCore Runtime:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore launch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You’ll get an Agent ARN for your deployed agent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Invoke the Deployed Agent:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;agentcore invoke &lt;span class="s1"&gt;'{"prompt": "Tell me a joke"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Architecture Diagram
&lt;/h3&gt;

&lt;p&gt;Here’s a quick summary of the workflow:&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.amazonaws.com%2Fuploads%2Farticles%2Fqwa3knt57g7g4tm73jmw.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.amazonaws.com%2Fuploads%2Farticles%2Fqwa3knt57g7g4tm73jmw.png" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Local testing uses HTTP. Deployment and cloud invocation use AWS endpoints over HTTPS for secure communication.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  My Takeaways
&lt;/h3&gt;

&lt;p&gt;AgentCore Runtime and the Strands SDK make deploying AI agents on AWS simple and production-ready. From local prototyping to scalable cloud hosting, you skip the usual cloud headaches and focus on building features.&lt;/p&gt;

&lt;p&gt;Let me know if you want a deeper dive into custom agent logic, more advanced deployment options, or troubleshooting advice!&lt;/p&gt;




</description>
      <category>aws</category>
      <category>agents</category>
      <category>ai</category>
    </item>
    <item>
      <title>HealthBuddy SG: AI-Powered Wellness</title>
      <dc:creator>joshyfruit</dc:creator>
      <pubDate>Mon, 15 Sep 2025 06:22:24 +0000</pubDate>
      <link>https://dev.to/joshyfruit/healthbuddy-sg-ai-powered-wellness-ji8</link>
      <guid>https://dev.to/joshyfruit/healthbuddy-sg-ai-powered-wellness-ji8</guid>
      <description>

&lt;h2&gt;
  
  
  HealthBuddy SG: AI-Powered Wellness for Singapore’s Tropics
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/google-ai-studio-2025-09-03"&gt;Google AI Studio Multimodal Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;What I Built&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;HealthBuddy SG is a next-generation health and wellness applet designed specifically for Singaporeans and anyone living in a humid, tropical climate. It empowers users to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Check their health with natural voice interaction&lt;/strong&gt; (symptom analyzer and recommendations)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan and analyze local food&lt;/strong&gt; for nutrition and climate-adapted advice&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track personal progress&lt;/strong&gt; and receive daily, hyper-local, climate-aware guidance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My goal: &lt;strong&gt;bring personalized, practical AI health support to the palm of every Singapore resident&lt;/strong&gt;, helping users thrive amidst heat, humidity, and busy urban life.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Demo&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;🔗 Live Applet: &lt;a href="https://healthbuddy-sg-ver1-192629822894.us-west1.run.app/" rel="noopener noreferrer"&gt;https://healthbuddy-sg-ver1-192629822894.us-west1.run.app/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots:
&lt;/h3&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.amazonaws.com%2Fuploads%2Farticles%2Ffxmij8k1pbzbf6xa9ne3.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.amazonaws.com%2Fuploads%2Farticles%2Ffxmij8k1pbzbf6xa9ne3.png" alt="Dashboard UI" width="379" height="667"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fx7lhqs3pb7bx5vwschpj.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.amazonaws.com%2Fuploads%2Farticles%2Fx7lhqs3pb7bx5vwschpj.png" alt="Progress Dashboard" width="378" height="669"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fd1o5atxw8hazrs6s4ve9.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.amazonaws.com%2Fuploads%2Farticles%2Fd1o5atxw8hazrs6s4ve9.png" alt="Image Scanner" width="379" height="670"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Flgikwwrdwhjxvmceai18.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.amazonaws.com%2Fuploads%2Farticles%2Flgikwwrdwhjxvmceai18.png" alt="Image Scanner Results" width="376" height="668"&gt;&lt;/a&gt;&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.amazonaws.com%2Fuploads%2Farticles%2Fc1ujoagwk8k7e773fgdo.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.amazonaws.com%2Fuploads%2Farticles%2Fc1ujoagwk8k7e773fgdo.png" alt="QuickDoc an Health AI Voice Assistant" width="378" height="669"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Demo:&lt;br&gt;
&lt;a href="https://youtu.be/tvuQoJtL4YM" rel="noopener noreferrer"&gt;🎥 Click here to watch HealthBuddy SG’s main features in action&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;How I Used Google AI Studio&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Google AI Studio was the backbone&lt;/strong&gt; for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rapidly building and refining prompts for symptom descriptions (English/Singapore-style)&lt;/li&gt;
&lt;li&gt;Testing and iterating on voice/audio input, food image recognition, and text-based queries&lt;/li&gt;
&lt;li&gt;Using the “Get code” feature to scaffold web endpoints and integrate Gemini in my workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Gemini’s &lt;strong&gt;multimodal APIs made it possible to interconnect speech, text, and image understanding&lt;/strong&gt;, enabling truly natural, cross-modal user experiences.&lt;/p&gt;

&lt;p&gt;Cloud Run allowed frictionless, scalable deployment, making the applet ready for public use and further enhancements.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Multimodal Features&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;HealthBuddy SG leverages the following Google AI multimodal capabilities:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Voice Symptom Analysis&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Users describe how they feel by speaking naturally; Gemini listens, analyzes, and provides health recommendations with Singapore context: “Is it haze related?”, “Possible dengue?”, “Stay hydrated advice”, etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Food Scanner for Local Cuisine&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upload food photos; Gemini identifies local dishes (laksa, roti prata, etc.), assesses their nutrition, and gives “heat/hydration” tips specific to Singapore’s climate.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Personalized Progress Dashboard&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visualizes health data, voice check-ins, and food analysis over time with clear cards and charts inspired by leading health apps.&lt;/li&gt;
&lt;li&gt;Delivers actionable, climate-adapted suggestions to boost user wellbeing.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Singapore-Specific Health Guidance&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every result is enhanced with real tips for local concerns (haze, dengue, humidity, heatstroke).&lt;/li&gt;
&lt;li&gt;Smart recommendations for when to seek help from nearby clinics, polyclinics, or pharmacies.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Professional, Responsive UI&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistent card-based design, friendly mascot, easily navigable for mobile or desktop, ensuring a joyful user journey.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why This Matters&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Singaporeans face unique health challenges—tropical illnesses, heat, and rapid urban pace. HealthBuddy SG uses &lt;strong&gt;state-of-the-art Google AI multimodal technology&lt;/strong&gt; to provide support that’s context-aware and actionable, helping every user make smarter daily wellness choices.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Solo submission by Joshua Cymon Gomez (&lt;a href="https://dev.toyour-devto-link"&gt;@joshyfruit &lt;/a&gt;), using open source and original code/UX—starter voice assistant, new modules, pro-level UI, and code structured for future growth.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  devchallenge #googleaichallenge #ai #gemini #singapore #healthtech
&lt;/h1&gt;




</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>ai</category>
      <category>gemini</category>
    </item>
  </channel>
</rss>
