Letta MCP Server
by oculairmedia
- src
#!/usr/bin/env node
import axios from 'axios';
import type { AxiosInstance, AxiosRequestConfig } from 'axios';
import dotenv from 'dotenv';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
McpError,
ErrorCode,
} from '@modelcontextprotocol/sdk/types.js';
// Load environment variables
dotenv.config();
// Types
interface MCPRequest {
id: string;
method: string;
params: any;
}
interface MCPResponse {
id: string;
result?: any;
error?: {
code: number;
message: string;
data?: any;
};
}
type HeadersConfig = AxiosRequestConfig['headers'];
class LettaServer {
private server: Server;
private api: AxiosInstance;
private apiBase: string;
private password: string;
private existingAgentId: string;
private addressBookId: string;
constructor() {
// Initialize MCP server
this.server = new Server(
{
name: 'letta-server',
version: '0.1.0',
},
{
capabilities: {
tools: {},
},
}
);
// Set up error handler
this.server.onerror = (error: any) => console.error('[MCP Error]', error);
// Set up tool handlers
this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'create_agent',
description: 'Create a new Letta agent with specified configuration',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the new agent',
},
description: {
type: 'string',
description: 'Description of the agent\'s purpose/role',
},
model: {
type: 'string',
description: 'The model to use for the agent',
default: 'openai/gpt-4',
},
embedding: {
type: 'string',
description: 'The embedding model to use',
default: 'openai/text-embedding-ada-002',
},
},
required: ['name', 'description'],
},
},
{
name: 'list_agents',
description: 'List all available agents in the Letta system',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Optional filter to search for specific agents',
},
},
required: [],
},
},
{
name: 'prompt_agent',
description: 'Send a message to an agent and get a response',
inputSchema: {
type: 'object',
properties: {
agent_id: {
type: 'string',
description: 'ID of the agent to prompt',
},
message: {
type: 'string',
description: 'Message to send to the agent',
},
},
required: ['agent_id', 'message'],
},
},
{
name: 'list_agent_tools',
description: 'List all tools available for a specific agent',
inputSchema: {
type: 'object',
properties: {
agent_id: {
type: 'string',
description: 'ID of the agent to list tools for',
},
},
required: ['agent_id'],
},
},
{
name: 'list_tools',
description: 'List all available tools on the Letta server',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Optional filter to search for specific tools by name or description',
},
page: {
type: 'number',
description: 'Page number for pagination (starts at 1)',
},
pageSize: {
type: 'number',
description: 'Number of tools per page (1-100, default: 10)',
},
},
required: [],
},
},
{
name: 'attach_tool',
description: 'Attach a tool to an agent',
inputSchema: {
type: 'object',
properties: {
tool_id: {
type: 'string',
description: 'The ID of the tool to attach',
},
agent_id: {
type: 'string',
description: 'The ID of the agent to attach the tool to',
},
},
required: ['tool_id', 'agent_id'],
},
},
{
name: 'list_memory_blocks',
description: 'List all memory blocks available in the Letta system',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Optional filter to search for specific blocks by name or content',
},
agent_id: {
type: 'string',
description: 'Optional agent ID to list blocks for a specific agent',
},
page: {
type: 'number',
description: 'Page number for pagination (starts at 1)',
},
pageSize: {
type: 'number',
description: 'Number of blocks per page (1-100, default: 10)',
},
},
required: [],
},
},
{
name: 'attach_memory_block',
description: 'Attach a memory block to an agent',
inputSchema: {
type: 'object',
properties: {
block_id: {
type: 'string',
description: 'The ID of the memory block to attach',
},
agent_id: {
type: 'string',
description: 'The ID of the agent to attach the memory block to',
},
label: {
type: 'string',
description: 'Optional label for the memory block (e.g., "persona", "human", "system")',
},
},
required: ['block_id', 'agent_id'],
},
},
{
name: 'create_memory_block',
description: 'Create a new memory block in the Letta system',
inputSchema: {
type: 'object',
properties: {
name: {
type: 'string',
description: 'Name of the memory block',
},
label: {
type: 'string',
description: 'Label for the memory block (e.g., "persona", "human", "system")',
},
value: {
type: 'string',
description: 'Content of the memory block',
},
agent_id: {
type: 'string',
description: 'Optional agent ID to create the block for a specific agent',
},
metadata: {
type: 'object',
description: 'Optional metadata for the memory block',
},
},
required: ['name', 'label', 'value'],
},
},
],
}));
this.server.setRequestHandler(CallToolRequestSchema, async (request: any) => {
if (request.params.name === 'create_agent') {
return this.handleCreateAgent(request.params.arguments);
} else if (request.params.name === 'list_agents') {
return this.handleListAgents(request.params.arguments);
} else if (request.params.name === 'prompt_agent') {
return this.handlePromptAgent(request.params.arguments);
} else if (request.params.name === 'list_agent_tools') {
return this.handleListAgentTools(request.params.arguments);
} else if (request.params.name === 'list_tools') {
return this.handleListTools(request.params.arguments);
} else if (request.params.name === 'attach_tool') {
return this.handleAttachTool(request.params.arguments);
} else if (request.params.name === 'list_memory_blocks') {
return this.handleListMemoryBlocks(request.params.arguments);
} else if (request.params.name === 'attach_memory_block') {
return this.handleAttachMemoryBlock(request.params.arguments);
} else if (request.params.name === 'create_memory_block') {
return this.handleCreateMemoryBlock(request.params.arguments);
}
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
});
// Validate environment variables
this.apiBase = process.env.LETTA_BASE_URL ?? '';
this.password = process.env.LETTA_PASSWORD ?? '';
if (!this.apiBase || !this.password) {
throw new Error('Missing required environment variables: LETTA_BASE_URL, LETTA_PASSWORD');
}
// Initialize axios instance
this.apiBase = `${this.apiBase}/v1`;
this.api = axios.create({
baseURL: this.apiBase,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// Constants
this.existingAgentId = "agent-755f1df6-6c53-4a62-8cf5-e1c441c3bd41"; // Pansil
this.addressBookId = "block-9e32f2b6-587b-4b9d-a5a7-c24c099fb781";
}
private async handleCreateAgent(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Validate arguments
if (!args.name || !args.description || typeof args.name !== 'string' || typeof args.description !== 'string') {
throw new Error('Invalid arguments: name and description must be strings');
}
const model = args.model ?? 'openai/gpt-4';
const embedding = args.embedding ?? 'openai/text-embedding-ada-002';
// Agent configuration
const agentConfig = {
name: args.name,
description: args.description,
agent_type: "memgpt_agent",
model: model,
llm_config: {
model: model.split('/')[1],
model_endpoint_type: model.split('/')[0],
context_window: 16000,
max_tokens: 1000,
temperature: 0.7,
frequency_penalty: 0.5,
presence_penalty: 0.5,
functions_config: {
allow: true,
functions: []
}
},
embedding: embedding,
parameters: {
context_window: 16000,
max_tokens: 1000,
temperature: 0.7,
presence_penalty: 0.5,
frequency_penalty: 0.5
},
core_memory: {
persona: `I am ${args.name}.\nMy primary role is to ${args.description}.\nI prioritize clear communication and effective collaboration.\nI maintain professionalism while being approachable and helpful.`,
human: "This is my section of core memory devoted to information about humans I interact with.",
system: "I am initialized and ready to assist with tasks.",
}
};
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`
};
// 1. Create agent
const createAgentResponse = await this.api.post('/agents', agentConfig, { headers });
const agentId = createAgentResponse.data.id;
// Update headers with agent ID
headers['user_id'] = agentId;
// 2. Create and attach persona block
const personaBlock = {
label: "persona",
name: `${args.name.toLowerCase()}_persona`,
value: agentConfig.core_memory.persona,
metadata: {
type: "persona",
version: "1.0",
last_updated: new Date().toISOString()
}
};
const createBlockResponse = await this.api.post('/blocks', personaBlock, { headers });
const blockId = createBlockResponse.data.id;
await this.api.patch(
`/agents/${agentId}/core-memory/blocks/attach/${blockId}`,
{},
{ headers }
);
// 3. Attach shared memory blocks
const sharedBlocks = [
{id: "block-cf7f4221-06d5-4859-b340-91a69d6d6eea", name: "shared_human_memory", label: "human"},
{id: "block-611d08a9-2cb5-4bad-986d-16be5722bcd9", name: "shared_task_memory", label: "task_coordination"},
{id: "block-9e32f2b6-587b-4b9d-a5a7-c24c099fb781", name: "agent_address_book", label: "address_book"},
{id: "block-9199ea55-efa1-4b1b-938b-2f1781e96ec2", name: "shared_understanding", label: "understanding"},
{id: "block-7335f3aa-477e-4518-b063-1cd6053b2e06", name: "team_rules", label: "rules"}
];
for (const block of sharedBlocks) {
await this.api.patch(
`/agents/${agentId}/core-memory/blocks/attach/${block.id}`,
{},
{ headers }
);
}
// 4. Register in address book using Pansil
headers['user_id'] = this.existingAgentId;
// Get agent info
const agentInfo = await this.api.get(`/agents/${agentId}`, { headers });
// Get current address book
const addressBookResponse = await this.api.get(`/blocks/${this.addressBookId}`, { headers });
const content = addressBookResponse.data.value;
// Add new agent entry
const timestamp = new Date().toISOString();
const capabilities = agentInfo.data.tools?.map((t: any) => t.name) ?? [];
const newAgentEntry = `
${content.split('Agent:').length}. Agent: ${agentInfo.data.name}
ID: ${agentInfo.data.id}
Type: ${agentInfo.data.agent_type}
Description: ${agentInfo.data.description || 'No description'}
Capabilities: ${capabilities.length ? capabilities.join(', ') : 'No specific capabilities'}
Last Active: ${timestamp}
`;
// Insert before Communication Guidelines
const parts = content.split("Communication Guidelines:");
const updatedContent = parts[0] + newAgentEntry + "\nCommunication Guidelines:" + parts[1];
await this.api.patch(
`/blocks/${this.addressBookId}`,
{ value: updatedContent },
{ headers }
);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `Agent ${args.name} created successfully with ID: ${agentId}`,
agent_id: agentId,
capabilities,
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleListAgents(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`
};
// Get the list of agents
const response = await this.api.get('/agents', { headers });
const agents = response.data;
// Apply filter if provided
let filteredAgents = agents;
if (args && args.filter) {
const filter = args.filter.toLowerCase();
filteredAgents = agents.filter((agent: any) =>
agent.name.toLowerCase().includes(filter) ||
(agent.description && agent.description.toLowerCase().includes(filter))
);
}
// Format the response
const formattedAgents = filteredAgents.map((agent: any) => ({
id: agent.id,
name: agent.name,
description: agent.description || 'No description',
type: agent.agent_type,
model: agent.model,
created_at: agent.created_at,
updated_at: agent.updated_at
}));
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
count: formattedAgents.length,
agents: formattedAgents
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handlePromptAgent(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Validate arguments
if (!args.agent_id || !args.message) {
throw new Error('Missing required arguments: agent_id and message');
}
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`,
'Authorization': `Bearer ${this.password}`
};
// First, check if the agent exists
const agentInfoResponse = await this.api.get(`/agents/${args.agent_id}`, { headers });
const agentName = agentInfoResponse.data.name;
// Send message to agent using the messages/stream endpoint
const response = await this.api.post(`/agents/${args.agent_id}/messages/stream`, {
messages: [
{
role: "user",
content: args.message
}
],
stream_steps: false,
stream_tokens: false
}, {
headers,
responseType: 'text'
});
// Extract the response
let responseText = "";
try {
// The response is in Server-Sent Events (SSE) format
if (typeof response.data === 'string') {
// Find lines that start with "data: "
const dataLines = response.data
.split('\n')
.filter(line => line.trim().startsWith('data: '));
// Process each data line
const messages = [];
for (const line of dataLines) {
try {
// Extract the JSON part after "data: "
const jsonStr = line.substring(6);
const eventData = JSON.parse(jsonStr);
// Extract the message content based on message type
if (eventData.message_type === 'assistant_message' && eventData.content) {
// This is the main response message
responseText = eventData.content;
break;
} else if (eventData.message_type === 'reasoning_message' && eventData.reasoning) {
// This is the reasoning message (agent's thought process)
messages.push(`[Reasoning]: ${eventData.reasoning}`);
} else if (eventData.delta && eventData.delta.content) {
// This is a streaming delta update
messages.push(eventData.delta.content);
}
} catch (jsonError) {
console.error("Error parsing SSE JSON:", jsonError);
// If we can't parse the JSON, just add the raw line
messages.push(line.substring(6));
}
}
// If we didn't find a specific assistant message, join all messages
if (!responseText && messages.length > 0) {
responseText = messages.join('\n');
}
// If we still don't have a response, use the raw data
if (!responseText) {
responseText = "Received response but couldn't extract message content";
}
} else if (response.data) {
// Handle non-string response (unlikely with SSE)
responseText = JSON.stringify(response.data);
}
} catch (error) {
console.error("Error parsing response:", error);
responseText = "Error parsing agent response";
}
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
agent_id: args.agent_id,
agent_name: agentName,
message: args.message,
response: responseText,
raw_response_length: typeof response.data === 'string' ? response.data.length : JSON.stringify(response.data).length
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleListAgentTools(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Validate arguments
if (!args.agent_id) {
throw new Error('Missing required argument: agent_id');
}
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`
};
// Get agent info to check if it exists and get its tools
const agentInfoResponse = await this.api.get(`/agents/${args.agent_id}`, { headers });
const agentName = agentInfoResponse.data.name;
// Extract tools from agent info
let tools = [];
if (agentInfoResponse.data.tools && Array.isArray(agentInfoResponse.data.tools)) {
tools = agentInfoResponse.data.tools;
}
// Format the response
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
agent_id: args.agent_id,
agent_name: agentName,
tool_count: tools.length,
tools: tools
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleListTools(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`
};
// Get all tools from the Letta server
const toolsResponse = await this.api.get('/tools', { headers });
let tools = toolsResponse.data;
// Apply filter if provided
if (args && args.filter && typeof args.filter === 'string') {
const filterLower = args.filter.toLowerCase();
tools = tools.filter((tool: any) =>
(tool.name && tool.name.toLowerCase().includes(filterLower)) ||
(tool.description && tool.description.toLowerCase().includes(filterLower))
);
}
// Apply pagination
const page = args && typeof args.page === 'number' ? Math.max(1, args.page) : 1;
const pageSize = args && typeof args.pageSize === 'number' ? Math.max(1, Math.min(100, args.pageSize)) : 10;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const totalTools = tools.length;
const totalPages = Math.ceil(totalTools / pageSize);
const paginatedTools = tools.slice(startIndex, endIndex);
// Format the response
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
pagination: {
page: page,
pageSize: pageSize,
totalTools: totalTools,
totalPages: totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1
},
tool_count: paginatedTools.length,
tools: paginatedTools
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleAttachTool(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Validate arguments
if (!args.tool_id) {
throw new Error('Missing required argument: tool_id');
}
if (!args.agent_id) {
throw new Error('Missing required argument: agent_id');
}
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`,
'user_id': args.agent_id
};
// Verify tool exists
const toolResponse = await this.api.get(`/tools/${args.tool_id}`, { headers });
const toolData = toolResponse.data;
const toolName = toolData.name || 'Unknown';
// Attach tool to agent
console.log(`Attaching tool ${toolName} (${args.tool_id}) to agent ${args.agent_id}...`);
const attachUrl = `/agents/${args.agent_id}/tools/attach/${args.tool_id}`;
const response = await this.api.patch(attachUrl, {}, { headers });
// Get updated agent data
const agentData = response.data;
// Check if tool is now in agent's tools
const attachedToolIds = agentData.tools?.map((tool: any) => tool.id) || [];
if (attachedToolIds.includes(args.tool_id)) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `Tool ${toolName} successfully attached to agent ${args.agent_id}.`,
agent_id: args.agent_id,
agent_name: agentData.name || 'Unknown',
tool_id: args.tool_id,
tool_name: toolName
}, null, 2),
}],
};
} else {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: `Tool ${args.tool_id} was not found in agent's tools after attachment.`,
}, null, 2),
}],
isError: true,
};
}
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleListMemoryBlocks(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`
};
// If agent_id is provided, set the user_id header
if (args && args.agent_id) {
headers['user_id'] = args.agent_id;
}
// Get all blocks from the Letta server
const blocksResponse = await this.api.get('/blocks', { headers });
let blocks = blocksResponse.data;
// Apply filter if provided
if (args && args.filter && typeof args.filter === 'string') {
const filterLower = args.filter.toLowerCase();
blocks = blocks.filter((block: any) =>
(block.name && block.name.toLowerCase().includes(filterLower)) ||
(block.label && block.label.toLowerCase().includes(filterLower)) ||
(block.value && typeof block.value === 'string' && block.value.toLowerCase().includes(filterLower))
);
}
// Apply pagination
const page = args && typeof args.page === 'number' ? Math.max(1, args.page) : 1;
const pageSize = args && typeof args.pageSize === 'number' ? Math.max(1, Math.min(100, args.pageSize)) : 10;
const startIndex = (page - 1) * pageSize;
const endIndex = startIndex + pageSize;
const totalBlocks = blocks.length;
const totalPages = Math.ceil(totalBlocks / pageSize);
const paginatedBlocks = blocks.slice(startIndex, endIndex);
// Format blocks for output
const formattedBlocks = paginatedBlocks.map((block: any) => {
// Truncate value if it's too long
let value = block.value;
if (typeof value === 'string' && value.length > 200) {
value = value.substring(0, 200) + '...';
}
return {
id: block.id,
name: block.name || 'Unnamed Block',
label: block.label || 'No Label',
value: value,
metadata: block.metadata || {},
created_at: block.created_at,
updated_at: block.updated_at
};
});
// Format the response
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
pagination: {
page: page,
pageSize: pageSize,
totalBlocks: totalBlocks,
totalPages: totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1
},
block_count: formattedBlocks.length,
blocks: formattedBlocks,
agent_specific: args && args.agent_id ? true : false
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleAttachMemoryBlock(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Validate arguments
if (!args.block_id) {
throw new Error('Missing required argument: block_id');
}
if (!args.agent_id) {
throw new Error('Missing required argument: agent_id');
}
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`,
'user_id': args.agent_id
};
// Verify block exists
const blockResponse = await this.api.get(`/blocks/${args.block_id}`, { headers });
const blockData = blockResponse.data;
const blockName = blockData.name || 'Unnamed Block';
// Determine label to use
const label = args.label || blockData.label || 'custom';
// Attach block to agent
console.log(`Attaching memory block ${blockName} (${args.block_id}) to agent ${args.agent_id} with label ${label}...`);
// Use the core-memory/blocks/attach endpoint
const attachUrl = `/agents/${args.agent_id}/core-memory/blocks/attach/${args.block_id}`;
// Send an empty object as the request body, as seen in handleCreateAgent method
const response = await this.api.patch(attachUrl, {}, { headers });
// Get updated agent data to verify attachment
const agentInfoResponse = await this.api.get(`/agents/${args.agent_id}`, { headers });
const agentData = agentInfoResponse.data;
const agentName = agentData.name || 'Unknown';
// Check if block is now in agent's memory blocks
// This is a simplified check - in a real implementation, you might want to
// check the agent's memory blocks more thoroughly
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `Memory block ${blockName} successfully attached to agent ${agentName} with label ${label}.`,
agent_id: args.agent_id,
agent_name: agentName,
block_id: args.block_id,
block_name: blockName,
label: label
}, null, 2),
}],
};
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
private async handleCreateMemoryBlock(args: any): Promise<{ content: { type: string; text: string; }[]; isError?: boolean }> {
try {
// Validate arguments
if (!args.name || typeof args.name !== 'string') {
throw new Error('Missing required argument: name (must be a string)');
}
if (!args.label || typeof args.label !== 'string') {
throw new Error('Missing required argument: label (must be a string)');
}
if (!args.value || typeof args.value !== 'string') {
throw new Error('Missing required argument: value (must be a string)');
}
// Headers for API requests
const headers: HeadersConfig = {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-BARE-PASSWORD': `password ${this.password}`
};
// If agent_id is provided, set the user_id header
if (args.agent_id) {
headers['user_id'] = args.agent_id;
}
// Prepare metadata
const metadata = args.metadata || {
type: args.label,
version: "1.0",
last_updated: new Date().toISOString()
};
// Prepare block data
const blockData = {
name: args.name,
label: args.label,
value: args.value,
metadata: metadata
};
// Create the memory block
console.log(`Creating memory block "${args.name}" with label "${args.label}"...`);
const createResponse = await this.api.post('/blocks', blockData, { headers });
const blockId = createResponse.data.id;
// If agent_id is provided, attach the block to the agent
if (args.agent_id) {
const attachUrl = `/agents/${args.agent_id}/core-memory/blocks/attach/${blockId}`;
await this.api.patch(attachUrl, {}, { headers });
// Get agent info
const agentInfoResponse = await this.api.get(`/agents/${args.agent_id}`, { headers });
const agentName = agentInfoResponse.data.name || 'Unknown';
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `Memory block "${args.name}" created and attached to agent ${agentName}.`,
block_id: blockId,
block_name: args.name,
agent_id: args.agent_id,
agent_name: agentName,
label: args.label
}, null, 2),
}],
};
} else {
// Just return the created block info
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
message: `Memory block "${args.name}" created successfully.`,
block_id: blockId,
block_name: args.name,
label: args.label
}, null, 2),
}],
};
}
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
success: false,
error: error.message,
details: error.response?.data || error,
}, null, 2),
}],
isError: true,
};
}
}
async run() {
try {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Letta MCP server running on stdio');
// Handle graceful shutdown
const cleanup = async () => {
await this.server.close();
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('uncaughtException', async (error: Error) => {
console.error('Uncaught exception:', error);
await cleanup();
});
} catch (err) {
const error = err instanceof Error ? err : new Error(String(err));
console.error('Failed to start server:', error);
process.exit(1);
}
}
}
const server = new LettaServer();
server.run().catch(console.error);