@letta-ai/letta-agent-sdk 0.5.4 → 0.5.6
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/README.md +26 -8
- package/dist/app-server-session.d.ts +1 -0
- package/dist/app-server-session.d.ts.map +1 -1
- package/dist/client-entry.js +102 -22
- package/dist/client-entry.js.map +8 -7
- package/dist/cloud-session.d.ts +1 -0
- package/dist/cloud-session.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22315 -7365
- package/dist/index.js.map +124 -8
- package/dist/mcp-runtime.d.ts +15 -0
- package/dist/mcp-runtime.d.ts.map +1 -0
- package/dist/mcp.d.ts +9 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/remote-client-session-core.d.ts.map +1 -1
- package/dist/remote-turn-coordinator.d.ts +2 -0
- package/dist/remote-turn-coordinator.d.ts.map +1 -1
- package/dist/types.d.ts +24 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/app-server-session.ts +107 -35
- package/src/cloud-session.ts +18 -1
- package/src/index.ts +6 -0
- package/src/mcp-runtime.ts +39 -0
- package/src/mcp.ts +203 -0
- package/src/remote-client-session-core.ts +2 -0
- package/src/remote-turn-coordinator.ts +6 -0
- package/src/types.ts +27 -0
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import {
|
|
3
|
+
getDefaultEnvironment,
|
|
4
|
+
StdioClientTransport,
|
|
5
|
+
} from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import type {
|
|
7
|
+
ConnectMcpServersOptions,
|
|
8
|
+
McpToolBridge,
|
|
9
|
+
} from "./mcp-runtime.js";
|
|
10
|
+
import type {
|
|
11
|
+
AgentToolResultContent,
|
|
12
|
+
AnyAgentTool,
|
|
13
|
+
McpServerConfig,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
const EMPTY_BRIDGE: McpToolBridge = {
|
|
17
|
+
tools: [],
|
|
18
|
+
close: async () => undefined,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const CLIENT_INFO = {
|
|
22
|
+
name: "@letta-ai/letta-agent-sdk",
|
|
23
|
+
version: "1",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Connect stdio MCP servers and expose their tools through Letta Code's
|
|
28
|
+
* external-tool protocol. A broken server is skipped so it cannot prevent the
|
|
29
|
+
* rest of a session from starting.
|
|
30
|
+
*/
|
|
31
|
+
export async function connectMcpServers(
|
|
32
|
+
servers: readonly McpServerConfig[] | undefined,
|
|
33
|
+
options: ConnectMcpServersOptions = {},
|
|
34
|
+
): Promise<McpToolBridge> {
|
|
35
|
+
if (!servers || servers.length === 0) return EMPTY_BRIDGE;
|
|
36
|
+
|
|
37
|
+
const log = options.log ?? ((message: string) => console.error(message));
|
|
38
|
+
const clients: Client[] = [];
|
|
39
|
+
const tools: AnyAgentTool[] = [];
|
|
40
|
+
const taken = new Set(options.reservedToolNames ?? []);
|
|
41
|
+
|
|
42
|
+
for (const server of servers) {
|
|
43
|
+
let client: Client | null = null;
|
|
44
|
+
try {
|
|
45
|
+
client = new Client(CLIENT_INFO);
|
|
46
|
+
await client.connect(
|
|
47
|
+
new StdioClientTransport({
|
|
48
|
+
command: server.command,
|
|
49
|
+
args: server.args ?? [],
|
|
50
|
+
env: {
|
|
51
|
+
...getDefaultEnvironment(),
|
|
52
|
+
...normalizeEnvironment(server.env),
|
|
53
|
+
},
|
|
54
|
+
cwd: server.cwd ?? options.cwd,
|
|
55
|
+
stderr: "inherit",
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const listed = await client.listTools();
|
|
60
|
+
clients.push(client);
|
|
61
|
+
for (const tool of listed.tools) {
|
|
62
|
+
const name = uniqueName(
|
|
63
|
+
`mcp__${sanitize(server.name)}__${sanitize(tool.name)}`,
|
|
64
|
+
taken,
|
|
65
|
+
);
|
|
66
|
+
tools.push(
|
|
67
|
+
bridgeTool(client, server.name, tool, name),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
log(
|
|
71
|
+
`MCP server "${server.name}" connected (${listed.tools.length} tool${listed.tools.length === 1 ? "" : "s"})`,
|
|
72
|
+
);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
if (client && !clients.includes(client)) {
|
|
75
|
+
await client.close().catch(() => undefined);
|
|
76
|
+
}
|
|
77
|
+
log(`MCP server "${server.name}" unavailable: ${String(error)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (clients.length === 0) return EMPTY_BRIDGE;
|
|
82
|
+
|
|
83
|
+
let closed = false;
|
|
84
|
+
return {
|
|
85
|
+
tools,
|
|
86
|
+
close: async () => {
|
|
87
|
+
if (closed) return;
|
|
88
|
+
closed = true;
|
|
89
|
+
await Promise.all(
|
|
90
|
+
clients.map((client) => client.close().catch(() => undefined)),
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function normalizeEnvironment(
|
|
97
|
+
env: McpServerConfig["env"],
|
|
98
|
+
): Record<string, string> {
|
|
99
|
+
if (!env) return {};
|
|
100
|
+
if (!Array.isArray(env)) return env;
|
|
101
|
+
|
|
102
|
+
const result: Record<string, string> = {};
|
|
103
|
+
for (const entry of env) {
|
|
104
|
+
result[entry.name] = entry.value;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface McpToolDefinition {
|
|
110
|
+
name: string;
|
|
111
|
+
title?: string;
|
|
112
|
+
description?: string;
|
|
113
|
+
inputSchema?: unknown;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function bridgeTool(
|
|
117
|
+
client: Client,
|
|
118
|
+
serverName: string,
|
|
119
|
+
tool: McpToolDefinition,
|
|
120
|
+
name: string,
|
|
121
|
+
): AnyAgentTool {
|
|
122
|
+
return {
|
|
123
|
+
name,
|
|
124
|
+
label: tool.title ?? tool.name,
|
|
125
|
+
description:
|
|
126
|
+
tool.description && tool.description.trim().length > 0
|
|
127
|
+
? tool.description
|
|
128
|
+
: `The ${tool.name} tool from the ${serverName} MCP server.`,
|
|
129
|
+
parameters: toolParameters(tool.inputSchema),
|
|
130
|
+
execute: async (_toolCallId, args, signal) => {
|
|
131
|
+
const result = await client.callTool(
|
|
132
|
+
{
|
|
133
|
+
name: tool.name,
|
|
134
|
+
arguments: isRecord(args) ? args : {},
|
|
135
|
+
},
|
|
136
|
+
undefined,
|
|
137
|
+
signal ? { signal } : undefined,
|
|
138
|
+
);
|
|
139
|
+
const content = toToolResultContent(result.content);
|
|
140
|
+
if (result.isError) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
content
|
|
143
|
+
.map((item) => item.text ?? "")
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
.join("\n") || `${tool.name} failed`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return { content };
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function toolParameters(inputSchema: unknown): Record<string, unknown> {
|
|
154
|
+
if (isRecord(inputSchema) && inputSchema.type === "object") {
|
|
155
|
+
return inputSchema;
|
|
156
|
+
}
|
|
157
|
+
return { type: "object", properties: {} };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toToolResultContent(content: unknown): AgentToolResultContent[] {
|
|
161
|
+
if (!Array.isArray(content)) return [];
|
|
162
|
+
const mapped: AgentToolResultContent[] = [];
|
|
163
|
+
for (const block of content) {
|
|
164
|
+
if (!isRecord(block)) continue;
|
|
165
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
166
|
+
mapped.push({ type: "text", text: block.text });
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
block.type === "image" &&
|
|
171
|
+
typeof block.data === "string" &&
|
|
172
|
+
typeof block.mimeType === "string"
|
|
173
|
+
) {
|
|
174
|
+
mapped.push({
|
|
175
|
+
type: "image",
|
|
176
|
+
data: block.data,
|
|
177
|
+
mimeType: block.mimeType,
|
|
178
|
+
});
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
mapped.push({ type: "text", text: JSON.stringify(block) });
|
|
182
|
+
}
|
|
183
|
+
return mapped;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function sanitize(name: string): string {
|
|
187
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function uniqueName(name: string, taken: Set<string>): string {
|
|
191
|
+
let candidate = name;
|
|
192
|
+
let suffix = 2;
|
|
193
|
+
while (taken.has(candidate)) {
|
|
194
|
+
candidate = `${name}_${suffix}`;
|
|
195
|
+
suffix += 1;
|
|
196
|
+
}
|
|
197
|
+
taken.add(candidate);
|
|
198
|
+
return candidate;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
202
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
203
|
+
}
|
|
@@ -92,6 +92,8 @@ export abstract class RemoteClientSessionCore implements LettaCodeSession {
|
|
|
92
92
|
this.turns = new RemoteTurnCoordinator({
|
|
93
93
|
label: config.label,
|
|
94
94
|
requestTimeoutMs: config.requestTimeoutMs,
|
|
95
|
+
autoHandlesToolApprovals:
|
|
96
|
+
mode.kind === "session" && typeof mode.options.canUseTool === "function",
|
|
95
97
|
onDeviceStatus: (status) => this.emitDeviceStatus(status),
|
|
96
98
|
});
|
|
97
99
|
}
|
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
type RemoteTurnCoordinatorConfig = {
|
|
36
36
|
label: string;
|
|
37
37
|
requestTimeoutMs?: number;
|
|
38
|
+
autoHandlesToolApprovals?: boolean;
|
|
38
39
|
onDeviceStatus(status: SessionDeviceStatus): void;
|
|
39
40
|
};
|
|
40
41
|
|
|
@@ -48,6 +49,7 @@ type RemoteTurnCoordinatorConfig = {
|
|
|
48
49
|
export class RemoteTurnCoordinator {
|
|
49
50
|
private readonly label: string;
|
|
50
51
|
private readonly requestTimeoutMs: number | undefined;
|
|
52
|
+
private readonly autoHandlesToolApprovals: boolean;
|
|
51
53
|
private readonly onDeviceStatus: (status: SessionDeviceStatus) => void;
|
|
52
54
|
private streamQueue: SDKMessage[] = [];
|
|
53
55
|
private streamResolvers: Array<(message: SDKMessage | null) => void> = [];
|
|
@@ -62,6 +64,7 @@ export class RemoteTurnCoordinator {
|
|
|
62
64
|
constructor(config: RemoteTurnCoordinatorConfig) {
|
|
63
65
|
this.label = config.label;
|
|
64
66
|
this.requestTimeoutMs = config.requestTimeoutMs;
|
|
67
|
+
this.autoHandlesToolApprovals = config.autoHandlesToolApprovals === true;
|
|
65
68
|
this.onDeviceStatus = config.onDeviceStatus;
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -239,6 +242,9 @@ export class RemoteTurnCoordinator {
|
|
|
239
242
|
active.observedTurnEvidence || active.observedRequiresApprovalStop;
|
|
240
243
|
if (!hadTurnEvidence) return;
|
|
241
244
|
if (status === "WAITING_ON_APPROVAL") {
|
|
245
|
+
// Loop status and approval requests arrive on different sockets. Keep a
|
|
246
|
+
// callback-backed turn open until its control request continues the turn.
|
|
247
|
+
if (this.autoHandlesToolApprovals) return;
|
|
242
248
|
this.completeActiveTurn({
|
|
243
249
|
runtime: active.runtime,
|
|
244
250
|
stopReason: "requires_approval",
|
package/src/types.ts
CHANGED
|
@@ -266,6 +266,27 @@ export interface AgentTool<TParams, TResult> {
|
|
|
266
266
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
267
267
|
export type AnyAgentTool = AgentTool<any, unknown>;
|
|
268
268
|
|
|
269
|
+
/** Environment variable passed to a stdio MCP server. */
|
|
270
|
+
export interface McpServerEnvironmentVariable {
|
|
271
|
+
name: string;
|
|
272
|
+
value: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* A stdio MCP server whose tools should be exposed to a Letta Code session.
|
|
277
|
+
*
|
|
278
|
+
* The environment accepts either the object form commonly used by MCP clients
|
|
279
|
+
* or the name/value array used by ACP clients such as Zed.
|
|
280
|
+
*/
|
|
281
|
+
export interface McpServerConfig {
|
|
282
|
+
name: string;
|
|
283
|
+
command: string;
|
|
284
|
+
args?: string[];
|
|
285
|
+
env?: Record<string, string> | McpServerEnvironmentVariable[];
|
|
286
|
+
/** Override the session cwd for this server process. */
|
|
287
|
+
cwd?: string;
|
|
288
|
+
}
|
|
289
|
+
|
|
269
290
|
// ═══════════════════════════════════════════════════════════════
|
|
270
291
|
// TOP-LEVEL CLIENT TYPES
|
|
271
292
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -646,6 +667,12 @@ export interface CreateSessionOptions {
|
|
|
646
667
|
*/
|
|
647
668
|
tools?: AnyAgentTool[];
|
|
648
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Stdio MCP servers whose tools execute in the SDK process and are exposed
|
|
672
|
+
* through Letta Code's external-tool protocol for this session.
|
|
673
|
+
*/
|
|
674
|
+
mcpServers?: McpServerConfig[];
|
|
675
|
+
|
|
649
676
|
/**
|
|
650
677
|
* Max automatic approval-conflict recovery attempts per runTurn() call.
|
|
651
678
|
* Set to 0 to disable automatic recovery.
|