letta-nightly 0.12.1.dev20251023104211__py3-none-any.whl → 0.13.0.dev20251024223017__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 (159) hide show
  1. letta/__init__.py +2 -3
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/simple_llm_request_adapter.py +8 -5
  4. letta/adapters/simple_llm_stream_adapter.py +22 -6
  5. letta/agents/agent_loop.py +10 -3
  6. letta/agents/base_agent.py +4 -1
  7. letta/agents/helpers.py +41 -9
  8. letta/agents/letta_agent.py +11 -10
  9. letta/agents/letta_agent_v2.py +47 -37
  10. letta/agents/letta_agent_v3.py +395 -300
  11. letta/agents/voice_agent.py +8 -6
  12. letta/agents/voice_sleeptime_agent.py +3 -3
  13. letta/constants.py +30 -7
  14. letta/errors.py +20 -0
  15. letta/functions/function_sets/base.py +55 -3
  16. letta/functions/mcp_client/types.py +33 -57
  17. letta/functions/schema_generator.py +135 -23
  18. letta/groups/sleeptime_multi_agent_v3.py +6 -11
  19. letta/groups/sleeptime_multi_agent_v4.py +227 -0
  20. letta/helpers/converters.py +78 -4
  21. letta/helpers/crypto_utils.py +6 -2
  22. letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
  23. letta/interfaces/anthropic_streaming_interface.py +3 -4
  24. letta/interfaces/gemini_streaming_interface.py +4 -6
  25. letta/interfaces/openai_streaming_interface.py +63 -28
  26. letta/llm_api/anthropic_client.py +7 -4
  27. letta/llm_api/deepseek_client.py +6 -4
  28. letta/llm_api/google_ai_client.py +3 -12
  29. letta/llm_api/google_vertex_client.py +1 -1
  30. letta/llm_api/helpers.py +90 -61
  31. letta/llm_api/llm_api_tools.py +4 -1
  32. letta/llm_api/openai.py +12 -12
  33. letta/llm_api/openai_client.py +53 -16
  34. letta/local_llm/constants.py +4 -3
  35. letta/local_llm/json_parser.py +5 -2
  36. letta/local_llm/utils.py +2 -3
  37. letta/log.py +171 -7
  38. letta/orm/agent.py +43 -9
  39. letta/orm/archive.py +4 -0
  40. letta/orm/custom_columns.py +15 -0
  41. letta/orm/identity.py +11 -11
  42. letta/orm/mcp_server.py +9 -0
  43. letta/orm/message.py +6 -1
  44. letta/orm/run_metrics.py +7 -2
  45. letta/orm/sqlalchemy_base.py +2 -2
  46. letta/orm/tool.py +3 -0
  47. letta/otel/tracing.py +2 -0
  48. letta/prompts/prompt_generator.py +7 -2
  49. letta/schemas/agent.py +41 -10
  50. letta/schemas/agent_file.py +3 -0
  51. letta/schemas/archive.py +4 -2
  52. letta/schemas/block.py +2 -1
  53. letta/schemas/enums.py +36 -3
  54. letta/schemas/file.py +3 -3
  55. letta/schemas/folder.py +2 -1
  56. letta/schemas/group.py +2 -1
  57. letta/schemas/identity.py +18 -9
  58. letta/schemas/job.py +3 -1
  59. letta/schemas/letta_message.py +71 -12
  60. letta/schemas/letta_request.py +7 -3
  61. letta/schemas/letta_stop_reason.py +0 -25
  62. letta/schemas/llm_config.py +8 -2
  63. letta/schemas/mcp.py +80 -83
  64. letta/schemas/mcp_server.py +349 -0
  65. letta/schemas/memory.py +20 -8
  66. letta/schemas/message.py +212 -67
  67. letta/schemas/providers/anthropic.py +13 -6
  68. letta/schemas/providers/azure.py +6 -4
  69. letta/schemas/providers/base.py +8 -4
  70. letta/schemas/providers/bedrock.py +6 -2
  71. letta/schemas/providers/cerebras.py +7 -3
  72. letta/schemas/providers/deepseek.py +2 -1
  73. letta/schemas/providers/google_gemini.py +15 -6
  74. letta/schemas/providers/groq.py +2 -1
  75. letta/schemas/providers/lmstudio.py +9 -6
  76. letta/schemas/providers/mistral.py +2 -1
  77. letta/schemas/providers/openai.py +7 -2
  78. letta/schemas/providers/together.py +9 -3
  79. letta/schemas/providers/xai.py +7 -3
  80. letta/schemas/run.py +7 -2
  81. letta/schemas/run_metrics.py +2 -1
  82. letta/schemas/sandbox_config.py +2 -2
  83. letta/schemas/secret.py +3 -158
  84. letta/schemas/source.py +2 -2
  85. letta/schemas/step.py +2 -2
  86. letta/schemas/tool.py +24 -1
  87. letta/schemas/usage.py +0 -1
  88. letta/server/rest_api/app.py +123 -7
  89. letta/server/rest_api/dependencies.py +3 -0
  90. letta/server/rest_api/interface.py +7 -4
  91. letta/server/rest_api/redis_stream_manager.py +16 -1
  92. letta/server/rest_api/routers/v1/__init__.py +7 -0
  93. letta/server/rest_api/routers/v1/agents.py +332 -322
  94. letta/server/rest_api/routers/v1/archives.py +127 -40
  95. letta/server/rest_api/routers/v1/blocks.py +54 -6
  96. letta/server/rest_api/routers/v1/chat_completions.py +146 -0
  97. letta/server/rest_api/routers/v1/folders.py +27 -35
  98. letta/server/rest_api/routers/v1/groups.py +23 -35
  99. letta/server/rest_api/routers/v1/identities.py +24 -10
  100. letta/server/rest_api/routers/v1/internal_runs.py +107 -0
  101. letta/server/rest_api/routers/v1/internal_templates.py +162 -179
  102. letta/server/rest_api/routers/v1/jobs.py +15 -27
  103. letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
  104. letta/server/rest_api/routers/v1/messages.py +23 -34
  105. letta/server/rest_api/routers/v1/organizations.py +6 -27
  106. letta/server/rest_api/routers/v1/providers.py +35 -62
  107. letta/server/rest_api/routers/v1/runs.py +30 -43
  108. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
  109. letta/server/rest_api/routers/v1/sources.py +26 -42
  110. letta/server/rest_api/routers/v1/steps.py +16 -29
  111. letta/server/rest_api/routers/v1/tools.py +17 -13
  112. letta/server/rest_api/routers/v1/users.py +5 -17
  113. letta/server/rest_api/routers/v1/voice.py +18 -27
  114. letta/server/rest_api/streaming_response.py +5 -2
  115. letta/server/rest_api/utils.py +187 -25
  116. letta/server/server.py +27 -22
  117. letta/server/ws_api/server.py +5 -4
  118. letta/services/agent_manager.py +148 -26
  119. letta/services/agent_serialization_manager.py +6 -1
  120. letta/services/archive_manager.py +168 -15
  121. letta/services/block_manager.py +14 -4
  122. letta/services/file_manager.py +33 -29
  123. letta/services/group_manager.py +10 -0
  124. letta/services/helpers/agent_manager_helper.py +65 -11
  125. letta/services/identity_manager.py +105 -4
  126. letta/services/job_manager.py +11 -1
  127. letta/services/mcp/base_client.py +2 -2
  128. letta/services/mcp/oauth_utils.py +33 -8
  129. letta/services/mcp_manager.py +174 -78
  130. letta/services/mcp_server_manager.py +1331 -0
  131. letta/services/message_manager.py +109 -4
  132. letta/services/organization_manager.py +4 -4
  133. letta/services/passage_manager.py +9 -25
  134. letta/services/provider_manager.py +91 -15
  135. letta/services/run_manager.py +72 -15
  136. letta/services/sandbox_config_manager.py +45 -3
  137. letta/services/source_manager.py +15 -8
  138. letta/services/step_manager.py +24 -1
  139. letta/services/streaming_service.py +581 -0
  140. letta/services/summarizer/summarizer.py +1 -1
  141. letta/services/tool_executor/core_tool_executor.py +111 -0
  142. letta/services/tool_executor/files_tool_executor.py +5 -3
  143. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  144. letta/services/tool_executor/tool_execution_manager.py +1 -1
  145. letta/services/tool_manager.py +10 -3
  146. letta/services/tool_sandbox/base.py +61 -1
  147. letta/services/tool_sandbox/local_sandbox.py +1 -3
  148. letta/services/user_manager.py +2 -2
  149. letta/settings.py +49 -5
  150. letta/system.py +14 -5
  151. letta/utils.py +73 -1
  152. letta/validators.py +105 -0
  153. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
  155. letta/schemas/letta_ping.py +0 -28
  156. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  157. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
