Skip to content

MCP Bridge — mounting external MCP servers

DuDuClaw can mount third-party Model Context Protocol servers next to its own internal MCP server, so an agent’s tool loop gains the external server’s tools without a hand-written Rust connector. This is how you wire Plane, Chatwoot, Invoice Ninja, Gmail/Calendar, WooCommerce, and any other MCP server into an agent.

  • MCP Bridge (this page): the SaaS already ships (or the community ships) an MCP server. You mount it by config. No code.
  • Native connector (e.g. duduclaw-odoo, duduclaw-erpnext): no usable MCP server exists, or you need deep credential isolation / edition gating / audit attribution that a generic mount can’t give.

Add one or more [[mcp.external]] tables to the agent’s agent.toml:

[[mcp.external]]
name = "chatwoot"
command = "npx"
args = ["-y", "@chatwoot/mcp-server-chatwoot"]
enabled = true # optional, default true
# env values: a plain literal; `env://VAR` to pull from the gateway process
# environment; or `secret://<backend>/<name>` to pull from the configured secret
# manager at spawn time (keep secrets OUT of agent.toml).
env = { CHATWOOT_BASE_URL = "https://app.chatwoot.com", CHATWOOT_API_TOKEN = "secret://vault/chatwoot_token" }
# Tool visibility (both optional):
allowed_tools = ["chatwoot_list_conversations", "chatwoot_get_conversation"] # allowlist = deny-by-default
denied_tools = ["chatwoot_delete_conversation"] # always removed

Remote servers that speak MCP Streamable HTTP (no local process) are mounted with url instead of command. For vendors DuDuClaw already knows, use a preset — the endpoint and the credential source are filled in for you (endpoint URLs live in DuDuClaw, so a vendor rename is a one-line fix, not a config migration):

[[mcp.external]]
preset = "google:gmail" # gmail|calendar|drive|docs|sheets|slides|chat
allowed_tools = ["search_threads", "get_thread", "create_draft"]

preset = "google:<svc>" expands to the official Google Workspace MCP endpoint plus bearer_token = "oauth://google" (the dashboard’s connected Google account, auto-refreshed).

For Google Workspace, prefer the native tools. All eight services (Gmail / Calendar / Sheets / Drive / Docs / Slides / Forms / Tasks) ship as native MCP tools on GA APIs — see google-workspace.md. The official MCP servers are Developer-Preview-only and their terms forbid exposing Pre-GA APIs to users outside your own domain, so they are an advanced self-hosted opt-in, not a shippable path (google-mcp.md explains the trade-off).

Spelled out by hand it looks like this — also how you mount anything without a preset (e.g. a self-hosted DocuSeal instance’s built-in /mcp endpoint):

[[mcp.external]]
name = "gmail"
url = "https://gmailmcp.googleapis.com/mcp/v1"
# bearer_token: literal, env://VAR, secret://<backend>/<name>, or
# oauth://google (reuses the dashboard's connected Google account token,
# auto-refreshed). Sent as `Authorization: Bearer <token>`.
bearer_token = "oauth://google"
# headers = { X-Custom = "env://MY_HEADER" } # optional extra headers
allowed_tools = ["search_threads", "get_thread", "create_draft"]

Field reference:

