letta-nightly 0.6.40.dev20250314173529__py3-none-any.whl → 0.6.40.dev20250314222759__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

letta/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.39"
1
+ __version__ = "0.6.40"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
letta/agent.py CHANGED
@@ -22,11 +22,11 @@ from letta.errors import ContextWindowExceededError
22
22
  from letta.functions.ast_parsers import coerce_dict_args_by_annotations, get_function_annotations_from_source
23
23
  from letta.functions.functions import get_function_from_module
24
24
  from letta.functions.helpers import execute_composio_action, generate_composio_action_from_func_name
25
+ from letta.functions.mcp_client.base_client import BaseMCPClient
25
26
  from letta.helpers import ToolRulesSolver
26
27
  from letta.helpers.composio_helpers import get_composio_api_key
27
28
  from letta.helpers.datetime_helpers import get_utc_time
28
29
  from letta.helpers.json_helpers import json_dumps, json_loads
29
- from letta.helpers.mcp_helpers import BaseMCPClient
30
30
  from letta.interface import AgentInterface
31
31
  from letta.llm_api.helpers import calculate_summarizer_cutoff, get_token_counts_for_messages, is_context_overflow_error
32
32
  from letta.llm_api.llm_api_tools import create
File without changes
@@ -0,0 +1,61 @@
1
+ import asyncio
2
+ from typing import List, Optional, Tuple
3
+
4
+ from mcp import ClientSession, Tool
5
+
6
+ from letta.functions.mcp_client.types import BaseServerConfig
7
+ from letta.log import get_logger
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class BaseMCPClient:
13
+ def __init__(self):
14
+ self.session: Optional[ClientSession] = None
15
+ self.stdio = None
16
+ self.write = None
17
+ self.initialized = False
18
+ self.loop = asyncio.new_event_loop()
19
+ self.cleanup_funcs = []
20
+
21
+ def connect_to_server(self, server_config: BaseServerConfig):
22
+ asyncio.set_event_loop(self.loop)
23
+ success = self._initialize_connection(server_config)
24
+
25
+ if success:
26
+ self.loop.run_until_complete(self.session.initialize())
27
+ self.initialized = True
28
+ else:
29
+ raise RuntimeError(
30
+ f"Connecting to MCP server failed. Please review your server config: {server_config.model_dump_json(indent=4)}"
31
+ )
32
+
33
+ def _initialize_connection(self, server_config: BaseServerConfig) -> bool:
34
+ raise NotImplementedError("Subclasses must implement _initialize_connection")
35
+
36
+ def list_tools(self) -> List[Tool]:
37
+ self._check_initialized()
38
+ response = self.loop.run_until_complete(self.session.list_tools())
39
+ return response.tools
40
+
41
+ def execute_tool(self, tool_name: str, tool_args: dict) -> Tuple[str, bool]:
42
+ self._check_initialized()
43
+ result = self.loop.run_until_complete(self.session.call_tool(tool_name, tool_args))
44
+ return str(result.content), result.isError
45
+
46
+ def _check_initialized(self):
47
+ if not self.initialized:
48
+ logger.error("MCPClient has not been initialized")
49
+ raise RuntimeError("MCPClient has not been initialized")
50
+
51
+ def cleanup(self):
52
+ try:
53
+ for cleanup_func in self.cleanup_funcs:
54
+ cleanup_func()
55
+ self.initialized = False
56
+ if not self.loop.is_closed():
57
+ self.loop.close()
58
+ except Exception as e:
59
+ logger.warning(e)
60
+ finally:
61
+ logger.info("Cleaned up MCP clients on shutdown.")
@@ -0,0 +1,21 @@
1
+ from mcp import ClientSession
2
+ from mcp.client.sse import sse_client
3
+
4
+ from letta.functions.mcp_client.base_client import BaseMCPClient
5
+ from letta.functions.mcp_client.types import SSEServerConfig
6
+
7
+ # see: https://modelcontextprotocol.io/quickstart/user
8
+ MCP_CONFIG_TOPLEVEL_KEY = "mcpServers"
9
+
10
+
11
+ class SSEMCPClient(BaseMCPClient):
12
+ def _initialize_connection(self, server_config: SSEServerConfig) -> bool:
13
+ sse_cm = sse_client(url=server_config.server_url)
14
+ sse_transport = self.loop.run_until_complete(sse_cm.__aenter__())
15
+ self.stdio, self.write = sse_transport
16
+ self.cleanup_funcs.append(lambda: self.loop.run_until_complete(sse_cm.__aexit__(None, None, None)))
17
+
18
+ session_cm = ClientSession(self.stdio, self.write)
19
+ self.session = self.loop.run_until_complete(session_cm.__aenter__())
20
+ self.cleanup_funcs.append(lambda: self.loop.run_until_complete(session_cm.__aexit__(None, None, None)))
21
+ return True
@@ -0,0 +1,103 @@
1
+ import sys
2
+ from contextlib import asynccontextmanager
3
+
4
+ import anyio
5
+ import anyio.lowlevel
6
+ import mcp.types as types
7
+ from anyio.streams.text import TextReceiveStream
8
+ from mcp import ClientSession, StdioServerParameters
9
+ from mcp.client.stdio import get_default_environment
10
+
11
+ from letta.functions.mcp_client.base_client import BaseMCPClient
12
+ from letta.functions.mcp_client.types import StdioServerConfig
13
+ from letta.log import get_logger
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ class StdioMCPClient(BaseMCPClient):
19
+ def _initialize_connection(self, server_config: StdioServerConfig) -> bool:
20
+ try:
21
+ server_params = StdioServerParameters(command=server_config.command, args=server_config.args)
22
+ stdio_cm = forked_stdio_client(server_params)
23
+ stdio_transport = self.loop.run_until_complete(stdio_cm.__aenter__())
24
+ self.stdio, self.write = stdio_transport
25
+ self.cleanup_funcs.append(lambda: self.loop.run_until_complete(stdio_cm.__aexit__(None, None, None)))
26
+
27
+ session_cm = ClientSession(self.stdio, self.write)
28
+ self.session = self.loop.run_until_complete(session_cm.__aenter__())
29
+ self.cleanup_funcs.append(lambda: self.loop.run_until_complete(session_cm.__aexit__(None, None, None)))
30
+ return True
31
+ except Exception:
32
+ return False
33
+
34
+
35
+ @asynccontextmanager
36
+ async def forked_stdio_client(server: StdioServerParameters):
37
+ """
38
+ Client transport for stdio: this will connect to a server by spawning a
39
+ process and communicating with it over stdin/stdout.
40
+ """
41
+ read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
42
+ write_stream, write_stream_reader = anyio.create_memory_object_stream(0)
43
+
44
+ try:
45
+ process = await anyio.open_process(
46
+ [server.command, *server.args],
47
+ env=server.env or get_default_environment(),
48
+ stderr=sys.stderr, # Consider logging stderr somewhere instead of silencing it
49
+ )
50
+ except OSError as exc:
51
+ raise RuntimeError(f"Failed to spawn process: {server.command} {server.args}") from exc
52
+
53
+ async def stdout_reader():
54
+ assert process.stdout, "Opened process is missing stdout"
55
+ buffer = ""
56
+ try:
57
+ async with read_stream_writer:
58
+ async for chunk in TextReceiveStream(
59
+ process.stdout,
60
+ encoding=server.encoding,
61
+ errors=server.encoding_error_handler,
62
+ ):
63
+ lines = (buffer + chunk).split("\n")
64
+ buffer = lines.pop()
65
+ for line in lines:
66
+ try:
67
+ message = types.JSONRPCMessage.model_validate_json(line)
68
+ except Exception as exc:
69
+ await read_stream_writer.send(exc)
70
+ continue
71
+ await read_stream_writer.send(message)
72
+ except anyio.ClosedResourceError:
73
+ await anyio.lowlevel.checkpoint()
74
+
75
+ async def stdin_writer():
76
+ assert process.stdin, "Opened process is missing stdin"
77
+ try:
78
+ async with write_stream_reader:
79
+ async for message in write_stream_reader:
80
+ json = message.model_dump_json(by_alias=True, exclude_none=True)
81
+ await process.stdin.send(
82
+ (json + "\n").encode(
83
+ encoding=server.encoding,
84
+ errors=server.encoding_error_handler,
85
+ )
86
+ )
87
+ except anyio.ClosedResourceError:
88
+ await anyio.lowlevel.checkpoint()
89
+
90
+ async def watch_process_exit():
91
+ returncode = await process.wait()
92
+ if returncode != 0:
93
+ raise RuntimeError(f"Subprocess exited with code {returncode}. Command: {server.command} {server.args}")
94
+
95
+ async with anyio.create_task_group() as tg, process:
96
+ tg.start_soon(stdout_reader)
97
+ tg.start_soon(stdin_writer)
98
+ tg.start_soon(watch_process_exit)
99
+
100
+ with anyio.move_on_after(0.2):
101
+ await anyio.sleep_forever()
102
+
103
+ yield read_stream, write_stream
@@ -0,0 +1,48 @@
1
+ from enum import Enum
2
+ from typing import List, Optional
3
+
4
+ from mcp import Tool
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class MCPTool(Tool):
9
+ """A simple wrapper around MCP's tool definition (to avoid conflict with our own)"""
10
+
11
+
12
+ class MCPServerType(str, Enum):
13
+ SSE = "sse"
14
+ STDIO = "stdio"
15
+
16
+
17
+ class BaseServerConfig(BaseModel):
18
+ server_name: str = Field(..., description="The name of the server")
19
+ type: MCPServerType
20
+
21
+
22
+ class SSEServerConfig(BaseServerConfig):
23
+ type: MCPServerType = MCPServerType.SSE
24
+ server_url: str = Field(..., description="The URL of the server (MCP SSE client will connect to this URL)")
25
+
26
+ def to_dict(self) -> dict:
27
+ values = {
28
+ "transport": "sse",
29
+ "url": self.server_url,
30
+ }
31
+ return values
32
+
33
+
34
+ class StdioServerConfig(BaseServerConfig):
35
+ type: MCPServerType = MCPServerType.STDIO
36
+ command: str = Field(..., description="The command to run (MCP 'local' client will run this command)")
37
+ args: List[str] = Field(..., description="The arguments to pass to the command")
38
+ env: Optional[dict[str, str]] = Field(None, description="Environment variables to set")
39
+
40
+ def to_dict(self) -> dict:
41
+ values = {
42
+ "transport": "stdio",
43
+ "command": self.command,
44
+ "args": self.args,
45
+ }
46
+ if self.env is not None:
47
+ values["env"] = self.env
48
+ return values
@@ -6,7 +6,7 @@ from composio.client.collections import ActionParametersModel
6
6
  from docstring_parser import parse
