Skip to main content
Glama

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 an upstream MCP server at a URL. Every upstream call runs in a standalone activity.

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 -->|"standalone activities"| U["Upstream MCP server<br>at a URL"]

Callers use the same connector and in-Workflow client for both. The tool name is the Nexus operation name in both cases.

Components

Directory

Component

Serves

src/connector/

Connector. A Go library and a binary. MCP server over stdio or Streamable HTTP, stateless or stateful.

Non-Temporal callers

src/in_workflow_client/python/

in_workflow_client: calls the tools from Workflow code. No AI SDK types.

Temporal callers

src/authoring/python/

nexus_backed_mcp: exposes Nexus operations as MCP tools. nexus_proxy_mcp: fronts an upstream MCP server with a Nexus service.

Tool authors

examples/

A Nexus-backed MCP server, a Nexus proxy MCP server, non-Temporal callers (OpenAI Agents SDK, Pydantic AI, LangChain, Anthropic SDK), and a Temporal caller

Related MCP server: SelfHeal MCP

Requirements

  • Go 1.26 or later

  • Python 3.11 or later and uv

  • just

  • Temporal CLI 1.9.1 or later. The dev server must enable standalone Nexus operations, standalone activities, and activity callbacks. just temporal in examples/ does this.

Build and test

uv sync
(cd examples && just build)   # builds bin/durable-mcp-connector
(cd tests && just test)       # Go and Python unit tests

Inbound: author a tool service

Keep the nexusrpc decorators. Add the MCP decorators below them.

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")
    @nexusrpc.handler.sync_operation
    async def lookup(self, ctx, input: LookupInput) -> str:
        """Short tool. A sync Nexus operation."""
        ...

    @nexus_mcp.tool(title="Run a report", schedule_to_close_timeout=timedelta(minutes=30))
    @temporalio.nexus.workflow_run_operation
    async def run_report(self, ctx, input: ReportInput) -> temporalio.nexus.WorkflowHandle[Report]:
        """Long tool. ReportWorkflow does the work."""
        return await ctx.start_workflow(ReportWorkflow.run, input, id=f"report-{ctx.request_id}")

Decorator

Put it

Does

@nexus_mcp.service

Below @nexusrpc.service

Adds the list_tools operation to the service definition

@nexus_mcp.service_handler

Below @nexusrpc.handler.service_handler

Adds the list_tools implementation to the handler

@nexus_mcp.tool(...)

Above a sync or async Nexus operation decorator

Marks the operation as a tool. Sets title, annotations, metadata, and schedule_to_close_timeout.

@nexus_mcp.exclude

Above a Nexus operation decorator

Keeps the operation out of the tools of an expose="all" handler

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(name, client_factory) is a Worker plugin. It registers a Nexus service named name and the activities that call the upstream server. The upstream server needs no change.

from datetime import timedelta

from nexus_proxy_mcp import MCPProxyPlugin, ToolPolicy, http_client_factory
from temporalio.common import RetryPolicy

proxy = MCPProxyPlugin(
    "weather-tools",
    http_client_factory("https://mcp.example.com/mcp", headers={"Authorization": f"Bearer {token}"}),
    tool_policy_overrides={
        "get_weather": ToolPolicy(start_to_close_timeout=timedelta(seconds=3)),
        "get_forecast_report": ToolPolicy(retry_policy=RetryPolicy(maximum_attempts=3)),
    },
)
worker = Worker(client, task_queue="weather-proxy", plugins=[proxy])
  • client_factory returns a new MCP client for each upstream call. Upstream credentials go there, so they stay in the Worker process. Activity inputs and results do not carry them. http_client_factory(url, headers=..., auth=...) covers Streamable HTTP with headers or an httpx2.Auth, for example an MCP OAuth provider.

  • ToolPolicy holds the activity options: start_to_close_timeout, schedule_to_close_timeout, schedule_to_start_timeout, heartbeat_timeout, and retry_policy. They have the names and types of Client.start_activity, and the proxy passes them to the activity as they are. tool_policy sets the default. tool_policy_overrides sets the policy for named tools.

  • list_tools returns the upstream tool list, read live on each call.

  • Any other operation name is an upstream tool name. The proxy forwards the call.

  • Every tool call is an async Nexus operation. It runs in a standalone activity, and shows in temporal activity list.

Limits:

  • Python only. The proxy accepts any tool name through nexusrpc internals. A nexusrpc upgrade can break this. tests/test_proxy.py checks it.

  • The upstream server cannot see the identity of the MCP caller. It sees only the credentials of the client factory.

  • Non-text upstream content (images, resources) becomes a placeholder line.

See ARCHITECTURE.md.

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({"my-tools": "my-tools-endpoint"})
tools = await client.list_tools()
result = await client.call_tool("lookup", {"key": "a"})

To give the tools to an AI SDK agent, wrap the client in the MCP server shape of that SDK. examples/mcp_clients/temporal_agent.py has a wrapper for the OpenAI Agents SDK.

From any MCP host, add the connector as a stdio MCP server:

{
  "mcpServers": {
    "my-tools": {
      "command": "durable-mcp-connector",
      "args": ["--service", "my-tools=my-tools-endpoint"],
      "env": {"TEMPORAL_ADDRESS": "localhost:7233", "TEMPORAL_NAMESPACE": "default"}
    }
  }
}

Connector flags

Flag

Default

Meaning

--service SERVICE=ENDPOINT

none

Nexus service and endpoint. Repeat for more services.

--transport

stdio

stdio or http (Streamable HTTP)

--addr

127.0.0.1:8080

Listen address for http

--stateful

false

Legacy. Keep MCP sessions and send the session ID to the Nexus handler. stdio: one session per connector process. http: one session per Mcp-Session-Id, ended after 30 idle minutes; needs sticky routing with more than one replica, and clients cannot use MCP 2026-07-28.

--wait-budget

30s

Longest time a tool call waits for a result before it returns running

--codec-endpoint

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.

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.

Use the connector as a Go library

The packages resolver, sano, and server in src/connector/ are public. Build the Temporal client yourself, so you control credentials, namespace, and the data converter. Then serve the MCP server with your own transport and middleware, for example auth. See server/example_test.go.

Examples

See examples.

Related MCP Connectors

Related MCP Servers