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.
When to use this vs a native connector
Section titled “When to use this vs a native connector”- 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.
Configuration
Section titled “Configuration”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-defaultdenied_tools = ["chatwoot_delete_conversation"] # always removedRemote 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|chatallowed_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 headersallowed_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 |
Semantics & safety
Section titled “Semantics & safety”- 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/listfails, the registry degrades to internal-only rather than losing all tools. allowed_toolsis deny-by-default: an allowlist means unlisted tools are hidden. Combine withdenied_toolsto blocklist specific dangerous tools.- For write/irreversible tools (billing, deletes), also list them in the agent’s
[capabilities] approval_required_toolsso they route through the HITLApprovalBroker— the MCP Bridge controls visibility, approvals control execution.
Live-verification runbook
Section titled “Live-verification runbook”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:
- Pick a server you can run locally, e.g. the reference everything-server:
終端機視窗 # in a scratch dir, confirm it speaks MCP over stdionpx -y @modelcontextprotocol/server-everything - 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 - Start the gateway and send the agent a message that would use the
echotool. Confirm in the logs:external MCP server mountedwithserver=everything- the agent can call
echobut NOT the server’s other tools (allowlist).
- Temporarily point an
env://credential at an unset var and restart — confirm the server is skipped withexternal 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.
Resolving credentials with secret://
Section titled “Resolving credentials with secret://”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:
[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).
Recipes — common SaaS servers
Section titled “Recipes — common SaaS servers”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 packageenv = { GOOGLE_OAUTH_TOKEN = "secret://vault/google_oauth" }allowed_tools = ["gmail_search", "gmail_get_thread", "gmail_create_draft"] # read + draft onlydenied_tools = ["gmail_send"] # ⚠ keep send behind approval, not autoProvision: a Google Cloud OAuth app; run the OAuth flow to mint the token.
gmail_send ⚠ → approval_required_tools.
Plane (official plane-mcp-server, mature)
Section titled “Plane (official plane-mcp-server, mature)”[[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 nameenv = { 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 Adapterenv = { 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.
Roadmap
Section titled “Roadmap”- Per-server call auditing to
tool_calls.jsonl(currently internal tools are attributed; external mounts are logged at connect time).