<?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: Enmanuel Magallanes Pinargote</title>
    <description>The latest articles on DEV Community by Enmanuel Magallanes Pinargote (@enmanuelmag).</description>
    <link>https://dev.to/enmanuelmag</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%2F1258109%2F1758118e-5a79-4085-9ce3-836a0cfedef0.jpeg</url>
      <title>DEV Community: Enmanuel Magallanes Pinargote</title>
      <link>https://dev.to/enmanuelmag</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/enmanuelmag"/>
    <language>en</language>
    <item>
      <title>The Map Is Not the Territory — Dep-Aware Agent Between Explorer and Builder</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Fri, 05 Jun 2026 14:29:23 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/the-map-is-not-the-territory-dep-aware-agent-between-explorer-and-builder-5b3a</link>
      <guid>https://dev.to/enmanuelmag/the-map-is-not-the-territory-dep-aware-agent-between-explorer-and-builder-5b3a</guid>
      <description>&lt;h2&gt;
  
  
  The intelligence problem
&lt;/h2&gt;

&lt;p&gt;A reconnaissance team maps the territory on Monday. They produce a thorough report: entry points, structural layout, active systems, the works. It's accurate. They did their job well.&lt;/p&gt;

&lt;p&gt;On Thursday, the strike team goes in with that report.&lt;/p&gt;

&lt;p&gt;What nobody checked: on Tuesday, they rotated the guard shift. On Wednesday, someone welded the east door shut. The report is still 90% correct. The 10% is where things go wrong.&lt;/p&gt;

&lt;p&gt;This is the operational gap that every intelligence agency, surgical team, and eventually every engineering org confronts: the distance between when analysis runs and when implementation starts is not zero. Things change in that gap. Nobody announces it. It doesn't feel like a change that matters.&lt;/p&gt;

&lt;p&gt;Until it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Explorer's map goes stale
&lt;/h2&gt;

&lt;p&gt;The standard harness workflow — Lead → Explorer → Builder → Reviewer — solves the right set of problems. Explorer maps the codebase so Builder doesn't go in blind. Builder implements against a real picture of the project, not a guess. Reviewer verifies before anything closes.&lt;/p&gt;

&lt;p&gt;But Explorer produces a snapshot. It's accurate at the time it ran. The codebase it analyzed and the codebase Builder starts writing against are only guaranteed to be the same if nothing changed between the two sessions.&lt;/p&gt;

&lt;p&gt;In practice, something almost always changes. A dependency gets bumped. A config shifts. Someone runs &lt;code&gt;npm install&lt;/code&gt; before the next session starts. Nobody announces it. Doesn't feel significant.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;drizzle-orm&lt;/code&gt; goes from 0.36 to 0.45 — a major bump — and Builder implements using query patterns that changed in the release. The Explorer's analysis was correct. The plan was solid. The code almost works. Nobody lied. Nobody missed a step. The workflow ran cleanly. The ground just shifted between phases and nobody checked.&lt;/p&gt;

&lt;p&gt;That's the failure mode v1.6.4 is built to close.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fifth role for a specific problem
&lt;/h2&gt;

&lt;p&gt;The standard harness workflow has four roles: Lead (orchestrates), Explorer (reads and maps), Builder (implements), Reviewer (validates). Each has a defined permission scope. Each has a clear position in the chain.&lt;/p&gt;

&lt;p&gt;The Consultant is a fifth role, but it's not part of the standard chain — it's conditional. Lead invokes it only when the situation warrants it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When &lt;code&gt;deps.check&lt;/code&gt; returns &lt;code&gt;significant: true&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;When the task explicitly touches &lt;code&gt;package.json&lt;/code&gt;, dependencies, or config files&lt;/li&gt;
&lt;li&gt;When &lt;code&gt;.harness/deps-lock.json&lt;/code&gt; doesn't exist yet (first task, no baseline)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For routine feature work with stable dependencies, the Consultant never enters the picture. The chain stays at four roles. The Consultant adds overhead only when that overhead is justified.&lt;/p&gt;

&lt;h2&gt;
  
  
  The MCP tools that make it work
&lt;/h2&gt;

&lt;p&gt;Two new tools ship with v1.6.4, and both are exclusive to the Consultant role — no other agent can call them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;deps.snapshot&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Captures the current state of &lt;code&gt;package.json&lt;/code&gt; (both &lt;code&gt;dependencies&lt;/code&gt; and &lt;code&gt;devDependencies&lt;/code&gt;) and writes it to &lt;code&gt;.harness/deps-lock.json&lt;/code&gt; with a timestamp. This is the baseline. It runs automatically the first time the Consultant is invoked on a fresh project.&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;"capturedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-05T10:23:00.000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Snapshot saved to .harness/deps-lock.json"&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;&lt;strong&gt;&lt;code&gt;deps.check&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Diffs the current &lt;code&gt;package.json&lt;/code&gt; against the stored baseline. Returns a structured result:&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;"significant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"added"&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="s2"&gt;"zod@3.24.0"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"removed"&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;"majorBumps"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"drizzle-orm"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"from"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.36.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"to"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.45.0"&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="nl"&gt;"advisory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Run npx autoskills to refresh agent skill files after dependency changes."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"snapshotDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-15T08:00:00.000Z"&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;&lt;code&gt;significant: true&lt;/code&gt; means something changed that warrants a full Consultant pass. Minor patches and no-change cases return &lt;code&gt;significant: false&lt;/code&gt;, and the Lead can skip the Consultant entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Consultant actually produces
&lt;/h2&gt;

&lt;p&gt;The Consultant's output is not a list of packages. It's structured advisory that the Builder reads before writing a single line of code.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;actions.write&lt;/code&gt;, the Consultant records four sections to the task's action history:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Patterns to follow&lt;/strong&gt; — existing conventions in the codebase the Builder must respect. Not general advice. Specific to what Explorer found.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risks &amp;amp; warnings&lt;/strong&gt; — what could go wrong given the current change set. A major ORM bump means query patterns may have changed. A new validation library means the existing pattern for form handling is now inconsistent. These aren't hypotheticals — they're derived from the actual diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Best practices&lt;/strong&gt; — key considerations for this task given the project's current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Dependency notes&lt;/strong&gt; — only when the task touches &lt;code&gt;package.json&lt;/code&gt;. What changed, what the breaking changes are, and whether agent skill files need to be refreshed.&lt;/p&gt;

&lt;p&gt;The Builder reads all of this via &lt;code&gt;actions.get(taskId)&lt;/code&gt; before starting. Explorer's analysis plus Consultant's advisory gives Builder the full picture — not just what the codebase looks like, but what changed and what to watch for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advisory-only constraint is not an accident
&lt;/h2&gt;

&lt;p&gt;The Consultant has &lt;code&gt;Read&lt;/code&gt; and &lt;code&gt;Bash&lt;/code&gt; access (for reading files and running safe diagnostic commands). It does not have write access. Cannot create files. Cannot modify source. Cannot touch the database.&lt;/p&gt;

&lt;p&gt;This is intentional, and it matters.&lt;/p&gt;

&lt;p&gt;An advisory role that can also write is a role that will, at some point, decide the advisory isn't enough and start making "small fixes" on the way to handing off. That's how you end up with an agent that's half-consultant, half-builder, and fully unclear about what it changed. The Consultant's value is precisely that it stays read-only: it can be thorough, it can be wrong, and it can be ignored — and none of those outcomes damage the codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mandatory docs criterion
&lt;/h2&gt;

