@letta-ai/letta-agent-sdk 0.5.5 → 0.5.7

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.
package/src/mcp.ts ADDED
@@ -0,0 +1,213 @@
1
+ import {
2
+ type ConnectedMcpServer,
3
+ connectMcpServer,
4
+ type McpServerConfig as LettaMcpServerConfig,
5
+ type McpToolDefinition,
6
+ } from "@letta-ai/letta-code/mcp-client";
7
+ import type {
8
+ ConnectMcpServersOptions,
9
+ McpToolBridge,
10
+ } from "./mcp-runtime.js";
11
+ import type {
12
+ AgentToolResultContent,
13
+ AnyAgentTool,
14
+ McpServerConfig,
15
+ McpServers,
16
+ } from "./types.js";
17
+
18
+ const EMPTY_BRIDGE: McpToolBridge = {
19
+ tools: [],
20
+ close: async () => undefined,
21
+ };
22
+
23
+ const CLIENT_INFO = {
24
+ name: "@letta-ai/letta-agent-sdk",
25
+ version: "1",
26
+ };
27
+
28
+ type NamedMcpServer = {
29
+ name: string;
30
+ config: McpServerConfig;
31
+ };
32
+
33
+ type ConnectionResult =
34
+ | {
35
+ server: NamedMcpServer;
36
+ connection: ConnectedMcpServer;
37
+ }
38
+ | {
39
+ server: NamedMcpServer;
40
+ error: unknown;
41
+ };
42
+
43
+ /**
44
+ * Connect session-scoped MCP servers through Letta Code's transport-neutral
45
+ * client and expose their tools through the external-tool protocol. A broken
46
+ * server is reported but cannot prevent healthy servers from loading.
47
+ */
48
+ export async function connectMcpServers(
49
+ servers: McpServers | undefined,
50
+ options: ConnectMcpServersOptions = {},
51
+ ): Promise<McpToolBridge> {
52
+ const namedServers = Object.entries(servers ?? {}).map(([name, config]) => ({
53
+ name,
54
+ config,
55
+ }));
56
+ if (namedServers.length === 0) return EMPTY_BRIDGE;
57
+
58
+ const log = options.log ?? ((message: string) => console.error(message));
59
+ const results = await Promise.all(
60
+ namedServers.map(async (server): Promise<ConnectionResult> => {
61
+ try {
62
+ const connection = await connectMcpServer(
63
+ toLettaMcpServerConfig(server, options.cwd),
64
+ {
65
+ clientInfo: CLIENT_INFO,
66
+ stderr: "inherit",
67
+ },
68
+ );
69
+ return { server, connection };
70
+ } catch (error) {
71
+ return { server, error };
72
+ }
73
+ }),
74
+ );
75
+
76
+ const connections: ConnectedMcpServer[] = [];
77
+ const tools: AnyAgentTool[] = [];
78
+ const taken = new Set(options.reservedToolNames ?? []);
79
+
80
+ for (const result of results) {
81
+ if ("error" in result) {
82
+ log(
83
+ `MCP server "${result.server.name}" unavailable: ${errorMessage(result.error)}`,
84
+ );
85
+ continue;
86
+ }
87
+
88
+ connections.push(result.connection);
89
+ for (const tool of result.connection.tools) {
90
+ const name = uniqueName(
91
+ `mcp__${sanitize(result.server.name)}__${sanitize(tool.name)}`,
92
+ taken,
93
+ );
94
+ tools.push(bridgeTool(result.connection, result.server.name, tool, name));
95
+ }
96
+ log(
97
+ `MCP server "${result.server.name}" connected (${result.connection.tools.length} tool${result.connection.tools.length === 1 ? "" : "s"})`,
98
+ );
99
+ }
100
+
101
+ let closed = false;
102
+ return {
103
+ tools,
104
+ close: async () => {
105
+ if (closed) return;
106
+ closed = true;
107
+ await Promise.allSettled(
108
+ connections.map((connection) => connection.close()),
109
+ );
110
+ },
111
+ };
112
+ }
113
+
114
+ function toLettaMcpServerConfig(
115
+ server: NamedMcpServer,
116
+ sessionCwd: string | undefined,
117
+ ): LettaMcpServerConfig {
118
+ const config = server.config;
119
+ if (config.type === "http" || config.type === "sse") {
120
+ return {
121
+ name: server.name,
122
+ transport: config.type,
123
+ url: config.url,
124
+ ...(config.headers ? { headers: config.headers } : {}),
125
+ };
126
+ }
127
+ return {
128
+ name: server.name,
129
+ transport: "stdio",
130
+ command: config.command,
131
+ args: config.args ?? [],
132
+ ...(config.env ? { env: config.env } : {}),
133
+ ...(config.cwd ?? sessionCwd
134
+ ? { cwd: config.cwd ?? sessionCwd }
135
+ : {}),
136
+ };
137
+ }
138
+
139
+ function bridgeTool(
140
+ connection: ConnectedMcpServer,
141
+ serverName: string,
142
+ tool: McpToolDefinition,
143
+ name: string,
144
+ ): AnyAgentTool {
145
+ return {
146
+ name,
147
+ label: tool.title ?? tool.name,
148
+ description:
149
+ tool.description && tool.description.trim().length > 0
150
+ ? tool.description
151
+ : `The ${tool.name} tool from the ${serverName} MCP server.`,
152
+ parameters: tool.inputSchema,
153
+ execute: async (_toolCallId, args, signal) => {
154
+ const result = await connection.callTool(
155
+ tool.name,
156
+ isRecord(args) ? args : {},
157
+ signal ? { signal } : undefined,
158
+ );
159
+ return {
160
+ content: toToolResultContent(result.content),
161
+ ...(result.isError === true ? { isError: true } : {}),
162
+ };
163
+ },
164
+ };
165
+ }
166
+
167
+ function toToolResultContent(content: unknown[]): AgentToolResultContent[] {
168
+ const mapped: AgentToolResultContent[] = [];
169
+ for (const block of content) {
170
+ if (!isRecord(block)) continue;
171
+ if (block.type === "text" && typeof block.text === "string") {
172
+ mapped.push({ type: "text", text: block.text });
173
+ continue;
174
+ }
175
+ if (
176
+ block.type === "image" &&
177
+ typeof block.data === "string" &&
178
+ typeof block.mimeType === "string"
179
+ ) {
180
+ mapped.push({
181
+ type: "image",
182
+ data: block.data,
183
+ mimeType: block.mimeType,
184
+ });
185
+ continue;
186
+ }
187
+ mapped.push({ type: "text", text: JSON.stringify(block) });
188
+ }
189
+ return mapped;
190
+ }
191
+
192
+ function sanitize(name: string): string {
193
+ return name.replace(/[^a-zA-Z0-9_-]/g, "_") || "tool";
194
+ }
195
+
196
+ function uniqueName(name: string, taken: Set<string>): string {
197
+ let candidate = name;
198
+ let suffix = 2;
199
+ while (taken.has(candidate)) {
200
+ candidate = `${name}_${suffix}`;
201
+ suffix += 1;
202
+ }
203
+ taken.add(candidate);
204
+ return candidate;
205
+ }
206
+
207
+ function errorMessage(error: unknown): string {
208
+ return error instanceof Error ? error.message : String(error);
209
+ }
210
+
211
+ function isRecord(value: unknown): value is Record<string, unknown> {
212
+ return typeof value === "object" && value !== null && !Array.isArray(value);
213
+ }
@@ -572,6 +572,7 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
572
572
 
