<?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: Alex LaGuardia</title>
    <description>The latest articles on DEV Community by Alex LaGuardia (@alexlaguardia).</description>
    <link>https://dev.to/alexlaguardia</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3832811%2Faba6514b-1248-4d62-a8ac-2568cd790b8f.jpeg</url>
      <title>DEV Community: Alex LaGuardia</title>
      <link>https://dev.to/alexlaguardia</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alexlaguardia"/>
    <language>en</language>
    <item>
      <title>Three freelancer tools died in two months. I built the replacement.</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Sat, 04 Apr 2026 10:18:46 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/three-freelancer-tools-died-in-two-months-i-built-the-replacement-4dbe</link>
      <guid>https://dev.to/alexlaguardia/three-freelancer-tools-died-in-two-months-i-built-the-replacement-4dbe</guid>
      <description>&lt;p&gt;HoneyBook hiked prices 89%. AND.CO shut down entirely. Bonsai got acquired by Zoom with no roadmap. All within two months of each other.&lt;/p&gt;

&lt;p&gt;I'm a freelancer who was paying $29/mo for HoneyBook. I used two features: proposals and invoice reminders. That's it. Two features out of fifty. The other forty-eight were bloat I paid for and never touched.&lt;/p&gt;

&lt;p&gt;Dubsado is so complex that a cottage industry of "setup specialists" charge $500-3,500 just to configure it. That tells you everything about how broken this market is.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://stampwerk.com" rel="noopener noreferrer"&gt;Stampwerk&lt;/a&gt;. Proposals, contracts, invoices, and follow-ups. $12/mo. No setup wizard. No onboarding call. No fifty features you'll never open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the AI actually does
&lt;/h2&gt;

&lt;p&gt;This isn't "AI-powered" as a marketing checkbox. The AI does two specific things that freelancers hate doing manually.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Proposal generation.&lt;/strong&gt; Answer 5 questions about a project and the LLM writes a full proposal -- scope, timeline, pricing breakdown, and payment terms.&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;# 5 inputs in, structured proposal out
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;groq_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;llama-3.3-70b-versatile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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;system&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROPOSAL_SYSTEM&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&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&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;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="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;client&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;client_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;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;project_desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;budget_range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;budget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeline&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;timeline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deliverables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deliverables&lt;/span&gt;
        &lt;span class="p"&gt;})}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;response_format&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;type&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;json_object&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;Not template-fill. The model reasons about scope and pricing based on the project description. You edit what it generates, not what a template assumes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Invoice follow-ups.&lt;/strong&gt; A background daemon runs hourly and chases overdue invoices on a 3-step escalation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Day 3&lt;/strong&gt; -- friendly check-in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 7&lt;/strong&gt; -- professional reminder with payment link&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Day 14&lt;/strong&gt; -- firm final notice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each message matches the escalation stage. This is the part of freelancing that everyone hates and nobody does consistently. Now it runs while you sleep.&lt;/p&gt;

&lt;h2&gt;
  
  
  The full pipeline
&lt;/h2&gt;

&lt;p&gt;The thesis behind Stampwerk is that these aren't separate features. They're one flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client signs up (Google or magic link, 30 seconds)&lt;/li&gt;
&lt;li&gt;Creates a project, answers 5 questions&lt;/li&gt;
&lt;li&gt;AI generates the proposal&lt;/li&gt;
&lt;li&gt;Client views it at a public link, accepts with one click&lt;/li&gt;
&lt;li&gt;Contract auto-generates from the accepted proposal terms&lt;/li&gt;
&lt;li&gt;Client e-signs&lt;/li&gt;
&lt;li&gt;Milestones trigger invoices with Stripe payment links&lt;/li&gt;
&lt;li&gt;Overdue invoices get automatic follow-ups&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One pipeline. Every step feeds the next. No configuration, no "setup specialist" needed.&lt;/p&gt;

&lt;p&gt;HoneyBook has all these features too. They also have fifty others, a $29-59/mo price tag, and an 89% price hike that tells you where they think the market is headed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these tech choices
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;42 routes, async, typed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;One file, WAL mode, zero ops burden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI&lt;/td&gt;
&lt;td&gt;Groq + Llama 3.3 70B&lt;/td&gt;
&lt;td&gt;Free inference, structured JSON output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments&lt;/td&gt;
&lt;td&gt;Stripe&lt;/td&gt;
&lt;td&gt;Payment links for clients, subscriptions for us&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Email&lt;/td&gt;
&lt;td&gt;Resend&lt;/td&gt;
&lt;td&gt;Transactional email under 3K sends is free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Next.js 14 + Tailwind&lt;/td&gt;
&lt;td&gt;SSR, file routing, fast iteration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Total infrastructure cost: $0/mo. AI calls are free through Groq. Email is free at this scale. Stripe only charges when money moves. The whole thing runs on a single server I already had.&lt;/p&gt;

&lt;p&gt;This matters because the competitors raised hundreds of millions. HoneyBook took $479M in funding. Dubsado bootstrapped to $2.5M ARR. Moxie raised ~$10M. When you compete against that, your margin has to be your moat. A $0 cost base means $12/mo is sustainable, not a loss leader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest trade-offs
&lt;/h2&gt;

&lt;p&gt;The retro arcade UI is polarizing. Some people think it's unprofessional for a business tool. I'm keeping it. If your target is corporate project managers, sure. If your target is solo freelancers who are tired of software that looks like every other SaaS dashboard, it works.&lt;/p&gt;

&lt;p&gt;No PDF export yet. No time tracking. No QuickBooks integration. No mobile app. These are real gaps. But I'd rather ship the core pipeline right and add features from real user feedback than build fifty features nobody asked for.&lt;/p&gt;

&lt;p&gt;That's how we got HoneyBook in the first place.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://stampwerk.com" rel="noopener noreferrer"&gt;stampwerk.com&lt;/a&gt; -- free tier gives you 5 clients, 5 projects, and full AI proposals. Pro is $12/mo for unlimited.&lt;/p&gt;




&lt;p&gt;Built with FastAPI, Next.js 14, SQLite, Groq, Stripe, and Resend. Questions about the stack or the business model welcome.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>ai</category>
      <category>freelancing</category>
    </item>
    <item>
      <title>I Built a Security Scanner That Uses AI to Review Its Own Findings</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Tue, 31 Mar 2026 09:53:59 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/i-built-a-security-scanner-that-uses-ai-to-review-its-own-findings-43o9</link>
      <guid>https://dev.to/alexlaguardia/i-built-a-security-scanner-that-uses-ai-to-review-its-own-findings-43o9</guid>
      <description>&lt;p&gt;Every AI coding tool ships code fast. None of them check if it's safe.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://github.com/AlexlaGuardia/Critik" rel="noopener noreferrer"&gt;Critik&lt;/a&gt; — an open-source security scanner that catches what your AI writes and your review misses. Regex and AST find the candidates. An LLM reviews each one with full file context, confirms the real problems, kills the false positives, and explains why in plain English.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;pip install critik&lt;/code&gt; and you're scanning in 30 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Numbers Are Ugly
&lt;/h2&gt;

&lt;p&gt;53% of teams that shipped AI-generated code later found security issues that passed review. Georgia Tech's Vibe Security Radar tracked 74 CVEs from AI coding tools in Q1 2026 alone — 6 in January, 15 in February, 35 in March. Accelerating.&lt;/p&gt;

&lt;p&gt;Here's what I keep finding when I scan AI-built projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardcoded API keys&lt;/strong&gt; — Cursor generates a Supabase client and pastes the service_role key right in the file&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL injection via f-strings&lt;/strong&gt; — Copilot autocompletes &lt;code&gt;db.execute(f"SELECT * FROM users WHERE id = {user_id}")&lt;/code&gt; without blinking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firebase rules wide open&lt;/strong&gt; — Bolt scaffolds &lt;code&gt;read: true, write: true&lt;/code&gt; and nobody touches it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NEXT_PUBLIC_ prefix on secrets&lt;/strong&gt; — the env var that hands your database URL to every browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not edge cases. Default patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tools That Exist Don't Fix This
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Snyk&lt;/strong&gt; charges $25-98/dev/mo. Built for enterprises with procurement budgets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Semgrep&lt;/strong&gt; is powerful. Also requires writing custom rules in a DSL. Steep curve. Recently relicensed behind commercial terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;npm audit&lt;/strong&gt; is, in Dan Abramov's words, "broken by design" — flags devDependency issues that can't touch production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bandit&lt;/strong&gt; and &lt;strong&gt;ESLint security plugins&lt;/strong&gt; catch patterns but have zero context. They flag &lt;code&gt;eval()&lt;/code&gt; in a test fixture the same way they flag &lt;code&gt;eval(user_input)&lt;/code&gt; in a request handler.&lt;/p&gt;

&lt;p&gt;That last one is the real problem. Static scanners are noisy. Developers learn to ignore them. Which means they ignore the real findings too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Passes. One Scanner.
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pass 1 — Static (fast, offline, free)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Regex patterns and Python AST parsing. Hardcoded secrets (16 patterns — AWS, Stripe, OpenAI, Anthropic), SQL injection, command injection, eval/exec, XSS, framework misconfigs. Runs in milliseconds. No API key needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass 2 — AI Review (optional, the whole point)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add &lt;code&gt;--ai&lt;/code&gt; and each finding goes to Groq's Llama 3.3 70B with the &lt;em&gt;full file&lt;/em&gt; as context. The model acts as a security analyst:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Verdict&lt;/strong&gt;: confirmed, false_positive, or needs_review&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence&lt;/strong&gt;: 0-100%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Why&lt;/strong&gt;: "This eval() parses trusted JSON config from a local file" vs "This eval() takes unsanitized user input from req.query"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix&lt;/strong&gt;: actual code, not generic advice&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The AI doesn't replace the scanner. It reviews the scanner's work.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Looks Like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;critik scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--ai&lt;/span&gt;
&lt;span class="go"&gt;
  Critik v0.4.0 — scanned 7 files  [AI]

  CRITICAL  nextjs-public-secret
  app/config.ts:18  Secret exposed to browser via NEXT_PUBLIC_ prefix
  | 18 | const key = process.env.NEXT_PUBLIC_SECRET_API_KEY
  CONFIRMED (95%)