&lt;p&gt;The same release ships a second change that's smaller in implementation but meaningful in practice: every task now gets a mandatory acceptance criterion appended by the Lead before any work begins.&lt;/p&gt;

&lt;p&gt;The criterion reads:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Docs/README analysis: [describe whether docs/, README.md, or other documentation files need to reflect this change and what specifically — or explicitly state 'no update needed' with brief reasoning]"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is the last acceptance criterion on every task, enforced by the Reviewer. If it's missing, Reviewer blocks with: "Missing mandatory docs/README analysis criterion. Lead must add it before builder proceeds." If it's present but the Builder's action summary is silent on it, Reviewer blocks with: "Docs analysis criterion is present but undocumented."&lt;/p&gt;

&lt;p&gt;The result is explicit documentation accountability on every single task — not as a policy document, not as a guideline in a CONTRIBUTING.md nobody reads, but as a hard gate at the end of the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The updated workflow
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lead
  ↓  (always)
Explorer
  ↓  (conditional: significant dep changes or task touches package.json)
Consultant
  ↓  (always)
Builder
  ↓  (always)
Reviewer  ←  enforces docs criterion + all acceptance criteria
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain is still Lead → Explorer → Builder → Reviewer for the majority of tasks. The Consultant enters when it earns its place. When it does, Builder gets a brief that covers not just what the codebase looks like, but what shifted and what that means.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting v1.6.4
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @cardor/agent-harness-kit init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you already have a harness in a project, sync the updated agent files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ahk build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;consultant.md&lt;/code&gt; agent file will be added to &lt;code&gt;.claude/agents/&lt;/code&gt;, &lt;code&gt;.opencode/agents/&lt;/code&gt;, and &lt;code&gt;.codex/agents/&lt;/code&gt; depending on your provider config. The two new MCP tools register automatically. The mandatory docs criterion is baked into the Lead's updated instructions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://github.com/enmanuelmag/agent-harness-kit" rel="noopener noreferrer"&gt;agent-harness-kit&lt;/a&gt; — provider-agnostic scaffolding for structured multi-agent AI workflows.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>security</category>
      <category>agents</category>
    </item>
    <item>
      <title>One Does Not Simply read_file('/etc/passwd') — Argument Policies Land in Heimdall MCP</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 26 May 2026 13:24:16 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/one-does-not-simply-readfileetcpasswd-argument-policies-land-in-heimdall-mcp-4p32</link>
      <guid>https://dev.to/enmanuelmag/one-does-not-simply-readfileetcpasswd-argument-policies-land-in-heimdall-mcp-4p32</guid>
      <description>&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Heimdall MCP is a transparent MCP proxy — it sits between your MCP client and any server, records every call as an OpenTelemetry span, and enforces per-server allow/deny policies without touching server code.&lt;/p&gt;

&lt;p&gt;The policy layer in v1.3 could answer one question: &lt;em&gt;can this tool be called at all?&lt;/em&gt; If &lt;code&gt;read_file&lt;/code&gt; was in the allowlist, every call to &lt;code&gt;read_file&lt;/code&gt; went through — regardless of the &lt;code&gt;path&lt;/code&gt; argument. That's a meaningful gap. The tool name check was never the full story.&lt;/p&gt;

&lt;p&gt;v1.4 closes it.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://stack.cardor.dev/heimdall" rel="noopener noreferrer"&gt;https://stack.cardor.dev/heimdall&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;A new &lt;code&gt;toolPolicies&lt;/code&gt; field in &lt;code&gt;heimdall.config.ts&lt;/code&gt; lets you define per-argument constraints on tools that already passed the name check. It's a separate field from &lt;code&gt;tools&lt;/code&gt; (which stays unchanged) — so every existing config works without modification.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// heimdall.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_directory&lt;/span&gt;&lt;span class="dl"&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;// unchanged&lt;/span&gt;
      &lt;span class="na"&gt;toolPolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// '*' applies to all tools — merged first, tool-specific overrides on conflict&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;deny_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.env$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.pem$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;isPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="na"&gt;allow_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// must resolve within cwd&lt;/span&gt;
              &lt;span class="na"&gt;deny_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.env$&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// never .env files&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;allow_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ascii&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;HeimdallConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  ArgConstraint fields
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isPath&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables path-aware matching instead of regex&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;allow_pattern&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`string \&lt;/td&gt;
&lt;td&gt;string[]`&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deny_pattern&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`string \&lt;/td&gt;
&lt;td&gt;string[]`&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;array_mode&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;`'all' \&lt;/td&gt;
&lt;td&gt;'any'`&lt;/td&gt;
&lt;td&gt;&lt;code&gt;'all'&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;case_sensitive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Regex flag; ignored for path matching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;warn_only&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;boolean&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;false&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Record violation in span without blocking&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Path scoping
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;isPath: true&lt;/code&gt;, patterns that look like directory roots are treated as containment checks rather than regex expressions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;"./"&lt;/code&gt; or &lt;code&gt;"."&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Arg must resolve within &lt;code&gt;process.cwd()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"/some/dir"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Arg must resolve within that directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;"~"&lt;/code&gt; / &lt;code&gt;"${HOME}/projects"&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Resolved to homedir&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"${CWD}/data"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resolved to cwd + &lt;code&gt;/data&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The resolver uses &lt;code&gt;path.resolve&lt;/code&gt; + &lt;code&gt;fs.realpathSync&lt;/code&gt; on the deepest existing ancestor of the path, which handles non-existent files (e.g. pre-creation checks), &lt;code&gt;../&lt;/code&gt; traversal, and symlink escapes. &lt;code&gt;/tmp&lt;/code&gt; on macOS resolves to &lt;code&gt;/private/tmp&lt;/code&gt; and containment is checked correctly.&lt;/p&gt;

&lt;p&gt;Patterns that don't look like directory roots (e.g. &lt;code&gt;"^/etc/.*"&lt;/code&gt;, &lt;code&gt;"\\.env$"&lt;/code&gt;) fall back to standard regex matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;warn_only&lt;/code&gt; mode
&lt;/h2&gt;

&lt;p&gt;Useful for gradual rollout. Set &lt;code&gt;warn_only: true&lt;/code&gt; on any constraint to observe violations without blocking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;isPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;allow_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warn_only&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The call is forwarded and these attributes appear in the OTel span:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;policy.arg_warning = true
policy.arg_warning_field = "path"
policy.arg_warning_message = "Tool arg 'path' does not match the allow policy"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Switch to &lt;code&gt;warn_only: false&lt;/code&gt; (the default) when you're ready to enforce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wildcard tool key and dot notation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;'*'&lt;/code&gt; in &lt;code&gt;toolPolicies&lt;/code&gt; applies constraints to all tools. Tool-specific entries are merged on top — specific wins on conflict, wildcard fills in fields the specific entry doesn't define.&lt;/p&gt;

&lt;p&gt;Dot notation accesses nested argument objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;toolPolicies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;my_tool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;options.target&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;isPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;allow_pattern&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Merge strategy (global + local configs)
&lt;/h2&gt;

