<?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: Vincent Grobler</title>
    <description>The latest articles on DEV Community by Vincent Grobler (@vincent_grobler_776512b17).</description>
    <link>https://dev.to/vincent_grobler_776512b17</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%2F3846599%2Fc6d60cd4-6be0-4be2-b28b-eac95f3c076c.png</url>
      <title>DEV Community: Vincent Grobler</title>
      <link>https://dev.to/vincent_grobler_776512b17</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vincent_grobler_776512b17"/>
    <language>en</language>
    <item>
      <title>We built an embeddable AI chat widget, hybrid RAG search, and agent portability — here's how (CrewForm v1.8.0)</title>
      <dc:creator>Vincent Grobler</dc:creator>
      <pubDate>Wed, 08 Apr 2026 14:45:09 +0000</pubDate>
      <link>https://dev.to/vincent_grobler_776512b17/we-built-an-embeddable-ai-chat-widget-hybrid-rag-search-and-agent-portability-heres-how-1cb3</link>
      <guid>https://dev.to/vincent_grobler_776512b17/we-built-an-embeddable-ai-chat-widget-hybrid-rag-search-and-agent-portability-heres-how-1cb3</guid>
      <description>&lt;p&gt;CrewForm is an open-source AI agent orchestration platform — think "Slack for AI workers." You create agents, wire them into teams (pipeline, orchestrator, or collaboration mode), and let them handle tasks. BYOK, self-hostable, all three agentic protocols (MCP + A2A + AG-UI).&lt;/p&gt;

&lt;p&gt;Today we're shipping v1.8.0 with three major features we're really excited about.&lt;/p&gt;

&lt;p&gt;💬 Embeddable Chat Widget&lt;br&gt;
You can now deploy any CrewForm agent as a chat bubble on your website with a single script tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;script
  &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://runner.crewform.tech/chat/widget.js"&lt;/span&gt;
  &lt;span class="na"&gt;data-key=&lt;/span&gt;&lt;span class="s"&gt;"cf_chat_your_key_here"&lt;/span&gt;
  &lt;span class="na"&gt;data-theme=&lt;/span&gt;&lt;span class="s"&gt;"dark"&lt;/span&gt;
  &lt;span class="na"&gt;data-position=&lt;/span&gt;&lt;span class="s"&gt;"bottom-right"&lt;/span&gt;
  &lt;span class="na"&gt;async&lt;/span&gt;
&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No React, no build step, no npm install. Paste it before closing body tag and your visitors can chat with your agent in real-time with streaming responses.&lt;/p&gt;

&lt;p&gt;What we built under the hood:&lt;/p&gt;

&lt;p&gt;Standalone Vite build that outputs a single widget.js bundle&lt;br&gt;
Shadow DOM isolation — zero CSS conflicts with your existing site&lt;br&gt;
Domain whitelisting via cf_chat_ API keys&lt;br&gt;
Session memory — conversations persist across page reloads&lt;br&gt;
Rate limiting per visitor&lt;br&gt;
Settings UI with embed snippet generator and live preview&lt;br&gt;
The widget connects directly to your task runner via SSE, so responses stream in word-by-word. We bundle it inside the Docker image so self-hosted deployments get it for free.&lt;/p&gt;

&lt;p&gt;🔍 Knowledge Base: Hybrid Search + Retrieval Testing&lt;br&gt;
Our RAG pipeline got a serious upgrade. Previously it was basic vector search — now it's production-grade.&lt;/p&gt;

&lt;p&gt;Hybrid Search&lt;br&gt;
We combine vector similarity (cosine distance via pgvector) with PostgreSQL full-text search (tsvector + ts_rank_cd):&lt;/p&gt;

&lt;p&gt;Final Score = (0.7 × cosine_similarity) + (0.3 × ts_rank)&lt;br&gt;
The over-fetch + rerank strategy retrieves 2× results from each method, deduplicates, and returns the top-K. This dramatically improves recall for queries that mix semantic meaning with specific keywords.&lt;/p&gt;