Field Required Meaning
name recommended Label for logs (a preset labels itself)
preset no Built-in vendor shorthand (google:gmail, google:calendar, google:drive, google:docs, google:sheets, google:slides, google:chat) supplying url + a default bearer_token. Unknown preset ⇒ server skipped; preset + url together ⇒ ambiguous, skipped
command one of command/url/preset stdio transport: executable to spawn (npx, node, python, an absolute path…)
url one of command/url/preset Streamable-HTTP transport: the remote MCP endpoint (https:// only; entries with both or neither command/url are skipped)
args no Argument vector (stdio only)
env no Child environment (stdio only). env://VAR pulls from the gateway’s env; secret://<backend>/<name> pulls from the secret manager (see below); a missing/unresolvable env:///secret:// credential disables the whole server (fail-safe — a server without its token would misbehave)
bearer_token no HTTP auth: literal, env://VAR, secret://…, or oauth://google; sent as Authorization: Bearer …. Unresolvable ⇒ server skipped
headers no Extra HTTP request headers; values support env:// and secret://
enabled no (default true) Set false to keep the config but not mount
allowed_tools no If set, ONLY these tools are exposed (deny-by-default)
denied_tools no Always removed, even if allow-listed
  • The internal duduclaw MCP server is always client 0, so on a tool-name collision the internal tool wins (external duplicates are dropped with a log).
  • Each external server is spawned independently; if one fails to connect it is skipped — the internal server and any other externals still serve. If the combined tools/list fails, the registry degrades to internal-only rather than losing all tools.
  • allowed_tools is deny-by-default: an allowlist means unlisted tools are hidden. Combine with denied_tools to blocklist specific dangerous tools.
  • For write/irreversible tools (billing, deletes), also list them in the agent’s [capabilities] approval_required_tools so they route through the HITL ApprovalBroker — the MCP Bridge controls visibility, approvals control execution.

The config parser and tool filter are covered by unit tests (crates/duduclaw-gateway/src/mcp_external.rs, crates/duduclaw-llm/src/mcp_client.rs). To verify an actual mount end-to-end you need a reachable MCP server:

  1. Pick a server you can run locally, e.g. the reference everything-server:
    終端機視窗
    # in a scratch dir, confirm it speaks MCP over stdio
    npx -y @modelcontextprotocol/server-everything
  2. Add it to a test agent’s agent.toml:
    [[mcp.external]]
    name = "everything"
    command = "npx"
    args = ["-y", "@modelcontextprotocol/server-everything"]
    allowed_tools = ["echo"] # prove the allowlist hides the rest
  3. Start the gateway and send the agent a message that would use the echo tool. Confirm in the logs:
    • external MCP server mounted with server=everything
    • the agent can call echo but NOT the server’s other tools (allowlist).
  4. Temporarily point an env:// credential at an unset var and restart — confirm the server is skipped with external MCP env credential unresolved … skipping server.

Expected: the agent gains exactly the allow-listed external tools; a broken external server never takes down the internal tool surface.

env values may reference the secret manager instead of holding a literal or an env:// process-env pull. At spawn time DuDuClaw resolves secret://<backend>/<name> against the [secret_manager] config in ~/.duduclaw/config.toml; an unresolvable ref drops the whole server (fail-safe, identical to a missing env://).

Backends: local (AES store), vault (HashiCorp Vault KV v2), env, onepassword (1Password Connect), infisical. Example:

config.toml
[secret_manager]
backend = "vault"
vault_addr = "https://vault.internal:8200"
vault_token_enc = "…" # keyfile-encrypted; never plaintext in prod
# agent.toml
[[mcp.external]]
name = "chatwoot"
command = "npx"
args = ["-y", "@chatwoot/mcp-server-chatwoot"]
env = { CHATWOOT_BASE_URL = "https://app.chatwoot.com", CHATWOOT_API_TOKEN = "secret://vault/chatwoot_token" }

See the module docs in crates/duduclaw-security/src/secret_manager/mod.rs for the full [secret_manager] field set (including 1Password / Infisical).

Each recipe is the agent.toml block plus the credentials to provision. Mount one, restart the agent, follow the live-verification runbook. Write/irreversible tools are marked ⚠ — list them in the agent’s [capabilities] approval_required_tools so they route through the HITL broker.

Status: these are PENDING-LIVE — the config shape + parsing are tested, but a live end-to-end mount needs the corresponding SaaS account. Server names reflect the ecosystem as of 2026-07; confirm the package/endpoint before use.

Gmail / Google Calendar (Google official remote MCP)

Section titled “Gmail / Google Calendar (Google official remote MCP)”
[[mcp.external]]
name = "gmail"
command = "npx"
args = ["-y", "@google/gmail-mcp"] # confirm the current official package
env = { GOOGLE_OAUTH_TOKEN = "secret://vault/google_oauth" }
allowed_tools = ["gmail_search", "gmail_get_thread", "gmail_create_draft"] # read + draft only
denied_tools = ["gmail_send"] # ⚠ keep send behind approval, not auto

Provision: a Google Cloud OAuth app; run the OAuth flow to mint the token. gmail_send ⚠ → approval_required_tools.

[[mcp.external]]
name = "plane"
command = "npx"
args = ["-y", "@makeplane/plane-mcp-server"]
env = { PLANE_API_KEY = "secret://vault/plane_api_key", PLANE_WORKSPACE_SLUG = "my-workspace" }
allowed_tools = ["plane_list_issues", "plane_get_issue", "plane_create_issue"]
denied_tools = ["plane_delete_issue"] # ⚠

Optional: a one-way sync worker can pull Plane issues into the Task Board (see IMPL-PLAN §E). Provision: a Plane API key + workspace slug.

Invoice Ninja (community Fuciuss/invoice-ninja-mcp)

Section titled “Invoice Ninja (community Fuciuss/invoice-ninja-mcp)”
[[mcp.external]]
name = "invoice-ninja"
command = "npx"
args = ["-y", "invoice-ninja-mcp"] # confirm package name
env = { INVOICE_NINJA_URL = "https://invoicing.example.com", INVOICE_NINJA_TOKEN = "secret://vault/invoiceninja_token" }
allowed_tools = ["in_list_invoices", "in_get_invoice", "in_create_invoice", "in_record_payment"]

Money is irreversible — put every write tool (in_create_invoice, in_record_payment, …) in approval_required_tools. Provision: an Invoice Ninja API token.

Chatwoot (official @chatwoot/mcp-server-chatwoot)

Section titled “Chatwoot (official @chatwoot/mcp-server-chatwoot)”
[[mcp.external]]
name = "chatwoot"
command = "npx"
args = ["-y", "@chatwoot/mcp-server-chatwoot"]
env = { CHATWOOT_BASE_URL = "https://app.chatwoot.com", CHATWOOT_API_TOKEN = "secret://vault/chatwoot_token" }
allowed_tools = ["chatwoot_list_conversations", "chatwoot_get_conversation", "chatwoot_create_message"]

Nine-channel inbox → one agent; draft replies through the ApprovalBroker (chatwoot_create_message ⚠ if you want human review before send). Provision: a Chatwoot API access token.

WooCommerce (official native MCP — dev preview)

Section titled “WooCommerce (official native MCP — dev preview)”
[[mcp.external]]
name = "woocommerce"
command = "npx"
args = ["-y", "@woocommerce/mcp-adapter"] # WordPress MCP Adapter
env = { WP_SITE_URL = "https://shop.example.com", WP_MCP_OAUTH_TOKEN = "secret://vault/woo_oauth" }
allowed_tools = ["wc_list_products", "wc_get_order", "wc_list_orders"]

Use OAuth 2.1 via the WordPress MCP Adapter — the legacy X-MCP-API-Key was deprecated 2026-06-23. Provision: the WP MCP Adapter plugin + an OAuth client.

DocuSeal (no server exists — build duduclaw-docuseal-mcp)

Section titled “DocuSeal (no server exists — build duduclaw-docuseal-mcp)”

No MCP server ships for DocuSeal yet; the path is to build a small one (REST + webhook: generate → send → webhook-complete) and mount it here, then contribute it upstream. Tracked in IMPL-PLAN §D as effort M.

Monica (personal PRM — thin MCP or IdentityProvider)

Section titled “Monica (personal PRM — thin MCP or IdentityProvider)”

No MCP server exists. Either a thin MCP over /api/contacts (birthdays, interaction history) or wire it as an IdentityProvider (see duduclaw-identity). Tracked in IMPL-PLAN §D.

  • Per-server call auditing to tool_calls.jsonl (currently internal tools are attributed; external mounts are logged at connect time).