&lt;p&gt;The same security-first semantics from the &lt;code&gt;tools&lt;/code&gt; field apply to &lt;code&gt;toolPolicies&lt;/code&gt; when merging a global and local config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;deny_pattern&lt;/code&gt;: union — denied by either = denied&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;allow_pattern&lt;/code&gt;: intersection — must be in both; empty/missing defers to the other side&lt;/li&gt;
&lt;li&gt;Structural fields (&lt;code&gt;isPath&lt;/code&gt;, &lt;code&gt;array_mode&lt;/code&gt;, &lt;code&gt;warn_only&lt;/code&gt;): local wins&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Heimdall could already do (context)
&lt;/h2&gt;

&lt;p&gt;For those new to the project: Heimdall MCP is a transparent proxy for any MCP server. Before v1.4 it already provided:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OpenTelemetry tracing&lt;/strong&gt; — every tool call as an OTel span with latency breakdown, request/response hashes, and error classification. Exportable to Jaeger, Tempo, or any OTLP backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent storage&lt;/strong&gt; — spans saved to SQLite, PostgreSQL, or MySQL via Drizzle ORM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool name policies&lt;/strong&gt; — per-server allow/deny lists for tools, prompts, and resources. Denied calls return JSON-RPC error &lt;code&gt;-32001&lt;/code&gt; and never reach the server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Body modes&lt;/strong&gt; — &lt;code&gt;full&lt;/code&gt;, &lt;code&gt;hash&lt;/code&gt;, or &lt;code&gt;redacted&lt;/code&gt; for request/response capture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four transport modes&lt;/strong&gt; — stdio subprocess, HTTP, SSE, or library (embed in your own Node.js app).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1.4 adds the argument layer on top of name-layer policies. Zero breaking changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;toolPolicies&lt;/code&gt; is a sibling field of &lt;code&gt;tools&lt;/code&gt; in &lt;code&gt;ServerPolicy&lt;/code&gt; — existing &lt;code&gt;tools&lt;/code&gt; allow/deny lists are untouched.&lt;/li&gt;
&lt;li&gt;Validation via &lt;code&gt;valibot&lt;/code&gt; at startup — descriptive errors for misconfigured constraints.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PolicyInterceptor&lt;/code&gt; pipeline position unchanged: name check runs first, then arg check. If name is blocked, arg check is skipped.&lt;/li&gt;
&lt;li&gt;All 192 tests pass including 43 new tests for path resolver and argument policies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/enmanuelmag/heimdall-mcp" rel="noopener noreferrer"&gt;https://github.com/enmanuelmag/heimdall-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;npm install -g @cardor/heimdall-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Changelog:&lt;/strong&gt; &lt;a href="https://github.com/enmanuelmag/heimdall-mcp/blob/main/CHANGELOG.md" rel="noopener noreferrer"&gt;https://github.com/enmanuelmag/heimdall-mcp/blob/main/CHANGELOG.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>claude</category>
      <category>security</category>
      <category>agents</category>
    </item>
    <item>
      <title>When smart is not enough — Multi-agent AI orchestration defaults</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Wed, 20 May 2026 14:00:00 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/when-smart-is-not-enough-multi-agent-ai-orchestration-defaults-4okg</link>
      <guid>https://dev.to/enmanuelmag/when-smart-is-not-enough-multi-agent-ai-orchestration-defaults-4okg</guid>
      <description>&lt;p&gt;Imagine a relay race where all four runners are Olympic-level athletes. Fast, technically perfect, experienced. Now imagine nobody told them the running order. The handoff protocol. Who holds the baton first.&lt;/p&gt;

&lt;p&gt;What happens? Someone starts. Maybe the right person, maybe not. The baton gets passed in the wrong direction. Two runners reach the same point at the same time. The team is brilliant. The race is chaos.&lt;/p&gt;

&lt;p&gt;This is the exact problem with multi-agent AI orchestration when you skip the config.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://stack.cardor.dev/ahk" rel="noopener noreferrer"&gt;https://stack.cardor.dev/ahk&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The orchestration gap
&lt;/h2&gt;

&lt;p&gt;The standard setup for a structured multi-agent workflow looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lead&lt;/strong&gt; — decomposes the task, plans, delegates&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explorer&lt;/strong&gt; — reads and maps the codebase, never writes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builder&lt;/strong&gt; — implements the plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer&lt;/strong&gt; — verifies, approves, or blocks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four specialized roles. Clean separation of concerns. The kind of structure that turns "fix this bug" into a systematic, auditable workflow with full history and no duplicated effort.&lt;/p&gt;

&lt;p&gt;The problem: most setups define these roles in markdown files with descriptions like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Use this agent to orchestrate a full task. Invoke when starting a new work session."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That description tells the model what the lead is supposed to do. It does not tell the tool to start with lead by default. Those are two different things. And when your AI tool opens a project without an explicit default agent config, it either picks the first agent it finds alphabetically, falls back to its own built-in default, or just asks you — which defeats the point of having a structured workflow in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The relay baton is a config field, not an instruction
&lt;/h2&gt;

&lt;p&gt;Here's what actually controls who goes first in each tool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; uses the &lt;code&gt;agent&lt;/code&gt; field in &lt;code&gt;.claude/settings.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;"agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lead"&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;This is the official, documented field for setting which subagent runs as the main session thread. A common mistake — including one we caught in our own materializer — is putting &lt;code&gt;default_agent: "lead"&lt;/code&gt; in &lt;code&gt;.claude/mcp.json&lt;/code&gt;. That field doesn't exist in Claude Code's config spec. It gets ignored silently, and your session starts wherever Claude decides.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OpenCode&lt;/strong&gt; uses &lt;code&gt;default_agent&lt;/code&gt; in &lt;code&gt;opencode.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;"default_agent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lead"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"compaction"&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;"auto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"prune"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"reserved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10000&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;This maps correctly. OpenCode also exposes compaction config for managing context window pressure during long orchestrated sessions — useful when lead, explorer, builder, and reviewer are all accumulating history in the same run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt; has no direct &lt;code&gt;default_agent&lt;/code&gt; field. Instead, Codex ships with a built-in agent named &lt;code&gt;default&lt;/code&gt;. You override it by creating &lt;code&gt;.codex/agents/default.toml&lt;/code&gt; with your lead agent's instructions and &lt;code&gt;name = "default"&lt;/code&gt;. Codex will use it as the entry point from that point on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"default"&lt;/span&gt;
&lt;span class="py"&gt;sandbox_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"read-only"&lt;/span&gt;

&lt;span class="py"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
Use this agent to orchestrate a full task from the harness backlog: decompose it
into a plan, delegate to explorer, builder, and reviewer in sequence.
"""&lt;/span&gt;

&lt;span class="py"&gt;developer_instructions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"""
# Lead Agent

