Skip to main content
Glama

Letta MCP Server

by oculairmedia
api-client.test.js15.7 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import { LettaServer } from '../../core/server.js'; import axios from 'axios'; import { createMockLettaServer } from '../utils/mock-server.js'; // Mock dependencies vi.mock('@modelcontextprotocol/sdk/server/index.js'); vi.mock('axios'); vi.mock('../../core/logger.js'); describe('API Client Configuration (LMP-85)', () => { let mockAxiosInstance; let originalEnv; beforeEach(() => { // Save original env vars originalEnv = { LETTA_BASE_URL: process.env.LETTA_BASE_URL, LETTA_PASSWORD: process.env.LETTA_PASSWORD, }; // Set up mock axios instance mockAxiosInstance = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), request: vi.fn(), defaults: { baseURL: '', headers: {}, }, }; axios.create.mockReturnValue(mockAxiosInstance); }); afterEach(() => { // Restore original env vars process.env.LETTA_BASE_URL = originalEnv.LETTA_BASE_URL; process.env.LETTA_PASSWORD = originalEnv.LETTA_PASSWORD; vi.clearAllMocks(); }); describe('Axios Instance Creation', () => { it('should create axios instance with correct base URL', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; new LettaServer(); expect(axios.create).toHaveBeenCalledWith({ baseURL: 'https://api.letta.com/v1', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }); }); it('should handle base URL with trailing slash', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com/'; process.env.LETTA_PASSWORD = 'test-password'; new LettaServer(); expect(axios.create).toHaveBeenCalledWith({ baseURL: 'https://api.letta.com//v1', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }); }); it('should handle base URL already containing /v1', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com/v1'; process.env.LETTA_PASSWORD = 'test-password'; new LettaServer(); // This is the current behavior - it appends /v1 regardless expect(axios.create).toHaveBeenCalledWith({ baseURL: 'https://api.letta.com/v1/v1', headers: { 'Content-Type': 'application/json', Accept: 'application/json', }, }); }); it('should set default headers', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; new LettaServer(); const createCall = axios.create.mock.calls[0][0]; expect(createCall.headers).toEqual({ 'Content-Type': 'application/json', Accept: 'application/json', }); }); it('should store axios instance as api property', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; const server = new LettaServer(); expect(server.api).toBe(mockAxiosInstance); }); }); describe('getApiHeaders method', () => { beforeEach(() => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; }); it('should return correct headers with password', () => { process.env.LETTA_PASSWORD = 'my-secret-password'; const server = new LettaServer(); const headers = server.getApiHeaders(); expect(headers).toEqual({ 'Content-Type': 'application/json', Accept: 'application/json', 'X-BARE-PASSWORD': 'password my-secret-password', Authorization: 'Bearer my-secret-password', }); }); it('should handle empty password', () => { process.env.LETTA_PASSWORD = ''; const server = new LettaServer(); const headers = server.getApiHeaders(); expect(headers).toEqual({ 'Content-Type': 'application/json', Accept: 'application/json', 'X-BARE-PASSWORD': 'password ', Authorization: 'Bearer ', }); }); it('should handle special characters in password', () => { process.env.LETTA_PASSWORD = 'p@$$w0rd!#$%^&*()'; const server = new LettaServer(); const headers = server.getApiHeaders(); expect(headers).toEqual({ 'Content-Type': 'application/json', Accept: 'application/json', 'X-BARE-PASSWORD': 'password p@$$w0rd!#$%^&*()', Authorization: 'Bearer p@$$w0rd!#$%^&*()', }); }); it('should return new object each time', () => { process.env.LETTA_PASSWORD = 'test-password'; const server = new LettaServer(); const headers1 = server.getApiHeaders(); const headers2 = server.getApiHeaders(); expect(headers1).not.toBe(headers2); expect(headers1).toEqual(headers2); }); it('should not modify returned headers object', () => { process.env.LETTA_PASSWORD = 'test-password'; const server = new LettaServer(); const headers = server.getApiHeaders(); const originalHeaders = { ...headers }; // Modify headers headers['Custom-Header'] = 'custom-value'; delete headers.Authorization; // Next call should return original headers const newHeaders = server.getApiHeaders(); expect(newHeaders).toEqual(originalHeaders); expect(newHeaders).not.toHaveProperty('Custom-Header'); }); }); describe('URL Construction', () => { it('should handle various URL formats', () => { const urlTests = [ { input: 'http://localhost:3000', expected: 'http://localhost:3000/v1' }, { input: 'https://api.letta.com', expected: 'https://api.letta.com/v1' }, { input: 'https://letta.com/api', expected: 'https://letta.com/api/v1' }, { input: 'http://192.168.1.1:8080', expected: 'http://192.168.1.1:8080/v1' }, ]; urlTests.forEach(({ input, expected }) => { process.env.LETTA_BASE_URL = input; process.env.LETTA_PASSWORD = 'test'; new LettaServer(); expect(axios.create).toHaveBeenCalledWith( expect.objectContaining({ baseURL: expected, }), ); vi.clearAllMocks(); }); }); it('should store the constructed API base URL', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; const server = new LettaServer(); expect(server.apiBase).toBe('https://api.letta.com/v1'); }); }); describe('API Method Availability', () => { beforeEach(() => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; }); it('should provide all HTTP method functions', () => { const server = new LettaServer(); expect(server.api.get).toBeDefined(); expect(server.api.post).toBeDefined(); expect(server.api.put).toBeDefined(); expect(server.api.patch).toBeDefined(); expect(server.api.delete).toBeDefined(); expect(server.api.request).toBeDefined(); }); it('should allow calling HTTP methods', async () => { const server = new LettaServer(); // Mock responses mockAxiosInstance.get.mockResolvedValue({ data: 'get response' }); mockAxiosInstance.post.mockResolvedValue({ data: 'post response' }); mockAxiosInstance.put.mockResolvedValue({ data: 'put response' }); mockAxiosInstance.patch.mockResolvedValue({ data: 'patch response' }); mockAxiosInstance.delete.mockResolvedValue({ data: 'delete response' }); // Test each method expect(await server.api.get('/test')).toEqual({ data: 'get response' }); expect(await server.api.post('/test', {})).toEqual({ data: 'post response' }); expect(await server.api.put('/test', {})).toEqual({ data: 'put response' }); expect(await server.api.patch('/test', {})).toEqual({ data: 'patch response' }); expect(await server.api.delete('/test')).toEqual({ data: 'delete response' }); }); }); describe('Integration with Tool Handlers', () => { beforeEach(() => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; }); it('should work with tool handler patterns', async () => { const server = new LettaServer(); const headers = server.getApiHeaders(); // Simulate a tool handler making an API call mockAxiosInstance.get.mockResolvedValue({ data: { agents: [] }, }); await server.api.get('/agents', { headers }); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/agents', { headers: expect.objectContaining({ Authorization: 'Bearer test-password', 'X-BARE-PASSWORD': 'password test-password', }), }); }); it('should handle API errors properly', async () => { const server = new LettaServer(); const apiError = new Error('API Error'); apiError.response = { status: 404, data: { error: 'Not found' }, }; mockAxiosInstance.get.mockRejectedValue(apiError); await expect(server.api.get('/agents')).rejects.toThrow('API Error'); }); }); describe('Mock Server API Client', () => { it('should work with mock server api', () => { const mockServer = createMockLettaServer(); expect(mockServer.api).toBeDefined(); expect(mockServer.api.get).toBeDefined(); expect(mockServer.getApiHeaders).toBeDefined(); const headers = mockServer.getApiHeaders(); expect(headers).toEqual({ Authorization: 'Bearer test-token', 'Content-Type': 'application/json', }); }); }); describe('Security Considerations', () => { beforeEach(() => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; }); it('should not expose password in axios defaults', () => { process.env.LETTA_PASSWORD = 'secret-password'; new LettaServer(); const createCall = axios.create.mock.calls[0][0]; expect(JSON.stringify(createCall)).not.toContain('secret-password'); }); it('should only include auth headers when explicitly requested', () => { process.env.LETTA_PASSWORD = 'secret-password'; const server = new LettaServer(); // The created axios instance should not have auth headers by default const createCall = axios.create.mock.calls[0][0]; expect(createCall.headers).not.toHaveProperty('Authorization'); expect(createCall.headers).not.toHaveProperty('X-BARE-PASSWORD'); // Auth headers should only be available through getApiHeaders const authHeaders = server.getApiHeaders(); expect(authHeaders.Authorization).toBe('Bearer secret-password'); }); }); describe('Environment Variable Edge Cases', () => { it('should handle very long base URLs', () => { const longUrl = 'https://' + 'a'.repeat(1000) + '.com'; process.env.LETTA_BASE_URL = longUrl; process.env.LETTA_PASSWORD = 'test'; new LettaServer(); expect(axios.create).toHaveBeenCalledWith( expect.objectContaining({ baseURL: longUrl + '/v1', }), ); }); it('should handle URLs with query parameters', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com?key=value'; process.env.LETTA_PASSWORD = 'test'; new LettaServer(); // Should append /v1 before query params (current behavior) expect(axios.create).toHaveBeenCalledWith( expect.objectContaining({ baseURL: 'https://api.letta.com?key=value/v1', }), ); }); it('should handle URLs with fragments', () => { process.env.LETTA_BASE_URL = 'https://api.letta.com#section'; process.env.LETTA_PASSWORD = 'test'; new LettaServer(); expect(axios.create).toHaveBeenCalledWith( expect.objectContaining({ baseURL: 'https://api.letta.com#section/v1', }), ); }); }); describe('Multiple Instance Handling', () => { beforeEach(() => { process.env.LETTA_BASE_URL = 'https://api.letta.com'; process.env.LETTA_PASSWORD = 'test-password'; }); it('should create separate axios instances for each server', () => { // Create different mock instances for each call const mockInstance1 = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), request: vi.fn(), }; const mockInstance2 = { get: vi.fn(), post: vi.fn(), put: vi.fn(), patch: vi.fn(), delete: vi.fn(), request: vi.fn(), }; axios.create.mockReturnValueOnce(mockInstance1).mockReturnValueOnce(mockInstance2); const server1 = new LettaServer(); const server2 = new LettaServer(); expect(axios.create).toHaveBeenCalledTimes(2); expect(server1.api).toBe(mockInstance1); expect(server2.api).toBe(mockInstance2); expect(server1.api).not.toBe(server2.api); }); it('should allow different passwords for different instances', () => { const server1 = new LettaServer(); process.env.LETTA_PASSWORD = 'different-password'; const server2 = new LettaServer(); const headers1 = server1.getApiHeaders(); const headers2 = server2.getApiHeaders(); expect(headers1.Authorization).toBe('Bearer test-password'); expect(headers2.Authorization).toBe('Bearer different-password'); }); }); });

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/oculairmedia/Letta-MCP-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server