&lt;p&gt;Metadata Tags&lt;br&gt;
Documents can now be tagged (e.g., "FAQ", "Technical", "Policy") with GIN-indexed filtering. Tag your docs, then filter search results by tag — both in the UI and via the API.&lt;/p&gt;

&lt;p&gt;Retrieval Tester&lt;br&gt;
This is my favorite part. Before deploying retrieval to agents, you can test it interactively:&lt;/p&gt;

&lt;p&gt;Type a query, see matched chunks with color-coded similarity scores&lt;br&gt;
Toggle between vector-only and hybrid search&lt;br&gt;
Filter by document or tag&lt;br&gt;
Adjust top-K (1–20)&lt;br&gt;
See response times for benchmarking&lt;br&gt;
No more "deploy and pray" — you can validate retrieval quality before it touches your agents.&lt;/p&gt;

&lt;p&gt;📦 Agent/Team Export &amp;amp; Import&lt;br&gt;
Share agents and teams as portable JSON files:&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;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"crewform-export"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&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;"Content Pipeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pipeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"agents"&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;"ref_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"researcher"&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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="nl"&gt;"ref_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"def"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"writer"&lt;/span&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&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;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;Export an agent or team with one click — the JSON includes everything (model, prompt, tools, voice profile, config). Team exports are self-contained with all member agents embedded inline.&lt;/p&gt;

&lt;p&gt;Import into any workspace — agent IDs get remapped automatically to maintain referential integrity in team configs. Custom tools are stripped (not portable across workspaces).&lt;/p&gt;

&lt;p&gt;This is the foundation for agent sharing — export a team, send it to a colleague, they import it and they're running.&lt;/p&gt;

&lt;p&gt;Also in this release&lt;br&gt;
AG-UI Rich Interactions — Agents can now pause mid-execution and ask for approval, data confirmation, or present choices&lt;br&gt;
Marketplace Agent README — Published agents can include rich Markdown documentation&lt;br&gt;
License Key Validation — HMAC-SHA256 cryptographic verification&lt;br&gt;
OpenTelemetry + Langfuse — Opt-in tracing for LLM calls, tool use, and team runs&lt;/p&gt;

&lt;p&gt;Try it&lt;br&gt;
🌐 Hosted: crewform.tech — free tier, no credit card&lt;br&gt;
🐳 Self-hosted: docker compose up -d&lt;br&gt;
⭐ GitHub: github.com/CrewForm/crewform&lt;br&gt;
📖 Docs: docs.crewform.tech&lt;br&gt;
💬 Discord: discord.gg/TAFasJCTWs&lt;br&gt;
We'd love feedback — especially on the hybrid search and retrieval tester. What features would you want next?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How We Turned CrewForm Agents Into MCP Tools</title>
      <dc:creator>Vincent Grobler</dc:creator>
      <pubDate>Wed, 01 Apr 2026 17:00:12 +0000</pubDate>
      <link>https://dev.to/vincent_grobler_776512b17/how-we-turned-crewform-agents-into-mcp-tools-4lc2</link>
      <guid>https://dev.to/vincent_grobler_776512b17/how-we-turned-crewform-agents-into-mcp-tools-4lc2</guid>
      <description>&lt;p&gt;Last month I shared &lt;a href="https://dev.to/vincent_grobler_776512b17/how-i-built-a-visual-drag-and-drop-workflow-builder-for-ai-agent-teams-react-flow-dagre-XXXXX"&gt;how we built a visual workflow builder for AI agent teams&lt;/a&gt;. This time: how we made those agents callable from Claude Desktop, Cursor, and any other MCP client — without the agents knowing or caring.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://github.com/CrewForm/crewform" rel="noopener noreferrer"&gt;CrewForm&lt;/a&gt; already supported MCP as a &lt;strong&gt;client&lt;/strong&gt; — our agents could call external MCP tool servers (GitHub, Brave Search, Postgres, etc.). But MCP is a two-way standard. We wanted to flip it around:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if your CrewForm agents could BE the tools?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Imagine configuring a "Content Writer" agent in CrewForm — with a specific model, system prompt, knowledge base, and tools — and then calling it from Claude Desktop as if it were any other MCP tool. No API wrappers. No custom integration code. Just add a URL to your &lt;code&gt;claude_desktop_config.json&lt;/code&gt; and go.&lt;/p&gt;