@@ -17,6 +17,34 @@ from letta.schemas.letta_message_content import (
17
17
  # ---------------------------
18
18
 
19
19
 
20
+ class MessageReturnType(str, Enum):
21
+ approval = "approval"
22
+ tool = "tool"
23
+
24
+
25
+ class MessageReturn(BaseModel):
26
+ type: MessageReturnType = Field(..., description="The message type to be created.")
27
+
28
+
29
+ class ApprovalReturn(MessageReturn):
30
+ type: Literal[MessageReturnType.approval] = Field(default=MessageReturnType.approval, description="The message type to be created.")
31
+ tool_call_id: str = Field(..., description="The ID of the tool call that corresponds to this approval")
32
+ approve: bool = Field(..., description="Whether the tool has been approved")
33
+ reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
34
+
35
+
36
+ class ToolReturn(MessageReturn):
37
+ type: Literal[MessageReturnType.tool] = Field(default=MessageReturnType.tool, description="The message type to be created.")
38
+ tool_return: str
39
+ status: Literal["success", "error"]
40
+ tool_call_id: str
41
+ stdout: Optional[List[str]] = None
42
+ stderr: Optional[List[str]] = None
43
+
44
+
45
+ LettaMessageReturnUnion = Annotated[Union[ApprovalReturn, ToolReturn], Field(discriminator="type")]
46
+
47
+
20
48
  class MessageType(str, Enum):
21
49
  system_message = "system_message"
22
50
  user_message = "user_message"
@@ -233,14 +261,6 @@ class ToolCallMessage(LettaMessage):
233
261
  return v
234
262
 
235
263
 
236
- class ToolReturn(BaseModel):
237
- tool_return: str
238
- status: Literal["success", "error"]
239
- tool_call_id: str
240
- stdout: Optional[List[str]] = None
241
- stderr: Optional[List[str]] = None
242
-
243
-
244
264
  class ToolReturnMessage(LettaMessage):
245
265
  """
246
266
  A message representing the return value of a tool call (generated by Letta executing the requested tool).
@@ -282,7 +302,12 @@ class ApprovalRequestMessage(LettaMessage):
282
302
  message_type: Literal[MessageType.approval_request_message] = Field(
283
303
  default=MessageType.approval_request_message, description="The type of the message."
284
304
  )
285
- tool_call: Union[ToolCall, ToolCallDelta] = Field(..., description="The tool call that has been requested by the llm to run")
305
+ tool_call: Union[ToolCall, ToolCallDelta] = Field(
306
+ ..., description="The tool call that has been requested by the llm to run", deprecated=True
307
+ )
308
+ tool_calls: Optional[Union[List[ToolCall], ToolCallDelta]] = Field(
309
+ None, description="The tool calls that have been requested by the llm to run, which are pending approval"
310
+ )
286
311
 
287
312
 
288
313
  class ApprovalResponseMessage(LettaMessage):
@@ -301,9 +326,10 @@ class ApprovalResponseMessage(LettaMessage):
301
326
  message_type: Literal[MessageType.approval_response_message] = Field(
302
327
  default=MessageType.approval_response_message, description="The type of the message."
303
328
  )
304
- approve: bool = Field(..., description="Whether the tool has been approved")
305
- approval_request_id: str = Field(..., description="The message ID of the approval request")
306
- reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
329
+ approvals: Optional[List[LettaMessageReturnUnion]] = Field(default=None, description="The list of approval responses")
330
+ approve: Optional[bool] = Field(None, description="Whether the tool has been approved", deprecated=True)
331
+ approval_request_id: Optional[str] = Field(None, description="The message ID of the approval request", deprecated=True)
332
+ reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status", deprecated=True)
307
333
 
308
334
 
309
335
  class AssistantMessage(LettaMessage):
@@ -327,6 +353,21 @@ class AssistantMessage(LettaMessage):
327
353
  )
328
354
 
329
355
 
356
+ class LettaPing(LettaMessage):
357
+ """
358
+ A ping message used as a keepalive to prevent SSE streams from timing out during long running requests.
359
+
360
+ Args:
361
+ id (str): The ID of the message
362
+ date (datetime): The date the message was created in ISO format
363
+ """
364
+
365
+ message_type: Literal["ping"] = Field(
366
+ "ping",
367
+ description="The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
368
+ )
369
+
370
+
330
371
  # NOTE: use Pydantic's discriminated unions feature: https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions
331
372
  LettaMessageUnion = Annotated[
332
373
  Union[
@@ -374,6 +415,24 @@ def create_letta_message_union_schema():
374
415
  }
375
416
 
376
417
 
418
+ def create_letta_ping_schema():
419
+ return {
420
+ "properties": {
421
+ "message_type": {
422
+ "type": "string",
423
+ "const": "ping",
424
+ "title": "Message Type",
425
+ "description": "The type of the message.",
426
+ "default": "ping",
427
+ }
428
+ },
429
+ "type": "object",
430
+ "required": ["message_type"],
431
+ "title": "LettaPing",
432
+ "description": "Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
433
+ }
434
+
435
+
377
436
  # --------------------------
378
437
  # Message Update API Schemas
379
438
  # --------------------------
@@ -15,15 +15,18 @@ class LettaRequest(BaseModel):
15
15
  )
16
16
  use_assistant_message: bool = Field(
17
17
  default=True,
18
- description="Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects.",
18
+ description="Whether the server should parse specific tool call arguments (default `send_message`) as `AssistantMessage` objects. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.",
19
+ deprecated=True,
19
20
  )
20
21
  assistant_message_tool_name: str = Field(
21
22
  default=DEFAULT_MESSAGE_TOOL,
22
- description="The name of the designated message tool.",
23
+ description="The name of the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.",
24
+ deprecated=True,
23
25
  )
24
26
  assistant_message_tool_kwarg: str = Field(
25
27
  default=DEFAULT_MESSAGE_TOOL_KWARG,
26
- description="The name of the message argument in the designated message tool.",
28
+ description="The name of the message argument in the designated message tool. Still supported for legacy agent types, but deprecated for letta_v1_agent onward.",
29
+ deprecated=True,
27
30
  )
28
31
 
29
32
  # filter to only return specific message types
@@ -34,6 +37,7 @@ class LettaRequest(BaseModel):
34
37
  enable_thinking: str = Field(
35
38
  default=True,
36
39
  description="If set to True, enables reasoning before responses or tool calls from the agent.",
40
+ deprecated=True,
37
41
  )
38
42
 
39
43
  @field_validator("messages", mode="before")
@@ -48,28 +48,3 @@ class LettaStopReason(BaseModel):
48
48
 
49
49
  message_type: Literal["stop_reason"] = Field("stop_reason", description="The type of the message.")
50
50
  stop_reason: StopReasonType = Field(..., description="The reason why execution stopped.")
51
-
52
-
53
- def create_letta_ping_schema():
54
- return {
55
- "properties": {
56
- "message_type": {
57
- "type": "string",
58
- "const": "ping",
59
- "title": "Message Type",
60
- "description": "The type of the message.",
61
- "default": "ping",
62
- }
63
- },
64
- "type": "object",
65
- "required": ["message_type"],
66
- "title": "LettaPing",
67
- "description": "Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
68
- }
69
-
70
-
71
- class LettaPing(BaseModel):
72
- message_type: Literal["ping"] = Field(
73
- "ping",
74
- description="The type of the message. Ping messages are a keep-alive to prevent SSE streams from timing out during long running requests.",
75
- )
@@ -13,6 +13,8 @@ class LLMConfig(BaseModel):
13
13
  """Configuration for Language Model (LLM) connection and generation parameters."""
14
14
 
15
15
  model: str = Field(..., description="LLM model name. ")
16
+ display_name: Optional[str] = Field(None, description="A human-friendly display name for the model.")
17
+
16
18
  model_endpoint_type: Literal[
17
19
  "openai",
18
20
  "anthropic",
@@ -78,6 +80,7 @@ class LLMConfig(BaseModel):
78
80
 
79
81
  # FIXME hack to silence pydantic protected namespace warning
80
82
  model_config = ConfigDict(protected_namespaces=())
83
+ parallel_tool_calls: Optional[bool] = Field(False, description="If set to True, enables parallel tool calling. Defaults to False.")
81
84
 
82
85
  @model_validator(mode="before")
83
86
  @classmethod
@@ -151,7 +154,10 @@ class LLMConfig(BaseModel):
151
154
  values["put_inner_thoughts_in_kwargs"] = False
152
155
 
153
156
  if values.get("model_endpoint_type") == "anthropic" and (
154
- model.startswith("claude-3-7-sonnet") or model.startswith("claude-sonnet-4") or model.startswith("claude-opus-4")
157
+ model.startswith("claude-3-7-sonnet")
158
+ or model.startswith("claude-sonnet-4")
159
+ or model.startswith("claude-opus-4")
160
+ or model.startswith("claude-haiku-4-5")
155
161
  ):
156
162
  values["put_inner_thoughts_in_kwargs"] = False
157
163
 
@@ -239,7 +245,7 @@ class LLMConfig(BaseModel):
239
245
  config.model.startswith("claude-opus-4")
240
246
  or config.model.startswith("claude-sonnet-4")
241
247
  or config.model.startswith("claude-3-7-sonnet")
242
- or config.model.startswith("claude-4-5-haiku")
248
+ or config.model.startswith("claude-haiku-4-5")
243
249
  )
244
250
 
245
251
  @classmethod
letta/schemas/mcp.py CHANGED
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from datetime import datetime
2
3
  from typing import Any, Dict, List, Optional, Union
3
4
 
@@ -13,7 +14,7 @@ from letta.functions.mcp_client.types import (
13
14
  )
14
15
  from letta.orm.mcp_oauth import OAuthSessionStatus
15
16
  from letta.schemas.letta_base import LettaBase
16
- from letta.schemas.secret import Secret, SecretDict
17
+ from letta.schemas.secret import Secret
17
18
  from letta.settings import settings
18
19
 
19
20
 
@@ -31,8 +32,8 @@ class MCPServer(BaseMCPServer):
31
32
  token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for authentication)")
32
33
  custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom authentication headers as key-value pairs")
33
34
 
34
- token_enc: Optional[str] = Field(None, description="Encrypted token")
35
- custom_headers_enc: Optional[str] = Field(None, description="Encrypted custom headers")
35
+ token_enc: Secret | None = Field(None, description="Encrypted token as Secret object")
36
+ custom_headers_enc: Secret | None = Field(None, description="Encrypted custom headers as Secret object")
36
37
 
37
38
  # stdio config
38
39
  stdio_config: Optional[StdioServerConfig] = Field(
@@ -48,55 +49,55 @@ class MCPServer(BaseMCPServer):
48
49
 
49
50
  def get_token_secret(self) -> Secret:
50
51
  """Get the token as a Secret object, preferring encrypted over plaintext."""
51
- return Secret.from_db(self.token_enc, self.token)
52
-
53
- def get_custom_headers_secret(self) -> SecretDict:
54
- """Get custom headers as a SecretDict object, preferring encrypted over plaintext."""
55
- return SecretDict.from_db(self.custom_headers_enc, self.custom_headers)
52
+ if self.token_enc is not None:
53
+ return self.token_enc
54
+ return Secret.from_db(None, self.token)
55
+
56
+ def get_custom_headers_secret(self) -> Secret:
57
+ """Get custom headers as a Secret object (stores JSON string), preferring encrypted over plaintext."""
58
+ if self.custom_headers_enc is not None:
59
+ return self.custom_headers_enc
60
+ # Fallback: convert plaintext dict to JSON string and wrap in Secret
61
+ if self.custom_headers is not None:
62
+ json_str = json.dumps(self.custom_headers)
63
+ return Secret.from_plaintext(json_str)
64
+ return Secret.from_plaintext(None)
65
+
66
+ def get_custom_headers_dict(self) -> Optional[Dict[str, str]]:
67
+ """Get custom headers as a plaintext dictionary."""
68
+ secret = self.get_custom_headers_secret()
69
+ json_str = secret.get_plaintext()
70
+ if json_str:
71
+ try:
72
+ return json.loads(json_str)
73
+ except (json.JSONDecodeError, TypeError):
74
+ return None
75
+ return None
56
76
 
57
77
  def set_token_secret(self, secret: Secret) -> None:
58
78
  """Set token from a Secret object, updating both encrypted and plaintext fields."""
79
+ self.token_enc = secret
59
80
  secret_dict = secret.to_dict()
60
- self.token_enc = secret_dict["encrypted"]
61
81
  # Only set plaintext during migration phase
62
- if not secret._was_encrypted:
82
+ if not secret.was_encrypted:
63
83
  self.token = secret_dict["plaintext"]
64
84
  else:
65
85
  self.token = None
66
86
 
67
- def set_custom_headers_secret(self, secret: SecretDict) -> None:
68
- """Set custom headers from a SecretDict object, updating both fields."""
87
+ def set_custom_headers_secret(self, secret: Secret) -> None:
88
+ """Set custom headers from a Secret object (containing JSON string), updating both fields."""
89
+ self.custom_headers_enc = secret
69
90
  secret_dict = secret.to_dict()
70
- self.custom_headers_enc = secret_dict["encrypted"]
71
- # Only set plaintext during migration phase
72
- if not secret._was_encrypted:
73
- self.custom_headers = secret_dict["plaintext"]
91
+ # Parse JSON string to dict for plaintext field
92
+ json_str = secret_dict.get("plaintext")
93
+ if json_str and not secret.was_encrypted:
94
+ try:
95
+ self.custom_headers = json.loads(json_str)
96
+ except (json.JSONDecodeError, TypeError):
97
+ self.custom_headers = None
74
98
  else:
75
99
  self.custom_headers = None
76
100
 
77
- def model_dump(self, to_orm: bool = False, **kwargs):
78
- """Override model_dump to handle encryption when saving to database."""
79
- data = super().model_dump(to_orm=to_orm, **kwargs)
80
-
81
- if to_orm and settings.encryption_key:
82
- # Encrypt token if present
83
- if self.token is not None:
84
- token_secret = Secret.from_plaintext(self.token)
85
- secret_dict = token_secret.to_dict()
86
- data["token_enc"] = secret_dict["encrypted"]
87
- # Keep plaintext for dual-write during migration
88
- data["token"] = secret_dict["plaintext"]
89
-
90
- # Encrypt custom headers if present
91
- if self.custom_headers is not None:
92
- headers_secret = SecretDict.from_plaintext(self.custom_headers)
93
- secret_dict = headers_secret.to_dict()
94
- data["custom_headers_enc"] = secret_dict["encrypted"]
95
- # Keep plaintext for dual-write during migration
96
- data["custom_headers"] = secret_dict["plaintext"]
97
-
98
- return data
99
-
100
101
  def to_config(
101
102
  self,
102
103
  environment_variables: Optional[Dict[str, str]] = None,
@@ -106,8 +107,8 @@ class MCPServer(BaseMCPServer):
106
107
  token_secret = self.get_token_secret()
107
108
  token_plaintext = token_secret.get_plaintext()
108
109
 
109
- headers_secret = self.get_custom_headers_secret()
110
- headers_plaintext = headers_secret.get_plaintext()
110
+ # Get custom headers as dict
111
+ headers_plaintext = self.get_custom_headers_dict()
111
112
 
112
113
  if self.server_type == MCPServerType.SSE:
113
114
  config = SSEServerConfig(
@@ -147,6 +148,7 @@ class MCPServer(BaseMCPServer):
147
148
  class UpdateSSEMCPServer(LettaBase):
148
149
  """Update an SSE MCP server"""
149
150
 
151
+ server_name: Optional[str] = Field(None, description="The name of the MCP server")
150
152
  server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
151
153
  token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for SSE authentication)")
152
154
  custom_headers: Optional[Dict[str, str]] = Field(None, description="Custom authentication headers as key-value pairs")
@@ -155,6 +157,7 @@ class UpdateSSEMCPServer(LettaBase):
155
157
  class UpdateStdioMCPServer(LettaBase):
156
158
  """Update a Stdio MCP server"""
157
159
 
160
+ server_name: Optional[str] = Field(None, description="The name of the MCP server")
158
161
  stdio_config: Optional[StdioServerConfig] = Field(
159
162
  None, description="The configuration for the server (MCP 'local' client will run this command)"
160
163
  )
@@ -163,6 +166,7 @@ class UpdateStdioMCPServer(LettaBase):
163
166
  class UpdateStreamableHTTPMCPServer(LettaBase):
164
167
  """Update a Streamable HTTP MCP server"""
165
168
 
169
+ server_name: Optional[str] = Field(None, description="The name of the MCP server")
166
170
  server_url: Optional[str] = Field(None, description="The URL path for the streamable HTTP server (e.g., 'example/mcp')")
167
171
  auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
168
172
  auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
@@ -194,6 +198,9 @@ class MCPOAuthSession(BaseMCPOAuth):
194
198
  authorization_url: Optional[str] = Field(None, description="OAuth authorization URL")
195
199
  authorization_code: Optional[str] = Field(None, description="OAuth authorization code")
196
200
 
201
+ # Encrypted authorization code (for internal use)
202
+ authorization_code_enc: Secret | None = Field(None, description="Encrypted OAuth authorization code as Secret object")
203
+
197
204
  # Token data
198
205
  access_token: Optional[str] = Field(None, description="OAuth access token")
199
206
  refresh_token: Optional[str] = Field(None, description="OAuth refresh token")
@@ -202,8 +209,8 @@ class MCPOAuthSession(BaseMCPOAuth):
202
209
  scope: Optional[str] = Field(None, description="OAuth scope")
203
210
 
204
211
  # Encrypted token fields (for internal use)
205
- access_token_enc: Optional[str] = Field(None, description="Encrypted OAuth access token")
206
- refresh_token_enc: Optional[str] = Field(None, description="Encrypted OAuth refresh token")
212
+ access_token_enc: Secret | None = Field(None, description="Encrypted OAuth access token as Secret object")
213
+ refresh_token_enc: Secret | None = Field(None, description="Encrypted OAuth refresh token as Secret object")
207
214
 
208
215
  # Client configuration
209
216
  client_id: Optional[str] = Field(None, description="OAuth client ID")
@@ -211,7 +218,7 @@ class MCPOAuthSession(BaseMCPOAuth):
211
218
  redirect_uri: Optional[str] = Field(None, description="OAuth redirect URI")
212
219
 
213
220
  # Encrypted client secret (for internal use)
214
- client_secret_enc: Optional[str] = Field(None, description="Encrypted OAuth client secret")
221
+ client_secret_enc: Secret | None = Field(None, description="Encrypted OAuth client secret as Secret object")
215
222
 
216
223
  # Session state
217
224
  status: OAuthSessionStatus = Field(default=OAuthSessionStatus.PENDING, description="Session status")
@@ -222,73 +229,63 @@ class MCPOAuthSession(BaseMCPOAuth):
222
229
 
223
230
  def get_access_token_secret(self) -> Secret:
224
231
  """Get the access token as a Secret object, preferring encrypted over plaintext."""
225
- return Secret.from_db(self.access_token_enc, self.access_token)
232
+ if self.access_token_enc is not None:
233
+ return self.access_token_enc
234
+ return Secret.from_db(None, self.access_token)
226
235
 
227
236
  def get_refresh_token_secret(self) -> Secret:
228
237
  """Get the refresh token as a Secret object, preferring encrypted over plaintext."""
229
- return Secret.from_db(self.refresh_token_enc, self.refresh_token)
238
+ if self.refresh_token_enc is not None:
239
+ return self.refresh_token_enc
240
+ return Secret.from_db(None, self.refresh_token)
230
241
 
231
242
  def get_client_secret_secret(self) -> Secret:
232
243
  """Get the client secret as a Secret object, preferring encrypted over plaintext."""
233
- return Secret.from_db(self.client_secret_enc, self.client_secret)
244
+ if self.client_secret_enc is not None:
245
+ return self.client_secret_enc
246
+ return Secret.from_db(None, self.client_secret)
247
+
248
+ def get_authorization_code_secret(self) -> Secret:
249
+ """Get the authorization code as a Secret object, preferring encrypted over plaintext."""
250
+ if self.authorization_code_enc is not None:
251
+ return self.authorization_code_enc
252
+ return Secret.from_db(None, self.authorization_code)
234
253
 
235
254
  def set_access_token_secret(self, secret: Secret) -> None:
236
255
  """Set access token from a Secret object."""
256
+ self.access_token_enc = secret
237
257
  secret_dict = secret.to_dict()
238
- self.access_token_enc = secret_dict["encrypted"]
239
- if not secret._was_encrypted:
258
+ if not secret.was_encrypted:
240
259
  self.access_token = secret_dict["plaintext"]
241
260
  else:
242
261
  self.access_token = None
243
262
 
244
263
  def set_refresh_token_secret(self, secret: Secret) -> None:
245
264
  """Set refresh token from a Secret object."""
265
+ self.refresh_token_enc = secret
246
266
  secret_dict = secret.to_dict()
247
- self.refresh_token_enc = secret_dict["encrypted"]
248
- if not secret._was_encrypted:
267
+ if not secret.was_encrypted:
249
268
  self.refresh_token = secret_dict["plaintext"]
250
269
  else:
251
270
  self.refresh_token = None
252
271
 
253
272
  def set_client_secret_secret(self, secret: Secret) -> None:
254
273
  """Set client secret from a Secret object."""
274
+ self.client_secret_enc = secret
255
275
  secret_dict = secret.to_dict()
256
- self.client_secret_enc = secret_dict["encrypted"]
257
- if not secret._was_encrypted:
276
+ if not secret.was_encrypted:
258
277
  self.client_secret = secret_dict["plaintext"]
259
278
  else:
260
279
  self.client_secret = None
261
280
 
262
- def model_dump(self, to_orm: bool = False, **kwargs):
263
- """Override model_dump to handle encryption when saving to database."""
264
- data = super().model_dump(to_orm=to_orm, **kwargs)
265
-
266
- if to_orm and settings.encryption_key:
267
- # Encrypt access token if present
268
- if self.access_token is not None:
269
- token_secret = Secret.from_plaintext(self.access_token)
270
- secret_dict = token_secret.to_dict()
271
- data["access_token_enc"] = secret_dict["encrypted"]
272
- # Keep plaintext for dual-write during migration
273
- data["access_token"] = secret_dict["plaintext"]
274
-
275
- # Encrypt refresh token if present
276
- if self.refresh_token is not None:
277
- token_secret = Secret.from_plaintext(self.refresh_token)
278
- secret_dict = token_secret.to_dict()
279
- data["refresh_token_enc"] = secret_dict["encrypted"]
280
- # Keep plaintext for dual-write during migration
281
- data["refresh_token"] = secret_dict["plaintext"]
282
-
283
- # Encrypt client secret if present
284
- if self.client_secret is not None:
285
- secret = Secret.from_plaintext(self.client_secret)
286
- secret_dict = secret.to_dict()
287
- data["client_secret_enc"] = secret_dict["encrypted"]
288
- # Keep plaintext for dual-write during migration
289
- data["client_secret"] = secret_dict["plaintext"]
290
-
291
- return data
281
+ def set_authorization_code_secret(self, secret: Secret) -> None:
282
+ """Set authorization code from a Secret object."""
283
+ self.authorization_code_enc = secret
284
+ secret_dict = secret.to_dict()
285
+ if not secret.was_encrypted:
286
+ self.authorization_code = secret_dict["plaintext"]
287
+ else:
288
+ self.authorization_code = None
292
289
 
293
290
 
294
291
  class MCPOAuthSessionCreate(BaseMCPOAuth):