letta-nightly 0.6.39.dev20250313162623__py3-none-any.whl → 0.6.40.dev20250314173529__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 (59) hide show
  1. letta/agent.py +13 -3
  2. letta/agents/ephemeral_agent.py +2 -1
  3. letta/agents/low_latency_agent.py +8 -0
  4. letta/dynamic_multi_agent.py +274 -0
  5. letta/functions/function_sets/base.py +1 -0
  6. letta/functions/function_sets/extras.py +2 -1
  7. letta/functions/function_sets/multi_agent.py +17 -0
  8. letta/functions/helpers.py +41 -0
  9. letta/helpers/converters.py +67 -0
  10. letta/helpers/mcp_helpers.py +26 -5
  11. letta/llm_api/openai.py +1 -1
  12. letta/memory.py +2 -1
  13. letta/orm/__init__.py +2 -0
  14. letta/orm/agent.py +69 -20
  15. letta/orm/custom_columns.py +15 -0
  16. letta/orm/group.py +33 -0
  17. letta/orm/groups_agents.py +13 -0
  18. letta/orm/message.py +7 -4
  19. letta/orm/organization.py +1 -0
  20. letta/orm/sqlalchemy_base.py +3 -3
  21. letta/round_robin_multi_agent.py +152 -0
  22. letta/schemas/agent.py +3 -0
  23. letta/schemas/enums.py +0 -4
  24. letta/schemas/group.py +65 -0
  25. letta/schemas/letta_message.py +167 -106
  26. letta/schemas/letta_message_content.py +192 -0
  27. letta/schemas/message.py +28 -36
  28. letta/serialize_schemas/__init__.py +1 -1
  29. letta/serialize_schemas/marshmallow_agent.py +108 -0
  30. letta/serialize_schemas/{agent_environment_variable.py → marshmallow_agent_environment_variable.py} +1 -1
  31. letta/serialize_schemas/marshmallow_base.py +52 -0
  32. letta/serialize_schemas/{block.py → marshmallow_block.py} +1 -1
  33. letta/serialize_schemas/{custom_fields.py → marshmallow_custom_fields.py} +12 -0
  34. letta/serialize_schemas/marshmallow_message.py +42 -0
  35. letta/serialize_schemas/{tag.py → marshmallow_tag.py} +12 -2
  36. letta/serialize_schemas/{tool.py → marshmallow_tool.py} +1 -1
  37. letta/serialize_schemas/pydantic_agent_schema.py +111 -0
  38. letta/server/rest_api/app.py +15 -0
  39. letta/server/rest_api/routers/v1/__init__.py +2 -0
  40. letta/server/rest_api/routers/v1/agents.py +46 -40
  41. letta/server/rest_api/routers/v1/groups.py +233 -0
  42. letta/server/rest_api/routers/v1/tools.py +31 -3
  43. letta/server/rest_api/utils.py +1 -1
  44. letta/server/server.py +267 -12
  45. letta/services/agent_manager.py +65 -28
  46. letta/services/group_manager.py +147 -0
  47. letta/services/helpers/agent_manager_helper.py +151 -1
  48. letta/services/message_manager.py +11 -3
  49. letta/services/passage_manager.py +15 -0
  50. letta/settings.py +5 -0
  51. letta/supervisor_multi_agent.py +103 -0
  52. {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/METADATA +1 -2
  53. {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/RECORD +56 -46
  54. letta/serialize_schemas/agent.py +0 -80
  55. letta/serialize_schemas/base.py +0 -64
  56. letta/serialize_schemas/message.py +0 -29
  57. {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/LICENSE +0 -0
  58. {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/WHEEL +0 -0
  59. {letta_nightly-0.6.39.dev20250313162623.dist-info → letta_nightly-0.6.40.dev20250314173529.dist-info}/entry_points.txt +0 -0
@@ -24,6 +24,7 @@ from letta.schemas.run import Run
24
24
  from letta.schemas.source import Source
25
25
  from letta.schemas.tool import Tool
26
26
  from letta.schemas.user import User
27
+ from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
27
28
  from letta.server.rest_api.utils import get_letta_server
28
29
  from letta.server.server import SyncServer
29
30
 
@@ -35,81 +36,82 @@ router = APIRouter(prefix="/agents", tags=["agents"])
35
36
  logger = get_logger(__name__)
36
37
 
37
38
 
38
- # TODO: This should be paginated
39
39
  @router.get("/", response_model=List[AgentState], operation_id="list_agents")
40
40
  def list_agents(
41
41
  name: Optional[str] = Query(None, description="Name of the agent"),
42
42
  tags: Optional[List[str]] = Query(None, description="List of tags to filter agents by"),
43
43
  match_all_tags: bool = Query(
44
44
  False,
45
- description="If True, only returns agents that match ALL given tags. Otherwise, return agents that have ANY of the passed in tags.",
45
+ description="If True, only returns agents that match ALL given tags. Otherwise, return agents that have ANY of the passed-in tags.",
46
46
  ),
47
- server: "SyncServer" = Depends(get_letta_server),
47
+ server: SyncServer = Depends(get_letta_server),
48
48
  actor_id: Optional[str] = Header(None, alias="user_id"),
49
49
  before: Optional[str] = Query(None, description="Cursor for pagination"),
50
50
  after: Optional[str] = Query(None, description="Cursor for pagination"),
51
- limit: Optional[int] = Query(None, description="Limit for pagination"),
51
+ limit: Optional[int] = Query(50, description="Limit for pagination"),
52
52
  query_text: Optional[str] = Query(None, description="Search agents by name"),
53
- project_id: Optional[str] = Query(None, description="Search agents by project id"),
54
- template_id: Optional[str] = Query(None, description="Search agents by template id"),
55
- base_template_id: Optional[str] = Query(None, description="Search agents by base template id"),
56
- identity_id: Optional[str] = Query(None, description="Search agents by identifier id"),
53
+ project_id: Optional[str] = Query(None, description="Search agents by project ID"),
54
+ template_id: Optional[str] = Query(None, description="Search agents by template ID"),
55
+ base_template_id: Optional[str] = Query(None, description="Search agents by base template ID"),
56
+ identity_id: Optional[str] = Query(None, description="Search agents by identity ID"),
57
57
  identifier_keys: Optional[List[str]] = Query(None, description="Search agents by identifier keys"),
58
+ include_relationships: Optional[List[str]] = Query(
59
+ None,
60
+ description=(
61
+ "Specify which relational fields (e.g., 'tools', 'sources', 'memory') to include in the response. "
62
+ "If not provided, all relationships are loaded by default. "
63
+ "Using this can optimize performance by reducing unnecessary joins."
64
+ ),
65
+ ),
58
66
  ):
59
67
  """
60
68
  List all agents associated with a given user.
61
- This endpoint retrieves a list of all agents and their configurations associated with the specified user ID.
69
+
70
+ This endpoint retrieves a list of all agents and their configurations
71
+ associated with the specified user ID.
62
72
  """
73
+
74
+ # Retrieve the actor (user) details
63
75
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
64
76
 
65
- # Use dictionary comprehension to build kwargs dynamically
66
- kwargs = {
67
- key: value
68
- for key, value in {
69
- "name": name,
70
- "project_id": project_id,
71
- "template_id": template_id,
72
- "base_template_id": base_template_id,
73
- }.items()
74
- if value is not None
75
- }
76
-
77
- # Call list_agents with the dynamic kwargs
78
- agents = server.agent_manager.list_agents(
77
+ # Call list_agents directly without unnecessary dict handling
78
+ return server.agent_manager.list_agents(
79
79
  actor=actor,
80
+ name=name,
80
81
  before=before,
81
82
  after=after,
82
83
  limit=limit,
83
84
  query_text=query_text,
84
85
  tags=tags,
85
86
  match_all_tags=match_all_tags,
86
- identifier_keys=identifier_keys,
87
+ project_id=project_id,
88
+ template_id=template_id,
89
+ base_template_id=base_template_id,
87
90
  identity_id=identity_id,
88
- **kwargs,
91
+ identifier_keys=identifier_keys,
92
+ include_relationships=include_relationships,
89
93
  )
90
- return agents
91
94
 
92
95
 
93
- @router.get("/{agent_id}/download", operation_id="download_agent_serialized")
94
- def download_agent_serialized(
96
+ @router.get("/{agent_id}/export", operation_id="export_agent_serialized", response_model=AgentSchema)
97
+ def export_agent_serialized(
95
98
  agent_id: str,
96
99
  server: "SyncServer" = Depends(get_letta_server),
97
100
  actor_id: Optional[str] = Header(None, alias="user_id"),
98
- ):
101
+ ) -> AgentSchema:
99
102
  """
100
- Download the serialized JSON representation of an agent.
103
+ Export the serialized JSON representation of an agent.
101
104
  """
102
105
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
103
106
 
104
107
  try:
105
- serialized_agent = server.agent_manager.serialize(agent_id=agent_id, actor=actor)
106
- return JSONResponse(content=serialized_agent, media_type="application/json")
108
+ return server.agent_manager.serialize(agent_id=agent_id, actor=actor)
107
109
  except NoResultFound:
108
110
  raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
109
111
 
110
112
 
111
- @router.post("/upload", response_model=AgentState, operation_id="upload_agent_serialized")
112
- async def upload_agent_serialized(
113
+ @router.post("/import", response_model=AgentState, operation_id="import_agent_serialized")
114
+ async def import_agent_serialized(
113
115
  file: UploadFile = File(...),
114
116
  server: "SyncServer" = Depends(get_letta_server),
115
117
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -121,15 +123,19 @@ async def upload_agent_serialized(
121
123
  project_id: Optional[str] = Query(None, description="The project ID to associate the uploaded agent with."),
122
124
  ):
123
125
  """
124
- Upload a serialized agent JSON file and recreate the agent in the system.
126
+ Import a serialized agent file and recreate the agent in the system.
125
127
  """
126
128
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
127
129
 
128
130
  try:
129
131
  serialized_data = await file.read()
130
132
  agent_json = json.loads(serialized_data)
133
+
134
+ # Validate the JSON against AgentSchema before passing it to deserialize
135
+ agent_schema = AgentSchema.model_validate(agent_json)
136
+
131
137
  new_agent = server.agent_manager.deserialize(
132
- serialized_agent=agent_json,
138
+ serialized_agent=agent_schema, # Ensure we're passing a validated AgentSchema
133
139
  actor=actor,
134
140
  append_copy_suffix=append_copy_suffix,
135
141
  override_existing_tools=override_existing_tools,
@@ -141,7 +147,7 @@ async def upload_agent_serialized(
141
147
  raise HTTPException(status_code=400, detail="Corrupted agent file format.")
142
148
 
143
149
  except ValidationError as e:
144
- raise HTTPException(status_code=422, detail=f"Invalid agent schema: {str(e)}")
150
+ raise HTTPException(status_code=422, detail=f"Invalid agent schema: {e.errors()}")
145
151
 
146
152
  except IntegrityError as e:
147
153
  raise HTTPException(status_code=409, detail=f"Database integrity error: {str(e)}")
@@ -149,9 +155,9 @@ async def upload_agent_serialized(
149
155
  except OperationalError as e:
150
156
  raise HTTPException(status_code=503, detail=f"Database connection error. Please try again later: {str(e)}")
151
157
 
152
- except Exception:
158
+ except Exception as e:
153
159
  traceback.print_exc()
154
- raise HTTPException(status_code=500, detail="An unexpected error occurred while uploading the agent.")
160
+ raise HTTPException(status_code=500, detail=f"An unexpected error occurred while uploading the agent: {str(e)}")
155
161
 
156
162
 
157
163
  @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="retrieve_agent_context_window")
@@ -530,7 +536,7 @@ def list_messages(
530
536
  )
531
537
 
532
538
 
533
- @router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUpdateUnion, operation_id="modify_message")
539
+ @router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_message")
534
540
  def modify_message(
535
541
  agent_id: str,
536
542
  message_id: str,
@@ -0,0 +1,233 @@
1
+ from typing import Annotated, List, Optional
2
+
3
+ from fastapi import APIRouter, Body, Depends, Header, Query
4
+ from pydantic import Field
5
+
6
+ from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
7
+ from letta.schemas.group import Group, GroupCreate, ManagerType
8
+ from letta.schemas.letta_message import LettaMessageUnion
9
+ from letta.schemas.letta_request import LettaRequest, LettaStreamingRequest
10
+ from letta.schemas.letta_response import LettaResponse
11
+ from letta.server.rest_api.utils import get_letta_server
12
+ from letta.server.server import SyncServer
13
+
14
+ router = APIRouter(prefix="/groups", tags=["groups"])
15
+
16
+
17
+ @router.post("/", response_model=Group, operation_id="create_group")
18
+ async def create_group(
19
+ server: SyncServer = Depends(get_letta_server),
20
+ request: GroupCreate = Body(...),
21
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
22
+ ):
23
+ """
24
+ Create a multi-agent group with a specified management pattern. When no
25
+ management config is specified, this endpoint will use round robin for
26
+ speaker selection.
27
+ """
28
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
29
+ return server.group_manager.create_group(request, actor=actor)
30
+
31
+
32
+ @router.get("/", response_model=List[Group], operation_id="list_groups")
33
+ def list_groups(
34
+ server: "SyncServer" = Depends(get_letta_server),
35
+ actor_id: Optional[str] = Header(None, alias="user_id"),
36
+ manager_type: Optional[ManagerType] = Query(None, description="Search groups by manager type"),
37
+ before: Optional[str] = Query(None, description="Cursor for pagination"),
38
+ after: Optional[str] = Query(None, description="Cursor for pagination"),
39
+ limit: Optional[int] = Query(None, description="Limit for pagination"),
40
+ project_id: Optional[str] = Query(None, description="Search groups by project id"),
41
+ ):
42
+ """
43
+ Fetch all multi-agent groups matching query.
44
+ """
45
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
46
+ return server.group_manager.list_groups(
47
+ project_id=project_id,
48
+ manager_type=manager_type,
49
+ before=before,
50
+ after=after,
51
+ limit=limit,
52
+ actor=actor,
53
+ )
54
+
55
+
56
+ @router.post("/", response_model=Group, operation_id="create_group")
57
+ def create_group(
58
+ group: GroupCreate = Body(...),
59
+ server: "SyncServer" = Depends(get_letta_server),
60
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
61
+ x_project: Optional[str] = Header(None, alias="X-Project"), # Only handled by next js middleware
62
+ ):
63
+ """
64
+ Create a new multi-agent group with the specified configuration.
65
+ """
66
+ try:
67
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
68
+ return server.group_manager.create_group(group, actor=actor)
69
+ except Exception as e:
70
+ raise HTTPException(status_code=500, detail=str(e))
71
+
72
+
73
+ @router.put("/", response_model=Group, operation_id="upsert_group")
74
+ def upsert_group(
75
+ group: GroupCreate = Body(...),
76
+ server: "SyncServer" = Depends(get_letta_server),
77
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
78
+ x_project: Optional[str] = Header(None, alias="X-Project"), # Only handled by next js middleware
79
+ ):
80
+ """
81
+ Create a new multi-agent group with the specified configuration.
82
+ """
83
+ try:
84
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
85
+ return server.group_manager.create_group(group, actor=actor)
86
+ except Exception as e:
87
+ raise HTTPException(status_code=500, detail=str(e))
88
+
89
+
90
+ @router.delete("/{group_id}", response_model=None, operation_id="delete_group")
91
+ def delete_group(
92
+ group_id: str,
93
+ server: "SyncServer" = Depends(get_letta_server),
94
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
95
+ ):
96
+ """
97
+ Delete a multi-agent group.
98
+ """
99
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
100
+ try:
101
+ server.group_manager.delete_group(group_id=group_id, actor=actor)
102
+ return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Group id={group_id} successfully deleted"})
103
+ except NoResultFound:
104
+ raise HTTPException(status_code=404, detail=f"Group id={group_id} not found for user_id={actor.id}.")
105
+
106
+
107
+ @router.post(
108
+ "/{group_id}/messages",
109
+ response_model=LettaResponse,
110
+ operation_id="send_group_message",
111
+ )
112
+ async def send_group_message(
113
+ agent_id: str,
114
+ server: SyncServer = Depends(get_letta_server),
115
+ request: LettaRequest = Body(...),
116
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
117
+ ):
118
+ """
119
+ Process a user message and return the group's response.
120
+ This endpoint accepts a message from a user and processes it through through agents in the group based on the specified pattern
121
+ """
122
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
123
+ result = await server.send_group_message_to_agent(
124
+ group_id=group_id,
125
+ actor=actor,
126
+ messages=request.messages,
127
+ stream_steps=False,
128
+ stream_tokens=False,
129
+ # Support for AssistantMessage
130
+ use_assistant_message=request.use_assistant_message,
131
+ assistant_message_tool_name=request.assistant_message_tool_name,
132
+ assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
133
+ )
134
+ return result
135
+
136
+
137
+ @router.post(
138
+ "/{group_id}/messages/stream",
139
+ response_model=None,
140
+ operation_id="send_group_message_streaming",
141
+ responses={
142
+ 200: {
143
+ "description": "Successful response",
144
+ "content": {
145
+ "text/event-stream": {"description": "Server-Sent Events stream"},
146
+ },
147
+ }
148
+ },
149
+ )
150
+ async def send_group_message_streaming(
151
+ group_id: str,
152
+ server: SyncServer = Depends(get_letta_server),
153
+ request: LettaStreamingRequest = Body(...),
154
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
155
+ ):
156
+ """
157
+ Process a user message and return the group's responses.
158
+ This endpoint accepts a message from a user and processes it through agents in the group based on the specified pattern.
159
+ It will stream the steps of the response always, and stream the tokens if 'stream_tokens' is set to True.
160
+ """
161
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
162
+ result = await server.send_group_message_to_agent(
163
+ group_id=group_id,
164
+ actor=actor,
165
+ messages=request.messages,
166
+ stream_steps=True,
167
+ stream_tokens=request.stream_tokens,
168
+ # Support for AssistantMessage
169
+ use_assistant_message=request.use_assistant_message,
170
+ assistant_message_tool_name=request.assistant_message_tool_name,
171
+ assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
172
+ )
173
+ return result
174
+
175
+
176
+ GroupMessagesResponse = Annotated[
177
+ List[LettaMessageUnion], Field(json_schema_extra={"type": "array", "items": {"$ref": "#/components/schemas/LettaMessageUnion"}})
178
+ ]
179
+
180
+
181
+ @router.get("/{group_id}/messages", response_model=GroupMessagesResponse, operation_id="list_group_messages")
182
+ def list_group_messages(
183
+ group_id: str,
184
+ server: "SyncServer" = Depends(get_letta_server),
185
+ after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."),
186
+ before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."),
187
+ limit: int = Query(10, description="Maximum number of messages to retrieve."),
188
+ use_assistant_message: bool = Query(True, description="Whether to use assistant messages"),
189
+ assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool."),
190
+ assistant_message_tool_kwarg: str = Query(DEFAULT_MESSAGE_TOOL_KWARG, description="The name of the message argument."),
191
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
192
+ ):
193
+ """
194
+ Retrieve message history for an agent.
195
+ """
196
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
197
+
198
+ return server.group_manager.list_group_messages(
199
+ group_id=group_id,
200
+ before=before,
201
+ after=after,
202
+ limit=limit,
203
+ actor=actor,
204
+ use_assistant_message=use_assistant_message,
205
+ assistant_message_tool_name=assistant_message_tool_name,
206
+ assistant_message_tool_kwarg=assistant_message_tool_kwarg,
207
+ )
208
+
209
+
210
+ '''
211
+ @router.patch("/{group_id}/reset-messages", response_model=None, operation_id="reset_group_messages")
212
+ def reset_group_messages(
213
+ group_id: str,
214
+ add_default_initial_messages: bool = Query(default=False, description="If true, adds the default initial messages after resetting."),
215
+ server: "SyncServer" = Depends(get_letta_server),
216
+ actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
217
+ ):
218
+ """
219
+ Resets the messages for all agents that are part of the multi-agent group.
220
+ TODO: only delete group messages not all messages!
221
+ """
222
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
223
+ group = server.group_manager.retrieve_group(group_id=group_id, actor=actor)
224
+ agent_ids = group.agent_ids
225
+ if group.manager_agent_id:
226
+ agent_ids.append(group.manager_agent_id)
227
+ for agent_id in agent_ids:
228
+ server.agent_manager.reset_messages(
229
+ agent_id=agent_id,
230
+ actor=actor,
231
+ add_default_initial_messages=add_default_initial_messages,
232
+ )
233
+ '''
@@ -13,7 +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
+ 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
@@ -333,7 +333,7 @@ def add_composio_tool(
333
333
 
334
334
 
335
335
  # Specific routes for MCP
336
- @router.get("/mcp/servers", response_model=dict[str, Union[SSEServerConfig, LocalServerConfig]], operation_id="list_mcp_servers")
336
+ @router.get("/mcp/servers", response_model=dict[str, Union[SSEServerConfig, StdioServerConfig]], operation_id="list_mcp_servers")
337
337
  def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
338
338
  """
339
339
  Get a list of all configured MCP servers
@@ -376,7 +376,7 @@ def add_mcp_tool(
376
376
  actor_id: Optional[str] = Header(None, alias="user_id"),
377
377
  ):
378
378
  """
379
- Add a new MCP tool by server + tool name
379
+ Register a new MCP tool as a Letta server by MCP server + tool name
380
380
  """
381
381
  actor = server.user_manager.get_user_or_default(user_id=actor_id)
382
382
 
@@ -399,3 +399,31 @@ def add_mcp_tool(
399
399
 
400
400
  tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
401
401
  return server.tool_manager.create_or_update_mcp_tool(tool_create=tool_create, actor=actor)
402
+
403
+
404
+ @router.put("/mcp/servers", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="add_mcp_server")
405
+ def add_mcp_server_to_config(
406
+ request: Union[StdioServerConfig, SSEServerConfig] = Body(...),
407
+ server: SyncServer = Depends(get_letta_server),
408
+ actor_id: Optional[str] = Header(None, alias="user_id"),
409
+ ):
410
+ """
411
+ Add a new MCP server to the Letta MCP server config
412
+ """
413
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
414
+ return server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
415
+
416
+
417
+ @router.delete(
418
+ "/mcp/servers/{mcp_server_name}", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="delete_mcp_server"
419
+ )
420
+ def delete_mcp_server_from_config(
421
+ mcp_server_name: str,
422
+ server: SyncServer = Depends(get_letta_server),
423
+ actor_id: Optional[str] = Header(None, alias="user_id"),
424
+ ):
425
+ """
426
+ Add a new MCP server to the Letta MCP server config
427
+ """
428
+ actor = server.user_manager.get_user_or_default(user_id=actor_id)
429
+ return server.delete_mcp_server_from_config(server_name=mcp_server_name)
@@ -18,7 +18,7 @@ from letta.errors import ContextWindowExceededError, RateLimitExceededError
18
18
  from letta.helpers.datetime_helpers import get_utc_time
19
19
  from letta.log import get_logger
20
20
  from letta.schemas.enums import MessageRole
21
- from letta.schemas.letta_message import TextContent
21
+ from letta.schemas.letta_message_content import TextContent
22
22
  from letta.schemas.message import Message
23
23
  from letta.schemas.usage import LettaUsageStatistics
24
24
  from letta.schemas.user import User