&lt;/span&gt;&lt;span class="gp"&gt;  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;The NEXT_PUBLIC_ prefix exposes this API key to every browser.
&lt;span class="go"&gt;  Fix: Rename to SECRET_API_KEY and access server-side only

  CRITICAL  aws-access-key              (dimmed — false positive)
  tests/fixtures/bad_secrets.py:2  AWS access key detected
  FALSE POS (100%)
&lt;/span&gt;&lt;span class="gp"&gt;  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;This file is &lt;span class="k"&gt;in &lt;/span&gt;tests/fixtures — fake credentials &lt;span class="k"&gt;for &lt;/span&gt;testing.
&lt;span class="go"&gt;
  HIGH      sql-fstring
  app/db.py:6  SQL injection via f-string in execute()
  | 6 | db.execute(f"SELECT * FROM users WHERE id = {user_id}")
  CONFIRMED (95%)
&lt;/span&gt;&lt;span class="gp"&gt;  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;This f-string takes unsanitized input, allowing SQL injection.
&lt;span class="go"&gt;  Fix: db.execute('SELECT * FROM users WHERE id = ?', (user_id,))

  ─────────────────────────────────────────────
  7 files scanned in 2714ms — 23 findings: 10 critical, 8 high, 5 medium
  AI analysis: 7 confirmed, 16 false positives
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without AI: 23 findings. Developer overwhelmed. Ignores all of them.&lt;/p&gt;

&lt;p&gt;With AI: 7 real issues. 16 dismissed with reasons. Developer fixes what matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the Hood
&lt;/h2&gt;

&lt;p&gt;Each API call sends the full file content (up to 8K chars) plus all findings for that file. One call per file — keeps tokens low, context high.&lt;/p&gt;

&lt;p&gt;The model sees imports, function signatures, data flow. It knows test fixtures are probably safe, &lt;code&gt;.env&lt;/code&gt; files are expected to have secrets, and &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; vars are intentionally client-exposed.&lt;/p&gt;

&lt;p&gt;Temperature 0.2. Structured JSON back. If the API is down, Critik falls back to regex-only. No crash, no hang.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Catches
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Patterns&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Secrets&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;AWS, GitHub, OpenAI, Anthropic, Stripe, Slack, DB URLs, JWTs, private keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Injection&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;SQL via f-string/concat, eval(), exec(), os.system(), subprocess shell=True, XSS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frameworks&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Supabase RLS, Firebase rules, NEXT_PUBLIC_ secrets, NextAuth, Prisma raw, Stripe webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Config&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;NODE_ENV in source, insecure cookies, open CORS, debug mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Missing auth on routes, open endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dotenv&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Exposed .env, sensitive vars unencrypted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Free. All of It.
&lt;/h2&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;critik
critik scan &lt;span class="nb"&gt;.&lt;/span&gt;                &lt;span class="c"&gt;# regex/AST only — offline, instant&lt;/span&gt;
critik scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--ai&lt;/span&gt;           &lt;span class="c"&gt;# + AI review (needs GROQ_API_KEY)&lt;/span&gt;
critik scan &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; fix   &lt;span class="c"&gt;# copy-paste fix prompts for Cursor/Claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pre-commit hook: &lt;code&gt;critik hook install&lt;/code&gt;. SARIF output for CI/CD. GitHub Action included. MIT license.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I hunt bugs on HackerOne and 0din. The vulnerabilities I find in production are the same patterns AI tools ship by default. Hardcoded keys. Missing auth. SQL injection. Open configs.&lt;/p&gt;

&lt;p&gt;The irony: AI coding tools are the biggest source of new vulnerabilities &lt;em&gt;and&lt;/em&gt; the best tool for catching them. A regex finds &lt;code&gt;eval()&lt;/code&gt;. Only an LLM can tell you if it's dangerous.&lt;/p&gt;

&lt;p&gt;Critik is the scanner I wanted. Now it exists.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Website&lt;/strong&gt;: &lt;a href="https://critik.dev" rel="noopener noreferrer"&gt;critik.dev&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/AlexlaGuardia/Critik" rel="noopener noreferrer"&gt;AlexlaGuardia/Critik&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;code&gt;pip install critik&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;critik scan .&lt;/code&gt; on your project. You might not like what you find.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>OAuth2, Two APIs, and Soft Deletes — Building an MCP Server for FreshBooks</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Thu, 26 Mar 2026 22:33:14 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/oauth2-two-apis-and-soft-deletes-building-an-mcp-server-for-freshbooks-2g03</link>
      <guid>https://dev.to/alexlaguardia/oauth2-two-apis-and-soft-deletes-building-an-mcp-server-for-freshbooks-2g03</guid>
      <description>&lt;p&gt;Most MCP servers assume your target API hands you an API key and gets out of the way. FreshBooks doesn't. It requires full OAuth2, splits its API across two different base URLs, and has resources that can only be soft-deleted. Building this server meant solving problems most MCP tutorials don't prepare you for.&lt;/p&gt;

&lt;p&gt;The result: 25 tools covering invoices, clients, expenses, payments, time tracking, projects, estimates, and reports — with the auth flow, API quirks, and deletion edge cases handled for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; lets AI assistants interact with external tools directly. With this server installed, you manage your entire freelance business without leaving your AI assistant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (manual):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into FreshBooks&lt;/li&gt;
&lt;li&gt;Navigate to Invoices → find overdue ones&lt;/li&gt;
&lt;li&gt;Note the client names and amounts&lt;/li&gt;
&lt;li&gt;Switch to your AI tool&lt;/li&gt;
&lt;li&gt;Type out all the details&lt;/li&gt;
&lt;li&gt;Ask what to do about it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;After (with mcp-freshbooks):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Which invoices are overdue? Draft follow-up messages for each client based on how late they are."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude calls &lt;code&gt;list_invoices&lt;/code&gt; with a status filter, gets the details, and drafts personalized follow-ups — all in one shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  25 Tools, Full Business Coverage
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invoices&lt;/strong&gt; (6): List, get, create, update, delete, send by email&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clients&lt;/strong&gt; (4): List, get, create, archive with full contact details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expenses&lt;/strong&gt; (3): List, get, create with category and tax support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt; (2): List, record payments against invoices&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time Entries&lt;/strong&gt; (2): List, create with project/service association&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Projects&lt;/strong&gt; (2): List, get with budget and billing details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimates&lt;/strong&gt; (2): List, get with line items&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports&lt;/strong&gt; (1): Profit &amp;amp; Loss report with date filtering&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth&lt;/strong&gt; (3): OAuth2 flow, identity check, connection test&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Decisions Worth Sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Full OAuth2 — No Shortcuts
&lt;/h3&gt;

&lt;p&gt;FreshBooks requires OAuth2. No API keys, no shortcuts. The server handles the entire flow: it spins up a local HTTPS callback server, opens the authorization URL, catches the redirect with the auth code, exchanges it for tokens, and persists them to &lt;code&gt;~/.mcp-freshbooks/tokens.json&lt;/code&gt;. Token refresh is automatic — you authenticate once and forget about it.&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;@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;freshbooks_authenticate&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;Start OAuth2 authentication. Returns a URL to open in your browser.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_config&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="nf"&gt;get_auth_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Spins up localhost:8555 HTTPS callback server in background
&lt;/span&gt;    &lt;span class="nf"&gt;start_callback_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&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;Open this URL to authorize:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the hardest part of the build. Most MCP servers assume API keys. When your platform demands OAuth2, you either solve it properly or your server is useless.&lt;/p&gt;

&lt;h3&gt;
  
  
  Two APIs, Two Base URLs
&lt;/h3&gt;

&lt;p&gt;FreshBooks has a split API: accounting resources (invoices, clients, expenses) live at &lt;code&gt;api.freshbooks.com/accounting/account/{account_id}/...&lt;/code&gt;, while project resources (projects, time entries) live at &lt;code&gt;api.freshbooks.com/projects/business/{business_id}/...&lt;/code&gt;. Different base URLs, different ID types.&lt;/p&gt;

&lt;p&gt;The client abstracts this completely:&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;ACCOUNTING_BASE&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.freshbooks.com/accounting/account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PROJECTS_BASE&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.freshbooks.com/projects/business&lt;/span&gt;&lt;span class="sh"&gt;"&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;accounting_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="n"&gt;account_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_ids&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;ACCOUNTING_BASE&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;account_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;resource&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="bp"&gt;...&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;projects_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...):&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;business_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;get_ids&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;PROJECTS_BASE&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;business_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;resource&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tools never think about which API base to use — they just call the right function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Soft Deletes vs Hard Deletes
&lt;/h3&gt;

&lt;p&gt;FreshBooks treats deletion differently depending on the resource. Invoices and estimates can be hard-deleted (actually removed). Clients and expenses can only be soft-deleted by setting &lt;code&gt;vis_state&lt;/code&gt; to 1 (archived). Delete a client with the wrong endpoint and you get a cryptic 400 error.&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;accounting_delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_id&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Hard-delete (invoices, estimates).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="bp"&gt;...&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;accounting_soft_delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapper_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Soft-delete via vis_state=1 (clients, expenses).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;accounting_update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapper_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vis_state&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each tool uses the correct method — the AI never needs to know about this distinction.&lt;/p&gt;