You are the lead agent. Your job is to orchestrate the workflow for one task at
a time. You coordinate — you do not implement.
...
"""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The second problem: roles without enforcement
&lt;/h2&gt;

&lt;p&gt;Getting the starting agent right is half the battle. The other half is making sure each agent stays in its lane once the relay is running.&lt;/p&gt;

&lt;p&gt;Instructions help. But instructions are text. A builder told "don't read source files outside src/" can still read them if nothing stops it. An explorer told "never write files" can still write if it decides that's the fastest path. One slightly off prompt, one edge case, and the contract breaks.&lt;/p&gt;

&lt;p&gt;This is where per-role sandboxing does real work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Claude Code&lt;/strong&gt; exposes &lt;code&gt;permissionMode&lt;/code&gt; as a frontmatter field on each agent &lt;code&gt;.md&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;explorer&lt;/span&gt;
&lt;span class="na"&gt;permissionMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plan&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;plan&lt;/code&gt; mode puts the session into read-only planning mode — file writes and edits are blocked at the tool level. Not instruction. Enforcement.&lt;/p&gt;

&lt;p&gt;For builder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="na"&gt;permissionMode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;acceptEdits&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;acceptEdits&lt;/code&gt; means file edits are auto-approved without a per-change confirmation prompt — the right tradeoff for an agent whose whole job is to implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Codex CLI&lt;/strong&gt; uses &lt;code&gt;sandbox_mode&lt;/code&gt; per agent TOML file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# explorer.toml&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"explorer"&lt;/span&gt;
&lt;span class="py"&gt;sandbox_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"read-only"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# builder.toml&lt;/span&gt;
&lt;span class="py"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"builder"&lt;/span&gt;
&lt;span class="py"&gt;sandbox_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"workspace-write"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full matrix across our four roles:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Claude Code permissionMode&lt;/th&gt;
&lt;th&gt;Codex sandbox_mode&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;lead&lt;/td&gt;
&lt;td&gt;plan&lt;/td&gt;
&lt;td&gt;read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;explorer&lt;/td&gt;
&lt;td&gt;plan&lt;/td&gt;
&lt;td&gt;read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;builder&lt;/td&gt;
&lt;td&gt;acceptEdits&lt;/td&gt;
&lt;td&gt;workspace-write&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;reviewer&lt;/td&gt;
&lt;td&gt;plan&lt;/td&gt;
&lt;td&gt;read-only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why this saves tokens, not just sanity
&lt;/h2&gt;

&lt;p&gt;There's an economic argument here that goes beyond correctness.&lt;/p&gt;

&lt;p&gt;When orchestration defaults are wrong, you burn tokens at the start of every session re-establishing context. Lead has to re-introduce itself. The wrong agent hands off awkwardly. The model spends turns figuring out where it is in the workflow instead of doing the work. Then you re-prompt. Then you clarify. Then you maybe restart.&lt;/p&gt;

&lt;p&gt;When defaults are right, the first turn is already positioned correctly. Lead picks up the task, decomposes it, passes the baton to explorer. Explorer maps the codebase, passes to builder. Builder implements, passes to reviewer. Clean chain. No re-orientation. The first session draft is the good one.&lt;/p&gt;

&lt;p&gt;The model's intelligence is the same. The setup is what changes the output quality on the first try. More predictable behavior, fewer iterations, less token waste — not because the model got smarter, but because it didn't have to guess its starting position.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Claude Code&lt;/th&gt;
&lt;th&gt;OpenCode&lt;/th&gt;
&lt;th&gt;Codex CLI&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Agent files&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/agents/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.opencode/agents/*.md&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.codex/agents/*.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File format&lt;/td&gt;
&lt;td&gt;Markdown + YAML frontmatter&lt;/td&gt;
&lt;td&gt;Markdown + YAML frontmatter&lt;/td&gt;
&lt;td&gt;TOML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Default agent field&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;agent&lt;/code&gt; in &lt;code&gt;settings.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;default_agent&lt;/code&gt; in &lt;code&gt;opencode.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Override built-in via &lt;code&gt;default.toml&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-role permissions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;permissionMode&lt;/code&gt; in frontmatter&lt;/td&gt;
&lt;td&gt;— (global only)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sandbox_mode&lt;/code&gt; in TOML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP config file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.claude/mcp.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;opencode.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.codex/config.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compaction config&lt;/td&gt;
&lt;td&gt;Not exposed&lt;/td&gt;
&lt;td&gt;&lt;code&gt;compaction.{auto,prune,reserved}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not applicable&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;All of this is automated in &lt;a href="https://github.com/cardor/agent-harness-kit" rel="noopener noreferrer"&gt;agent-harness-kit&lt;/a&gt;. One command scaffolds the full structure — agents, MCP config, default agent wiring, sandbox settings — for whichever provider you choose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ahk init &lt;span class="nt"&gt;--provider&lt;/span&gt; codex-cli
npx ahk init &lt;span class="nt"&gt;--provider&lt;/span&gt; claude-code
npx ahk init &lt;span class="nt"&gt;--provider&lt;/span&gt; opencode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The materializer knows which field to write to, in which file, in which format. You get correct defaults without cross-referencing three different docs pages — or discovering six months later that your &lt;code&gt;default_agent&lt;/code&gt; key in mcp.json was doing nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The relay metaphor holds
&lt;/h2&gt;

&lt;p&gt;Back to the runners. The baton problem isn't about ability — it's about protocol. Define the protocol in config, not in prose. Each runner in their lane, with the right access, in the right order.&lt;/p&gt;

&lt;p&gt;The relay runs itself. You just watch the race.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://github.com/cardor/agent-harness-kit" rel="noopener noreferrer"&gt;agent-harness-kit&lt;/a&gt; — provider-agnostic scaffolding for structured multi-agent AI workflows.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devtools</category>
      <category>agents</category>
      <category>claude</category>
      <category>ai</category>
    </item>
    <item>
      <title>You Shall Not Pass — Allow/Deny Policies for MCP Tools Are Now in Heimdall</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 19 May 2026 16:00:00 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/you-shall-not-pass-allowdeny-policies-for-mcp-tools-are-now-in-heimdall-4fp3</link>
      <guid>https://dev.to/enmanuelmag/you-shall-not-pass-allowdeny-policies-for-mcp-tools-are-now-in-heimdall-4fp3</guid>
      <description>&lt;h2&gt;
  
  
  What shipped
&lt;/h2&gt;

&lt;p&gt;Heimdall MCP is a transparent MCP proxy — and v1.2 adds the piece that was missing: a two-level policy config that lets you define exactly which tools, prompts, and resources each MCP server is allowed to expose to the agent.&lt;/p&gt;

&lt;p&gt;No changes to the server. No custom middleware. A single config file next to your code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Website:&lt;/strong&gt; &lt;a href="https://stack.cardor.dev/heimdall" rel="noopener noreferrer"&gt;https://stack.cardor.dev/heimdall&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The config: two levels, one merge
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;Path&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;Local&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{project-root}/heimdall.config.{ts,js,json}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-repo rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.config/heimdall/heimdall.config.{ts,js,json}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;User/org-wide rules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are optional. If neither exists, Heimdall stays fully transparent — backward compatible.&lt;/p&gt;

&lt;p&gt;When both exist, they merge with &lt;strong&gt;security-first semantics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deny → union:&lt;/strong&gt; denied by either = denied. Global deny cannot be overridden locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allow → intersection:&lt;/strong&gt; must pass both. Wildcard &lt;code&gt;*&lt;/code&gt; means "defer to the other side."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deny beats allow&lt;/strong&gt; within any single config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This means your global config enforces a floor the team can't accidentally loosen. Local configs can only add &lt;em&gt;more&lt;/em&gt; restrictions, never fewer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Config format
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// heimdall.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HeimdallConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cardor/heimdall-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// default: applies to servers without an explicit entry&lt;/span&gt;
  &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="c1"&gt;// servers: keyed by --server-name flag (or serverInfo.name from initialize)&lt;/span&gt;
  &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_directory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;write_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;create_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;move_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file:///etc/*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file:///root/*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;allow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;describe_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;list_tables&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;execute&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;drop_table&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;truncate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="nx"&gt;satisfies&lt;/span&gt; &lt;span class="nx"&gt;HeimdallConfig&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full TypeScript with type checking. Also works as &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.mjs&lt;/code&gt;, or &lt;code&gt;.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens when a tool is blocked
&lt;/h2&gt;

&lt;p&gt;The call never reaches the real server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[TelemetryInterceptor] → [PolicyInterceptor] → [ForwardInterceptor]
                                ↑
                         blocks here, returns JSON-RPC error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Client receives:&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;"jsonrpc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"error"&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;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;-32001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tool 'write_file' is not permitted by policy"&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;The OTel span is still recorded with &lt;code&gt;policy.blocked = true&lt;/code&gt;, &lt;code&gt;mcp.error.code = -32001&lt;/code&gt;, and the tool name — full audit trail of what was attempted, blocked, and when.&lt;/p&gt;

&lt;h2&gt;
  
  
  List responses are also filtered
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tools/list&lt;/code&gt;, &lt;code&gt;prompts/list&lt;/code&gt;, and &lt;code&gt;resources/list&lt;/code&gt; responses are filtered before the client sees them — denied entries are removed. The agent doesn't even know a denied tool exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server name matching
&lt;/h2&gt;

&lt;p&gt;Policy entries are keyed by server name. Use &lt;code&gt;--server-name&lt;/code&gt; to set it explicitly in your MCP config:&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;"filesystem"&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;"heimdall-mcp"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqlite://~/.heimdall/traces.db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--server-name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"filesystem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@modelcontextprotocol/server-filesystem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/user/projects"&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;&lt;code&gt;--server-name&lt;/code&gt; overrides &lt;code&gt;serverInfo.name&lt;/code&gt; from the &lt;code&gt;initialize&lt;/code&gt; response — for both policy lookup and the &lt;code&gt;mcp.server.name&lt;/code&gt; OTel attribute. Consistent everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two new CLI commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scaffold a local config with commented examples&lt;/span&gt;
heimdall-mcp init

&lt;span class="c"&gt;# Scaffold a global config&lt;/span&gt;
heimdall-mcp init &lt;span class="nt"&gt;--global&lt;/span&gt;

&lt;span class="c"&gt;# Validate config, show merged policy, detect allow+deny conflicts&lt;/span&gt;
heimdall-mcp health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;health&lt;/code&gt; output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Config files loaded&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;global&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.config/heimdall/heimdall.config.ts&lt;/span&gt;
  &lt;span class="na"&gt;local&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;./heimdall.config.ts&lt;/span&gt;

&lt;span class="na"&gt;Default policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow=[*] deny=[none]&lt;/span&gt;

&lt;span class="na"&gt;Server policies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow=[read_file, list_directory, search_files] deny=[write_file, create_file, delete_file, move_file]&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allow=[query, describe_table, list_tables] deny=[execute, drop_table, truncate]&lt;/span&gt;

&lt;span class="s"&gt;No conflicts detected.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the same name appears in both allow and deny, &lt;code&gt;health&lt;/code&gt; exits 1 and lists all conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Library API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ProxyBuilder&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cardor/heimdall-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;HeimdallConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@cardor/heimdall-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HeimdallConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filesystem&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;deny&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;write_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;delete_file&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ProxyBuilder&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inbound&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdio&lt;/span&gt;&lt;span class="dl"&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;outbound&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdio&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;npx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@modelcontextprotocol/server-filesystem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tmp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqlite://./traces.db&lt;/span&gt;&lt;span class="dl"&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;config&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// attach policy&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serverName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;filesystem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// match config key&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&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;h2&gt;
  
  
  Implementation notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Config loaded via &lt;code&gt;jiti&lt;/code&gt; — supports &lt;code&gt;.ts&lt;/code&gt;, &lt;code&gt;.js&lt;/code&gt;, &lt;code&gt;.mjs&lt;/code&gt;, &lt;code&gt;.cjs&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt; without pre-compilation (same loader Vite/Nuxt use)&lt;/li&gt;
&lt;li&gt;Schema validated by &lt;code&gt;valibot&lt;/code&gt; at startup — descriptive errors for invalid configs&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PolicyInterceptor&lt;/code&gt; sits between &lt;code&gt;TelemetryInterceptor&lt;/code&gt; and &lt;code&gt;ForwardInterceptor&lt;/code&gt; — blocked calls are recorded but never forwarded&lt;/li&gt;
&lt;/ul&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/enmanuelmag/heimdall-mcp" rel="noopener noreferrer"&gt;https://github.com/enmanuelmag/heimdall-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm:&lt;/strong&gt; &lt;code&gt;npm install -g @cardor/heimdall-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>security</category>
      <category>mcp</category>
      <category>claude</category>
    </item>
    <item>
      <title>Stop flying blind with MCP calls</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Thu, 14 May 2026 18:08:51 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/stop-flying-blind-with-mcp-calls-1po</link>
      <guid>https://dev.to/enmanuelmag/stop-flying-blind-with-mcp-calls-1po</guid>
      <description>&lt;p&gt;After getting frustrated not knowing what was actually happening inside MCP servers — which tools were slow, which failed silently, what inputs Claude was sending.&lt;/p&gt;

&lt;p&gt;The idea: a transparent proxy that sits between your MCP client (Claude Desktop, OpenCode, Cursor) and any MCP server, captures every JSON-RPC message as an OpenTelemetry span, and persists it to a storage backend you configure.&lt;/p&gt;

&lt;p&gt;No modifications to the target server required. You just wrap it in mcp.json:&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="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;"heimdall-mcp"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"--store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqlite://~/.heimdall/traces.db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-server.js"&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;For remote servers (HTTP/SSE):&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="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;"heimdall-mcp"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"--store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres://..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"--target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://remote-server/sse"&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;Storage options: SQLite (local WASM, no native deps), Postgres, MySQL, or any OTLP-compatible backend.&lt;/p&gt;

&lt;p&gt;Also ships as a TypeScript library with a fluent builder API if you want to embed it directly.&lt;/p&gt;

&lt;p&gt;Why I think this is useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLM agents are increasingly orchestrating MCP tools, but observability tooling hasn't caught up&lt;/li&gt;
&lt;li&gt;IBM's ContextForge does something similar but it's a heavy Python gateway — this is meant to be a lightweight npm package&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still early (v0.1), but the core proxy + SQLite store is working. Looking for feedback on the interceptor API design and whether the OTel semantic conventions mapping makes sense.&lt;/p&gt;

&lt;p&gt;Website: &lt;a href="https://stack.cardor.dev/heimdall" rel="noopener noreferrer"&gt;https://stack.cardor.dev/heimdall&lt;/a&gt;&lt;br&gt;
GitHub: &lt;a href="https://github.com/enmanuelmag/heimdall-mcp" rel="noopener noreferrer"&gt;https://github.com/enmanuelmag/heimdall-mcp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check the repo for more feature planned on roadmap 🚀&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>agents</category>
      <category>claude</category>
      <category>ai</category>
    </item>
    <item>
      <title>Heimdall MCP: Add OpenTelemetry tracing to any MCP server without touching its code</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Sat, 09 May 2026 14:03:48 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/heimdall-mcp-add-opentelemetry-tracing-to-any-mcp-server-without-touching-its-code-4ae6</link>
      <guid>https://dev.to/enmanuelmag/heimdall-mcp-add-opentelemetry-tracing-to-any-mcp-server-without-touching-its-code-4ae6</guid>
      <description>&lt;p&gt;If you've been building with MCP servers lately, you've probably hit this wall: something goes wrong (or just feels slow) and you have zero visibility into what actually happened.&lt;/p&gt;

&lt;p&gt;Which tool did Claude call? What input did it send? Did the server error silently? How long did it take?&lt;/p&gt;

&lt;p&gt;That's the gap Heimdall fills.&lt;/p&gt;




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

&lt;p&gt;Heimdall is a transparent proxy for MCP servers. It sits between your MCP client (Claude Desktop, OpenCode, Cursor, or any other) and your MCP server — local or remote — and records every interaction as an OpenTelemetry span.&lt;/p&gt;

&lt;p&gt;No modifications to your server. No SDK to integrate. No infra to run. Just wrap your existing server and start seeing what's happening inside.&lt;/p&gt;

&lt;p&gt;The name comes from Norse mythology: Heimdall is the guardian of the Bifrost bridge, with the ability to see and hear everything that crosses between worlds. That's exactly the role this proxy plays.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem in one screenshot
&lt;/h2&gt;

&lt;p&gt;You're running a Claude agent that orchestrates 5+ MCP tools. One of them is consistently slow. Another occasionally returns empty results. You have no way to know which one, when, or why — unless you dig into logs manually.&lt;/p&gt;

&lt;p&gt;With Heimdall, every tool call becomes a structured span:&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;"name"&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.tool.call"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&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;"gen_ai.tool.name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"search_documents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mcp.duration_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;843&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mcp.status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ok"&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;"events"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"request"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"body"&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;"query"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"quarterly report"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"response"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"body"&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;"results"&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="err"&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;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;Stored. Queryable. Yours.&lt;/p&gt;




&lt;h2&gt;
  
  
  Zero config wrapping via mcp.json
&lt;/h2&gt;

&lt;p&gt;The easiest way to use Heimdall requires zero access to the server's source code. Just install it globally and update your &lt;code&gt;mcp.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @cardor/heimdall-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt; — your current config:&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;"my-server"&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;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"my-server.js"&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;&lt;strong&gt;After&lt;/strong&gt; — wrapped with Heimdall:&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;"my-server"&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;"heimdall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"--store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqlite://~/.heimdall/traces.db"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-server.js"&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;That's it. Restart your client and every tool call starts being recorded. The &lt;code&gt;--&lt;/code&gt; separator tells Heimdall where its args end and the real server command begins.&lt;/p&gt;

&lt;p&gt;For remote servers (HTTP or SSE), it's just as simple:&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;"remote-server"&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;"heimdall"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&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="s2"&gt;"--store"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"postgres://user:pass@localhost/mydb"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"--target"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://my-remote-mcp.com/sse"&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;Heimdall exposes stdio to your client, talks HTTP/SSE to the real server, and intercepts everything in between.&lt;/p&gt;




&lt;h2&gt;
  
  
  What gets recorded
&lt;/h2&gt;

&lt;p&gt;Heimdall captures all standard MCP events:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;What's recorded&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/call&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tool name, input args, response, duration, status&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tools/list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Available tools and their schemas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;resources/read&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;URI, MIME type, response size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;prompts/get&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prompt name, arguments, rendered output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;initialize&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Client/server versions, negotiated capabilities&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Total session duration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every span follows the &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/mcp/" rel="noopener noreferrer"&gt;OpenTelemetry gen_ai.* semantic conventions&lt;/a&gt;,&lt;br&gt;
so if you later want to send traces to Jaeger, Honeycomb, or Grafana Tempo, the data is already well-formed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Storage options
&lt;/h2&gt;

&lt;p&gt;Pick the backend that fits your setup:&lt;/p&gt;
&lt;h3&gt;
  
  
  SQLite — local file, zero infra
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--store&lt;/span&gt; sqlite://./traces.db
&lt;span class="c"&gt;# or with absolute path&lt;/span&gt;
&lt;span class="nt"&gt;--store&lt;/span&gt; sqlite:///Users/you/.heimdall/traces.db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Best for: local development, single-machine setups, quick debugging sessions.&lt;/p&gt;

&lt;p&gt;Uses &lt;code&gt;@libsql/client&lt;/code&gt; under the hood — pure WASM, no native bindings, no node-gyp.&lt;/p&gt;
&lt;h3&gt;
  
  
  Postgres — your existing database
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--store&lt;/span&gt; postgres://user:password@localhost:5432/mydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Best for: production environments, teams sharing observability data,&lt;br&gt;
long-term storage with SQL queries across multiple servers.&lt;/p&gt;
&lt;h3&gt;
  
  
  MySQL
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--store&lt;/span&gt; mysql://user:password@localhost:3306/mydb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Same use case as Postgres. Pick whichever you already run.&lt;/p&gt;

&lt;p&gt;All three use &lt;a href="https://orm.drizzle.team/" rel="noopener noreferrer"&gt;drizzle-orm&lt;/a&gt; with a shared schema, so the query experience is consistent regardless of which backend you choose.&lt;/p&gt;


&lt;h2&gt;
  
  
  Using it as a TypeScript library
&lt;/h2&gt;

&lt;p&gt;If you have access to your server's code and want tighter integration, Heimdall ships as a fully-typed TypeScript library with a fluent builder API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;McpProxy&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heimdall-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;McpProxy&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inbound&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdio&lt;/span&gt;&lt;span class="dl"&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;outbound&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="dl"&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;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sqlite://./traces.db&lt;/span&gt;&lt;span class="dl"&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;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;proxy&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;You can also plug in custom interceptors — for example, to redact sensitive fields before they're persisted, or to add your own business logic on top of the tracing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;McpProxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Interceptor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;heimdall-mcp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// Custom interceptor: redact API keys from tool inputs before storing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;redactSecrets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Interceptor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redact-secrets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sanitized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;redactSensitiveFields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&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;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sanitized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;McpProxy&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="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inbound&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stdio&lt;/span&gt;&lt;span class="dl"&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;outbound&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:3001&lt;/span&gt;&lt;span class="dl"&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;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres://user:pass@host/db&lt;/span&gt;&lt;span class="dl"&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;intercept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redactSecrets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Design goals
&lt;/h2&gt;

&lt;p&gt;A few decisions worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No native dependencies.&lt;/strong&gt; SQLite runs via WASM (&lt;code&gt;@libsql/client&lt;/code&gt;), Postgres via the pure-JS &lt;code&gt;postgres&lt;/code&gt; package, MySQL via &lt;code&gt;mysql2&lt;/code&gt;. You can install and run Heimdall in any Node 22+ environment without compilation steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No policy opinions.&lt;/strong&gt; Heimdall doesn't block, approve, or modify tool calls. It only observes and records. If you need access control or rate limiting, pair it with a tool like &lt;a href="https://github.com/behrensd/mcpwall" rel="noopener noreferrer"&gt;mcpwall&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transport mixing.&lt;/strong&gt; Your client connects via stdio. Your server can be stdio, HTTP, or SSE. Heimdall bridges them transparently — useful for wrapping local CLI servers behind an HTTP interface without touching their code.&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;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @cardor/heimdall-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitHub: &lt;a href="https://github.com/enmanuelmag/heimdall-mcp" rel="noopener noreferrer"&gt;github.com/enmanuelmag/heimdall-mcp&lt;/a&gt;&lt;br&gt;
Website: &lt;a href="https://heimdall-mcp.cardor.dev" rel="noopener noreferrer"&gt;https://heimdall-mcp.cardor.dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you try it out, I'd love to hear what you think — especially feedback on the interceptor API design and the OTel schema. Open an issue or find me on X [&lt;a class="mentioned-user" href="https://dev.to/enmanuelmag"&gt;@enmanuelmag&lt;/a&gt;].&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>opentelemetry</category>
      <category>devtools</category>
      <category>agents</category>
    </item>
    <item>
      <title>I got tired of AI agents roaming my codebase — so I built a harness layer</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Wed, 06 May 2026 00:55:08 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/i-got-tired-of-ai-agents-roaming-my-codebase-so-i-built-a-harness-layer-1o88</link>
      <guid>https://dev.to/enmanuelmag/i-got-tired-of-ai-agents-roaming-my-codebase-so-i-built-a-harness-layer-1o88</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every time I open Claude Code or any MCP-compatible AI tool, the same thing happens: the agent starts working, does &lt;em&gt;something&lt;/em&gt;, and I have no idea what it changed, what it tried, or why it got stuck. Across sessions, it's even worse — the agent has no memory of what was already attempted.&lt;/p&gt;

&lt;p&gt;If you want multiple agents working in parallel or in sequence, good luck coordinating them without race conditions, double work, or conflicting changes.&lt;/p&gt;

&lt;p&gt;I wanted something I could actually trust to run on my codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.npmjs.com/package/@cardor/agent-harness-kit" rel="noopener noreferrer"&gt;&lt;strong&gt;agent-harness-kit&lt;/strong&gt;&lt;/a&gt; (&lt;code&gt;ahk&lt;/code&gt;) is a scaffolding layer for structured multi-agent workflows. One command drops it into any project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx ahk init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;t creates a local MCP server (runs on stdio, no ports needed), a SQLite database, a task backlog, a health gate, and four agent definition files.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The 4-agent workflow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lead → Explorer → Builder → Reviewer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lead&lt;/strong&gt; decomposes the task into a plan. Never reads source files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explorer&lt;/strong&gt; maps the codebase. Never writes files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Builder&lt;/strong&gt; implements. Only writes to &lt;code&gt;writablePaths&lt;/code&gt; you define.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer&lt;/strong&gt; checks acceptance criteria. Runs health check before approving.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each role has its own Markdown file you can customize. They're created once and never overwritten.&lt;/p&gt;

&lt;h3&gt;
  
  
  Atomic task claiming
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// SQLite transaction — no double-work possible&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two agents can't grab the same task. The second one gets &lt;code&gt;task_already_claimed&lt;/code&gt; and moves on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Health gate
&lt;/h3&gt;

&lt;p&gt;Before any agent starts or closes a task, it runs your &lt;code&gt;health.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
npm &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
curl &lt;span class="nt"&gt;-sf&lt;/span&gt; http://localhost:3000/health &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"All checks passed."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it exits with anything other than 0, the task stays open. You define what "healthy" means for your project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Full audit trail
&lt;/h3&gt;

&lt;p&gt;Every action is recorded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;actions&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;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;             &lt;span class="c1"&gt;// start&lt;/span&gt;
&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;files_modified&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;src/auth.ts, src/routes/login.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;result&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// close&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ahk export --json&lt;/code&gt; dumps the full history.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provider-agnostic
&lt;/h2&gt;

&lt;p&gt;Works with Claude Code today. Moving to OpenCode? One command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ahk migrate &lt;span class="nt"&gt;--to&lt;/span&gt; opencode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your task history, agent definitions, and config stay intact.&lt;/p&gt;

&lt;h2&gt;
  
  
  No cloud, no native deps
&lt;/h2&gt;

&lt;p&gt;Everything lives in &lt;code&gt;.harness/harness.db&lt;/code&gt; (SQLite, gitignored). Uses &lt;code&gt;node:sqlite&lt;/code&gt; built-in — no &lt;code&gt;node-gyp&lt;/code&gt;, no native compilation.&lt;/p&gt;

&lt;p&gt;Requirements: Node ≥ 22 or Bun.&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;npx ahk init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Interactive setup. Asks for your project name, AI provider, docs path, and an optional first task. Creates everything in under a minute.&lt;/p&gt;




&lt;p&gt;GitHub: &lt;a href="https://github.com/enmanuelmag/agent-harness-kit" rel="noopener noreferrer"&gt;github.com/enmanuelmag/agent-harness-kit&lt;/a&gt;&lt;br&gt;
npm: &lt;code&gt;@cardor/agent-harness-kit&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I'd love feedback on the health gate design and the atomic claiming approach — those were the trickiest parts to get right.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>tooling</category>
      <category>agents</category>
    </item>
    <item>
      <title>Security Logger</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 16 Jan 2024 13:56:10 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/security-logger-kba</link>
      <guid>https://dev.to/enmanuelmag/security-logger-kba</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Disclaimer: The only real data in this site was hidden to protect the privacy of the user. The rest of the data is fake.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This project is build with ViteJs and ReactJs, the main idea is to create a web app and also a native desktop app to register new visitors on urbanization, the web app is for the directive (i.e urbanization president) and the desktop app is for the security guard.&lt;/p&gt;

&lt;p&gt;The app allow to create user three types of user:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Admin: Can create new users and also can see all the logs&lt;/li&gt;
  &lt;li&gt;Security guard: Can create, read and update new logs&lt;/li&gt;
  &lt;li&gt;Guest: Can only read the logs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The records are stored first in the &lt;strong&gt;IndexDB&lt;/strong&gt; and then are synced with the Firestore database, the app also has a &lt;strong&gt;offline mode&lt;/strong&gt;, so the user can use the app without internet connection and when the connection is available the app will &lt;strong&gt;sync the data with the database&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The records could be filtered by visitor name, visitor ID, date, and also by the house number, the app also has a &lt;strong&gt;search bar&lt;/strong&gt; that allow to search by name quickly. Also, you can exported the entire records of filtered records to a XLSX file or a PDF file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep learning integration
&lt;/h2&gt;

&lt;p&gt;This app also has a integration with a other personal project, that consist in a &lt;strong&gt;Deep Learning model&lt;/strong&gt; that can detect and extract the text of visitor name and ID from a image. So the app can extract the image from the security camera that focus the visitor ID and the model will extract the text and fill the form with the data. The information can be edited by the security guard too. The model is build with &lt;strong&gt;Tensorflow&lt;/strong&gt; based on a little version of YOLOv5.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv5w1e7jbyy6t8moct0c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyv5w1e7jbyy6t8moct0c.png" alt="Web app view" width="800" height="417"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently the app is only available in Spanish, and deploy or installation is only available previos contact and agreement. The app will have more tools powered by AI to detect and extract more information as vehicle plate, visitor face, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech used
&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Firebase (Auth, Firestore)&lt;/li&gt;
  &lt;li&gt;ReactJs, React Router, React Context&lt;/li&gt;
  &lt;li&gt;React-Query&lt;/li&gt;
  &lt;li&gt;Zod (Validation)&lt;/li&gt;
  &lt;li&gt;Tensorflow&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ia</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Budgetfy</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 16 Jan 2024 13:50:59 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/budgetfy-ii7</link>
      <guid>https://dev.to/enmanuelmag/budgetfy-ii7</guid>
      <description>&lt;p&gt;In this web app you can create budget to define your expenses and incomes as unique events or recurrent events as you usually create events for a calendar, in order to plan your &lt;strong&gt;budget through slice of time that you can define&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The Budget view has show you one card per moth with three sections. The first section is Category summary, it show the total amount of money for each category that you have defined.&lt;/p&gt;

&lt;p&gt;The second section is Timeline, that show the following information:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Timeline: show all the events for that month (expenses and incomes) and the estimated balance for that month. Also if you clic on the event you can mark it as paid (for expenses) or received (for incomes). This will update the balance.&lt;/li&gt;
  &lt;li&gt;Balance: section you will see all the money available without expenses for that month and the monthly balance, it mean the difference between the incomes and the expenses for that specific month.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Finally the third section is the &lt;strong&gt;Timeline plot&lt;/strong&gt;, that show the evolution of the incomes, expenses and balance for the the defined range of time on the budget creation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe575gwljmpr8up8y713y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe575gwljmpr8up8y713y.png" alt="Web app view" width="800" height="464"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can create a account for free using a email and password or &lt;strong&gt;Google account&lt;/strong&gt;, then you can create your budget and start to add your incomes and expenses. Visit the &lt;a rel="noopener noreferrer" href="https://budgetfy.cardor.dev"&gt;website&lt;/a&gt; to see more details.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech used
&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Firebase (Auth, Firestore)&lt;/li&gt;
  &lt;li&gt;ReactJs, React Router, React Context&lt;/li&gt;
  &lt;li&gt;React-Query&lt;/li&gt;
  &lt;li&gt;Highcharts&lt;/li&gt;
  &lt;li&gt;Zod (Validation)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>react</category>
      <category>finances</category>
      <category>firebase</category>
      <category>webdev</category>
    </item>
    <item>
      <title>IAflow</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 16 Jan 2024 13:42:19 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/iaflow-j3m</link>
      <guid>https://dev.to/enmanuelmag/iaflow-j3m</guid>
      <description>&lt;p&gt;This library help to create models with identifiers, checkpoints, logs and metadata automatically, in order to make the training process more efficient and traceable.&lt;/p&gt;

&lt;p&gt;For install the library, you can use pip:&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;iaflow
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can create the &lt;code&gt;ia_make&lt;/code&gt; with the following code:&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;ia_maker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IAFlow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;models_folder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./models&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;checkpoint_params&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;monitor&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;val_loss&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;save_best_only&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;save_weights_only&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;tensorboard_params&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;histogram_freq&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;write_graph&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&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_images&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&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;And then you can add the model with the following code:&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;model_1_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ia_maker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;model_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;model_1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;model_params&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;input_shape&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;load_model_params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt;
  &lt;span class="n"&gt;compile_params&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;metrics&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;accuracy&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;optimizer&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;adam&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;loss&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;mse&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;Finally, you can train the model with the following code:&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;ia_maker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;train&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;model_1_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;epochs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;dataset_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;dataset_1&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;This library has integration with &lt;a href="https://github.com/enmanuelmag/notify_function" rel="noopener noreferrer"&gt;Notifier Status Function&lt;/a&gt;, so you can send notifications to Telegram when the training process is finished. To check how to use it and more feature as managing Dataset and Models with the library, you can check the &lt;a rel="noopener noreferrer" href="https://iaflow.cardor.dev"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech used
&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Python&lt;/li&gt;
  &lt;li&gt;Tensorflow&lt;/li&gt;
  &lt;li&gt;Telegram API&lt;/li&gt;
  &lt;li&gt;Notifier Status Function (personal lib)&lt;/li&gt;
  &lt;li&gt;Webhooks&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>deeplearning</category>
      <category>tensorflow</category>
      <category>python</category>
    </item>
    <item>
      <title>Mi Horario Web (MHW)</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 16 Jan 2024 13:39:51 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/mi-horario-web-mhw-epp</link>
      <guid>https://dev.to/enmanuelmag/mi-horario-web-mhw-epp</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Disclaimer: Currently we are working on improve and update of NodeJS version of the Lambda functions, so the website is not working properly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an online timetable generator for students of the ESPOL University. It allows to obtain information of the planned courses, statistics of the teacher in charge and to generate options of schedules according to the courses and selected parallels.&lt;/p&gt;

&lt;p&gt;The project is developed in React and Material-UI, MongoDB as main database for all courses information and FireStore as realtime database for update de progress of schedule generation on the client side.&lt;/p&gt;

&lt;p&gt;The steps to create schedules are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Select the subjects that you will take.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Selected the course numbers that you would like to choose. In this section you can also see the course's teacher, his/her qualification and student's opinios about him/her.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The you can execute the schedules generations, the process can take a while, so a progress bar is show up with the progress update in real time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally you can see all the options of schedules with out conflict on hours class or exams. You can also download the information as a table (just course numbers and subject) or download the calendar view.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0xknwh4d4ddq3f05loe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj0xknwh4d4ddq3f05loe.png" alt="View of MHW web app" width="800" height="404"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech used
&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;React&lt;/li&gt;
  &lt;li&gt;Material-UI&lt;/li&gt;
  &lt;li&gt;MongoDB&lt;/li&gt;
  &lt;li&gt;Highcharts&lt;/li&gt;
  &lt;li&gt;AWS State Machine&lt;/li&gt;
  &lt;li&gt;AWS Lambda Functions&lt;/li&gt;
  &lt;li&gt;Firebase (Auth, Firestore)&lt;/li&gt;
  &lt;li&gt;Github Actions&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>firebase</category>
      <category>react</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Notifier Function Status</title>
      <dc:creator>Enmanuel Magallanes Pinargote</dc:creator>
      <pubDate>Tue, 16 Jan 2024 13:34:38 +0000</pubDate>
      <link>https://dev.to/enmanuelmag/notifier-function-status-4bd6</link>
      <guid>https://dev.to/enmanuelmag/notifier-function-status-4bd6</guid>
      <description>&lt;p&gt;This library provides a decorator to show a toast in your screen and if you setup, send a email when your function end. All that you need to do is use a decorator and some specific parameters, like in the following example:&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;notifier&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;notify&lt;/span&gt;

&lt;span class="c1"&gt;# Send a message to telegram
&lt;/span&gt;&lt;span class="nd"&gt;@notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your_api_token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chat_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;your_chat_id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;your_function&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Hello World!&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;Another way is by manually calling the 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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Notifier&lt;/span&gt;

&lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Notifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Execution for&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;api_token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;your_api_token&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
  &lt;span class="n"&gt;chat_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;your_chat_id&lt;/span&gt;&lt;span class="sh"&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&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;Hello World &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&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="c1"&gt;# You can also override all the previous parameters,
# on the constructor, by passing them to the function
&lt;/span&gt;&lt;span class="nf"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Execution done&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Finished!&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;Check these features a more detailed on the &lt;a rel="noopener noreferrer" href="https://github.com/enmanuelmag/notify_function"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech used
&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;Python&lt;/li&gt;
  &lt;li&gt;Telegram API&lt;/li&gt;
  &lt;li&gt;Discord webhooks&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