&lt;h2&gt;
  
  
  The protocol
&lt;/h2&gt;

&lt;p&gt;MCP (Model Context Protocol) uses JSON-RPC 2.0 over HTTP. A client connects, discovers tools, and calls them. The key methods:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;initialize&lt;/code&gt;&lt;/strong&gt; — Handshake. Client sends its info, server responds with capabilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools/list&lt;/code&gt;&lt;/strong&gt; — Discovery. Returns all available tools with names, descriptions, and input schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;tools/call&lt;/code&gt;&lt;/strong&gt; — Execution. Client sends a tool name + arguments, server returns the result.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. The simplicity is the point.&lt;/p&gt;

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

&lt;p&gt;We added a single HTTP handler (&lt;code&gt;mcpServer.ts&lt;/code&gt;, ~300 lines) that mounts at &lt;code&gt;POST /mcp&lt;/code&gt; alongside our existing A2A and AG-UI protocol handlers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool discovery
&lt;/h3&gt;

&lt;p&gt;When a client calls &lt;code&gt;tools/list&lt;/code&gt;, we query for all agents in the workspace with &lt;code&gt;is_mcp_published = true&lt;/code&gt; and map them to MCP tools:&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;// Each published agent becomes an MCP tool&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;agents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&amp;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="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;a-z0-9&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/g&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="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^_|_$/g&lt;/span&gt;&lt;span class="p"&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="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;description&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="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;`CrewForm agent: &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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;inputSchema&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`The prompt or task to send to &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="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;required&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;message&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The name transformation matters — MCP tool names must be lowercase alphanumeric with underscores, max 64 chars. An agent called "Blog Content Writer v2" becomes &lt;code&gt;blog_content_writer_v2&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tool execution
&lt;/h3&gt;

&lt;p&gt;When a client calls &lt;code&gt;tools/call&lt;/code&gt;, we:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Look up the agent by the tool name&lt;/li&gt;
&lt;li&gt;Create a task record in the database&lt;/li&gt;
&lt;li&gt;The existing task runner picks it up (same execution path as any other task)&lt;/li&gt;
&lt;li&gt;Poll for completion&lt;/li&gt;
&lt;li&gt;Return the result as a JSON-RPC response
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create task — same as pressing "Run" in the UI&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;task&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tasks&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;insert&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;agent_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="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;workspace_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;workspaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dispatched&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp&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;select&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Poll until complete (with timeout)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pollForCompletion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&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="mi"&gt;120&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is important: &lt;strong&gt;the agent runs with its full configuration&lt;/strong&gt; — model, system prompt, tools (including MCP tools it consumes), knowledge base, voice profile. It's not a prompt relay. It's a fully orchestrated agent execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;We reuse our existing API key infrastructure. MCP clients authenticate with a Bearer token that maps to a workspace:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization&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;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;authHeader&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &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="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Check api_keys table for mcp-server or a2a provider&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keyRecord&lt;/span&gt; &lt;span class="p"&gt;}&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;supabase&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api_keys&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;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workspace_id&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;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;encrypted_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;provider&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mcp-server&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;a2a&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;single&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One key, one workspace, only that workspace's published agents are exposed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The transport question
&lt;/h2&gt;

