letta-nightly 0.6.38.dev20250312104155__py3-none-any.whl → 0.6.39.dev20250313162623__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.

Files changed (41) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +49 -11
  3. letta/agents/low_latency_agent.py +3 -2
  4. letta/constants.py +3 -0
  5. letta/functions/function_sets/base.py +1 -1
  6. letta/functions/helpers.py +14 -0
  7. letta/functions/schema_generator.py +47 -0
  8. letta/helpers/mcp_helpers.py +108 -0
  9. letta/llm_api/cohere.py +1 -1
  10. letta/llm_api/helpers.py +1 -2
  11. letta/llm_api/llm_api_tools.py +0 -1
  12. letta/local_llm/utils.py +30 -20
  13. letta/log.py +1 -1
  14. letta/memory.py +1 -1
  15. letta/orm/__init__.py +1 -0
  16. letta/orm/block.py +8 -0
  17. letta/orm/enums.py +2 -0
  18. letta/orm/identities_blocks.py +13 -0
  19. letta/orm/identity.py +9 -0
  20. letta/orm/sqlalchemy_base.py +4 -4
  21. letta/schemas/identity.py +3 -0
  22. letta/schemas/message.py +68 -62
  23. letta/schemas/tool.py +39 -2
  24. letta/server/rest_api/app.py +15 -0
  25. letta/server/rest_api/chat_completions_interface.py +2 -0
  26. letta/server/rest_api/interface.py +46 -13
  27. letta/server/rest_api/routers/v1/agents.py +2 -2
  28. letta/server/rest_api/routers/v1/blocks.py +5 -1
  29. letta/server/rest_api/routers/v1/tools.py +71 -1
  30. letta/server/server.py +102 -5
  31. letta/services/agent_manager.py +2 -0
  32. letta/services/block_manager.py +10 -1
  33. letta/services/identity_manager.py +54 -14
  34. letta/services/summarizer/summarizer.py +1 -1
  35. letta/services/tool_manager.py +6 -0
  36. letta/settings.py +11 -12
  37. {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/METADATA +4 -3
  38. {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/RECORD +41 -39
  39. {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/LICENSE +0 -0
  40. {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/WHEEL +0 -0
  41. {letta_nightly-0.6.38.dev20250312104155.dist-info → letta_nightly-0.6.39.dev20250313162623.dist-info}/entry_points.txt +0 -0
@@ -1,4 +1,4 @@
1
- from typing import List, Optional
1
+ from typing import List, Optional, Union
2
2
 
3
3
  from composio.client import ComposioClientError, HTTPError, NoItemsFound
4
4
  from composio.client.collections import ActionModel, AppModel
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException
13
13
 
14
14
  from letta.errors import LettaToolCreateError
15
15
  from letta.helpers.composio_helpers import get_composio_api_key
16
+ from letta.helpers.mcp_helpers import LocalServerConfig, MCPTool, SSEServerConfig
16
17
  from letta.log import get_logger
17
18
  from letta.orm.errors import UniqueConstraintViolationError
18
19
  from letta.schemas.letta_message import ToolReturnMessage
@@ -329,3 +330,72 @@ def add_composio_tool(
329
330
  "composio_action_name": composio_action_name,
330
331
  },
331
332
  )
333
+
334
+
335
+ # Specific routes for MCP
336
+ @router.get("/mcp/servers", response_model=dict[str, Union[SSEServerConfig, LocalServerConfig]], operation_id="list_mcp_servers")
337
+ def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
338
+ """
339
+ Get a list of all configured MCP servers
340
+ """
341
+ actor = server.user_manager.get_user_or_default(user_id=user_id)
342
+ return server.get_mcp_servers()
343
+
344
+
345
+ # NOTE: async because the MCP client/session calls are async
346
+ # TODO: should we make the return type MCPTool, not Tool (since we don't have ID)?
347
+ @router.get("/mcp/servers/{mcp_server_name}/tools", response_model=List[MCPTool], operation_id="list_mcp_tools_by_server")
348
+ def list_mcp_tools_by_server(
349
+ mcp_server_name: str,
350
+ server: SyncServer = Depends(get_letta_server),
351
+ actor_id: Optional[str] = Header(None, alias="user_id"),
352
+ ):
353
+ """
354
+ Get a list of all tools for a specific MCP server
355
+ """
356
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
357
+ try:
358
+ return server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
359
+ except ValueError as e:
360
+ # ValueError means that the MCP server name doesn't exist
361
+ raise HTTPException(
362
+ status_code=400, # Bad Request
363
+ detail={
364
+ "code": "MCPServerNotFoundError",
365
+ "message": str(e),
366
+ "mcp_server_name": mcp_server_name,
367
+ },
368
+ )
369
+
370
+
371
+ @router.post("/mcp/servers/{mcp_server_name}/{mcp_tool_name}", response_model=Tool, operation_id="add_mcp_tool")
372
+ def add_mcp_tool(
373
+ mcp_server_name: str,
374
+ mcp_tool_name: str,
375
+ server: SyncServer = Depends(get_letta_server),
376
+ actor_id: Optional[str] = Header(None, alias="user_id"),
377
+ ):
378
+ """
379
+ Add a new MCP tool by server + tool name
380
+ """
381
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
382
+
383
+ available_tools = server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
384
+ # See if the tool is in the avaialable list
385
+ mcp_tool = None
386
+ for tool in available_tools:
387
+ if tool.name == mcp_tool_name:
388
+ mcp_tool = tool
389
+ break
390
+ if not mcp_tool:
391
+ raise HTTPException(
392
+ status_code=400, # Bad Request
393
+ detail={
394
+ "code": "MCPToolNotFoundError",
395
+ "message": f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
396
+ "mcp_tool_name": mcp_tool_name,
397
+ },
398
+ )
399
+
400
+ tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
401
+ return server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, actor=actor)
letta/server/server.py CHANGED
@@ -21,6 +21,15 @@ from letta.config import LettaConfig
21
21
  from letta.data_sources.connectors import DataConnector, load_data
22
22
  from letta.helpers.datetime_helpers import get_utc_time
23
23
  from letta.helpers.json_helpers import json_dumps, json_loads
24
+ from letta.helpers.mcp_helpers import (
25
+ BaseMCPClient,
26
+ LocalMCPClient,
27
+ LocalServerConfig,
28
+ MCPServerType,
29
+ MCPTool,
30
+ SSEMCPClient,
31
+ SSEServerConfig,
32
+ )
24
33
 
25
34
  # TODO use custom interface
26
35
  from letta.interface import AgentInterface # abstract
@@ -314,6 +323,31 @@ class SyncServer(Server):
314
323
  if model_settings.xai_api_key:
315
324
  self._enabled_providers.append(xAIProvider(api_key=model_settings.xai_api_key))
316
325
 
326
+ # For MCP
327
+ """Initialize the MCP clients (there may be multiple)"""
328
+ mcp_server_configs = self.get_mcp_servers()
329
+ self.mcp_clients: Dict[str, BaseMCPClient] = {}
330
+
331
+ for server_name, server_config in mcp_server_configs.items():
332
+ if server_config.type == MCPServerType.SSE:
333
+ self.mcp_clients[server_name] = SSEMCPClient()
334
+ elif server_config.type == MCPServerType.LOCAL:
335
+ self.mcp_clients[server_name] = LocalMCPClient()
336
+ else:
337
+ raise ValueError(f"Invalid MCP server config: {server_config}")
338
+ try:
339
+ self.mcp_clients[server_name].connect_to_server(server_config)
340
+ except:
341
+ logger.exception(f"Failed to connect to MCP server: {server_name}")
342
+ raise
343
+
344
+ # Print out the tools that are connected
345
+ for server_name, client in self.mcp_clients.items():
346
+ logger.info(f"Attempting to fetch tools from MCP server: {server_name}")
347
+ mcp_tools = client.list_tools()
348
+ logger.info(f"MCP tools connected: {', '.join([t.name for t in mcp_tools])}")
349
+ logger.debug(f"MCP tools: {', '.join([str(t) for t in mcp_tools])}")
350
+
317
351
  def load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
318
352
  """Updated method to load agents from persisted storage"""
319
353
  agent_lock = self.per_agent_lock_manager.get_lock(agent_id)
@@ -322,7 +356,7 @@ class SyncServer(Server):
322
356
 
323
357
  interface = interface or self.default_interface_factory()
324
358
  if agent_state.agent_type == AgentType.memgpt_agent:
325
- agent = Agent(agent_state=agent_state, interface=interface, user=actor)
359
+ agent = Agent(agent_state=agent_state, interface=interface, user=actor, mcp_clients=self.mcp_clients)
326
360
  elif agent_state.agent_type == AgentType.offline_memory_agent:
327
361
  agent = OfflineMemoryAgent(agent_state=agent_state, interface=interface, user=actor)
328
362
  else:
@@ -601,11 +635,12 @@ class SyncServer(Server):
601
635
 
602
636
  if isinstance(message, Message):
603
637
  # Can't have a null text field
604
- if message.text is None or len(message.text) == 0:
605
- raise ValueError(f"Invalid input: '{message.text}'")
638
+ message_text = message.content[0].text
639
+ if message_text is None or len(message_text) == 0:
640
+ raise ValueError(f"Invalid input: '{message_text}'")
606
641
  # If the input begins with a command prefix, reject
607
- elif message.text.startswith("/"):
608
- raise ValueError(f"Invalid input: '{message.text}'")
642
+ elif message_text.startswith("/"):
643
+ raise ValueError(f"Invalid input: '{message_text}'")
609
644
 
610
645
  else:
611
646
  raise TypeError(f"Invalid input: '{message}' - type {type(message)}")
@@ -1172,6 +1207,68 @@ class SyncServer(Server):
1172
1207
  actions = self.get_composio_client(api_key=api_key).actions.get(apps=[composio_app_name])
1173
1208
  return actions
1174
1209
 
1210
+ # MCP wrappers
1211
+ # TODO support both command + SSE servers (via config)
1212
+ def get_mcp_servers(self) -> dict[str, Union[SSEServerConfig, LocalServerConfig]]:
1213
+ """List the MCP servers in the config (doesn't test that they are actually working)"""
1214
+ mcp_server_list = {}
1215
+
1216
+ # Attempt to read from ~/.letta/mcp_config.json
1217
+ mcp_config_path = os.path.join(constants.LETTA_DIR, constants.MCP_CONFIG_NAME)
1218
+ if os.path.exists(mcp_config_path):
1219
+ with open(mcp_config_path, "r") as f:
1220
+
1221
+ try:
1222
+ mcp_config = json.load(f)
1223
+ except Exception as e:
1224
+ logger.error(f"Failed to parse MCP config file ({mcp_config_path}) as json: {e}")
1225
+ return mcp_server_list
1226
+
1227
+ # Proper formatting is "mcpServers" key at the top level,
1228
+ # then a dict with the MCP server name as the key,
1229
+ # with the value being the schema from StdioServerParameters
1230
+ if "mcpServers" in mcp_config:
1231
+ for server_name, server_params_raw in mcp_config["mcpServers"].items():
1232
+
1233
+ # No support for duplicate server names
1234
+ if server_name in mcp_server_list:
1235
+ logger.error(f"Duplicate MCP server name found (skipping): {server_name}")
1236
+ continue
1237
+
1238
+ if "url" in server_params_raw:
1239
+ # Attempt to parse the server params as an SSE server
1240
+ try:
1241
+ server_params = SSEServerConfig(
1242
+ server_name=server_name,
1243
+ server_url=server_params_raw["url"],
1244
+ )
1245
+ mcp_server_list[server_name] = server_params
1246
+ except Exception as e:
1247
+ logger.error(f"Failed to parse server params for MCP server {server_name} (skipping): {e}")
1248
+ continue
1249
+ else:
1250
+ # Attempt to parse the server params as a StdioServerParameters
1251
+ try:
1252
+ server_params = LocalServerConfig(
1253
+ server_name=server_name,
1254
+ command=server_params_raw["command"],
1255
+ args=server_params_raw.get("args", []),
1256
+ )
1257
+ mcp_server_list[server_name] = server_params
1258
+ except Exception as e:
1259
+ logger.error(f"Failed to parse server params for MCP server {server_name} (skipping): {e}")
1260
+ continue
1261
+
1262
+ # If the file doesn't exist, return empty dictionary
1263
+ return mcp_server_list
1264
+
1265
+ def get_tools_from_mcp_server(self, mcp_server_name: str) -> List[MCPTool]:
1266
+ """List the tools in an MCP server. Requires a client to be created."""
1267
+ if mcp_server_name not in self.mcp_clients:
1268
+ raise ValueError(f"No client was created for MCP server: {mcp_server_name}")
1269
+
1270
+ return self.mcp_clients[mcp_server_name].list_tools()
1271
+
1175
1272
  @trace_method
1176
1273
  async def send_message_to_agent(
1177
1274
  self,
@@ -337,6 +337,7 @@ class AgentManager:
337
337
  match_all_tags: bool = False,
338
338
  query_text: Optional[str] = None,
339
339
  identifier_keys: Optional[List[str]] = None,
340
+ identity_id: Optional[str] = None,
340
341
  **kwargs,
341
342
  ) -> List[PydanticAgentState]:
342
343
  """
@@ -353,6 +354,7 @@ class AgentManager:
353
354
  organization_id=actor.organization_id if actor else None,
354
355
  query_text=query_text,
355
356
  identifier_keys=identifier_keys,
357
+ identity_id=identity_id,
356
358
  **kwargs,
357
359
  )
358
360
 
@@ -64,6 +64,8 @@ class BlockManager:
64
64
  label: Optional[str] = None,
65
65
  is_template: Optional[bool] = None,
66
66
  template_name: Optional[str] = None,
67
+ identifier_keys: Optional[List[str]] = None,
68
+ identity_id: Optional[str] = None,
67
69
  id: Optional[str] = None,
68
70
  after: Optional[str] = None,
69
71
  limit: Optional[int] = 50,
@@ -81,7 +83,14 @@ class BlockManager:
81
83
  if id:
82
84
  filters["id"] = id
83
85
 
84
- blocks = BlockModel.list(db_session=session, after=after, limit=limit, **filters)
86
+ blocks = BlockModel.list(
87
+ db_session=session,
88
+ after=after,
89
+ limit=limit,
90
+ identifier_keys=identifier_keys,
91
+ identity_id=identity_id,
92
+ **filters,
93
+ )
85
94
 
86
95
  return [block.to_pydantic() for block in blocks]
87
96
 
@@ -5,6 +5,7 @@ from sqlalchemy.exc import NoResultFound
5
5
  from sqlalchemy.orm import Session
6
6
 
7
7
  from letta.orm.agent import Agent as AgentModel
8
+ from letta.orm.block import Block as BlockModel
8
9
  from letta.orm.identity import Identity as IdentityModel
9
10
  from letta.schemas.identity import Identity as PydanticIdentity
10
11
  from letta.schemas.identity import IdentityCreate, IdentityType, IdentityUpdate
@@ -58,9 +59,24 @@ class IdentityManager:
58
59
  @enforce_types
59
60
  def create_identity(self, identity: IdentityCreate, actor: PydanticUser) -> PydanticIdentity:
60
61
  with self.session_maker() as session:
61
- new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids"}, exclude_unset=True))
62
+ new_identity = IdentityModel(**identity.model_dump(exclude={"agent_ids", "block_ids"}, exclude_unset=True))
62
63
  new_identity.organization_id = actor.organization_id
63
- self._process_agent_relationship(session=session, identity=new_identity, agent_ids=identity.agent_ids, allow_partial=False)
64
+ self._process_relationship(
65
+ session=session,
66
+ identity=new_identity,
67
+ relationship_name="agents",
68
+ model_class=AgentModel,
69
+ item_ids=identity.agent_ids,
70
+ allow_partial=False,
71
+ )
72
+ self._process_relationship(
73
+ session=session,
74
+ identity=new_identity,
75
+ relationship_name="blocks",
76
+ model_class=BlockModel,
77
+ item_ids=identity.block_ids,
78
+ allow_partial=False,
79
+ )
64
80
  new_identity.create(session, actor=actor)
65
81
  return new_identity.to_pydantic()
66
82
 
@@ -124,9 +140,26 @@ class IdentityManager:
124
140
  new_properties = existing_identity.properties + [prop.model_dump() for prop in identity.properties]
125
141
  existing_identity.properties = new_properties
126
142
 
127
- self._process_agent_relationship(
128
- session=session, identity=existing_identity, agent_ids=identity.agent_ids, allow_partial=False, replace=replace
129
- )
143
+ if identity.agent_ids is not None:
144
+ self._process_relationship(
145
+ session=session,
146
+ identity=existing_identity,
147
+ relationship_name="agents",
148
+ model_class=AgentModel,
149
+ item_ids=identity.agent_ids,
150
+ allow_partial=False,
151
+ replace=replace,
152
+ )
153
+ if identity.block_ids is not None:
154
+ self._process_relationship(
155
+ session=session,
156
+ identity=existing_identity,
157
+ relationship_name="blocks",
158
+ model_class=BlockModel,
159
+ item_ids=identity.block_ids,
160
+ allow_partial=False,
161
+ replace=replace,
162
+ )
130
163
  existing_identity.update(session, actor=actor)
131
164
  return existing_identity.to_pydantic()
132
165
 
@@ -141,26 +174,33 @@ class IdentityManager:
141
174
  session.delete(identity)
142
175
  session.commit()
143
176
 
144
- def _process_agent_relationship(
145
- self, session: Session, identity: IdentityModel, agent_ids: List[str], allow_partial=False, replace=True
177
+ def _process_relationship(
178
+ self,
179
+ session: Session,
180
+ identity: PydanticIdentity,
181
+ relationship_name: str,
182
+ model_class,
183
+ item_ids: List[str],
184
+ allow_partial=False,
185
+ replace=True,
146
186
  ):
147
- current_relationship = getattr(identity, "agents", [])
148
- if not agent_ids:
187
+ current_relationship = getattr(identity, relationship_name, [])
188
+ if not item_ids:
149
189
  if replace:
150
- setattr(identity, "agents", [])
190
+ setattr(identity, relationship_name, [])
151
191
  return
152
192
 
153
193
  # Retrieve models for the provided IDs
154
- found_items = session.query(AgentModel).filter(AgentModel.id.in_(agent_ids)).all()
194
+ found_items = session.query(model_class).filter(model_class.id.in_(item_ids)).all()
155
195
 
156
196
  # Validate all items are found if allow_partial is False
157
- if not allow_partial and len(found_items) != len(agent_ids):
158
- missing = set(agent_ids) - {item.id for item in found_items}
197
+ if not allow_partial and len(found_items) != len(item_ids):
198
+ missing = set(item_ids) - {item.id for item in found_items}
159
199
  raise NoResultFound(f"Items not found in agents: {missing}")
160
200
 
161
201
  if replace:
162
202
  # Replace the relationship
163
- setattr(identity, "agents", found_items)
203
+ setattr(identity, relationship_name, found_items)
164
204
  else:
165
205
  # Extend the relationship (only add new items)
166
206
  current_ids = {item.id for item in current_relationship}
@@ -96,7 +96,7 @@ class Summarizer:
96
96
  )
97
97
 
98
98
  messages = await self.summarizer_agent.step(UserMessage(content=summary_request_text))
99
- current_summary = "\n".join([m.text for m in messages])
99
+ current_summary = "\n".join([m.content[0].text for m in messages])
100
100
  current_summary = f"{self.summary_prefix}{current_summary}"
101
101
 
102
102
  return updated_in_context_messages, current_summary, True
@@ -56,6 +56,12 @@ class ToolManager:
56
56
 
57
57
  return tool
58
58
 
59
+ @enforce_types
60
+ def create_or_update_mcp_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
61
+ return self.create_or_update_tool(
62
+ PydanticTool(tool_type=ToolType.EXTERNAL_MCP, name=tool_create.json_schema["name"], **tool_create.model_dump()), actor
63
+ )
64
+
59
65
  @enforce_types
60
66
  def create_or_update_composio_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
61
67
  return self.create_or_update_tool(
letta/settings.py CHANGED
@@ -119,18 +119,17 @@ class ModelSettings(BaseSettings):
119
119
 
120
120
  env_cors_origins = os.getenv("ACCEPTABLE_ORIGINS")
121
121
 
122
- cors_origins = (
123
- [
124
- "http://letta.localhost",
125
- "http://localhost:8283",
126
- "http://localhost:8083",
127
- "http://localhost:3000",
128
- "http://localhost:4200",
129
- ]
130
- + [env_cors_origins]
131
- if env_cors_origins
132
- else []
133
- )
122
+ cors_origins = [
123
+ "http://letta.localhost",
124
+ "http://localhost:8283",
125
+ "http://localhost:8083",
126
+ "http://localhost:3000",
127
+ "http://localhost:4200",
128
+ ]
129
+
130
+ # attach the env_cors_origins to the cors_origins if it exists
131
+ if env_cors_origins:
132
+ cors_origins.extend(env_cors_origins.split(","))
134
133
 
135
134
  # read pg_uri from ~/.letta/pg_uri or set to none, this is to support Letta Desktop
136
135
  default_pg_uri = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.6.38.dev20250312104155
3
+ Version: 0.6.39.dev20250313162623
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -32,7 +32,7 @@ Requires-Dist: colorama (>=0.4.6,<0.5.0)
32
32
  Requires-Dist: composio-core (>=0.7.7,<0.8.0)
33
33
  Requires-Dist: composio-langchain (>=0.7.7,<0.8.0)
34
34
  Requires-Dist: datamodel-code-generator[http] (>=0.25.0,<0.26.0) ; extra == "desktop" or extra == "all"
35
- Requires-Dist: datasets (>=2.14.6,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"
35
+ Requires-Dist: datasets (>=2.14.6,<3.0.0)
36
36
  Requires-Dist: demjson3 (>=3.0.6,<4.0.0)
37
37
  Requires-Dist: docker (>=7.1.0,<8.0.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
38
38
  Requires-Dist: docstring-parser (>=0.16,<0.17)
@@ -50,11 +50,12 @@ Requires-Dist: isort (>=5.13.2,<6.0.0) ; extra == "dev" or extra == "all"
50
50
  Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
51
51
  Requires-Dist: langchain (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
52
52
  Requires-Dist: langchain-community (>=0.3.7,<0.4.0) ; extra == "external-tools" or extra == "desktop" or extra == "all"
53
- Requires-Dist: letta_client (>=0.1.54,<0.2.0)
53
+ Requires-Dist: letta_client (>=0.1.65,<0.2.0)
54
54
  Requires-Dist: llama-index (>=0.12.2,<0.13.0)
55
55
  Requires-Dist: llama-index-embeddings-openai (>=0.3.1,<0.4.0)
56
56
  Requires-Dist: locust (>=2.31.5,<3.0.0) ; extra == "dev" or extra == "desktop" or extra == "all"
57
57
  Requires-Dist: marshmallow-sqlalchemy (>=1.4.1,<2.0.0)
58
+ Requires-Dist: mcp (>=1.3.0,<2.0.0)
58
59
  Requires-Dist: nltk (>=3.8.1,<4.0.0)
59
60
  Requires-Dist: numpy (>=1.26.2,<2.0.0)
60
61
  Requires-Dist: openai (>=1.60.0,<2.0.0)