573
573
  async [Symbol.asyncDispose](): Promise<void> {
574
574
  this.close();
575
+ await this.onCoreDisposed();
575
576
  }
576
577
 
577
578
  async changeDeviceState(
@@ -634,6 +635,10 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
634
635
  // Optional hook for subclass-owned resources outside the controller.
635
636
  }
636
637
 
638
+ protected async onCoreDisposed(): Promise<void> {
639
+ // Optional hook for awaiting asynchronous cleanup started by onCoreClose().
640
+ }
641
+
637
642
  protected currentOptions(): LettaCodeClientSessionOptions | CreateAgentOptions {
638
643
  return this.mode.options;
639
644
  }
package/src/types.ts CHANGED
@@ -227,6 +227,8 @@ export interface AgentToolResultContent {
227
227
  */
228
228
  export interface AgentToolResult<T> {
229
229
  content: AgentToolResultContent[];
230
+ /** Whether the tool completed with a model-visible error result. */
231
+ isError?: boolean;
230
232
  details?: T;
231
233
  }
232
234
 
@@ -266,6 +268,39 @@ export interface AgentTool<TParams, TResult> {
266
268
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
267
269
  export type AnyAgentTool = AgentTool<any, unknown>;
268
270
 
271
+ /** A local MCP process connected over stdin/stdout. */
272
+ export interface McpStdioServerConfig {
273
+ type?: "stdio";
274
+ command: string;
275
+ args?: string[];
276
+ env?: Record<string, string>;
277
+ /** Override the session cwd for this server process. */
278
+ cwd?: string;
279
+ }
280
+
281
+ /** A remote MCP server using Streamable HTTP. */
282
+ export interface McpHttpServerConfig {
283
+ type: "http";
284
+ url: string;
285
+ headers?: Record<string, string>;
286
+ }
287
+
288
+ /** A remote MCP server using the legacy SSE transport. */
289
+ export interface McpSseServerConfig {
290
+ type: "sse";
291
+ url: string;
292
+ headers?: Record<string, string>;
293
+ }
294
+
295
+ /** One MCP server definition. The key in {@link McpServers} supplies its name. */
296
+ export type McpServerConfig =
297
+ | McpStdioServerConfig
298
+ | McpHttpServerConfig
299
+ | McpSseServerConfig;
300
+
301
+ /** MCP servers keyed by the name used in `mcp__<server>__<tool>`. */
302
+ export type McpServers = Record<string, McpServerConfig>;
303
+
269
304
  // ═══════════════════════════════════════════════════════════════
270
305
  // TOP-LEVEL CLIENT TYPES
271
306
  // ═══════════════════════════════════════════════════════════════
@@ -646,6 +681,13 @@ export interface CreateSessionOptions {
646
681
  */
647
682
  tools?: AnyAgentTool[];
648
683
 
684
+ /**
685
+ * Session-scoped MCP servers keyed by server name. Stdio processes and
686
+ * remote HTTP/SSE connections run in the Node SDK process and are exposed
687
+ * through Letta Code's external-tool protocol.
688
+ */
689
+ mcpServers?: McpServers;
690
+
649
691
  /**
650
692
  * Max automatic approval-conflict recovery attempts per runTurn() call.
651
693
  * Set to 0 to disable automatic recovery.
package/src/validation.ts CHANGED
@@ -8,6 +8,7 @@ import type {
8
8
  CreateSessionOptions,
9
9
  CreateAgentOptions,
10
10
  MemoryItem,
11
+ McpServers,
11
12
  CreateBlock,
12
13
  SystemPromptPreset,
13
14
  SkillSource,
@@ -173,11 +174,41 @@ function validateReasoningEffort(value: unknown): void {
173
174
  }
174
175
  }
175
176
 
177
+ function validateMcpServers(servers: McpServers | undefined): void {
178
+ if (servers === undefined) return;
179
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
180
+ throw new Error("Invalid mcpServers. Expected an object keyed by server name.");
181
+ }
182
+ for (const [name, config] of Object.entries(servers)) {
183
+ if (name.length === 0) {
184
+ throw new Error("Invalid mcpServers. Server names must be non-empty.");
185
+ }
186
+ if (!config || typeof config !== "object" || Array.isArray(config)) {
187
+ throw new Error(`Invalid MCP server '${name}'. Expected a configuration object.`);
188
+ }
189
+ if (config.type === "http" || config.type === "sse") {
190
+ if (typeof config.url !== "string" || config.url.length === 0) {
191
+ throw new Error(`Invalid MCP server '${name}'. Expected a non-empty url.`);
192
+ }
193
+ continue;
194
+ }
195
+ if (config.type !== undefined && config.type !== "stdio") {
196
+ throw new Error(
197
+ `Invalid MCP server '${name}' type '${String(config.type)}'. Valid values: stdio, http, sse.`,
198
+ );
199
+ }
200
+ if (typeof config.command !== "string" || config.command.length === 0) {
201
+ throw new Error(`Invalid MCP server '${name}'. Expected a non-empty command.`);
202
+ }
203
+ }
204
+ }
205
+
176
206
  /**
177
207
  * Validate CreateSessionOptions (used by createSession and resumeSession).
178
208
  */
179
209
  export function validateCreateSessionOptions(options: CreateSessionOptions): void {
180
210
  validateSkillSources(options.skillSources);
211
+ validateMcpServers(options.mcpServers);
181
212
  validateReasoningEffort(options.reasoningEffort);
182
213
  validateDreamingOptions(options.dreaming);
183
214
  validateApprovalRecoveryOptions(options);