&lt;p&gt;MCP supports three transports: &lt;code&gt;stdio&lt;/code&gt; (local processes), &lt;code&gt;sse&lt;/code&gt; (server-sent events), and &lt;code&gt;streamable-http&lt;/code&gt; (HTTP-based). We went with &lt;strong&gt;Streamable HTTP&lt;/strong&gt; because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Our task runner is already an HTTP server&lt;/li&gt;
&lt;li&gt;No WebSocket infrastructure needed&lt;/li&gt;
&lt;li&gt;Works behind proxies, load balancers, and CDNs&lt;/li&gt;
&lt;li&gt;Claude Desktop and Cursor both support it natively&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each request is a self-contained JSON-RPC call. No persistent connections to manage.&lt;/p&gt;

&lt;h2&gt;
  
  
  UI: one-click publishing
&lt;/h2&gt;

&lt;p&gt;On the backend, publishing is just a boolean flag: &lt;code&gt;is_mcp_published&lt;/code&gt;. But the UX matters.&lt;/p&gt;

&lt;p&gt;On each agent's detail page, there's a toggle button. Click "MCP Publish" → the agent appears as an MCP tool. Click again → it disappears. The config snippet in Settings updates automatically.&lt;/p&gt;

&lt;p&gt;We also added a &lt;strong&gt;"Generate MCP API Key"&lt;/strong&gt; button in Settings → MCP Servers. One click generates a &lt;code&gt;cf_mcp_&lt;/code&gt; prefixed key, shows it once for copying, and it's ready to paste into &lt;code&gt;claude_desktop_config.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config snippet
&lt;/h2&gt;

&lt;p&gt;This was a small detail that made a big difference in adoption. Instead of making users construct the JSON config manually, we generate it for them:&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;"crewform"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://runner.crewform.tech/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;"headers"&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;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Bearer cf_mcp_a1b2c3d4..."&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;Copy button. Done. Restart Claude Desktop and your agents are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like in practice
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;You build a "Code Reviewer" agent in CrewForm with GPT-4o, a system prompt about code quality, and access to GitHub MCP tools&lt;/li&gt;
&lt;li&gt;You click "MCP Publish" on that agent&lt;/li&gt;
&lt;li&gt;You paste the config into Claude Desktop&lt;/li&gt;
&lt;li&gt;Now when you're chatting with Claude and say "review this pull request for security issues", Claude can delegate to your CrewForm agent — which uses its own model, prompt, and tools to do the actual review&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent runs on your infrastructure, with your API keys, using your configuration. Claude just sees a tool that returns a result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three protocols, one platform
&lt;/h2&gt;

&lt;p&gt;CrewForm is now a &lt;strong&gt;triple-protocol platform&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP Client&lt;/strong&gt; — Your agents can use external tools (GitHub, Slack, databases)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Server&lt;/strong&gt; — External clients can use your agents as tools&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A2A&lt;/strong&gt; — Agents can delegate tasks to other agents across frameworks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AG-UI&lt;/strong&gt; — Real-time streaming for frontend integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three are open-source and ship with the same codebase.&lt;/p&gt;

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

