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:
-
initialize— Handshake. Client sends its info, server responds with capabilities. -
tools/list— Discovery. Returns all available tools with names, descriptions, and input schemas. -
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']
}
}));
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:
- Look up the agent by the tool name
- Create a task record in the database
- The existing task runner picks it up (same execution path as any other task)
- Poll for completion
- 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);
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();
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..."
}
}
}
}
Copy button. Done. Restart Claude Desktop and your agents are available.
What it looks like in practice
- You build a "Code Reviewer" agent in CrewForm with GPT-4o, a system prompt about code quality, and access to GitHub MCP tools
- You click "MCP Publish" on that agent
- You paste the config into Claude Desktop
- 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)