Skip to main content
Glama
2389-research

MCP Agent Social Media Server

resources.test.ts24.8 kB
// ABOUTME: Unit tests for resource handlers and registration // ABOUTME: Tests all resource types including posts, agents, feed, and notifications import { URL } from 'node:url'; import { jest } from '@jest/globals'; // Mock logger jest.mock('../src/logger.js', () => ({ logger: { info: jest.fn(), debug: jest.fn(), warn: jest.fn(), error: jest.fn(), }, })); // Mock config jest.mock('../src/config.js', () => ({ config: { teamName: 'test-team', }, })); // Mock ResourceTemplate const mockResourceTemplate = jest.fn().mockImplementation((pattern, options) => ({ pattern, options, })); jest.mock('@modelcontextprotocol/sdk/server/mcp.js', () => ({ ResourceTemplate: mockResourceTemplate, })); import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js'; import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types.js'; import type { IApiClient } from '../src/api-client.js'; import { readAgentPostsResource, readAgentProfileResource } from '../src/resources/agents.js'; import { readFeedResource, readNotificationsResource } from '../src/resources/feed.js'; import { type ResourceContext, listResources, registerResources } from '../src/resources/index.js'; import { readPostResource, readThreadResource } from '../src/resources/posts.js'; import type { SessionManager } from '../src/session-manager.js'; // Test type interfaces interface MockServer { resource: jest.MockedFunction<(...args: any[]) => any>; } describe('Resource Handlers', () => { let mockApiClient: jest.Mocked<IApiClient>; let mockSessionManager: jest.Mocked<SessionManager>; let mockContext: ResourceContext; let mockExtra: RequestHandlerExtra<ServerRequest, ServerNotification>; let mockServer: MockServer; const mockPosts = [ { id: 'post-123', author_name: 'agent1', content: 'This is a test post about AI development', tags: ['ai', 'development'], timestamp: '2025-01-01T10:00:00Z', created_at: '2025-01-01T10:00:00Z', updated_at: '2025-01-01T10:00:00Z', thread_id: 'thread-456', parent_post_id: null, }, { id: 'post-456', author_name: 'agent2', content: 'Great insights on machine learning trends', tags: ['ml', 'trends'], timestamp: '2025-01-01T11:00:00Z', created_at: '2025-01-01T11:00:00Z', updated_at: '2025-01-01T11:00:00Z', thread_id: 'thread-789', parent_post_id: null, }, { id: 'post-789', author_name: 'agent1', content: 'Reply to the AI development discussion', tags: ['ai', 'discussion'], timestamp: '2025-01-01T12:00:00Z', created_at: '2025-01-01T12:00:00Z', updated_at: '2025-01-01T12:00:00Z', thread_id: 'thread-456', parent_post_id: 'post-123', }, ]; beforeEach(() => { jest.clearAllMocks(); mockApiClient = { fetchPosts: jest.fn(), createPost: jest.fn(), deletePost: jest.fn(), } as jest.Mocked<IApiClient>; mockSessionManager = { getSession: jest.fn(), createSession: jest.fn(), destroySession: jest.fn(), } as jest.Mocked<Pick<SessionManager, 'getSession' | 'createSession'>> & { destroySession: jest.MockedFunction<(...args: any[]) => any>; }; mockContext = { apiClient: mockApiClient, sessionManager: mockSessionManager, }; mockExtra = { signal: new AbortController().signal, } as RequestHandlerExtra<ServerRequest, ServerNotification>; mockServer = { resource: jest.fn(), }; // Default posts mock - setup to match different filter scenarios mockApiClient.fetchPosts.mockImplementation((_teamName, options = {}) => { let filteredPosts = [...mockPosts]; // Only apply filters when they are explicitly provided if (options.agent_filter) { filteredPosts = filteredPosts.filter((p) => p.author_name === options.agent_filter); } if (options.thread_id) { filteredPosts = filteredPosts.filter((p) => p.thread_id === options.thread_id); } if (options.tag_filter) { filteredPosts = filteredPosts.filter((p) => p.tags?.includes(options.tag_filter)); } // For posts without specific filters, return all posts (like readPostResource does) const limit = options.limit || 10; const offset = options.offset || 0; const paginatedPosts = filteredPosts.slice(offset, offset + limit); return Promise.resolve({ posts: paginatedPosts, total: filteredPosts.length, }); }); // Default session mock mockSessionManager.getSession.mockReturnValue({ id: 'test-session', agentName: 'test-agent', createdAt: new Date(), lastActivity: new Date(), }); }); describe('registerResources', () => { it('should register all resources with the server', () => { registerResources(mockServer, mockContext); expect(mockServer.resource).toHaveBeenCalledTimes(6); const calls = mockServer.resource.mock.calls; const resourceNames = calls.map((call) => call[0]); expect(resourceNames).toContain('social-feed'); expect(resourceNames).toContain('notifications'); expect(resourceNames).toContain('post'); expect(resourceNames).toContain('thread'); expect(resourceNames).toContain('agent-profile'); expect(resourceNames).toContain('agent-posts'); }); it('should register fixed URI resources with correct URIs', () => { registerResources(mockServer, mockContext); const feedCall = mockServer.resource.mock.calls.find((call) => call[0] === 'social-feed'); expect(feedCall[1]).toBe('social://feed'); const notificationsCall = mockServer.resource.mock.calls.find( (call) => call[0] === 'notifications', ); expect(notificationsCall[1]).toBe('social://notifications'); }); it('should register template resources with ResourceTemplate instances', () => { registerResources(mockServer, mockContext); const postCall = mockServer.resource.mock.calls.find((call) => call[0] === 'post'); expect(postCall[1]).toBeInstanceOf(Object); expect(postCall[1]).toHaveProperty('_uriTemplate'); }); it('should register resources with correct metadata', () => { registerResources(mockServer, mockContext); const feedCall = mockServer.resource.mock.calls.find((call) => call[0] === 'social-feed'); expect(feedCall[2].description).toContain('Real-time social media feed'); expect(feedCall[2].mimeType).toBe('application/json'); }); }); describe('listResources', () => { it('should return fixed resources', async () => { const result = await listResources(mockExtra, mockContext); expect(result.resources).toEqual( expect.arrayContaining([ expect.objectContaining({ uri: 'social://feed', name: 'Social Media Feed', mimeType: 'application/json', }), expect.objectContaining({ uri: 'social://notifications', name: 'Notifications', mimeType: 'application/json', }), ]), ); }); it('should include dynamic post resources', async () => { const result = await listResources(mockExtra, mockContext); const postResources = result.resources.filter((r) => r.uri.startsWith('social://posts/')); expect(postResources.length).toBeGreaterThan(0); expect(postResources[0]).toEqual( expect.objectContaining({ uri: 'social://posts/post-123', name: 'Post by agent1', mimeType: 'application/json', }), ); }); it('should include dynamic agent profile resources', async () => { const result = await listResources(mockExtra, mockContext); const agentResources = result.resources.filter((r) => r.uri.includes('/agents/')); expect(agentResources.length).toBeGreaterThan(0); expect(agentResources[0]).toEqual( expect.objectContaining({ uri: expect.stringMatching(/^social:\/\/agents\/agent[12]\/profile$/), name: expect.stringMatching(/^agent[12]'s Profile$/), mimeType: 'application/json', }), ); }); it('should handle API errors gracefully', async () => { mockApiClient.fetchPosts.mockRejectedValue(new Error('API Error')); const result = await listResources(mockExtra, mockContext); // Should still return fixed resources expect(result.resources).toEqual( expect.arrayContaining([ expect.objectContaining({ uri: 'social://feed' }), expect.objectContaining({ uri: 'social://notifications' }), ]), ); }); it('should limit dynamic resources to avoid overwhelming list', async () => { const largeMockPosts = Array(20) .fill(0) .map((_, i) => ({ ...mockPosts[0], id: `post-${i}`, author_name: `agent-${i}`, })); mockApiClient.fetchPosts.mockResolvedValue({ posts: largeMockPosts, total: largeMockPosts.length, }); const result = await listResources(mockExtra, mockContext); const postResources = result.resources.filter((r) => r.uri.startsWith('social://posts/')); const agentResources = result.resources.filter((r) => r.uri.includes('/agents/')); // Should limit to 3 posts and 3 agents expect(postResources.length).toBe(3); expect(agentResources.length).toBe(3); }); }); describe('Post Resources', () => { describe('readPostResource', () => { it('should read a valid post by ID', async () => { const uri = new URL('social://host//posts/post-123'); const result = await readPostResource(uri, mockContext); expect(result.contents).toHaveLength(1); expect(result.contents[0].mimeType).toBe('application/json'); const content = JSON.parse(result.contents[0].text); expect(content.post).toEqual( expect.objectContaining({ id: 'post-123', author_name: 'agent1', content: 'This is a test post about AI development', }), ); }); it('should handle non-existent post ID', async () => { const uri = new URL('social://host//posts/non-existent'); const result = await readPostResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error).toContain('Post not found'); }); it('should handle invalid URI format', async () => { const uri = new URL('social://host//posts/'); const result = await readPostResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error).toContain('Invalid post URI'); }); it('should handle API errors gracefully', async () => { mockApiClient.fetchPosts.mockRejectedValue(new Error('API Error')); const uri = new URL('social://host//posts/post-123'); const result = await readPostResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error).toContain('Failed to read post resource'); }); it('should include post metadata and statistics', async () => { const uri = new URL('social://host//posts/post-123'); const result = await readPostResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); // Simple post resource structure without metadata expect(content.post.id).toBe('post-123'); }); }); describe('readThreadResource', () => { it('should read a complete thread', async () => { const uri = new URL('social://host//threads/thread-456'); const result = await readThreadResource(uri, mockContext); expect(result.contents).toHaveLength(1); const content = JSON.parse(result.contents[0].text); expect(content.thread).toEqual( expect.objectContaining({ threadId: 'thread-456', posts: expect.arrayContaining([ expect.objectContaining({ id: 'post-123' }), expect.objectContaining({ id: 'post-789' }), ]), participantCount: 1, postCount: 2, }), ); }); it('should handle empty thread', async () => { mockApiClient.fetchPosts.mockResolvedValue({ posts: [], total: 0 }); const uri = new URL('social://host//threads/empty-thread'); const result = await readThreadResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error).toContain('Thread not found'); }); it('should organize posts in chronological order', async () => { const uri = new URL('social://host//threads/thread-456'); const result = await readThreadResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); const posts = content.thread.posts; expect(new Date(posts[0].created_at).getTime()).toBeLessThanOrEqual( new Date(posts[1].created_at).getTime(), ); }); it('should include thread statistics', async () => { const uri = new URL('social://host//threads/thread-456'); const result = await readThreadResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.thread.postCount).toBe(2); expect(content.thread.participantCount).toBe(1); }); }); }); describe('Agent Resources', () => { describe('readAgentProfileResource', () => { it('should read agent profile with statistics', async () => { const uri = new URL('social://host//agents/agent1/profile'); const result = await readAgentProfileResource(uri, mockContext); expect(result.contents).toHaveLength(1); const content = JSON.parse(result.contents[0].text); expect(content.profile).toEqual( expect.objectContaining({ agentName: 'agent1', postCount: 2, firstSeenAt: expect.any(String), lastSeenAt: expect.any(String), }), ); }); it('should handle non-existent agent', async () => { const uri = new URL('social://host//agents/unknown-agent/profile'); const result = await readAgentProfileResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error || content.profile).toBeDefined(); // API returns empty posts, not error }); it('should include popular tags and activity metrics', async () => { const uri = new URL('social://host//agents/agent1/profile'); const result = await readAgentProfileResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.profile.agentName).toBe('agent1'); expect(content.profile.postCount).toBe(2); }); }); describe('readAgentPostsResource', () => { it('should read all posts by specific agent', async () => { const uri = new URL('social://host//agents/agent1/posts'); const result = await readAgentPostsResource(uri, mockContext); expect(result.contents).toHaveLength(1); const content = JSON.parse(result.contents[0].text); expect(content.posts).toHaveLength(2); expect(content.agentName).toBe('agent1'); expect(content.total).toBe(2); }); it('should support pagination parameters', async () => { // The agent posts resource doesn't parse URL query parameters // It uses hardcoded limit=50, offset=0, so all agent1 posts will be returned const uri = new URL('social://host//agents/agent1/posts?limit=1&offset=0'); const result = await readAgentPostsResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.posts).toHaveLength(2); // agent1 has 2 posts in mockPosts expect(content.agentName).toBe('agent1'); expect(content.total).toBeDefined(); }); it('should handle agent with no posts', async () => { mockApiClient.fetchPosts.mockResolvedValue({ posts: [], total: 0 }); const uri = new URL('social://host//agents/silent-agent/posts'); const result = await readAgentPostsResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.posts).toHaveLength(0); expect(content.agentName).toBe('silent-agent'); expect(content.total).toBe(0); }); }); }); describe('Feed Resources', () => { describe('readFeedResource', () => { it('should read social media feed', async () => { const uri = new URL('social://feed'); const result = await readFeedResource(uri, mockContext); expect(result.contents).toHaveLength(1); const content = JSON.parse(result.contents[0].text); expect(content.posts || content.error).toBeDefined(); if (content.posts) { expect(content).toEqual( expect.objectContaining({ posts: expect.arrayContaining([ expect.objectContaining({ id: 'post-123' }), expect.objectContaining({ id: 'post-456' }), expect.objectContaining({ id: 'post-789' }), ]), lastUpdated: expect.any(Number), }), ); } }); it('should support feed filtering via query parameters', async () => { // Note: the current feed implementation doesn't parse query parameters // It returns all posts regardless of filters const uri = new URL('social://feed?agent_filter=agent1'); const result = await readFeedResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.posts || content.error).toBeDefined(); if (content.posts) { expect(content.posts.length).toBeGreaterThan(0); } }); it('should support tag filtering', async () => { // Note: the current feed implementation doesn't parse query parameters // It returns all posts regardless of filters const uri = new URL('social://feed?tag_filter=ai'); const result = await readFeedResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.posts || content.error).toBeDefined(); if (content.posts) { expect(content.posts.length).toBeGreaterThan(0); } }); it('should handle empty feed gracefully', async () => { mockApiClient.fetchPosts.mockResolvedValue({ posts: [], total: 0 }); const uri = new URL('social://feed'); const result = await readFeedResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.posts).toHaveLength(0); expect(content.lastUpdated).toBeDefined(); }); }); describe('readNotificationsResource', () => { it('should read notifications for authenticated user', async () => { const uri = new URL('social://notifications'); const result = await readNotificationsResource(uri, mockContext); expect(result.contents).toHaveLength(1); const content = JSON.parse(result.contents[0].text); expect(content.notifications).toEqual(expect.any(Array)); expect(content.unreadCount).toEqual(expect.any(Number)); }); it('should handle unauthenticated user', async () => { mockSessionManager.getSession.mockReturnValue(null); const uri = new URL('social://notifications'); const result = await readNotificationsResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error).toContain('Not authenticated'); }); it('should filter notifications by user', async () => { const uri = new URL('social://notifications'); const result = await readNotificationsResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); // Notifications should be for the authenticated user expect(content.unreadCount).toEqual(expect.any(Number)); }); }); }); describe('Error Handling', () => { it('should handle malformed URIs gracefully', async () => { const badUri = new URL('social://invalid/format'); // Should not throw for any resource handler await expect(readPostResource(badUri, mockContext)).resolves.toBeDefined(); await expect(readThreadResource(badUri, mockContext)).resolves.toBeDefined(); await expect(readAgentProfileResource(badUri, mockContext)).resolves.toBeDefined(); }); it('should handle API timeouts', async () => { mockApiClient.fetchPosts.mockImplementation( () => new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 100)), ); const uri = new URL('social://feed'); const result = await readFeedResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); expect(content.error).toContain('Failed to read feed resource'); }); it('should validate resource URIs properly', async () => { const testCases = [ { uri: 'social://posts/', expectError: true }, { uri: 'social://posts/valid-id', expectError: false }, { uri: 'social://agents//profile', expectError: true }, { uri: 'social://agents/valid-agent/profile', expectError: false }, ]; for (const testCase of testCases) { const uri = new URL(testCase.uri); const result = await readPostResource(uri, mockContext); const content = JSON.parse(result.contents[0].text); if (testCase.expectError) { expect(content.error).toBeDefined(); } else { // Should either have data or a "not found" error, not a validation error expect(content.error?.includes('Invalid')).toBeFalsy(); } } }); }); describe('Integration Tests', () => { it('should work with complete resource workflow', async () => { // Register resources registerResources(mockServer, mockContext); // List resources const list = await listResources(mockExtra, mockContext); expect(list.resources.length).toBeGreaterThan(0); // Read a specific resource const uri = new URL('social://host//posts/post-123'); const result = await readPostResource(uri, mockContext); expect(result.contents).toHaveLength(1); expect(mockApiClient.fetchPosts).toHaveBeenCalled(); }); it('should handle concurrent resource reads', async () => { const uris = [ new URL('social://host//posts/post-123'), new URL('social://host//agents/agent1/profile'), new URL('social://feed'), ]; const promises = [ readPostResource(uris[0], mockContext), readAgentProfileResource(uris[1], mockContext), readFeedResource(uris[2], mockContext), ]; const results = await Promise.all(promises); expect(results).toHaveLength(3); for (const result of results) { expect(result.contents).toHaveLength(1); expect(result.contents[0].mimeType).toBe('application/json'); } }); it('should maintain consistent data across related resources', async () => { // Read post and its thread const postResult = await readPostResource( new URL('social://host//posts/post-123'), mockContext, ); const threadResult = await readThreadResource( new URL('social://host//threads/thread-456'), mockContext, ); const postContent = JSON.parse(postResult.contents[0].text); const threadContent = JSON.parse(threadResult.contents[0].text); // Post should be included in its thread const postInThread = threadContent.thread.posts.find((p) => p.id === 'post-123'); expect(postInThread).toBeDefined(); expect(postInThread.content).toBe(postContent.post.content); }); it('should handle resource caching appropriately', async () => { // Make multiple calls to same resource const uri = new URL('social://feed'); await readFeedResource(uri, mockContext); await readFeedResource(uri, mockContext); // Should call API each time (no implicit caching) expect(mockApiClient.fetchPosts).toHaveBeenCalledTimes(2); }); }); });

Latest Blog Posts

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/2389-research/mcp-socialmedia'

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