Durable MCP Connector
Connects MCP clients and Temporal Workflows to Temporal Nexus services: it exposes Nexus operations as MCP tools (inbound) and proxies upstream MCP servers behind a Nexus service so that every upstream call runs in a standalone Temporal activity (outbound). Tool names map to Nexus operation names, with support for both sync Nexus operations and asynchronous workflow-run operations.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Durable MCP Connectorget the weather in San Francisco"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Durable MCP Connector
MCP connector for Temporal Nexus. The MCP client can be a Temporal Workflow or any other MCP client.
Inbound: the MCP server is a Nexus service.
Outbound: a Nexus proxy fronts upstream MCP servers over HTTP or stdio. Every tool call runs in a standalone Activity. Discovery runs outside Activities.
This repository is a prototype. See ARCHITECTURE.md for the design.
flowchart LR
C["MCP client<br>not on Temporal"] -->|"MCP"| K["Connector"]
W["Agent Workflow"] --> A["in_workflow_client"]
K -->|"Nexus"| S["Inbound:<br>Nexus service with MCP tools"]
A -->|"Nexus"| S
K -->|"Nexus"| P["Outbound:<br>Nexus proxy"]
A -->|"Nexus"| P
P -->|"tool calls: standalone Activities"| U["Upstream MCP servers<br>HTTP or stdio"]
D["Background discovery"] -->|"tools/list"| U
D -->|"tool snapshot"| PCallers use the same connector and in-Workflow client for both. The tool name is the Nexus operation name in both cases.
Components
Directory | Feature | Languages |
| MCP server over HTTP or stdio. A library and binary for callers outside Temporal. | |
| List and call tools from Workflow code through Nexus. | |
| Expose native Nexus operations as MCP tools. | |
| Worker plugins that front upstream MCP servers. Tool calls run in standalone Activities. |
Implementation code lives under src/. Go uses one module at the repository root.
Python uses a uv workspace with one package per feature. examples/ holds runnable
hosts and callers. tests/ holds cross-component integration tests. Unit tests
stay with each implementation.
Related MCP server: SelfHeal MCP
Requirements
Go 1.26 or later
Python 3.11 or later and uv
Temporal CLI 1.9.1 or later. The dev server must enable standalone Nexus operations, standalone activities, and activity callbacks.
just temporaldoes this.
The prototype pins Temporal and Nexus RPC SDK fork commits for dynamic service
composition. Python pins are in pyproject.toml and uv.lock; Go replacements
are in go.mod. Imports keep their normal SDK package names.
The first Python install builds the Temporal Rust bridge and requires Rust and
Protocol Buffers (protoc).
Build and test
uv sync
just build # builds the connector and Go examples
just test # Go and Python unit tests
just fmt # format Go source and test filesFor integration tests, run just temporal in a separate terminal, then
just test-integration. Use just test-all to run unit and integration tests
together. See tests/README.md.
Author a tool service
Go authors use authoring.NewService and authoring.RegisterTool with native
Nexus operations. See the Go guide and
tool Worker.
Run just nexus-backed-go from examples/ in place of the Python tool Worker.
Keep the nexusrpc decorators. Add the MCP decorators below them. Handlers use Temporal's unified operation API in both languages.
from datetime import timedelta
import nexus_backed_mcp as nexus_mcp
import nexusrpc
import nexusrpc.handler
import temporalio.nexus
@nexusrpc.service(name="my-tools")
@nexus_mcp.service # declares list_tools
class MyService:
lookup: nexusrpc.Operation[LookupInput, str]
run_report: nexusrpc.Operation[ReportInput, Report]
@nexusrpc.handler.service_handler(service=MyService)
@nexus_mcp.service_handler # implements list_tools
class MyTools:
@nexus_mcp.tool(title="Look up a key")
@temporalio.nexus.temporal_operation
async def lookup(
self,
ctx: temporalio.nexus.TemporalStartOperationContext,
client: temporalio.nexus.TemporalNexusClient,
input: LookupInput,
) -> temporalio.nexus.TemporalOperationResult[str]:
"""Short tool. A sync Nexus operation."""
return temporalio.nexus.TemporalOperationResult.sync("Found a value.")
@nexus_mcp.tool(title="Run a report", schedule_to_close_timeout=timedelta(minutes=30))
@temporalio.nexus.temporal_operation
async def run_report(
self,
ctx: temporalio.nexus.TemporalStartOperationContext,
client: temporalio.nexus.TemporalNexusClient,
input: ReportInput,
) -> temporalio.nexus.TemporalOperationResult[Report]:
"""Long tool. ReportWorkflow does the work."""
return await client.start_workflow(ReportWorkflow.run, input, id=f"report-{ctx.request_id}")Decorator | Put it | Does |
| Below | Adds the |
| Below | Adds the |
| Above | Marks the operation as a tool. Sets title, annotations, metadata, and |
| Above | Keeps the operation out of the tools of an |
Tools are opt-in by default. An operation without @nexus_mcp.tool is not a tool. With
@nexus_mcp.service_handler(expose="all"), every operation is a tool, except those with
@nexus_mcp.exclude. The tool name is the Nexus operation name. The description
defaults to the method docstring.
schedule_to_close_timeout bounds each call of the tool. The connector and the
in-Workflow client set it on the Nexus operation.
Outbound: front an upstream MCP server
MCPProxyPlugin(upstreams=[...]) registers one Nexus service per upstream. Services
can share a Worker and endpoint. The upstream servers need no changes.
Go provides the same proxy through outbound.NewPlugin([]outbound.Upstream).
See the Go library guide and
example Worker.
Run just nexus-proxy-go from examples/ to use it instead of the Python Worker.
from datetime import timedelta
from nexus_proxy_mcp import MCPProxyPlugin, Upstream, http_client_factory
from temporalio.common import RetryPolicy
from temporalio.worker import Worker
proxy = MCPProxyPlugin(upstreams=[
Upstream(
service="weather-tools",
client_factory=http_client_factory("https://mcp.example.com/mcp",
headers={"Authorization": f"Bearer {token}"}),
activity_options_overrides={
"get_weather": {"start_to_close_timeout": timedelta(seconds=3)},
"get_forecast_report": {
"start_to_close_timeout": timedelta(hours=48),
"retry_policy": RetryPolicy(maximum_attempts=3),
},
},
),
])
worker = Worker(client, task_queue="weather-proxy", plugins=[proxy])Each
Upstreamtakes a factory for an official MCP SDK client. The factory owns HTTP or stdio, authentication, and cleanup. Credentials stay in the Worker.Native SAA options go on
Upstream.activity_optionsor inactivity_options_overrides[tool_name]. Only supplied keys change. Priority, fairness, search attributes, summary, and start delay use SDK types.The proxy refreshes each tool catalog in the background.
list_toolsreturns its latest usable snapshot. Discovery does not schedule Activities.The default attempt timeout is one minute. Read-only and other eligible tools use the default Temporal retry policy, with no attempt cap. Explicit destructive, non-idempotent tools get one attempt. An explicit retry policy replaces inference.
Existing calls keep their recorded inputs/options after discovery changes. A retried Nexus start reattaches before reading discovery.
Tool results keep their text, images, resources, structured content, metadata, and
isErrorflag. Transport failures use stable Temporal error types.
See Activity defaults, error handling, and the proxy example, which includes HTTP and stdio factories in one plugin.
Call the tools
From Workflow code, use InWorkflowClient. It returns MCP tool definitions and MCP tool
results:
from in_workflow_client import InWorkflowClient
client = InWorkflowClient(service="my-tools", endpoint="my-tools-endpoint")
tools = await client.list_tools()
result = await client.call_tool("lookup", {"key": "a"})Go Workflows use workflowtransport.NewClient, ListTools(ctx), and
CallTool(ctx, name, arguments), with workflow.Context. See the
Go guide and
caller Worker.
Run just workflow-caller-go from examples/ for a call without an LLM.
Use in_workflow_client.openai_agents.WorkflowNexusMCPServer(service=..., endpoint=...)
for the OpenAI Agents SDK. The core client has no AI SDK dependency.
temporal_agent.py uses this binding.
From any MCP host, add the connector as a stdio MCP server:
{
"mcpServers": {
"my-tools": {
"command": "durable-mcp-connector",
"args": ["--route", "{service: my-tools, endpoint: my-tools-endpoint}"],
"env": {"TEMPORAL_ADDRESS": "localhost:7233", "TEMPORAL_NAMESPACE": "default"}
}
}
}Connector flags
Flag | Default | Meaning |
| none | One complete Nexus service/endpoint route. Required; repeat for more routes. |
|
|
|
|
| Listen address for |
|
| Initial wait before returning a task to a task-capable caller. Other callers wait until completion or recognized cancellation. |
| none | Logical deployment name, shared by replicas and included in operation IDs. |
| generated | Override the prefix before the random request ID. It is for naming and visibility; task access does not depend on it. |
| none | URL of a remote codec server. Set it when the Nexus handler encodes payloads, for example to encrypt them. The codec must match the codec of the handler. |
| none | YAML file with inbound OAuth resource-server settings. Requires |
The connector reads Temporal connection settings from the environment and from a
temporal.toml profile: TEMPORAL_ADDRESS, TEMPORAL_NAMESPACE, TEMPORAL_PROFILE,
TEMPORAL_CONFIG_FILE, and related variables. Set the caller namespace with
TEMPORAL_NAMESPACE.
Each --route has named service and endpoint fields. Repeat the flag to expose
multiple services. Quote each object in the shell. JSON objects also work.
Inbound HTTP authentication
Use --auth-config to require access tokens from your identity provider:
# Generate once. Store this secret and load the same value on every replica.
export MCP_TASK_SIGNING_KEY="$(openssl rand -base64 32)"
bin/durable-mcp-connector --transport http --addr 127.0.0.1:8080 \
--route '{service: my-tools, endpoint: my-tools-endpoint}' \
--auth-config examples/inbound_auth.yamlSet the public resource URL, trusted issuer, and API audience in
inbound_auth.yaml. Configure the identity provider
to issue access tokens for that audience and the required scopes. Terminate HTTPS
at your reverse proxy. MCP is served at the path in auth.resource, /mcp in the
example. The metadata endpoint is public.
The MCP client handles login and token refresh. The connector checks each request's token and keeps it out of Temporal calls and history. Tasks are scoped to the verified issuer and subject. Token renewal and another replica keep the same scope. All callers with valid tokens and the required scopes can use the configured tools. Hosts can add MCP middleware for finer permissions.
Each task ID contains a signed reference to the Temporal namespace, operation ID, run ID, and caller hash. The client retains this string and sends it back to poll or cancel. The connector verifies its signature and owner before accessing Temporal. It keeps no task-to-owner table. The JSON inside the task ID is readable; its HMAC signature prevents changes. The caller hash and JWT are not sent to Temporal.
HTTP requires MCP_TASK_SIGNING_KEY: at least 32 random bytes encoded as base64.
Share the key and namespace across replicas and keep them across restarts.
Replacing the key invalidates existing task IDs. This key is separate from the
identity provider's signing keys. For local stdio examples, an unset variable
selects a generated process-local key; set it for cross-process task access.
Without --auth-config, the existing HTTP and stdio modes keep their behavior.
See the sign-in flow and
library options.
Use the connector as a Go library
Call inbound.NewServer with the Temporal client, matching Namespace, service
routes, and TaskSigningKey (at least 32 random bytes).
The result is a standard *mcp.Server. The host supplies credentials, namespace,
TLS, and the data converter or codec on the Temporal client. The host then serves
HTTP or stdio and supplies its own middleware. For built-in HTTP OAuth, use
inbound.NewHTTPHandler(server, inbound.HTTPOptions{Auth: authConfig}).
Auth configuration uses inboundauth.Config from src/inbound/go/auth.
See the HTTP middleware example.
For HTTP, use NewHTTPHandler to support current and legacy clients. Current
requests are stateless; closing the response stream cancels that call. Older
HTTP clients use temporary SDK sessions. Route each session to the same replica
so explicit cancellation notifications reach the active call. Stdio clients also
use explicit MCP cancellation.
After a task is returned, use tasks/cancel.
Examples
See examples.
This server cannot be deployed
Maintenance
Related MCP Connectors
Model Context Protocol server for the Apideck Unified API. Connect any MCP-compatible agent framework to 100+ accounting systems, HRIS platforms, file storage providers, and more through one integration. More information https://www.apideck.com/mcp-server
Stateless MCP gateway and OTel span-streaming bridge for hosted MCP servers.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
MCP server for progressive tool usage at any scale (see https://klavis.ai)
Related MCP Servers
- AlicenseNot gradedqualityFmaintenanceAn MCP server that proxies tool calls to a running SentinelX Core instance with OIDC/OAuth authentication. It enables secure integration of SentinelX Core's HTTP agent capabilities through the MCP protocol.1MIT
- AlicenseAqualityDmaintenanceSelf-healing proxy for MCP servers that wraps tool calls with automatic retry, circuit breaker protection, and observability.522 npmMIT
- FlicenseAqualityDmaintenanceUniversal MCP proxy server that discovers, searches, and executes tools across all configured MCP servers from a single entry point.7-
- AlicenseAqualityDmaintenanceAn MCP server for interacting with Temporal Cloud workflows across multiple regions, providing tools to list, describe, terminate, and retrieve step results from workflows via natural language.71MIT