&lt;p&gt;The MCP Server is live on &lt;a href="https://crewform.tech" rel="noopener noreferrer"&gt;crewform.tech&lt;/a&gt; and fully open-source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;a href="https://github.com/CrewForm/crewform" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://docs.crewform.tech/mcp-server-publishing" rel="noopener noreferrer"&gt;Docs — MCP Server Publishing&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.gg/TAFasJCTWs" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building MCP servers or have agents you want to expose as tools, I'd love to hear how you're approaching it. Drop a comment or find us on Discord.&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>ai</category>
      <category>mcp</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How We Built a Visual Drag-and-Drop Workflow Builder for AI Agent Teams (React Flow + Dagre)</title>
      <dc:creator>Vincent Grobler</dc:creator>
      <pubDate>Mon, 30 Mar 2026 17:01:36 +0000</pubDate>
      <link>https://dev.to/vincent_grobler_776512b17/how-we-built-a-visual-drag-and-drop-workflow-builder-for-ai-agent-teams-react-flow-dagre-211j</link>
      <guid>https://dev.to/vincent_grobler_776512b17/how-we-built-a-visual-drag-and-drop-workflow-builder-for-ai-agent-teams-react-flow-dagre-211j</guid>
      <description>&lt;p&gt;A few days ago, I shared &lt;a href="https://dev.to/vincent_grobler_776512b17/i-built-an-open-source-platform-for-orchestrating-ai-agent-teams-heres-what-i-learned-4g05"&gt;how I built CrewForm&lt;/a&gt; — an open-source platform for orchestrating AI agent teams through a web UI. The "What's next" section mentioned an interactive workflow canvas. We shipped it. Here's how it works under the hood.&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%2Fraw.githubusercontent.com%2FCrewForm%2Fcrewform%2Fmain%2F.github%2Fassets%2Fpipeline-run-hero.gif" 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%2Fraw.githubusercontent.com%2FCrewForm%2Fcrewform%2Fmain%2F.github%2Fassets%2Fpipeline-run-hero.gif" alt="CrewForm Visual Workflow Builder" width="480" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;In the original version, pipeline teams were configured through a dropdown-based step list. It worked, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You couldn't see the full flow at a glance&lt;/li&gt;
&lt;li&gt;Rearranging steps meant a lot of up/down clicking&lt;/li&gt;
&lt;li&gt;Non-technical team members found it confusing&lt;/li&gt;
&lt;li&gt;There was no visual feedback during execution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We needed a canvas where you could see agents as nodes, connections as edges, and execution status in real-time.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Flow&lt;/strong&gt; — The canvas library. Handles nodes, edges, panning, zooming, selection, and minimap out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dagre&lt;/strong&gt; — Auto-layout engine. Given a directed graph, it computes x/y positions for every node so nothing overlaps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSE (Server-Sent Events)&lt;/strong&gt; — Real-time execution state. The task runner pushes status updates, and the canvas highlights the active node.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Layout system: TB and LR
&lt;/h2&gt;

&lt;p&gt;One design decision that took a few iterations: &lt;strong&gt;directional layouts&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Pipeline workflows naturally flow in one direction. Some people think top-to-bottom (like a flowchart), others think left-to-right (like a timeline). We support both.&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getLayoutedElements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TB&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dagreGraph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;dagre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;graphlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Graph&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;dagreGraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setGraph&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;rankdir&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;nodesep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ranksep&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;dagreGraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setDefaultEdgeLabel&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({}));&lt;/span&gt;

  &lt;span class="nx"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dagreGraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;node&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;edges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dagreGraph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setEdge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;edge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;dagre&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dagreGraph&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ... map positions back to React Flow nodes&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tricky part was &lt;strong&gt;edge handles&lt;/strong&gt;. React Flow connects edges to specific handles (connection points) on each node — top, bottom, left, right. When you toggle from TB to LR, every edge needs its &lt;code&gt;sourceHandle&lt;/code&gt; and &lt;code&gt;targetHandle&lt;/code&gt; updated:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getHandleIds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;LR&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;sourceHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;left&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;sourceHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bottom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetHandle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;top&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, edges default to the first available handle (usually top), and you get connections going the wrong way. This was one of those bugs that seems obvious in hindsight but took a few rounds of debugging.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inserting agents between steps
&lt;/h2&gt;

&lt;p&gt;This was the feature that made the canvas feel like a real tool rather than a visualisation.&lt;/p&gt;

&lt;p&gt;Right-click any edge → "Insert Agent Here" → pick an agent → done. The new agent splits the edge into two, and dagre re-layouts everything automatically.&lt;/p&gt;

&lt;p&gt;The implementation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove the original edge (A → C)&lt;/li&gt;
&lt;li&gt;Insert the new node (B)&lt;/li&gt;
&lt;li&gt;Create two new edges (A → B, B → C)&lt;/li&gt;
&lt;li&gt;Re-run dagre layout&lt;/li&gt;
&lt;li&gt;Animate the transition with React Flow's &lt;code&gt;fitView()&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Real-time execution
&lt;/h2&gt;

