Skip to content

MCP Tool Servers

Heartbit agents can discover and use tools from external MCP (Model Context Protocol) servers. Tools are discovered automatically at startup — no code changes needed.

Add MCP server URLs to your agent config:

[[agents]]
name = "researcher"
description = "Research specialist"
system_prompt = "You are a research specialist."
mcp_servers = ["http://localhost:8000/mcp"]

Multiple servers are supported:

mcp_servers = [
"http://localhost:8000/mcp",
"http://localhost:9000/mcp"
]

For servers requiring authentication, use the object form with an auth_header:

mcp_servers = [
{ url = "http://gateway:8080/mcp", auth_header = "Bearer tok_xxx" }
]

You can mix simple URLs and authenticated entries:

mcp_servers = [
"http://localhost:8000/mcp",
{ url = "http://gateway:8080/mcp", auth_header = "Bearer tok_xxx" }
]

For quick setup without a config file, use the HEARTBIT_MCP_SERVERS environment variable:

Terminal window
export HEARTBIT_MCP_SERVERS="http://localhost:8000/mcp,http://localhost:9000/mcp"

Heartbit uses Streamable HTTP as the MCP transport. The client connects to the server’s HTTP endpoint, sends JSON-RPC requests, and receives responses. The MCP protocol version is 2025-11-25.

At startup, the client:

  1. Sends an initialize request with client capabilities
  2. Receives the server’s capabilities and tool definitions
  3. Registers discovered tools with the agent

Tool calls are forwarded to the MCP server as tools/call JSON-RPC requests.

In multi-tenant deployments, each user needs their own credentials when calling MCP servers. Heartbit supports this through the AuthProvider trait:

Returns the same auth header for all users. This is used automatically when you set auth_header in the config:

use heartbit::StaticAuthProvider;
let auth = StaticAuthProvider::new(Some("Bearer tok_xxx".into()));

Implements RFC 8693 Token Exchange to obtain per-user delegated tokens from an identity provider:

[daemon.auth]
jwks_url = "https://idp.example.com/.well-known/jwks.json"
[daemon.auth.token_exchange]
exchange_url = "https://idp.example.com/oauth/token"
client_id = "heartbit-agent"
client_secret = "secret123"
agent_token = "agent-cred-token"
scopes = ["crm:read", "crm:write"]

How it works:

  1. User submits a task with a JWT
  2. The daemon stashes the raw JWT as the subject token
  3. When the agent calls an MCP tool, TokenExchangeAuthProvider exchanges the user’s subject token for a scoped delegated token using RFC 8693
  4. The delegated token is injected into the MCP request’s Authorization header
  5. Tokens are cached per (tenant_id, user_id) tuple with TTL-based expiry

This enables the agent to call MCP servers on behalf of the user with properly scoped permissions.

FieldRequiredDescription
exchange_urlYesToken exchange endpoint URL
client_idYesOAuth client ID for the daemon
client_secretYesOAuth client secret for the daemon
agent_tokenConditionalStatic agent token (required if tenant_id not set)
tenant_idConditionalNHI tenant ID for client_credentials grant (auto-fetches agent token)
scopesNoOAuth scopes for the delegated token

Either agent_token or tenant_id must be provided. When tenant_id is set, the agent token is auto-fetched via client_credentials grant and cached.

For custom MCP client setup in code:

use heartbit::{McpClient, StaticAuthProvider, TokenExchangeAuthProvider};
use std::sync::Arc;
// Static auth
let client = McpClient::new(
"http://localhost:8000/mcp",
Some(Arc::new(StaticAuthProvider::new(Some("Bearer tok".into())))),
).await?;
// Token exchange auth
let auth = TokenExchangeAuthProvider::new(
"https://idp.example.com/oauth/token",
"client-id",
"client-secret",
"agent-token",
)
.with_scopes(vec!["read".into(), "write".into()])
.with_user_tokens(user_tokens_map);
let client = McpClient::new(
"http://gateway:8080/mcp",
Some(Arc::new(auth)),
).await?;
// Discovered tools are available via client.tools()