&lt;h3&gt;
  
  
  The search[key] Query Format
&lt;/h3&gt;

&lt;p&gt;FreshBooks uses a non-standard query parameter format for filters: &lt;code&gt;search[status]=2&amp;amp;search[date_from]=2024-01-01&lt;/code&gt;. Not &lt;code&gt;status=2&lt;/code&gt;, not &lt;code&gt;filter[status]=2&lt;/code&gt; — specifically &lt;code&gt;search[key]&lt;/code&gt;. Get the format wrong and the API silently ignores your filters.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_build_search_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;params&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;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&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;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&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;search[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;][]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="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;str&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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&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;value&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;params&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tools accept clean Python dicts and handle the formatting internally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started in 2 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&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;mcp-freshbooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create OAuth App
&lt;/h3&gt;

&lt;p&gt;Go to &lt;a href="https://my.freshbooks.com/#/developer" rel="noopener noreferrer"&gt;my.freshbooks.com/#/developer&lt;/a&gt;, create an app, and note the client ID and secret. Set the redirect URI to &lt;code&gt;https://localhost:8555/callback&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&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;"freshbooks"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-freshbooks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"FRESHBOOKS_CLIENT_ID"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-client-id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"FRESHBOOKS_CLIENT_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-client-secret"&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;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;p&gt;Then ask Claude to run &lt;code&gt;freshbooks_authenticate&lt;/code&gt; — it will give you a URL to authorize. One-time setup, tokens auto-refresh after that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add freshbooks &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;FRESHBOOKS_CLIENT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;id &lt;/span&gt;&lt;span class="nv"&gt;FRESHBOOKS_CLIENT_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret mcp-freshbooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Same JSON config as Claude Desktop in &lt;code&gt;.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Add invoice line item support from day one.&lt;/strong&gt; The current &lt;code&gt;create_invoice&lt;/code&gt; accepts line items as a JSON string, which works but isn't the cleanest interface. A dedicated line-item builder would be more ergonomic for the AI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handle plan-gated features more gracefully.&lt;/strong&gt; FreshBooks gates features by plan tier — time tracking, projects, and advanced reports require paid plans. The error handling catches 403s and explains this, but detecting plan limits upfront would be smoother.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for MCP Server Builders
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Solve OAuth2 properly.&lt;/strong&gt; If your target platform requires it, don't punt — build the full flow with token persistence and auto-refresh. It's the difference between a demo and a tool people actually use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Abstract API inconsistencies.&lt;/strong&gt; If the platform has split APIs, different deletion behaviors, or non-standard query formats — hide all of it. The AI should never deal with platform quirks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Handle plan-tier errors.&lt;/strong&gt; SaaS platforms gate features by pricing tier. Catch permission errors and explain what's happening instead of returning raw 403s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persist tokens securely.&lt;/strong&gt; Store tokens in a well-known location (&lt;code&gt;~/.mcp-freshbooks/&lt;/code&gt;) with clear documentation. Users shouldn't have to re-authenticate every session.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/AlexlaGuardia/mcp-freshbooks" rel="noopener noreferrer"&gt;AlexlaGuardia/mcp-freshbooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/mcp-freshbooks/" rel="noopener noreferrer"&gt;mcp-freshbooks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part of a series of production-grade MCP servers I'm building for underserved SaaS platforms. Also available: &lt;a href="https://github.com/AlexlaGuardia/mcp-mailchimp" rel="noopener noreferrer"&gt;Mailchimp&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-woocommerce" rel="noopener noreferrer"&gt;WooCommerce&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-activecampaign" rel="noopener noreferrer"&gt;ActiveCampaign&lt;/a&gt;. Follow me here or on &lt;a href="https://github.com/AlexlaGuardia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to catch the next one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update (April 2026):&lt;/strong&gt; Since publishing, mcp-freshbooks has been expanded from 25 to &lt;strong&gt;53 tools&lt;/strong&gt; (v0.2.0). New coverage includes recurring invoices, 5 typed reports (P&amp;amp;L, tax, accounts aging, revenue, expense), and workflow tools like invoice-from-time and estimate-to-invoice conversion. &lt;code&gt;pip install mcp-freshbooks&lt;/code&gt; to get the latest.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>freshbooks</category>
    </item>
    <item>
      <title>Zero to 33 Tools: Building the First MCP Server for ActiveCampaign</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Thu, 26 Mar 2026 22:32:11 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/zero-to-33-tools-building-the-first-mcp-server-for-activecampaign-f8j</link>
      <guid>https://dev.to/alexlaguardia/zero-to-33-tools-building-the-first-mcp-server-for-activecampaign-f8j</guid>
      <description>&lt;p&gt;I searched GitHub, npm, PyPI, and every MCP registry I could find for an ActiveCampaign MCP server. Zero results. Not a bad one, not an incomplete one — nothing. For a platform with 185,000 paying customers and a full-featured API, that gap felt worth filling.&lt;/p&gt;

&lt;p&gt;So I built it from scratch: 33 tools covering contacts, deals, automations, tags, pipelines, campaigns, custom fields, and webhooks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; lets AI assistants interact with external tools directly. With this server installed, you manage your CRM and marketing automation without leaving your AI assistant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (manual):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Log into ActiveCampaign&lt;/li&gt;
&lt;li&gt;Navigate to Contacts → search for a customer&lt;/li&gt;
&lt;li&gt;Check their tags, deals, automation history&lt;/li&gt;
&lt;li&gt;Switch to your AI tool&lt;/li&gt;
&lt;li&gt;Describe what you found&lt;/li&gt;
&lt;li&gt;Ask for analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;After (with mcp-activecampaign):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Find all contacts tagged 'enterprise-lead' and show me their deal pipeline status. Which ones haven't been contacted in 30 days?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude calls &lt;code&gt;list_contacts&lt;/code&gt; with a tag filter, then &lt;code&gt;list_deals&lt;/code&gt; for each, and gives you an actionable priority list — all in one shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  33 Tools, Full CRM Coverage
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contacts&lt;/strong&gt; (7): List, get, create, update, delete, search, manage tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deals&lt;/strong&gt; (5): List, get, create, update, delete with pipeline/stage support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags&lt;/strong&gt; (4): List, create, add to contact, remove from contact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lists&lt;/strong&gt; (2): List all, get details with subscriber counts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automations&lt;/strong&gt; (3): List, get details, add contacts to automations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipelines&lt;/strong&gt; (2): List pipelines, list stages within pipelines&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom Fields&lt;/strong&gt; (3): List fields, get values, set values per contact&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Campaigns&lt;/strong&gt; (2): List campaigns with stats, get full campaign details&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Accounts&lt;/strong&gt; (2): List and get company/organization records&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; (3): List, create, delete for real-time event handling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Decisions Worth Sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Client-Side Rate Limiting
&lt;/h3&gt;

&lt;p&gt;ActiveCampaign enforces 5 requests per second per account. Hit the limit and you get 429s that can cascade. Instead of reacting to failures, the client prevents them:&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;MAX_RPS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;MIN_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;MAX_RPS&lt;/span&gt;  &lt;span class="c1"&gt;# 0.2s between requests
&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;_throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Enforce 5 req/s rate limit.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_request&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIN_INTERVAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIN_INTERVAL&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;elapsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_last_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An asyncio lock ensures thread safety, and &lt;code&gt;time.monotonic()&lt;/code&gt; avoids clock-drift edge cases. If a 429 still slips through (burst from another client), there's a fallback that respects the &lt;code&gt;Retry-After&lt;/code&gt; header:&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;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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;Retry-After&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;await&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;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;method&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="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Belt and suspenders. The AI never sees rate limit errors.&lt;/p&gt;

&lt;h3&gt;
  
  
  The "Deal Groups" Translation
&lt;/h3&gt;

&lt;p&gt;ActiveCampaign calls pipelines "deal groups" internally. The API endpoint is &lt;code&gt;/dealGroups&lt;/code&gt;, stages are filtered by &lt;code&gt;d_groupid&lt;/code&gt;, and creating a deal requires a &lt;code&gt;group&lt;/code&gt; field — not &lt;code&gt;pipeline&lt;/code&gt;. This naming inconsistency trips up every integration:&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;@mcp.tool&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;def&lt;/span&gt; &lt;span class="nf"&gt;list_pipelines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&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;0&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 deal pipelines (called &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;deal groups&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; in AC API).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;data&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;ac&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;/dealGroups&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&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;dealGroups&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;pipelines&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;id&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="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;title&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="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;title&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="p"&gt;),&lt;/span&gt;
            &lt;span class="bp"&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 tools use the word "pipeline" (what users expect) while the client sends "dealGroup" (what the API expects). The AI works with natural language; the translation happens silently.&lt;/p&gt;

&lt;h3&gt;
  
  
  URL Normalization with API Path
&lt;/h3&gt;

&lt;p&gt;ActiveCampaign API URLs look like &lt;code&gt;https://youraccountname.api-us1.com/api/3/contacts&lt;/code&gt;. Users might pass just the account URL, or include the &lt;code&gt;/api/3&lt;/code&gt; suffix. The client normalizes both:&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;base_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base_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;base_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/api/3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small thing, but it eliminates a common setup failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read-Only Automations (And Being Honest About It)
&lt;/h3&gt;

&lt;p&gt;ActiveCampaign's API doesn't support creating automations programmatically — you can only list them, view details, and add contacts to existing ones. The tool docstrings say this explicitly:&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;@mcp.tool&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;def&lt;/span&gt; &lt;span class="nf"&gt;list_automations&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 automations (read-only — AC API doesn&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t support creating automations).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the AI knows the boundary, it can suggest alternatives ("Create the automation in the AC dashboard, then I can add contacts to it") instead of failing mysteriously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started in 2 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&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;mcp-activecampaign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Get Your API Credentials
&lt;/h3&gt;

