DEV Community

Vincent Grobler
Vincent Grobler

Posted on

How We Turned CrewForm Agents Into MCP Tools

Last month I shared how we built a visual workflow builder for AI agent teams. This time: how we made those agents callable from Claude Desktop, Cursor, and any other MCP client — without the agents knowing or caring.

The idea

CrewForm already supported MCP as a client — 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:

What if your CrewForm agents could BE the tools?

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 claude_desktop_config.json and go.

The protocol

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

  1. initialize — Handshake. Client sends its info, server responds with capabilities.
  2. tools/list — Discovery. Returns all available tools with names, descriptions, and input schemas.
  3. tools/call — Execution. Client sends a tool name + arguments, server returns the result.

That's it. The simplicity is the point.

Implementation

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

Tool discovery

When a client calls tools/list, we query for all agents in the workspace with is_mcp_published = true and map them to MCP tools:

// Each published agent becomes an MCP tool
const tools = agents.map(agent => ({
  name: agent.name
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, '_')
    .replace(/^_|_$/g, '')
    .slice(0, 64),
  description: agent.description || `CrewForm agent: ${agent.name}`,
  inputSchema: {
    type: 'object',
    properties: {
      message: {
        type: 'string',
        description: `The prompt or task to send to ${agent.name}`
      }
    },
    required: ['message']
  }
}));
Enter fullscreen mode Exit fullscreen mode

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

Tool execution

When a client calls tools/call, we:

  1. Look up the agent by the tool name
  2. Create a task record in the database
  3. The existing task runner picks it up (same execution path as any other task)
  4. Poll for completion
  5. Return the result as a JSON-RPC response
// Create task — same as pressing "Run" in the UI
const { data: task } = await supabase
  .from('tasks')
  .insert({
    agent_id: agent.id,
    workspace_id: workspaceId,
    input: message,
    status: 'dispatched',
    source: 'mcp',
  })
  .select()
  .single();

// Poll until complete (with timeout)
const result = await pollForCompletion(task.id, 120_000);
Enter fullscreen mode Exit fullscreen mode

This is important: the agent runs with its full configuration — 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.

Authentication

We reuse our existing API key infrastructure. MCP clients authenticate with a Bearer token that maps to a workspace:

const authHeader = req.headers.get('authorization');
const token = authHeader?.replace('Bearer ', '');

// Check api_keys table for mcp-server or a2a provider
const { data: keyRecord } = await supabase
  .from('api_keys')
  .select('workspace_id')
  .eq('encrypted_key', token)
  .in('provider', ['mcp-server', 'a2a'])
  .single();
Enter fullscreen mode Exit fullscreen mode

One key, one workspace, only that workspace's published agents are exposed.

The transport question

MCP supports three transports: stdio (local processes), sse (server-sent events), and streamable-http (HTTP-based). We went with Streamable HTTP because:

  • Our task runner is already an HTTP server
  • No WebSocket infrastructure needed
  • Works behind proxies, load balancers, and CDNs
  • Claude Desktop and Cursor both support it natively

Each request is a self-contained JSON-RPC call. No persistent connections to manage.

UI: one-click publishing

On the backend, publishing is just a boolean flag: is_mcp_published. But the UX matters.

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.

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

The config snippet

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:

{
  "mcpServers": {
    "crewform": {
      "url": "https://runner.crewform.tech/mcp",
      "headers": {
        "Authorization": "Bearer cf_mcp_a1b2c3d4..."
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Copy button. Done. Restart Claude Desktop and your agents are available.

What it looks like in practice

  1. You build a "Code Reviewer" agent in CrewForm with GPT-4o, a system prompt about code quality, and access to GitHub MCP tools
  2. You click "MCP Publish" on that agent
  3. You paste the config into Claude Desktop
  4. 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

The agent runs on your infrastructure, with your API keys, using your configuration. Claude just sees a tool that returns a result.

Three protocols, one platform

CrewForm is now a triple-protocol platform:

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

All three are open-source and ship with the same codebase.

Try it

The MCP Server is live on crewform.tech and fully open-source:

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.

Top comments (0)