7
7
  from pydantic import BaseModel
8
8
 
9
- from letta.helpers.mcp_helpers import MCPTool
9
+ from letta.functions.mcp_client.types import MCPTool
10
10
 
11
11
 
12
12
  def is_optional(annotation):
letta/schemas/tool.py CHANGED
@@ -17,12 +17,12 @@ from letta.functions.helpers import (
17
17
  generate_mcp_tool_wrapper,
18
18
  generate_model_from_args_json_schema,
19
19
  )
20
+ from letta.functions.mcp_client.types import MCPTool
20
21
  from letta.functions.schema_generator import (
21
22
  generate_schema_from_args_schema_v2,
22
23
  generate_tool_schema_for_composio,
23
24
  generate_tool_schema_for_mcp,
24
25
  )
25
- from letta.helpers.mcp_helpers import MCPTool
26
26
  from letta.log import get_logger
27
27
  from letta.orm.enums import ToolType
28
28
  from letta.schemas.letta_base import LettaBase
@@ -12,8 +12,8 @@ from composio.exceptions import (
12
12
  from fastapi import APIRouter, Body, Depends, Header, HTTPException
13
13
 
14
14
  from letta.errors import LettaToolCreateError
15
+ from letta.functions.mcp_client.types import MCPTool, SSEServerConfig, StdioServerConfig
15
16
  from letta.helpers.composio_helpers import get_composio_api_key
16
- from letta.helpers.mcp_helpers import MCPTool, SSEServerConfig, StdioServerConfig
17
17
  from letta.log import get_logger
18
18
  from letta.orm.errors import UniqueConstraintViolationError
19
19
  from letta.schemas.letta_message import ToolReturnMessage
letta/server/server.py CHANGED
@@ -20,18 +20,12 @@ from letta.agent import Agent, save_agent
20
20
  from letta.config import LettaConfig
21
21
  from letta.data_sources.connectors import DataConnector, load_data
22
22
  from letta.dynamic_multi_agent import DynamicMultiAgent
23
+ from letta.functions.mcp_client.base_client import BaseMCPClient
24
+ from letta.functions.mcp_client.sse_client import MCP_CONFIG_TOPLEVEL_KEY, SSEMCPClient
25
+ from letta.functions.mcp_client.stdio_client import StdioMCPClient
26
+ from letta.functions.mcp_client.types import MCPServerType, MCPTool, SSEServerConfig, StdioServerConfig
23
27
  from letta.helpers.datetime_helpers import get_utc_time
24
28
  from letta.helpers.json_helpers import json_dumps, json_loads
25
- from letta.helpers.mcp_helpers import (
26
- MCP_CONFIG_TOPLEVEL_KEY,
27
- BaseMCPClient,
28
- MCPServerType,
29
- MCPTool,
30
- SSEMCPClient,
31
- SSEServerConfig,
32
- StdioMCPClient,
33
- StdioServerConfig,
34
- )
35
29
 
36
30
  # TODO use custom interface
37
31
  from letta.interface import AgentInterface # abstract
@@ -343,11 +337,12 @@ class SyncServer(Server):
343
337
  self.mcp_clients[server_name] = StdioMCPClient()
344
338
  else:
345
339
  raise ValueError(f"Invalid MCP server config: {server_config}")
340
+
346
341
  try:
347
342
  self.mcp_clients[server_name].connect_to_server(server_config)
348
- except:
349
- logger.exception(f"Failed to connect to MCP server: {server_name}")
350
- raise
343
+ except Exception as e:
344
+ logger.error(e)
345
+ self.mcp_clients.pop(server_name)
351
346
 
352
347
  # Print out the tools that are connected
353
348
  for server_name, client in self.mcp_clients.items():
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.6.40.dev20250314173529
3
+ Version: 0.6.40.dev20250314222759
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -1,6 +1,6 @@
1
- letta/__init__.py,sha256=q6k2pwvx7XD4fcSf0faPYn9dD9Yj6778hhgrULdXd6s,918
1
+ letta/__init__.py,sha256=UHqXlWcxu1BBS0f_6UszKphaK2mFq0xvQby9G3WcIFc,918
2
2
  letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
3
- letta/agent.py,sha256=CK6E-ZeFOuzoJxgegogDNVcgfUMS9SAebpYI7HTvS2Q,68128
3
+ letta/agent.py,sha256=xBXIgaeiGXDBpgNKX0-2w10iRBBvcLgBK0W85jTiSX8,68141
4
4
  letta/agents/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  letta/agents/base_agent.py,sha256=8IMB7UK4ft-Wi-ZYjX7akqQUhk_cSswRgepqeZyvCMs,1550
6
6
  letta/agents/ephemeral_agent.py,sha256=DBMXT50UQoqjkvl_Piwle3Fy7iXopy15oMWwnWzbpvo,2751
@@ -29,13 +29,17 @@ letta/functions/function_sets/multi_agent.py,sha256=QTRvK7G-B_2ShIP7hw6Xw50P5VMM
29
29
  letta/functions/functions.py,sha256=NyWLh7a-f4mXti5vM1oWDwXzfA58VmVVqL03O9vosKY,5672
30
30
  letta/functions/helpers.py,sha256=0I-ezZeM3slhAifpdlR5k2Rn6GxExD6xACCKuoYmE8M,29119
31
31
  letta/functions/interface.py,sha256=s_PPp5WDvGH_y9KUpMlORkdC141ITczFk3wsevrrUD8,2866
32
- letta/functions/schema_generator.py,sha256=wbV8tDx5RIkrSP9JNVo3PUuulN-WZie1WWLg69QTOjE,22320
32
+ letta/functions/mcp_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ letta/functions/mcp_client/base_client.py,sha256=uHp4XuXFy3avCkc3XYr1IxEPTHZWADqrDk-kR-oFyLY,2144
34
+ letta/functions/mcp_client/sse_client.py,sha256=VElaM87sEMTvam4t3IJgWGhyqs5BL4YVErqVPv2hQug,961
35
+ letta/functions/mcp_client/stdio_client.py,sha256=qy9vHB99oI4pCAQ4KmQUKY0bTboaQI4pnPm9DPQfPvY,4210
36
+ letta/functions/mcp_client/types.py,sha256=nmcnQn2EpxXzXg5_pWPsHZobfxO6OucaUgz1bVvam7o,1411
37
+ letta/functions/schema_generator.py,sha256=4hiDQpHemyfKWME-5X6xJuSiv7g9V_BgAPFegohHBIM,22327
33
38
  letta/helpers/__init__.py,sha256=p0luQ1Oe3Skc6sH4O58aHHA3Qbkyjifpuq0DZ1GAY0U,59
34
39
  letta/helpers/composio_helpers.py,sha256=6CWV483vE3N-keQlblexxBiQHxorMAgQuvbok4adGO4,949
35
40
  letta/helpers/converters.py,sha256=3qHoPDPa7ycPeeE6eLYZ8mad1mA6oGPgY95lDyhb3_A,8971
36
41
  letta/helpers/datetime_helpers.py,sha256=7U5ZJkE0cLki4sG8ukIHZSAoFfQpLWQu2kFekETy6Zg,2633
37
42
  letta/helpers/json_helpers.py,sha256=PWZ5HhSqGXO4e563dM_8M72q7ScirjXQ4Rv1ckohaV8,396
38
- letta/helpers/mcp_helpers.py,sha256=T1Jz2emYlDBbTsqZKckUGWiDjeoD_7lOUGx7GWU1mM8,4789
39
43
  letta/helpers/tool_execution_helper.py,sha256=bskCscuz2nqoUboFcYA7sQGeikdEyqiYnNpO4gLQTdc,7469
40
44
  letta/helpers/tool_rule_solver.py,sha256=z-2Zq_qWykgWanFZYxtxUee4FkMnxqvntXe2tomoH68,6774
41
45
  letta/humans/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -199,7 +203,7 @@ letta/schemas/run.py,sha256=SRqPRziINIiPunjOhE_NlbnQYgxTvqmbauni_yfBQRA,2085
199
203
  letta/schemas/sandbox_config.py,sha256=SZCo3FSMz-DIBMKGu0atT4tsVFXGsqMFPaJnjrxpkX4,5993
200
204
  letta/schemas/source.py,sha256=-BQVolcXA2ziCu2ztR6cbTdGUc8G7vGJy7rvpdf1hpg,2880
201
205
  letta/schemas/step.py,sha256=WkcVnruUUOWLKwiWPn2Gfal4EQZPNLqlsd9859xhgsw,2224
202
- letta/schemas/tool.py,sha256=A6HS_nHoxAOVyv4u6NMxzMNsp0PG40kY2uDS8FA06M4,12284
206
+ letta/schemas/tool.py,sha256=PXWxEqzM-kADijlsJzu0ZYtWLnjpq4ZUNX4NzesIWNQ,12291
203
207
  letta/schemas/tool_rule.py,sha256=2YQZba4fXS3u4j8pIk7BDujfq8rnxSVMwJSyaVgApH4,2149
204
208
  letta/schemas/usage.py,sha256=8oYRH-JX0PfjIu2zkT5Uu3UWQ7Unnz_uHiO8hRGI4m0,912
205
209
  letta/schemas/user.py,sha256=V32Tgl6oqB3KznkxUz12y7agkQicjzW7VocSpj78i6Q,1526
@@ -243,12 +247,12 @@ letta/server/rest_api/routers/v1/sandbox_configs.py,sha256=9hqnnMwJ3wCwO-Bezu3Xl
243
247
  letta/server/rest_api/routers/v1/sources.py,sha256=rpQhaRHyzGUK43LX623L8BBLqL85HJ6fUYPMvI4k3kA,8434
244
248
  letta/server/rest_api/routers/v1/steps.py,sha256=DVVwaxLNbNAgWpr2oQkrNjdS-wi0bP8kVJZUO-hiaf8,3275
245
249
  letta/server/rest_api/routers/v1/tags.py,sha256=coydgvL6-9cuG2Hy5Ea7QY3inhTHlsf69w0tcZenBus,880
246
- letta/server/rest_api/routers/v1/tools.py,sha256=UVEl7nQr74C-VV02Enw6dx9S8UvPMnM97Ju-X92GswA,17204
250
+ letta/server/rest_api/routers/v1/tools.py,sha256=pWIlYUksq9QNFSmknB_DdSB5zs77zaHoj-Ha-WxYkO8,17211
247
251
  letta/server/rest_api/routers/v1/users.py,sha256=G5DBHSkPfBgVHN2Wkm-rVYiLQAudwQczIq2Z3YLdbVo,2277
248
252
  letta/server/rest_api/routers/v1/voice.py,sha256=7J0L-Nkz65m0PXcpQI2ATMIZzumDDSUzgtIus7d-tv8,2461
249
253
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
250
254
  letta/server/rest_api/utils.py,sha256=aF0u__Q33-aPWAiHi9JA0jKAjqnwbVKzJdD5NgFpnOU,13828
251
- letta/server/server.py,sha256=w2vCdTz4rETNA_pmVmEMniUIygIlIeD6Qud_dZNcfVQ,75772
255
+ letta/server/server.py,sha256=Ml4-MklB28x3iAONfp3m2oSbrhVctAvvxUWApeWIWrk,75884
252
256
  letta/server/startup.sh,sha256=eo7zz4HGu5ryOshfbOSGbXpUDDyoaP7fTq4z8269uaw,1939
253
257
  letta/server/static_files/assets/index-048c9598.js,sha256=mR16XppvselwKCcNgONs4L7kZEVa4OEERm4lNZYtLSk,146819
254
258
  letta/server/static_files/assets/index-0e31b727.css,sha256=SBbja96uiQVLDhDOroHgM6NSl7tS4lpJRCREgSS_hA8,7672
@@ -290,8 +294,8 @@ letta/supervisor_multi_agent.py,sha256=dw6XPAxZcyjAXEVYkMIxJFhZdR2m2_rq-fYev5hZE
290
294
  letta/system.py,sha256=dnOrS2FlRMwijQnOvfrky0Lg8wEw-FUq2zzfAJOUSKA,8477
291
295
  letta/tracing.py,sha256=h_-c2lIKHmA7yCLOvgaHijMabmRC__FAl2rZtVKufUM,8017
292
296
  letta/utils.py,sha256=AdHrQ2OQ3V4XhJ1LtYwbLUO71j2IJY37cIUxXPgaaRY,32125
293
- letta_nightly-0.6.40.dev20250314173529.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
294
- letta_nightly-0.6.40.dev20250314173529.dist-info/METADATA,sha256=vByuTR-Z0aV0d2OsTuIqBwOJjtb41l7WPqM62GTdUvQ,22886
295
- letta_nightly-0.6.40.dev20250314173529.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
296
- letta_nightly-0.6.40.dev20250314173529.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
297
- letta_nightly-0.6.40.dev20250314173529.dist-info/RECORD,,
297
+ letta_nightly-0.6.40.dev20250314222759.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
298
+ letta_nightly-0.6.40.dev20250314222759.dist-info/METADATA,sha256=_O99vV3C3JQYXICthAEXivxmz4g0rYe2ByP1bNOXu6Y,22886
299
+ letta_nightly-0.6.40.dev20250314222759.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
300
+ letta_nightly-0.6.40.dev20250314222759.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
301
+ letta_nightly-0.6.40.dev20250314222759.dist-info/RECORD,,
@@ -1,129 +0,0 @@
1
- import asyncio
2
- from enum import Enum
3
- from typing import List, Optional, Tuple
4
-
5
- from mcp import ClientSession, StdioServerParameters, Tool
6
- from mcp.client.sse import sse_client
7
- from mcp.client.stdio import stdio_client
8
- from pydantic import BaseModel, Field
9
-
10
- from letta.log import get_logger
11
-
12
- logger = get_logger(__name__)
13
-
14
- # see: https://modelcontextprotocol.io/quickstart/user
15
- MCP_CONFIG_TOPLEVEL_KEY = "mcpServers"
16
-
17
-
18
- class MCPTool(Tool):
19
- """A simple wrapper around MCP's tool definition (to avoid conflict with our own)"""
20
-
21
-
22
- class MCPServerType(str, Enum):
23
- SSE = "sse"
24
- STDIO = "stdio"
25
-
26
-
27
- class BaseServerConfig(BaseModel):
28
- server_name: str = Field(..., description="The name of the server")
29
- type: MCPServerType
30
-
31
-
32
- class SSEServerConfig(BaseServerConfig):
33
- type: MCPServerType = MCPServerType.SSE
34
- server_url: str = Field(..., description="The URL of the server (MCP SSE client will connect to this URL)")
35
-
36
- def to_dict(self) -> dict:
37
- values = {
38
- "transport": "sse",
39
- "url": self.server_url,
40
- }
41
- return values
42
-
43
-
44
- class StdioServerConfig(BaseServerConfig):
45
- type: MCPServerType = MCPServerType.STDIO
46
- command: str = Field(..., description="The command to run (MCP 'local' client will run this command)")
47
- args: List[str] = Field(..., description="The arguments to pass to the command")
48
- env: Optional[dict[str, str]] = Field(None, description="Environment variables to set")
49
-
50
- def to_dict(self) -> dict:
51
- values = {
52
- "transport": "stdio",
53
- "command": self.command,
54
- "args": self.args,
55
- }
56
- if self.env is not None:
57
- values["env"] = self.env
58
- return values
59
-
60
-
61
- class BaseMCPClient:
62
- def __init__(self):
63
- self.session: Optional[ClientSession] = None
64
- self.stdio = None
65
- self.write = None
66
- self.initialized = False
67
- self.loop = asyncio.new_event_loop()
68
- self.cleanup_funcs = []
69
-
70
- def connect_to_server(self, server_config: BaseServerConfig):
71
- asyncio.set_event_loop(self.loop)
72
- self._initialize_connection(server_config)
73
- self.loop.run_until_complete(self.session.initialize())
74
- self.initialized = True
75
-
76
- def _initialize_connection(self, server_config: BaseServerConfig):
77
- raise NotImplementedError("Subclasses must implement _initialize_connection")
78
-
79
- def list_tools(self) -> List[Tool]:
80
- self._check_initialized()
81
- response = self.loop.run_until_complete(self.session.list_tools())
82
- return response.tools
83
-
84
- def execute_tool(self, tool_name: str, tool_args: dict) -> Tuple[str, bool]:
85
- self._check_initialized()
86
- result = self.loop.run_until_complete(self.session.call_tool(tool_name, tool_args))
87
- return str(result.content), result.isError
88
-
89
- def _check_initialized(self):
90
- if not self.initialized:
91
- logger.error("MCPClient has not been initialized")
92
- raise RuntimeError("MCPClient has not been initialized")
93
-
94
- def cleanup(self):
95
- try:
96
- for cleanup_func in self.cleanup_funcs:
97
- cleanup_func()
98
- self.initialized = False
99
- if not self.loop.is_closed():
100
- self.loop.close()
101
- except Exception as e:
102
- logger.warning(e)
103
- finally:
104
- logger.info("Cleaned up MCP clients on shutdown.")
105
-
106
-
107
- class StdioMCPClient(BaseMCPClient):
108
- def _initialize_connection(self, server_config: StdioServerConfig):
109
- server_params = StdioServerParameters(command=server_config.command, args=server_config.args)
110
- stdio_cm = stdio_client(server_params)
111
- stdio_transport = self.loop.run_until_complete(stdio_cm.__aenter__())
112
- self.stdio, self.write = stdio_transport
113
- self.cleanup_funcs.append(lambda: self.loop.run_until_complete(stdio_cm.__aexit__(None, None, None)))
114
-
115
- session_cm = ClientSession(self.stdio, self.write)
116
- self.session = self.loop.run_until_complete(session_cm.__aenter__())
117
- self.cleanup_funcs.append(lambda: self.loop.run_until_complete(session_cm.__aexit__(None, None, None)))
118
-
119
-
120
- class SSEMCPClient(BaseMCPClient):
121
- def _initialize_connection(self, server_config: SSEServerConfig):
122
- sse_cm = sse_client(url=server_config.server_url)
123
- sse_transport = self.loop.run_until_complete(sse_cm.__aenter__())
124
- self.stdio, self.write = sse_transport
125
- self.cleanup_funcs.append(lambda: self.loop.run_until_complete(sse_cm.__aexit__(None, None, None)))
126
-
127
- session_cm = ClientSession(self.stdio, self.write)
128
- self.session = self.loop.run_until_complete(session_cm.__aenter__())
129
- self.cleanup_funcs.append(lambda: self.loop.run_until_complete(session_cm.__aexit__(None, None, None)))