&lt;p&gt;ActiveCampaign → Settings → Developer. You need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API URL&lt;/strong&gt;: &lt;code&gt;https://youraccountname.api-us1.com&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API Key&lt;/strong&gt;: The key shown on that page&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&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;"activecampaign"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-activecampaign"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"ACTIVECAMPAIGN_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://youraccountname.api-us1.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ACTIVECAMPAIGN_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-api-key"&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;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;
  
  
  Claude Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add activecampaign &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;ACTIVECAMPAIGN_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://youraccountname.api-us1.com &lt;span class="nv"&gt;ACTIVECAMPAIGN_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;key mcp-activecampaign
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Same JSON config as Claude Desktop in &lt;code&gt;.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Add deal note support.&lt;/strong&gt; ActiveCampaign deals have a notes system that's heavily used by sales teams. I covered the core CRUD but skipped notes — they're high-value for AI-assisted sales workflows.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build contact-to-deal linking tools.&lt;/strong&gt; The current &lt;code&gt;create_deal&lt;/code&gt; accepts a &lt;code&gt;contact_id&lt;/code&gt;, but there's no tool to view or manage the contact-deal association after creation. That relationship is central to how AC users think about their CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for MCP Server Builders
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit proactively, not reactively.&lt;/strong&gt; Client-side throttling is always better than hitting limits and retrying. Use &lt;code&gt;asyncio.Lock&lt;/code&gt; + &lt;code&gt;time.monotonic()&lt;/code&gt; for a clean implementation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Translate internal naming to user naming.&lt;/strong&gt; If the API calls something "dealGroups" but users call it "pipelines," use the user's word in your tools. The translation is invisible and the ergonomics improve dramatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document API limitations in docstrings.&lt;/strong&gt; If the platform doesn't support creating a resource via API, say so in the tool description. The AI uses docstrings to decide what to suggest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be the first mover.&lt;/strong&gt; When you search for "[Platform] MCP server" and find zero results, that's a signal. 185K customers and nobody built this? Ship it.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/AlexlaGuardia/mcp-activecampaign" rel="noopener noreferrer"&gt;AlexlaGuardia/mcp-activecampaign&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/mcp-activecampaign/" rel="noopener noreferrer"&gt;mcp-activecampaign&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part of a series of production-grade MCP servers I'm building for underserved SaaS platforms. Also available: &lt;a href="https://github.com/AlexlaGuardia/mcp-mailchimp" rel="noopener noreferrer"&gt;Mailchimp&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-woocommerce" rel="noopener noreferrer"&gt;WooCommerce&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-freshbooks" rel="noopener noreferrer"&gt;FreshBooks&lt;/a&gt;. Follow me here or on &lt;a href="https://github.com/AlexlaGuardia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to catch the next one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update (April 2026):&lt;/strong&gt; Since publishing, mcp-activecampaign has been expanded from 33 to &lt;strong&gt;65 tools&lt;/strong&gt; (v0.2.0). New coverage includes lead scoring, saved segments, campaign create/send, forms, goals, deal custom fields, CRM notes and tasks, event tracking, ecommerce, and bulk import. &lt;code&gt;pip install mcp-activecampaign&lt;/code&gt; to get the latest.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>activecampaign</category>
    </item>
    <item>
      <title>How I Built a Full Product in One Night with 3 Parallel AI Agents</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Wed, 25 Mar 2026 22:12:15 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/how-i-built-a-full-product-in-one-night-with-3-parallel-ai-agents-1bpk</link>
      <guid>https://dev.to/alexlaguardia/how-i-built-a-full-product-in-one-night-with-3-parallel-ai-agents-1bpk</guid>
      <description>&lt;p&gt;Last Thursday night I sat down to add session handoff to my Python library. I stood up 8 hours later with a complete product: MCP server, REST API, embedded dashboard, event triggers, signal compaction. From 1,200 lines to 5,900. From a CLI tool to something with a web UI.&lt;/p&gt;

&lt;p&gt;Here's how, and why the technique matters more than the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup
&lt;/h2&gt;

&lt;p&gt;I'd built &lt;a href="https://github.com/AlexlaGuardia/Vigil" rel="noopener noreferrer"&gt;Vigil&lt;/a&gt; — an awareness daemon for AI agents. It worked great as a CLI tool: emit signals, compile state, boot agents with context. But it was missing the features that make it a real product: session handoff, an MCP server for Claude/Cursor integration, a REST API, and a dashboard.&lt;/p&gt;

&lt;p&gt;Each of these features lives in its own module. They share the database layer but have no code dependencies on each other. That's the key insight.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technique: Parallel AI Windows
&lt;/h2&gt;