&lt;p&gt;When a pipeline runs, the canvas comes alive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The active agent node gets highlighted&lt;/li&gt;
&lt;li&gt;Camera auto-follows the executing node&lt;/li&gt;
&lt;li&gt;A side panel shows the live transcript (streamed via SSE)&lt;/li&gt;
&lt;li&gt;Completed steps get a success indicator&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This uses Supabase Realtime subscriptions on the task execution table. When the task runner updates a step's status, the canvas reflects it instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Persisting layout direction
&lt;/h2&gt;

&lt;p&gt;Small detail that matters: layout direction is saved per-team using an &lt;code&gt;useRef&lt;/code&gt; to avoid stale closures in React's render cycle. When a user opens a team they configured as LR last week, it opens in LR.&lt;/p&gt;

&lt;p&gt;The direction is stored in the team's JSONB configuration column alongside the graph structure, so it survives page reloads and re-renders without triggering unnecessary edge recalculations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start with the canvas earlier.&lt;/strong&gt; The dropdown-based config was a stepping stone, but users immediately understood the canvas better. Visual-first should have been day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;useRef&lt;/code&gt; for shared state from the start.&lt;/strong&gt; React Flow callbacks capture stale closure values. We hit this bug twice before learning to use refs for anything that edges or layout callbacks depend on.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The visual canvas is live on &lt;a href="https://crewform.tech" rel="noopener noreferrer"&gt;crewform.tech&lt;/a&gt; and fully open-source:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⭐ &lt;a href="https://github.com/CrewForm/crewform" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📖 &lt;a href="https://docs.crewform.tech" rel="noopener noreferrer"&gt;Docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💬 &lt;a href="https://discord.gg/TAFasJCTWs" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built anything with React Flow + dagre, I'd love to compare notes. And if you have ideas for the canvas — multi-select, copy-paste node groups, conditional branching — drop a comment or open an issue.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>react</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built an open-source platform for orchestrating AI agent teams — here's what I learned</title>
      <dc:creator>Vincent Grobler</dc:creator>
      <pubDate>Fri, 27 Mar 2026 20:05:31 +0000</pubDate>
      <link>https://dev.to/vincent_grobler_776512b17/i-built-an-open-source-platform-for-orchestrating-ai-agent-teams-heres-what-i-learned-4g05</link>
      <guid>https://dev.to/vincent_grobler_776512b17/i-built-an-open-source-platform-for-orchestrating-ai-agent-teams-heres-what-i-learned-4g05</guid>
      <description>&lt;p&gt;For the past few months, I've been building CrewForm — an open-source platform for creating and orchestrating multi-agent AI workflows through a web UI.&lt;/p&gt;

&lt;p&gt;Think of it as the visual, no-code alternative to frameworks like CrewAI or LangGraph. Instead of writing Python scripts to wire agents together, you configure everything in a dashboard.&lt;/p&gt;

&lt;p&gt;I wanted to share what it does, how it's built, and some lessons from the journey.&lt;/p&gt;

&lt;p&gt;What it does&lt;br&gt;
You create AI agents by picking a model (15 LLM providers supported — OpenAI, Anthropic, Gemini, Groq, Ollama, etc.), writing a system prompt, and attaching tools.&lt;/p&gt;

&lt;p&gt;Then you combine agents into teams that run in one of three modes:&lt;/p&gt;

&lt;p&gt;Pipeline — agents execute sequentially, each passing output to the next. Great for content workflows (research → write → edit → format).&lt;br&gt;
Orchestrator — a "brain" agent receives the task, breaks it down, and delegates sub-tasks to specialist workers. Best for complex, dynamic problems.&lt;br&gt;
Collaboration — agents discuss in a shared thread, debate approaches, and converge on a consensus. Useful for analysis and decision-making.&lt;br&gt;
Each mode has a visual workflow canvas where you can see the execution graph — pipeline chains, orchestrator delegation trees, or collaboration circles.&lt;/p&gt;

