MCP Server Connection¶
The MCP connection type configures access to MCP (Model Context Protocol) servers. Three transport types are supported: Streamable HTTP, SSE, and stdio.
Default Connection IDs¶
The MCPHook uses mcp_default by default.
Configuring the Connection¶
- Transport (Extra field)
The transport type:
http(default),sse, orstdio.http: Streamable HTTP — the recommended transport for remote servers.sse: Server-Sent Events — deprecated in favor of Streamable HTTP.stdio: Run the MCP server as a subprocess communicating over stdin/stdout.
- Host
The server URL. Required for
httpandssetransports.Examples:
http://localhost:3001/mcp,https://mcp.example.com/v1- Auth Token (Password field)
Optional authentication token for the MCP server. Sent as a static
Authorization: Bearer <token>header on HTTP/SSE transports. For short-lived or minted tokens, use atoken_providerinstead (see below).- Command (Extra field)
The command to run for
stdiotransport. Required when transport isstdio.Examples:
uvx,python,node- Arguments (Extra field)
JSON array of arguments for the stdio command.
Examples:
["mcp-run-python"],["-m", "my_mcp_server"]- Environment (Extra field)
JSON object of environment variables for the
stdiosubprocess. Ignored forhttp/sse. For a secret that lives in a different connection or is minted fresh per call, use anenv_providerinstead of storing it here (see below).Examples:
{"MY_SERVER_MODE": "readonly"}
Examples¶
HTTP transport (remote MCP server)
{
"conn_type": "mcp",
"host": "http://localhost:3001/mcp"
}
SSE transport
{
"conn_type": "mcp",
"host": "http://localhost:3001/sse",
"extra": "{\"transport\": \"sse\"}"
}
Stdio transport (subprocess)
{
"conn_type": "mcp",
"extra": "{\"transport\": \"stdio\", \"command\": \"uvx\", \"args\": [\"mcp-run-python\"]}"
}
Stdio with custom timeout
{
"conn_type": "mcp",
"extra": "{\"transport\": \"stdio\", \"command\": \"python\", \"args\": [\"-m\", \"my_server\"], \"timeout\": 30}"
}
Stdio with subprocess environment variables
{
"conn_type": "mcp",
"extra": "{\"transport\": \"stdio\", \"command\": \"my-mcp-server\", \"args\": [], \"env\": {\"SERVER_MODE\": \"readonly\"}}"
}
Short-lived or minted tokens¶
Some MCP endpoints require a freshly minted, short-lived token rather than a
static one. For example, Snowflake managed MCP servers
are best authenticated with a key-pair JWT: the private key never
leaves your environment and the signed JWT expires after about an hour, so it
cannot be stored as a static connection password. The same applies to OAuth /
refresh tokens, Workload Identity Federation, and GitHub App installation tokens.
For these, pass a token_provider callable to MCPHook or MCPToolset
instead of a static token. It is called once, the first time a given hook or
toolset instance establishes a connection (the result is then cached for that
instance’s lifetime), and its return value is used as the bearer token, so a
fresh token is minted (and registered with secret masking so it does not leak
into task logs) without ever being written to the connection:
from airflow.providers.common.ai.toolsets.mcp import MCPToolset
def mint_snowflake_jwt() -> str:
# Sign a short-lived JWT from the Snowflake connection's key-pair.
...
toolset = MCPToolset(
mcp_conn_id="snowflake_managed_mcp",
token_provider=mint_snowflake_jwt,
)
token_provider is resolved in DAG code (it is a Python callable, not a stored
connection field), so the signing key stays in your environment and is never baked
into the serialized DAG.
Secrets in stdio subprocess environments¶
The stdio transport runs the MCP server as a local subprocess, and many such
servers read credentials from their own environment rather than accepting them
as arguments – for example, a server that reaches Splunk needs a Splunk API key
in SPLUNK_API_KEY. Extra.env (like the rest of extra) is Fernet-encrypted
at rest, the same as password, so it is a fine place for a static value that
genuinely belongs to this connection.
Use env_provider instead when the credential has no stable static form to
store here at all – the same situation token_provider exists for on
HTTP/SSE:
It lives in a different connection. A server that reaches Splunk needs a Splunk credential, not a credential for the MCP server itself; duplicating it into this connection’s
Extra.envmeans two places to rotate and a real chance the copies drift.It’s minted fresh per call – an OAuth token, a Vault lease, an STS-assumed role – so there is no fixed value to store anywhere, on this connection or any other.
env_provider is called once, the first time a given hook or toolset instance
establishes a connection (the result is then cached for that instance’s
lifetime). Its return value is merged over Extra.env (env_provider keys
win on conflicts), and – as a secondary benefit – every value it returns is
explicitly registered with secret masking regardless of key name, unlike
Extra.env, which is only masked in the Connections UI/API and task logs if
the key name happens to match a fixed set of sensitive-looking names
(api_key, token, secret, password, etc.).
The example below covers the first case – the Splunk credential is already managed as its own Airflow connection:
def _mint_splunk_env() -> dict[str, str]:
from airflow.providers.common.compat.sdk import BaseHook
password = BaseHook.get_connection("splunk_default").password
if not password:
raise ValueError("splunk_default connection has no password set")
return {"SPLUNK_API_KEY": password}
@dag(tags=["example"])
def example_mcp_stdio_env_provider():
"""Use a stdio MCP server whose subprocess needs a secret from another connection."""
AgentOperator(
task_id="stdio_env_agent",
prompt="Investigate the ticket and summarize findings.",
llm_conn_id="pydanticai_default",
system_prompt="You are a support triage agent with access to MCP tools.",
toolsets=[
MCPToolset(mcp_conn_id="spacefarer_mcp", env_provider=_mint_splunk_env),
],
)
Like token_provider, env_provider is resolved in DAG code, so the secret is
fetched at task-execution time and never baked into the serialized DAG.