&lt;p&gt;I opened three Claude Code terminal windows, each working on a separate file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window 1:&lt;/strong&gt; Session handoff protocol (&lt;code&gt;handoff.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window 2:&lt;/strong&gt; Signal compaction engine (&lt;code&gt;compaction.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window 3:&lt;/strong&gt; MCP server mode (&lt;code&gt;mcp_server.py&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each window had the same context: the existing codebase, the database schema, the module interfaces. But they worked independently, writing to separate files. No merge conflicts.&lt;/p&gt;

&lt;p&gt;While those three were building, I reviewed their output periodically and planned the next batch. When all three finished, I opened a single integration window that wired everything together: imports, CLI commands, shared database migrations.&lt;/p&gt;

&lt;p&gt;Then I repeated the pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Window 1:&lt;/strong&gt; REST API (&lt;code&gt;api.py&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window 2:&lt;/strong&gt; Dashboard templates (5 HTML pages)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Window 3:&lt;/strong&gt; Event triggers (&lt;code&gt;triggers.py&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same idea. Independent files, parallel execution, single integration pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Made It Work
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Clean module boundaries.&lt;/strong&gt; Every feature was a new file that imported from existing modules (&lt;code&gt;db.py&lt;/code&gt;, &lt;code&gt;signals.py&lt;/code&gt;, &lt;code&gt;awareness.py&lt;/code&gt;). No feature needed to modify another feature's code. This isn't accidental — I designed the architecture knowing I'd build this way.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Stable interfaces.&lt;/strong&gt; The database schema and the &lt;code&gt;VigilDB&lt;/code&gt; API were stable. All three windows could &lt;code&gt;from vigil.db import VigilDB&lt;/code&gt; and trust that the interface wouldn't change under them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Integration is the bottleneck, not implementation.&lt;/strong&gt; Each module took 30-60 minutes to build. Integration — wiring CLI commands, updating &lt;code&gt;__init__.py&lt;/code&gt;, running the test suite — took 20 minutes per batch. The integration step is where I caught type mismatches, missing imports, and interface disagreements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Mid-session code audit.&lt;/strong&gt; After the first batch (handoff + compaction + MCP), I ran a full code audit before starting batch two. Found 3 critical issues and 6 important ones. Fixing them before building the REST API prevented those bugs from propagating.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before (v0.1)&lt;/th&gt;
&lt;th&gt;After (v1.0)&lt;/th&gt;
&lt;th&gt;Delta&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Lines of code&lt;/td&gt;
&lt;td&gt;1,278&lt;/td&gt;
&lt;td&gt;5,922&lt;/td&gt;
&lt;td&gt;+4,644&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Modules&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;+6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;48&lt;/td&gt;
&lt;td&gt;196&lt;/td&gt;
&lt;td&gt;+148&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CLI commands&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;+7&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;14 commits over 8 hours. Zero merge conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with the integration test, not the unit tests.&lt;/strong&gt; Each window wrote unit tests for its module. But the integration tests — "emit a signal, compile awareness, check the dashboard shows it" — came last. If I'd written those first, I'd have caught interface mismatches earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define the REST API schema before building the dashboard.&lt;/strong&gt; Window 2 (dashboard) had to guess what the API response shapes would be, because Window 1 (API) was still being built. A shared types file or API schema would have eliminated that friction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond My Project
&lt;/h2&gt;

&lt;p&gt;The parallel AI window technique works for any codebase with clean module boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microservices:&lt;/strong&gt; Each window builds a different service&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend components:&lt;/strong&gt; Each window builds a different page/component&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data pipelines:&lt;/strong&gt; Each window builds a different stage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The constraint is the same: modules must be independently buildable with stable interfaces between them. If feature B needs to call feature A's code and that code doesn't exist yet, you can't parallelize them.&lt;/p&gt;

&lt;p&gt;This is also a strong argument for writing clean interfaces first. The 30 minutes I spent designing &lt;code&gt;VigilDB&lt;/code&gt;'s API saved hours of integration pain later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Product
&lt;/h2&gt;

&lt;p&gt;Vigil v1.5.0 is on PyPI. It gives AI agents persistent awareness across sessions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Awareness daemon&lt;/strong&gt; compiles system state every 90 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frame-based tool filtering&lt;/strong&gt; reduces context by 75-85%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal protocol&lt;/strong&gt; lets agents coordinate without direct communication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session handoff&lt;/strong&gt; with structured summaries and resume&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP server&lt;/strong&gt; with 12 tools (Claude Code, Cursor, Claude Desktop)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;REST API&lt;/strong&gt; with 20 endpoints and SSE event stream&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard&lt;/strong&gt; with live updates
&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;vigil-agent
vigil init
vigil daemon start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AlexlaGuardia/Vigil" rel="noopener noreferrer"&gt;github.com/AlexlaGuardia/Vigil&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you're building with AI coding assistants and want to move faster, try the parallel window technique on your next feature batch. The key is architecture that supports it — clean boundaries, stable interfaces, independent files.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>productivity</category>
      <category>python</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I Built a Nervous System for AI Agents (Not Another Memory Store)</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Wed, 25 Mar 2026 22:09:18 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/i-built-a-nervous-system-for-ai-agents-not-another-memory-store-5a8a</link>
      <guid>https://dev.to/alexlaguardia/i-built-a-nervous-system-for-ai-agents-not-another-memory-store-5a8a</guid>
      <description>&lt;h2&gt;
  
  
  The Problem Nobody Talks About
&lt;/h2&gt;

&lt;p&gt;Everyone's building AI agents. Nobody's building the infrastructure to keep them aware.&lt;/p&gt;

&lt;p&gt;I've been running ~95 MCP tools across multiple AI agents for the past year — a coding assistant, a trading system, a creative writing setup. Three problems kept hitting me:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Cold starts.&lt;/strong&gt; Every new session starts from zero. The agent has no idea what happened 5 minutes ago in a different session.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Token bloat.&lt;/strong&gt; Loading 95 tool definitions into context burns ~50,000 tokens before the agent does a single useful thing. That's real money and real context window wasted on tools the agent won't use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. No coordination.&lt;/strong&gt; Multiple agents working on the same system can't hand off work or share awareness without me copy-pasting context between them.&lt;/p&gt;

&lt;p&gt;The existing tools (Mem0, Letta, LangGraph) solve pieces of this. Mem0 does memory retrieval. Letta does stateful agents. LangGraph does workflow state. But none of them give agents &lt;strong&gt;awareness&lt;/strong&gt; — a continuously-compiled understanding of what's happening right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What If Agents Had a Nervous System?
&lt;/h2&gt;

&lt;p&gt;Memory stores are filing cabinets. You put stuff in, you pull stuff out. That's useful, but it's not how awareness works.&lt;/p&gt;

&lt;p&gt;Your nervous system doesn't wait for you to query it. It continuously processes signals from your environment and compiles them into a state that's instantly available. You don't boot up every morning and run &lt;code&gt;SELECT * FROM memories WHERE relevant = true&lt;/code&gt;. You just... know what's going on.&lt;/p&gt;

&lt;p&gt;That's what I built.&lt;/p&gt;

&lt;h2&gt;
  
  
  Vigil: The Six Ideas
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Awareness Daemon
&lt;/h3&gt;

&lt;p&gt;A background process runs every 90 seconds, reading signals from agents and compiling them into "hot context" — a structured snapshot any agent can boot from instantly.&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;vigil&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VigilDaemon&lt;/span&gt;

&lt;span class="n"&gt;daemon&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VigilDaemon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vigil.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;compile_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;awareness_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWARENESS.md&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;daemon&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When an agent starts a session, it calls &lt;code&gt;compiler.boot()&lt;/code&gt; and gets full context in under a second: active frame, current work, recent signals, priority queue. No startup latency.&lt;/p&gt;

&lt;p&gt;The daemon also writes an &lt;code&gt;AWARENESS.md&lt;/code&gt; file — human-readable, version-controllable. My agents and I read the same file.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Frame-Based Tool Filtering
&lt;/h3&gt;

&lt;p&gt;This was the biggest win. Instead of loading all tools into every context, you tag tools with "frames" — named context modes.&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;vigil.registry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tool_count&lt;/span&gt;

&lt;span class="nd"&gt;@tool&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;deploy&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;Deploy to production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;frames&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;backend&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;devops&lt;/span&gt;&lt;span class="sh"&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;def&lt;/span&gt; &lt;span class="nf"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@tool&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;write_chapter&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;Write a story chapter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;frames&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;creative&lt;/span&gt;&lt;span class="sh"&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;def&lt;/span&gt; &lt;span class="nf"&gt;write_chapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nd"&gt;@tool&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;health&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;Health check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;frames&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;core&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# Always visible
&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;health&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nf"&gt;tool_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;              &lt;span class="c1"&gt;# 3 (all tools)
&lt;/span&gt;&lt;span class="nf"&gt;tool_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;backend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# 2 (deploy + health)
&lt;/span&gt;&lt;span class="nf"&gt;tool_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;creative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# 2 (write_chapter + health)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent in "backend" mode never sees creative writing tools. In my setup, this took tool definitions from 95 down to 14-25 per session — a &lt;strong&gt;75-85% reduction&lt;/strong&gt; in tool-definition tokens. The LLM also makes better tool choices with fewer irrelevant options.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Signal Protocol
&lt;/h3&gt;

&lt;p&gt;Agents communicate through signals — short, categorized messages with content budgets:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Budget&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;observation&lt;/td&gt;
&lt;td&gt;400 chars&lt;/td&gt;
&lt;td&gt;Regular activity updates&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;handoff&lt;/td&gt;
&lt;td&gt;600 chars&lt;/td&gt;
&lt;td&gt;Session conclusions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;summary&lt;/td&gt;
&lt;td&gt;800 chars&lt;/td&gt;
&lt;td&gt;Comprehensive summaries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;alert&lt;/td&gt;
&lt;td&gt;300 chars&lt;/td&gt;
&lt;td&gt;Urgent notifications&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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;vigil&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SignalBus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VigilDB&lt;/span&gt;

&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;VigilDB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vigil.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SignalBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend-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;Deployed auth service v2. Tests passing.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;bus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend-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;Dashboard layout refactored for mobile.&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;Content budgets prevent runaway data. The daemon reads these signals, synthesizes them into the awareness summary, and moves on. Agents don't talk to each other — they emit into the bus and the daemon handles the rest.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Session Handoff
&lt;/h3&gt;

&lt;p&gt;This is what makes multi-session work actually work. Agents end sessions with structured summaries:&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;vigil&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HandoffProtocol&lt;/span&gt;

&lt;span class="n"&gt;proto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HandoffProtocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;agent_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;backend-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;summary&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Shipped auth v2 with JWT tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;files_touched&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;auth.py&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;middleware.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;decisions&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;Switched from session cookies to JWT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;next_steps&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;Add rate limiting&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;Write integration tests&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="c1"&gt;# Next morning, different agent resumes
&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;proto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;morning-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;# Includes: last handoff, signals since, pending next steps
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Handoff chains track continuity across sessions. The resume context tells the next agent exactly what happened, what decisions were made, and what to do next. No more "remind me what we were working on."&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Signal Compaction
&lt;/h3&gt;

&lt;p&gt;Signals accumulate. Without compaction, your awareness context grows forever. Vigil uses tiered retention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Raw signals&lt;/strong&gt; — kept for 7 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily summaries&lt;/strong&gt; — synthesized from raw, kept for 30 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weekly digests&lt;/strong&gt; — synthesized from daily, kept for 90 days&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monthly snapshots&lt;/strong&gt; — permanent archive
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vigil compact &lt;span class="nt"&gt;--dry-run&lt;/span&gt;  &lt;span class="c"&gt;# Preview what would be compacted&lt;/span&gt;
vigil compact            &lt;span class="c"&gt;# Run it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;History stays manageable without losing important context.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Event Triggers
&lt;/h3&gt;

&lt;p&gt;Pattern-match on signals and fire actions automatically:&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;vigil&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TriggerManager&lt;/span&gt;

&lt;span class="n"&gt;triggers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TriggerManager&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;triggers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;alert-to-slack&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;signal_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alert&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_pattern&lt;/span&gt;&lt;span class="o"&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="n"&gt;action_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webhook&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;action_config&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;url&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;https://hooks.slack.com/...&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;"If any agent emits an alert, post to Slack." "If the backend agent goes silent for 2 hours, create a focus item." Triggers turn Vigil from a passive awareness layer into an active coordination system.&lt;/p&gt;

&lt;h2&gt;
  
  
  MCP Server: The Distribution Play
&lt;/h2&gt;

&lt;p&gt;Everything above is available as an MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vigil serve                          &lt;span class="c"&gt;# stdio (Claude Code, Claude Desktop)&lt;/span&gt;
vigil serve &lt;span class="nt"&gt;--transport&lt;/span&gt; sse          &lt;span class="c"&gt;# SSE (remote clients)&lt;/span&gt;
vigil serve &lt;span class="nt"&gt;--transport&lt;/span&gt; http         &lt;span class="c"&gt;# REST API + dashboard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;12 MCP tools: boot, compile, signal, status, signals, handoff, resume, chain, stale, focus, frames, agents.&lt;/p&gt;

&lt;p&gt;Any MCP-compatible client (Claude Code, Cursor, Windsurf, Claude Desktop) connects and gets persistent awareness. The agent boots with context, emits signals during work, and hands off when done. Next session picks up where it left off.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Modules&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lines of code&lt;/td&gt;
&lt;td&gt;7,100+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;252&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP tools&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REST endpoints&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard pages&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;0 (stdlib only, MCP is optional)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;SQLite (zero setup)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why Not [Existing Tool]?
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Vigil&lt;/th&gt;
&lt;th&gt;Mem0&lt;/th&gt;
&lt;th&gt;Letta&lt;/th&gt;
&lt;th&gt;LangGraph&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Approach&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Awareness daemon&lt;/td&gt;
&lt;td&gt;Memory retrieval&lt;/td&gt;
&lt;td&gt;Stateful runtime&lt;/td&gt;
&lt;td&gt;State machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Context&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Pre-compiled, instant&lt;/td&gt;
&lt;td&gt;Query on demand&lt;/td&gt;
&lt;td&gt;LLM-managed&lt;/td&gt;
&lt;td&gt;Checkpoint-based&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool filtering&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Frame-based (75-85% savings)&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Signal protocol + handoff&lt;/td&gt;
&lt;td&gt;Shared memory&lt;/td&gt;
&lt;td&gt;Single agent&lt;/td&gt;
&lt;td&gt;Graph edges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Compaction&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tiered retention&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;LLM-managed&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;MCP native&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Built-in server&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Infrastructure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;td&gt;API + LLM costs&lt;/td&gt;
&lt;td&gt;Full runtime&lt;/td&gt;
&lt;td&gt;LangChain ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't competitors — they're complementary. Vigil handles awareness and coordination. Mem0 handles deep memory. Use both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started
&lt;/h2&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;vigil-agent

vigil init
vigil signal my-agent &lt;span class="s2"&gt;"Starting work on the auth system"&lt;/span&gt;
vigil daemon start
vigil status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or as an MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"vigil-agent[mcp]"&lt;/span&gt;
vigil serve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;252 tests. MIT license. Zero external dependencies.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/AlexlaGuardia/Vigil" rel="noopener noreferrer"&gt;github.com/AlexlaGuardia/Vigil&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;PyPI:&lt;/strong&gt; &lt;a href="https://pypi.org/project/vigil-agent/" rel="noopener noreferrer"&gt;pypi.org/project/vigil-agent&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is v1.5.0. The roadmap includes a hosted multi-tenant platform, federation protocol for cross-org agent coordination, and eventually a hardware device (Pi-based always-on awareness hub). If you're building multi-agent systems and fighting the same problems, I'd love to hear how you're solving them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Managing a WooCommerce Store from Claude — 34 MCP Tools I Wish Existed</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Wed, 25 Mar 2026 21:37:48 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/managing-a-woocommerce-store-from-claude-34-mcp-tools-i-wish-existed-3d6l</link>
      <guid>https://dev.to/alexlaguardia/managing-a-woocommerce-store-from-claude-34-mcp-tools-i-wish-existed-3d6l</guid>
      <description>&lt;p&gt;WooCommerce runs 36% of all online stores — over 5 million active sites. If you're using Claude or Cursor and want to check orders, update products, or pull sales reports, you're copy-pasting from WordPress admin like it's 2015.&lt;/p&gt;

&lt;p&gt;I built 34 MCP tools that let your AI assistant manage a WooCommerce store directly. Products, orders, customers, coupons, shipping, reports — full CRUD, not just read-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; lets AI assistants interact with external tools directly. With this server installed, you stop tab-switching between Claude and your WordPress admin panel.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (manual):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open WordPress admin&lt;/li&gt;
&lt;li&gt;Navigate to WooCommerce → Orders&lt;/li&gt;
&lt;li&gt;Filter by date range&lt;/li&gt;
&lt;li&gt;Export or copy the data&lt;/li&gt;
&lt;li&gt;Paste into Claude&lt;/li&gt;
&lt;li&gt;Ask for analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;After (with mcp-woocommerce):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Show me all orders from this week. Which products are selling best and what's my average order value?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude calls &lt;code&gt;list_orders&lt;/code&gt;, then &lt;code&gt;get_sales_report&lt;/code&gt;, and gives you the analysis — all in one conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  34 Tools, Full CRUD
&lt;/h2&gt;

&lt;p&gt;Not just reading data — you can manage your entire store:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Products&lt;/strong&gt; (7): List, get, create, update, delete, manage variations, batch operations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orders&lt;/strong&gt; (5): List, get, create, update, add notes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customers&lt;/strong&gt; (4): List, get, create, update with full address support&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coupons&lt;/strong&gt; (3): List, get, create with discount types and usage limits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports&lt;/strong&gt; (4): Sales, top sellers, orders totals, and customer totals&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shipping&lt;/strong&gt; (3): Zones, methods, and zone-method configuration&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhooks&lt;/strong&gt; (3): List, create, delete for real-time event handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments&lt;/strong&gt; (2): List gateways and toggle enable/disable&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System&lt;/strong&gt; (3): Store info, status checks, and settings&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Decisions Worth Sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  URL Normalization
&lt;/h3&gt;

&lt;p&gt;WooCommerce's REST API lives at &lt;code&gt;/wp-json/wc/v3&lt;/code&gt; under whatever domain the store uses. Users will pass in &lt;code&gt;mystore.com&lt;/code&gt;, &lt;code&gt;https://mystore.com&lt;/code&gt;, or &lt;code&gt;https://mystore.com/wp-json/wc/v3&lt;/code&gt;. The client handles all of them:&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;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;store_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="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&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;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;base&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;https://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/wp-json/wc/v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base&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;base&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/wp-json/wc/v3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One less thing for users to get wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP Basic Auth (The Simple Kind)
&lt;/h3&gt;

&lt;p&gt;WooCommerce uses consumer key/secret pairs over HTTPS — no OAuth dance, no token refresh, no expiration headaches. You generate keys in WordPress admin (WooCommerce → Settings → Advanced → REST API), set the permission level (read, write, or read/write), and you're done.&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;base_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;auth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;consumer_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;consumer_secret&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;follow_redirects&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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 &lt;code&gt;follow_redirects=True&lt;/code&gt; is important — many WordPress installs have redirect rules (www ↔ non-www, HTTP → HTTPS) that will silently break API calls without it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Array Responses vs Object Responses
&lt;/h3&gt;

&lt;p&gt;Unlike most APIs that wrap results in &lt;code&gt;{"data": [...], "total": N}&lt;/code&gt;, WooCommerce returns bare arrays for list endpoints. Pagination info comes in response headers (&lt;code&gt;X-WP-Total&lt;/code&gt;, &lt;code&gt;X-WP-TotalPages&lt;/code&gt;), not the body. Every tool handles this:&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;data&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;wc&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;/products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;products&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="k"&gt;if&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;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&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;products&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="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;_fmt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&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;products&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;products&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI gets a consistent format regardless of WooCommerce's quirks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Structured Error Codes
&lt;/h3&gt;

&lt;p&gt;WooCommerce returns machine-readable error codes (&lt;code&gt;woocommerce_rest_product_invalid_id&lt;/code&gt;, &lt;code&gt;woocommerce_rest_cannot_view&lt;/code&gt;) alongside human messages. The client preserves both:&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;raise&lt;/span&gt; &lt;span class="nc"&gt;WooCommerceError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;code&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;unknown_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;data&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;message&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;No details provided&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;When the AI hits an error, it gets enough context to explain what went wrong and suggest a fix — not just "400 Bad Request."&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started in 2 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&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;mcp-woocommerce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Generate API Keys
&lt;/h3&gt;

&lt;p&gt;WordPress Admin → WooCommerce → Settings → Advanced → REST API → Add Key.&lt;/p&gt;

&lt;p&gt;Set permissions to &lt;strong&gt;Read/Write&lt;/strong&gt; and save the consumer key and secret.&lt;/p&gt;

&lt;h3&gt;
  
  
  Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&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;"woocommerce"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-woocommerce"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"WOOCOMMERCE_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://yourstore.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"WOOCOMMERCE_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ck_your_key"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"WOOCOMMERCE_SECRET"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cs_your_secret"&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;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;
  
  
  Claude Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add woocommerce &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;WOOCOMMERCE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;https://yourstore.com &lt;span class="nv"&gt;WOOCOMMERCE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ck_key &lt;span class="nv"&gt;WOOCOMMERCE_SECRET&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cs_secret mcp-woocommerce
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Same JSON config as Claude Desktop in &lt;code&gt;.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Add batch endpoints sooner.&lt;/strong&gt; WooCommerce supports batch create/update/delete for products, orders, and coupons in a single API call. I added product batch operations but should have done it across all resources from the start — it's a massive time saver for bulk operations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with a staging store.&lt;/strong&gt; WooCommerce's behavior varies wildly depending on installed plugins, theme, and WordPress version. A clean WooCommerce install acts differently from one running 30 plugins with custom REST API filters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for MCP Server Builders
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Handle URL normalization.&lt;/strong&gt; Don't assume users will pass the exact base URL your client expects. Accept the most natural input and normalize it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Follow redirects.&lt;/strong&gt; WordPress sites love redirects. If your HTTP client doesn't follow them, you'll get mysterious failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalize response shapes.&lt;/strong&gt; If the API returns arrays sometimes and objects other times, your tools should present a consistent format to the AI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preserve error codes.&lt;/strong&gt; Machine-readable error codes help the AI reason about failures and suggest fixes. Don't throw them away.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/AlexlaGuardia/mcp-woocommerce" rel="noopener noreferrer"&gt;AlexlaGuardia/mcp-woocommerce&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/mcp-woocommerce/" rel="noopener noreferrer"&gt;mcp-woocommerce&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;This is part of a series of production-grade MCP servers I'm building for underserved SaaS platforms. Also available: &lt;a href="https://github.com/AlexlaGuardia/mcp-mailchimp" rel="noopener noreferrer"&gt;Mailchimp&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-freshbooks" rel="noopener noreferrer"&gt;FreshBooks&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-activecampaign" rel="noopener noreferrer"&gt;ActiveCampaign&lt;/a&gt;. Follow me here or on &lt;a href="https://github.com/AlexlaGuardia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to catch the next one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update (April 2026):&lt;/strong&gt; Since publishing, mcp-woocommerce has been expanded from 34 to &lt;strong&gt;73 tools&lt;/strong&gt; (v0.2.0). New coverage includes refunds, reports, shipping zones, tax rates, payment gateways, webhooks, system status, product variations, and order notes. &lt;code&gt;pip install mcp-woocommerce&lt;/code&gt; to get the latest.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>woocommerce</category>
    </item>
    <item>
      <title>33 Tools for Mailchimp in One MCP Server — Here's How I Built It</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Tue, 24 Mar 2026 23:37:33 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/33-tools-for-mailchimp-in-one-mcp-server-heres-how-i-built-it-295k</link>
      <guid>https://dev.to/alexlaguardia/33-tools-for-mailchimp-in-one-mcp-server-heres-how-i-built-it-295k</guid>
      <description>&lt;p&gt;I wanted to manage my Mailchimp campaigns from Claude without copy-pasting data between tabs. The existing MCP servers for Mailchimp were either read-only, incomplete, or abandoned weekend projects with 3-5 tools.&lt;/p&gt;

&lt;p&gt;So I built one with 33 tools that actually covers the full API — campaigns, audiences, members, segments, templates, automations, and reports. Read and write.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;MCP&lt;/a&gt; lets AI assistants interact with external tools directly. With this server installed, instead of tab-switching between Claude and Mailchimp, you just ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before (manual):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Mailchimp&lt;/li&gt;
&lt;li&gt;Navigate to Campaigns → Reports&lt;/li&gt;
&lt;li&gt;Find last campaign&lt;/li&gt;
&lt;li&gt;Copy the stats&lt;/li&gt;
&lt;li&gt;Paste into Claude&lt;/li&gt;
&lt;li&gt;Ask for analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;After (with mcp-mailchimp):&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How did my last campaign perform? Compare the open rate to my previous 5 campaigns."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Claude calls &lt;code&gt;list_campaigns&lt;/code&gt;, then &lt;code&gt;get_campaign_report&lt;/code&gt; for each, and gives you the analysis — all in one shot.&lt;/p&gt;

&lt;h2&gt;
  
  
  33 Tools, Full Read/Write
&lt;/h2&gt;

&lt;p&gt;Not just listing data — you can actually do things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Campaigns&lt;/strong&gt; (8): Create, send, schedule, replicate, test, update&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Content&lt;/strong&gt; (2): Get and set campaign HTML/templates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reports&lt;/strong&gt; (3): Performance stats, click-level details, open tracking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audiences&lt;/strong&gt; (3): List, get details, create new lists&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Members&lt;/strong&gt; (6): Add, update, upsert, archive, search, activity history&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tags&lt;/strong&gt; (2): List and manage member tags&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Segments&lt;/strong&gt; (3): List, view members, create from emails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Templates&lt;/strong&gt; (2): List and view template HTML&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automations&lt;/strong&gt; (3): List, pause, start classic workflows&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Account&lt;/strong&gt; (1): Validate API key and get account info&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technical Decisions Worth Sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Subscriber Hash Gotcha
&lt;/h3&gt;

&lt;p&gt;Mailchimp doesn't identify members by email or UUID. It uses an MD5 hash of the lowercased email. Get this wrong and you get silent 404s — no error message, just empty responses.&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;@staticmethod&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subscriber_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every Mailchimp integration learns this the hard way. My client handles it automatically so the tools just accept plain email addresses.&lt;/p&gt;

&lt;h3&gt;
  
  
  Upsert Over Separate Create/Update
&lt;/h3&gt;

&lt;p&gt;Instead of separate "add" and "update" tools, I use Mailchimp's PUT endpoint for member management (&lt;code&gt;add_or_update_member&lt;/code&gt;). It's a safe upsert — exists? update. New? create.&lt;/p&gt;

&lt;p&gt;The critical detail: it uses &lt;code&gt;status_if_new&lt;/code&gt; so it won't accidentally resubscribe someone who previously unsubscribed. That's a compliance landmine most implementations miss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Formatted Responses, Not API Dumps
&lt;/h3&gt;

&lt;p&gt;Mailchimp's API responses are verbose — nested &lt;code&gt;_links&lt;/code&gt;, redundant metadata, fields nobody needs. Every tool extracts just the useful fields:&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;@mcp.tool&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;def&lt;/span&gt; &lt;span class="nf"&gt;get_campaign_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;campaign_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="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 performance report for a sent campaign.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;mc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;r&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;mc&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/reports/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;campaign_id&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;return&lt;/span&gt; &lt;span class="nf"&gt;_fmt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;emails_sent&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;emails_sent&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;opens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;opens&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="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;unique_opens&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;opens&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="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;open_rate&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="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clicks&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unique&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;clicks&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="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;unique_clicks&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;clicks&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="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;click_rate&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="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bounces&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hard&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;bounces&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="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;hard_bounces&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;soft&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;bounces&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="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;soft_bounces&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="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unsubscribes&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;unsubscribed&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI gets clean data it can reason about. No parsing gymnastics.&lt;/p&gt;

&lt;h3&gt;
  
  
  FastMCP Makes Tool Definition Trivial
&lt;/h3&gt;

&lt;p&gt;The official &lt;code&gt;mcp&lt;/code&gt; Python SDK with &lt;code&gt;FastMCP&lt;/code&gt; generates JSON Schema from type hints and docstrings automatically. A tool is just an async function:&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;@mcp.tool&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;def&lt;/span&gt; &lt;span class="nf"&gt;list_campaigns&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="nb"&gt;str&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="n"&gt;list_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&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="n"&gt;count&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;offset&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;0&lt;/span&gt;&lt;span class="p"&gt;,&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 email campaigns. Filter by status or audience.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="c1"&gt;# ... implementation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No schema files, no registration boilerplate. Type hints become parameter descriptions. Docstrings become tool descriptions. It just works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started in 2 Minutes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install
&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;mcp-mailchimp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Claude Desktop
&lt;/h3&gt;

&lt;p&gt;Add to &lt;code&gt;claude_desktop_config.json&lt;/code&gt;:&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;"mailchimp"&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;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-mailchimp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&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;"MAILCHIMP_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"your-key-us21"&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;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;
  
  
  Claude Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add mailchimp &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;env &lt;/span&gt;&lt;span class="nv"&gt;MAILCHIMP_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-key mcp-mailchimp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cursor
&lt;/h3&gt;

&lt;p&gt;Same JSON config as Claude Desktop in &lt;code&gt;.cursor/mcp.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Start with fewer tools.&lt;/strong&gt; 33 tools gives comprehensive coverage, but 15-20 would handle 95% of use cases. The automation and segment tools are niche — campaigns and members are where the value is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test with real data sooner.&lt;/strong&gt; Mailchimp's free tier is now just 250 contacts and 500 sends/month. I developed mostly against the API docs, which meant some edge cases only showed up during real testing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for MCP Server Builders
&lt;/h2&gt;

&lt;p&gt;If you're building MCP servers, here's what Mailchimp specifically taught me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Watch for silent identity schemes.&lt;/strong&gt; Mailchimp uses MD5 hashes of lowercased emails as member IDs — no error if you get it wrong, just empty responses. If your target API has non-obvious ID formats, abstract them away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prefer upsert over create+update.&lt;/strong&gt; Mailchimp's PUT endpoint does a safe upsert with &lt;code&gt;status_if_new&lt;/code&gt;, so it won't accidentally resubscribe someone. Idempotent operations are always safer when an AI is making the calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strip verbose responses.&lt;/strong&gt; Mailchimp returns &lt;code&gt;_links&lt;/code&gt;, nested metadata, and redundant fields in every response. Extract only what the AI needs to reason about — the fewer tokens, the better the analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write copy-paste configs.&lt;/strong&gt; Claude Desktop, Claude Code, Cursor — give people the exact JSON block. Every friction point between install and first use loses a user.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/AlexlaGuardia/mcp-mailchimp" rel="noopener noreferrer"&gt;AlexlaGuardia/mcp-mailchimp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href="https://pypi.org/project/mcp-mailchimp/" rel="noopener noreferrer"&gt;mcp-mailchimp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License&lt;/strong&gt;: MIT&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This is part of a series of production-grade MCP servers I'm building for underserved SaaS platforms. Also available: &lt;a href="https://github.com/AlexlaGuardia/mcp-woocommerce" rel="noopener noreferrer"&gt;WooCommerce&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-freshbooks" rel="noopener noreferrer"&gt;FreshBooks&lt;/a&gt;, &lt;a href="https://github.com/AlexlaGuardia/mcp-activecampaign" rel="noopener noreferrer"&gt;ActiveCampaign&lt;/a&gt;. Follow me here or on &lt;a href="https://github.com/AlexlaGuardia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; to catch the next one.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Update (April 2026):&lt;/strong&gt; Since publishing, mcp-mailchimp has been expanded from 33 to &lt;strong&gt;71 tools&lt;/strong&gt; (v0.3.0). New coverage includes e-commerce (stores, orders, carts, revenue attribution), audience analytics (growth history, locations, email client stats), webhooks, merge fields, interest groups, A/B test results, member notes, file manager, landing pages, and batch operations. &lt;code&gt;pip install mcp-mailchimp&lt;/code&gt; to get the latest.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>mailchimp</category>
    </item>
    <item>
      <title>I built a CLI inspector for MCP servers</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Thu, 19 Mar 2026 09:19:57 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/i-built-a-cli-inspector-for-mcp-servers-2f1m</link>
      <guid>https://dev.to/alexlaguardia/i-built-a-cli-inspector-for-mcp-servers-2f1m</guid>
      <description>&lt;p&gt;I run a custom MCP server with 100+ tools. Every time I added a tool or changed a schema, my only options were reading the source or wiring up a test client.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://github.com/AlexlaGuardia/MCPcat" rel="noopener noreferrer"&gt;mcpcat&lt;/a&gt; — four commands, ~250 lines of Python.&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;# See what a server exposes&lt;/span&gt;
mcpcat tools http://localhost:8100/mcp

&lt;span class="c"&gt;# Inspect a specific tool's schema&lt;/span&gt;
mcpcat inspect http://localhost:8100/mcp cortex_signal

&lt;span class="c"&gt;# Call a tool directly&lt;/span&gt;
mcpcat call http://localhost:8100/mcp health

&lt;span class="c"&gt;# Check if a server is alive&lt;/span&gt;
mcpcat ping http://localhost:8100/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first version only supported the old SSE transport. It hung on my own server because I use streamable HTTP. Now it auto-detects both.&lt;/p&gt;

&lt;p&gt;If you're building or consuming MCP servers and want something like &lt;code&gt;curl&lt;/code&gt; but for MCP, give it a try:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/AlexlaGuardia/MCPcat.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I wrote a longer post about the build process and the transport detection bug here: &lt;a href="https://alexlaguardia.dev/writing/mcpcat" rel="noopener noreferrer"&gt;alexlaguardia.dev/writing/mcpcat&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Built with Python, Typer, httpx, and Rich. MIT licensed. PRs welcome.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>python</category>
      <category>cli</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I Built a Cognitive AI Layer That Routes Thoughts to the Right Brain</title>
      <dc:creator>Alex LaGuardia</dc:creator>
      <pubDate>Wed, 18 Mar 2026 22:33:46 +0000</pubDate>
      <link>https://dev.to/alexlaguardia/how-i-built-a-cognitive-ai-layer-that-routes-thoughts-to-the-right-brain-31f</link>
      <guid>https://dev.to/alexlaguardia/how-i-built-a-cognitive-ai-layer-that-routes-thoughts-to-the-right-brain-31f</guid>
      <description>&lt;p&gt;Most AI projects call an API and return the response. I built something different — a cognitive layer that classifies every input, routes it to the optimal LLM provider based on complexity and cost, executes tools autonomously in an iterative loop, and compresses memory so conversations never lose context.&lt;/p&gt;

&lt;p&gt;It's called &lt;a href="https://github.com/AlexlaGuardia/Akatskii" rel="noopener noreferrer"&gt;Akatskii&lt;/a&gt;, and it runs as the brain behind Luna, a personal AI assistant I use daily for managing a SaaS platform, trading systems, and creative projects.&lt;/p&gt;

&lt;p&gt;This isn't a tutorial. It's what I learned building a production cognitive architecture from scratch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I needed an AI assistant that could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Think at different speeds.&lt;/strong&gt; A status check doesn't need Claude's reasoning. A complex architecture decision doesn't belong on Groq's fast path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use tools autonomously.&lt;/strong&gt; Not "call one function and return" — actually reason through multi-step problems, calling tools iteratively until the task is done.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remember everything.&lt;/strong&gt; Conversations shouldn't break when you hit context limits. The system should compress, not truncate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost close to zero.&lt;/strong&gt; I'm running this 24/7. Paying $0.15/1K tokens for every status check is insane.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No existing framework solved all four. So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Router: Regex Before LLM
&lt;/h2&gt;

&lt;p&gt;The most counterintuitive decision I made: use regex for routing, not an LLM.&lt;/p&gt;

&lt;p&gt;Here's why. About 80% of inputs to an AI assistant are predictable. "What's the system status?" is always a status check. "Why are posts failing?" always needs reasoning. Pattern matching handles these instantly, deterministically, and for free.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User: "check system health"
→ Regex match: REFLEX (confidence: 0.9)
→ Route to Groq (free, ~150ms)
→ Pre-fetch system_health capability data
→ LLM generates response grounded in real data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only when regex confidence drops below 0.8 does the system fall back to an LLM for classification — Groq's Llama 3.3 70B, which adds ~100ms and costs nothing on the free tier.&lt;/p&gt;

&lt;p&gt;Result: 80% of routing is instant and free. The remaining 20% costs fractions of a cent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five Thought Types, Three Providers
&lt;/h2&gt;

&lt;p&gt;Every input gets classified into one of five thought types, each mapped to a different LLM provider:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reflex&lt;/td&gt;
&lt;td&gt;Groq Llama 3.3 70B&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Status checks, lookups, simple queries&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Executive&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Reasoning, planning, debugging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sensory&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Image analysis, multimodal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Creative&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;Narrative, prose, worldbuilding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deep&lt;/td&gt;
&lt;td&gt;Claude Sonnet 4&lt;/td&gt;
&lt;td&gt;Paid&lt;/td&gt;
&lt;td&gt;Complex architecture (explicit escalation only)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The key insight: Claude is never invoked unless the user explicitly asks for it ("need claude", "escalate", "deep dive"). Everything else runs on free providers. My monthly LLM cost for a 24/7 AI assistant: effectively zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Agentic Loop: Let the LLM Decide When It's Done
&lt;/h2&gt;

&lt;p&gt;This is where it gets interesting. When a thought requires tools, the brain enters an iterative loop:&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;for&lt;/span&gt; &lt;span class="n"&gt;iteration&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;15&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call_with_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;available_tools&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# LLM decided it's done
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;

    &lt;span class="c1"&gt;# Execute tool calls (parallel if multiple)
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt; &lt;span class="ow"&gt;in&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;tool_calls&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="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execute_mcp_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;messages&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;tool_result&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="c1"&gt;# Loop: LLM sees results, decides next action
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM reasons, calls tools, gets results, reasons again — up to 15 iterations. It decides when the task is complete, not the developer. Multiple tool calls in a single response execute in parallel via &lt;code&gt;asyncio.gather&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the iteration limit is hit, instead of silently returning incomplete work, the system runs a &lt;strong&gt;continuation pass&lt;/strong&gt; — a second invocation with the partial results as context, giving the LLM one more chance to synthesize a complete response.&lt;/p&gt;

&lt;p&gt;The tool bridge connects to a &lt;a href="https://github.com/AlexlaGuardia/guardia-mcp" rel="noopener noreferrer"&gt;Model Context Protocol (MCP) server&lt;/a&gt; with 40+ tools via direct Python imports — no HTTP overhead, no serialization, no auth handshake. The LLM can check system health, query databases, manage content pipelines, execute trades, and signal other agents.&lt;/p&gt;

&lt;h2&gt;
  
  
  Memory Compaction: Compress, Don't Truncate
&lt;/h2&gt;

&lt;p&gt;Long conversations hit context limits. Most systems handle this by dropping old messages. That's like treating amnesia by forgetting harder.&lt;/p&gt;

&lt;p&gt;Akatskii's compaction engine does something different:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A daemon monitors conversation token count&lt;/li&gt;
&lt;li&gt;At 180K tokens, it triggers compaction&lt;/li&gt;
&lt;li&gt;Gemini extracts structured facts: decisions, preferences, technical details, context&lt;/li&gt;
&lt;li&gt;It generates dual summaries — a short continuity hint (2-3 lines) and a detailed snapshot (1-2 paragraphs)&lt;/li&gt;
&lt;li&gt;Facts persist to a knowledge base. Snapshots persist as episodic memory.&lt;/li&gt;
&lt;li&gt;The conversation resumes with compressed context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Target compression: 3x. A 180K token conversation becomes ~60K tokens of distilled context, and nothing meaningful is lost. The user sees: "A moment while I gather my thoughts..." — then the conversation continues seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Room-Aware Personality
&lt;/h2&gt;

&lt;p&gt;Luna isn't one personality. She adapts based on context.&lt;/p&gt;

&lt;p&gt;In an engineering room, she's technical and direct. In a trading room, she's analytical and cautious. In a creative room, she writes prose. Each room also filters which tools are available — the trading room gets market tools, the build room gets code execution, and they can't cross boundaries.&lt;/p&gt;

&lt;p&gt;Same brain, different capabilities, different voice. This is implemented through room configs that define system prompts, tool allowlists, and persona adjustments per context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background Autonomy
&lt;/h2&gt;

&lt;p&gt;Three daemon processes run independently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Heartbeat&lt;/strong&gt; (every 5 min): evaluates trigger conditions, syncs calendar, fires proactive notifications&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scout&lt;/strong&gt; (every 4 hours): monitors HackerNews, Reddit, and GitHub for relevant news, scores findings, caches for later surfacing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadow Runner&lt;/strong&gt;: picks up queued tasks and executes them through the full agentic loop — autonomously, with no user interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Shadow Runner is the most interesting. It's essentially the same brain, but running headless on queued work. It loads room-specific configs, decomposes complex tasks, retries on failure, and reports results back to the task queue. Autonomous execution without supervision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The router patterns are hand-crafted.&lt;/strong&gt; 40 regex patterns work, but they're brittle. A learned classifier (fine-tuned on routing decisions) would generalize better. I haven't done this because the regex approach works well enough and costs nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tool definitions are static.&lt;/strong&gt; The MCP bridge maps capabilities to tools at import time. A more dynamic system would discover tools at runtime and let the LLM choose from the full inventory. MCP's tool discovery protocol supports this — I just haven't needed it yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compaction loses nuance.&lt;/strong&gt; Fact extraction captures the "what" but sometimes misses the "how we got there." The reasoning chain that led to a decision is harder to compress than the decision itself. I'm exploring storing reasoning traces alongside facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Runtime&lt;/td&gt;
&lt;td&gt;Python 3.11, FastAPI, asyncio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM Providers&lt;/td&gt;
&lt;td&gt;Gemini 2.5 Flash, Groq Llama 3.3 70B, Claude Sonnet 4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tools&lt;/td&gt;
&lt;td&gt;Model Context Protocol (MCP) — 40+ tools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory&lt;/td&gt;
&lt;td&gt;SQLite + fastembed (all-MiniLM-L6-v2, 384-dim vectors)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Voice&lt;/td&gt;
&lt;td&gt;OpenAI Whisper (STT), ElevenLabs soundboard + Edge TTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Process Management&lt;/td&gt;
&lt;td&gt;PM2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;The full source is at &lt;a href="https://github.com/AlexlaGuardia/Akatskii" rel="noopener noreferrer"&gt;github.com/AlexlaGuardia/Akatskii&lt;/a&gt;. The MCP server it connects to is at &lt;a href="https://github.com/AlexlaGuardia/guardia-mcp" rel="noopener noreferrer"&gt;github.com/AlexlaGuardia/guardia-mcp&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you're building something similar — or if you've solved any of the problems I mentioned differently — I'd genuinely like to hear about it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Alex, a full-stack software engineer building AI systems, game engines, and production platforms. I write Python, Rust, and TypeScript. Find me on &lt;a href="https://github.com/AlexlaGuardia" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://linkedin.com/in/alex-laguardia-a28a37216" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>architecture</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