&lt;p&gt;The protocol story&lt;br&gt;
One thing I'm particularly excited about is protocol support. CrewForm implements all three emerging agentic protocols:&lt;/p&gt;

&lt;p&gt;MCP (Model Context Protocol) — your agents can connect to any MCP-compatible tool server. There are thousands of these now — file systems, databases, APIs, web scrapers. One config, and your agent has new capabilities.&lt;/p&gt;

&lt;p&gt;A2A (Agent-to-Agent) — agents in CrewForm can collaborate with agents from completely different frameworks. Publish your agent as an A2A endpoint, or consume external ones.&lt;/p&gt;

&lt;p&gt;AG-UI (Agent-User Interaction) — real-time SSE streaming so you can build frontends that show what agents are doing as they work.&lt;/p&gt;

&lt;p&gt;Self-hosting &amp;amp; local models&lt;br&gt;
This was a non-negotiable. If you're processing sensitive data through AI, you need to own the infrastructure.&lt;/p&gt;

&lt;p&gt;bash&lt;br&gt;
docker compose up -d&lt;br&gt;
That's the entire setup. Pair with Ollama, and you get fully local AI — Llama 3.3, DeepSeek R1, Qwen 2.5, Mistral, and more. Zero API keys, zero data leaving your network.&lt;/p&gt;

&lt;p&gt;Knowledge Base (RAG)&lt;br&gt;
Upload documents (TXT, MD, CSV, JSON), CrewForm chunks, and embeds them using pgvector, and agents search the knowledge base at runtime. No external vector DB needed — it's all in Postgres.&lt;/p&gt;

&lt;p&gt;Tech stack&lt;br&gt;
For anyone curious about the architecture:&lt;/p&gt;

&lt;p&gt;Frontend: React + TypeScript + Vite, TailwindCSS, React Flow (workflow canvas)&lt;br&gt;
Backend: Supabase — Auth, PostgreSQL with RLS, Realtime, Edge Functions, Storage&lt;br&gt;
Task runner: Node.js — polls for pending tasks, runs the execution engine&lt;br&gt;
Vectors: pgvector extension in Postgres&lt;br&gt;
Hosting: Vercel (frontend), Railway (task runner), Supabase Cloud (backend)&lt;br&gt;
Self-hosted: Docker Compose for the full stack&lt;br&gt;
The decision to build on Supabase was pivotal. Row Level Security means every database query is automatically scoped to the user's workspace — security by default, not by convention. And real-time subscriptions meant live task execution updates without managing WebSocket servers.&lt;/p&gt;

&lt;p&gt;Bring Your Own Keys&lt;br&gt;
CrewForm is BYOK — bring your own API keys. You plug in your keys for whatever providers you use, and we never touch them. No per-token markup, no middleman.&lt;/p&gt;

&lt;p&gt;Currently supporting: OpenAI, Anthropic, Google Gemini, Groq, Mistral, Cohere, Perplexity, DeepSeek, Together AI, Fireworks, OpenRouter, xAI, Cerebras, HuggingFace, and Ollama.&lt;/p&gt;

&lt;p&gt;What's next&lt;br&gt;
Interactive workflow canvas (drag-to-add agents, connect nodes to define execution order)&lt;br&gt;
Cost optimisation dashboard&lt;br&gt;
Enterprise features (SSO, audit log streaming)&lt;br&gt;
More marketplace templates&lt;br&gt;
Try it&lt;br&gt;
🔗 GitHub: github.com/CrewForm/crewform&lt;br&gt;
🌐 Website: crewform.tech&lt;br&gt;
📚 Docs: docs.crewform.tech&lt;br&gt;
💬 Discord: discord.gg/TAFasJCTWs&lt;br&gt;
Licensed under AGPL-3.0. Would love feedback from the dev community — especially on the self-hosting experience and what features you'd want to see next!&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
