Skip to content

Commit f3acf3f

Browse files
committed
Add docstrings
1 parent 0fd7886 commit f3acf3f

File tree

3 files changed

+238
-9
lines changed

3 files changed

+238
-9
lines changed

temporalio/contrib/openai_agents/_mcp.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,26 @@
2323

2424

2525
class StatelessTemporalMCPServer(MCPServer):
26+
"""A stateless MCP server implementation for Temporal workflows.
27+
28+
This class wraps an MCP server to make it stateless by executing each MCP operation
29+
as a separate Temporal activity. Each operation (list_tools, call_tool, etc.) will
30+
connect to the underlying server, execute the operation, and then clean up the connection.
31+
32+
This approach is suitable for simple use cases where connection overhead is acceptable
33+
and you don't need to maintain state between operations.
34+
"""
35+
2636
def __init__(
2737
self, server: Union[MCPServer, str], config: Optional[ActivityConfig] = None
2838
):
39+
"""Initialize the stateless temporal MCP server.
40+
41+
Args:
42+
server: Either an MCPServer instance or a string name for the server.
43+
config: Optional activity configuration for Temporal activities. Defaults to
44+
1-minute start-to-close timeout if not provided.
45+
"""
2946
self.server = server if isinstance(server, MCPServer) else None
3047
self._name = (server if isinstance(server, str) else server.name) + "-stateless"
3148
self.config = config or ActivityConfig(
@@ -35,19 +52,49 @@ def __init__(
3552

3653
@property
3754
def name(self) -> str:
55+
"""Get the server name with '-stateless' suffix.
56+
57+
Returns:
58+
The server name with '-stateless' appended.
59+
"""
3860
return self._name
3961

4062
async def connect(self) -> None:
63+
"""Connect to the MCP server.
64+
65+
For stateless servers, this is a no-op since connections are made
66+
on a per-operation basis.
67+
"""
4168
pass
4269

4370
async def cleanup(self) -> None:
71+
"""Clean up the MCP server connection.
72+
73+
For stateless servers, this is a no-op since connections are cleaned
74+
up after each operation.
75+
"""
4476
pass
4577

4678
async def list_tools(
4779
self,
4880
run_context: Optional[RunContextWrapper[Any]] = None,
4981
agent: Optional[AgentBase] = None,
5082
) -> list[MCPTool]:
83+
"""List available tools from the MCP server.
84+
85+
This method executes a Temporal activity to connect to the MCP server,
86+
retrieve the list of available tools, and clean up the connection.
87+
88+
Args:
89+
run_context: Optional run context wrapper (unused in stateless mode).
90+
agent: Optional agent base (unused in stateless mode).
91+
92+
Returns:
93+
A list of available MCP tools.
94+
95+
Raises:
96+
ActivityError: If the underlying Temporal activity fails.
97+
"""
5198
return await workflow.execute_activity(
5299
self.name + "-list-tools",
53100
args=[],
@@ -58,6 +105,21 @@ async def list_tools(
58105
async def call_tool(
59106
self, tool_name: str, arguments: Optional[dict[str, Any]]
60107
) -> CallToolResult:
108+
"""Call a specific tool on the MCP server.
109+
110+
This method executes a Temporal activity to connect to the MCP server,
111+
call the specified tool with the given arguments, and clean up the connection.
112+
113+
Args:
114+
tool_name: The name of the tool to call.
115+
arguments: Optional dictionary of arguments to pass to the tool.
116+
117+
Returns:
118+
The result of the tool call.
119+
120+
Raises:
121+
ActivityError: If the underlying Temporal activity fails.
122+
"""
61123
return await workflow.execute_activity(
62124
self.name + "-call-tool",
63125
args=[tool_name, arguments],
@@ -66,6 +128,17 @@ async def call_tool(
66128
)
67129

68130
async def list_prompts(self) -> ListPromptsResult:
131+
"""List available prompts from the MCP server.
132+
133+
This method executes a Temporal activity to connect to the MCP server,
134+
retrieve the list of available prompts, and clean up the connection.
135+
136+
Returns:
137+
A list of available prompts.
138+
139+
Raises:
140+
ActivityError: If the underlying Temporal activity fails.
141+
"""
69142
return await workflow.execute_activity(
70143
self.name + "-list-prompts",
71144
args=[],
@@ -76,6 +149,21 @@ async def list_prompts(self) -> ListPromptsResult:
76149
async def get_prompt(
77150
self, name: str, arguments: Optional[dict[str, Any]] = None
78151
) -> GetPromptResult:
152+
"""Get a specific prompt from the MCP server.
153+
154+
This method executes a Temporal activity to connect to the MCP server,
155+
retrieve the specified prompt with optional arguments, and clean up the connection.
156+
157+
Args:
158+
name: The name of the prompt to retrieve.
159+
arguments: Optional dictionary of arguments for the prompt.
160+
161+
Returns:
162+
The prompt result.
163+
164+
Raises:
165+
ActivityError: If the underlying Temporal activity fails.
166+
"""
79167
return await workflow.execute_activity(
80168
self.name + "-get-prompt",
81169
args=[name, arguments],
@@ -84,6 +172,17 @@ async def get_prompt(
84172
)
85173

86174
def get_activities(self) -> Sequence[Callable]:
175+
"""Get the Temporal activities for this MCP server.
176+
177+
Creates and returns the Temporal activity functions that handle MCP operations.
178+
Each activity manages its own connection lifecycle (connect -> operate -> cleanup).
179+
180+
Returns:
181+
A sequence of Temporal activity functions.
182+
183+
Raises:
184+
ValueError: If no MCP server instance was provided during initialization.
185+
"""
87186
server = self.server
88187
if server is None:
89188
raise ValueError(
@@ -130,12 +229,35 @@ async def get_prompt(
130229

131230

132231
class StatefulTemporalMCPServer(MCPServer):
232+
"""A stateful MCP server implementation for Temporal workflows.
233+
234+
This class wraps an MCP server to maintain a persistent connection throughout
235+
the workflow execution. It creates a dedicated worker that stays connected to
236+
the MCP server and processes operations on a dedicated task queue.
237+
238+
This approach is more efficient for workflows that make multiple MCP calls,
239+
as it avoids connection overhead, but requires more resources to maintain
240+
the persistent connection and worker.
241+
242+
The caller will have to handle cases where the dedicated worker fails, as Temporal is
243+
unable to seamlessly recreate any lost state in that case.
244+
"""
245+
133246
def __init__(
134247
self,
135248
server: Union[MCPServer, str],
136249
config: Optional[ActivityConfig] = None,
137250
connect_config: Optional[ActivityConfig] = None,
138251
):
252+
"""Initialize the stateful temporal MCP server.
253+
254+
Args:
255+
server: Either an MCPServer instance or a string name for the server.
256+
config: Optional activity configuration for MCP operation activities.
257+
Defaults to 1-minute start-to-close and 30-second schedule-to-start timeouts.
258+
connect_config: Optional activity configuration for the connection activity.
259+
Defaults to 1-hour start-to-close timeout.
260+
"""
139261
self.server = server if isinstance(server, MCPServer) else None
140262
self._name = (server if isinstance(server, str) else server.name) + "-stateful"
141263
self.config = config or ActivityConfig(
@@ -150,9 +272,20 @@ def __init__(
150272

151273
@property
152274
def name(self) -> str:
275+
"""Get the server name with '-stateful' suffix.
276+
277+
Returns:
278+
The server name with '-stateful' appended.
279+
"""
153280
return self._name
154281

155282
async def connect(self) -> None:
283+
"""Connect to the MCP server and start the dedicated worker.
284+
285+
This method creates a dedicated task queue for this workflow and starts
286+
a long-running activity that maintains the connection and runs a worker
287+
to handle MCP operations.
288+
"""
156289
self.config["task_queue"] = workflow.info().workflow_id + "-" + self.name
157290
self._connect_handle = workflow.start_activity(
158291
self.name + "-connect",
@@ -161,21 +294,55 @@ async def connect(self) -> None:
161294
)
162295

163296
async def cleanup(self) -> None:
297+
"""Clean up the MCP server connection.
298+
299+
This method cancels the long-running connection activity, which will
300+
cause the dedicated worker to shut down and the MCP server connection
301+
to be closed.
302+
"""
164303
if self._connect_handle:
165304
self._connect_handle.cancel()
166305

167306
async def __aenter__(self):
307+
"""Async context manager entry point.
308+
309+
Returns:
310+
This server instance after connecting.
311+
"""
168312
await self.connect()
169313
return self
170314

171315
async def __aexit__(self, exc_type, exc_value, traceback):
316+
"""Async context manager exit point.
317+
318+
Args:
319+
exc_type: Exception type if an exception occurred.
320+
exc_value: Exception value if an exception occurred.
321+
traceback: Exception traceback if an exception occurred.
322+
"""
172323
await self.cleanup()
173324

174325
async def list_tools(
175326
self,
176327
run_context: Optional[RunContextWrapper[Any]] = None,
177328
agent: Optional[AgentBase] = None,
178329
) -> list[MCPTool]:
330+
"""List available tools from the MCP server.
331+
332+
This method executes a Temporal activity on the dedicated task queue
333+
to retrieve the list of available tools from the persistent MCP connection.
334+
335+
Args:
336+
run_context: Optional run context wrapper (unused in stateful mode).
337+
agent: Optional agent base (unused in stateful mode).
338+
339+
Returns:
340+
A list of available MCP tools.
341+
342+
Raises:
343+
ApplicationError: If the MCP worker fails to schedule or heartbeat.
344+
ActivityError: If the underlying Temporal activity fails.
345+
"""
179346
try:
180347
logger.info("Executing list-tools: %s", self.config)
181348
return await workflow.execute_activity(
@@ -208,6 +375,21 @@ async def list_tools(
208375
async def call_tool(
209376
self, tool_name: str, arguments: Optional[dict[str, Any]]
210377
) -> CallToolResult:
378+
"""Call a specific tool on the MCP server.
379+
380+
This method executes a Temporal activity on the dedicated task queue
381+
to call the specified tool using the persistent MCP connection.
382+
383+
Args:
384+
tool_name: The name of the tool to call.
385+
arguments: Optional dictionary of arguments to pass to the tool.
386+
387+
Returns:
388+
The result of the tool call.
389+
390+
Raises:
391+
ActivityError: If the underlying Temporal activity fails.
392+
"""
211393
return await workflow.execute_activity(
212394
self.name + "-call-tool",
213395
args=[tool_name, arguments],
@@ -216,6 +398,17 @@ async def call_tool(
216398
)
217399

218400
async def list_prompts(self) -> ListPromptsResult:
401+
"""List available prompts from the MCP server.
402+
403+
This method executes a Temporal activity on the dedicated task queue
404+
to retrieve the list of available prompts from the persistent MCP connection.
405+
406+
Returns:
407+
A list of available prompts.
408+
409+
Raises:
410+
ActivityError: If the underlying Temporal activity fails.
411+
"""
219412
return await workflow.execute_activity(
220413
self.name + "-list-prompts",
221414
args=[],
@@ -226,6 +419,21 @@ async def list_prompts(self) -> ListPromptsResult:
226419
async def get_prompt(
227420
self, name: str, arguments: Optional[dict[str, Any]] = None
228421
) -> GetPromptResult:
422+
"""Get a specific prompt from the MCP server.
423+
424+
This method executes a Temporal activity on the dedicated task queue
425+
to retrieve the specified prompt using the persistent MCP connection.
426+
427+
Args:
428+
name: The name of the prompt to retrieve.
429+
arguments: Optional dictionary of arguments for the prompt.
430+
431+
Returns:
432+
The prompt result.
433+
434+
Raises:
435+
ActivityError: If the underlying Temporal activity fails.
436+
"""
229437
return await workflow.execute_activity(
230438
self.name + "-get-prompt",
231439
args=[name, arguments],
@@ -234,6 +442,18 @@ async def get_prompt(
234442
)
235443

236444
def get_activities(self) -> Sequence[Callable]:
445+
"""Get the Temporal activities for this stateful MCP server.
446+
447+
Creates and returns the Temporal activity functions that handle MCP operations
448+
and connection management. This includes a long-running connect activity that
449+
maintains the MCP connection and runs a dedicated worker.
450+
451+
Returns:
452+
A sequence containing the connect activity function.
453+
454+
Raises:
455+
ValueError: If no MCP server instance was provided during initialization.
456+
"""
237457
server = self.server
238458
if server is None:
239459
raise ValueError(

0 commit comments

Comments
 (0)