LibreChat/packages/data-schemas/src/methods/mcpServer.spec.ts
Atef Bellaaj 1edec579a5
🏗️ feat: Dynamic MCP Server Infrastructure with Access Control (#10787)
* Feature: Dynamic MCP Server with Full UI Management

* 🚦 feat: Add MCP Connection Status icons to MCPBuilder panel (#10805)

* feature: Add MCP server connection status icons to MCPBuilder panel

* refactor: Simplify MCPConfigDialog rendering in MCPBuilderPanel

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>

* fix: address code review feedback for MCP server management

- Fix OAuth secret preservation to avoid mutating input parameter
  by creating a merged config copy in ServerConfigsDB.update()

- Improve error handling in getResourcePermissionsMap to propagate
  critical errors instead of silently returning empty Map

- Extract duplicated MCP server filter logic by exposing selectableServers
  from useMCPServerManager hook and using it in MCPSelect component

* test: Update PermissionService tests to throw errors on invalid resource types

- Changed the test for handling invalid resource types to ensure it throws an error instead of returning an empty permissions map.
- Updated the expectation to check for the specific error message when an invalid resource type is provided.

* feat: Implement retry logic for MCP server creation to handle race conditions

- Enhanced the createMCPServer method to include retry logic with exponential backoff for handling duplicate key errors during concurrent server creation.
- Updated tests to verify that all concurrent requests succeed and that unique server names are generated.
- Added a helper function to identify MongoDB duplicate key errors, improving error handling during server creation.

* refactor: StatusIcon to use CircleCheck for connected status

- Replaced the PlugZap icon with CircleCheck in the ConnectedStatusIcon component to better represent the connected state.
- Ensured consistent icon usage across the component for improved visual clarity.

* test: Update AccessControlService tests to throw errors on invalid resource types

- Modified the test for invalid resource types to ensure it throws an error with a specific message instead of returning an empty permissions map.
- This change enhances error handling and improves test coverage for the AccessControlService.

* fix: Update error message for missing server name in MCP server retrieval

- Changed the error message returned when the server name is not provided from 'MCP ID is required' to 'Server name is required' for better clarity and accuracy in the API response.

---------

Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com>
Co-authored-by: Danny Avila <danny@librechat.ai>
2025-12-04 15:37:23 -05:00

827 lines
28 KiB
TypeScript

import mongoose from 'mongoose';
import { MongoMemoryServer } from 'mongodb-memory-server';
import type { MCPOptions } from 'librechat-data-provider';
import type * as t from '~/types';
import { createMCPServerMethods } from './mcpServer';
import mcpServerSchema from '~/schema/mcpServer';
let mongoServer: MongoMemoryServer;
let MCPServer: mongoose.Model<t.MCPServerDocument>;
let methods: ReturnType<typeof createMCPServerMethods>;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
const mongoUri = mongoServer.getUri();
MCPServer = mongoose.models.MCPServer || mongoose.model('MCPServer', mcpServerSchema);
methods = createMCPServerMethods(mongoose);
await mongoose.connect(mongoUri);
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
beforeEach(async () => {
await mongoose.connection.dropDatabase();
});
describe('MCPServer Model Tests', () => {
const authorId = new mongoose.Types.ObjectId();
const authorId2 = new mongoose.Types.ObjectId();
const createSSEConfig = (title?: string, description?: string): MCPOptions => ({
type: 'sse',
url: 'https://example.com/mcp',
...(title && { title }),
...(description && { description }),
});
describe('createMCPServer', () => {
test('should create server with title and generate slug from title', async () => {
const config = createSSEConfig('My Test Server', 'A test server');
const server = await methods.createMCPServer({ config, author: authorId });
expect(server).toBeDefined();
expect(server.serverName).toBe('my-test-server');
expect(server.config.title).toBe('My Test Server');
expect(server.config.description).toBe('A test server');
expect(server.author.toString()).toBe(authorId.toString());
expect(server.createdAt).toBeInstanceOf(Date);
expect(server.updatedAt).toBeInstanceOf(Date);
});
test('should create server without title and use nanoid', async () => {
const config: MCPOptions = {
type: 'sse',
url: 'https://example.com/mcp',
};
const server = await methods.createMCPServer({ config, author: authorId });
expect(server).toBeDefined();
expect(server.serverName).toMatch(/^mcp-[a-zA-Z0-9_-]{16}$/);
expect(server.config.title).toBeUndefined();
});
test('should handle title with special characters', async () => {
const config = createSSEConfig('My @#$% Server!!! 123');
const server = await methods.createMCPServer({ config, author: authorId });
expect(server.serverName).toBe('my-server-123');
});
test('should handle title with only spaces and special chars', async () => {
const config = createSSEConfig(' @#$% ');
const server = await methods.createMCPServer({ config, author: authorId });
// Should fallback to 'mcp-server'
expect(server.serverName).toBe('mcp-server');
});
test('should handle title with multiple spaces', async () => {
const config = createSSEConfig('My Multiple Spaces Server');
const server = await methods.createMCPServer({ config, author: authorId });
expect(server.serverName).toBe('my-multiple-spaces-server');
});
test('should handle string author ID', async () => {
const config = createSSEConfig('String Author Test');
const server = await methods.createMCPServer({
config,
author: authorId.toString(),
});
expect(server).toBeDefined();
expect(server.author.toString()).toBe(authorId.toString());
});
test('should create server with stdio config', async () => {
const config: MCPOptions = {
type: 'stdio',
command: 'node',
args: ['server.js'],
title: 'Stdio Server',
};
const server = await methods.createMCPServer({ config, author: authorId });
expect(server.serverName).toBe('stdio-server');
expect(server.config.type).toBe('stdio');
});
});
describe('findNextAvailableServerName', () => {
test('should return base name when no duplicates exist', async () => {
// Create server directly via model to set up initial state
await MCPServer.create({
serverName: 'other-server',
config: createSSEConfig('Other Server'),
author: authorId,
});
const config = createSSEConfig('Test Server');
const server = await methods.createMCPServer({ config, author: authorId });
expect(server.serverName).toBe('test-server');
});
test('should append -2 when base name exists', async () => {
// Create first server
await methods.createMCPServer({
config: createSSEConfig('Test Server'),
author: authorId,
});
// Create second server with same title
const server = await methods.createMCPServer({
config: createSSEConfig('Test Server'),
author: authorId,
});
expect(server.serverName).toBe('test-server-2');
});
test('should find next available number in sequence', async () => {
// Create servers with sequential names
await MCPServer.create({
serverName: 'test-server',
config: createSSEConfig('Test Server'),
author: authorId,
});
await MCPServer.create({
serverName: 'test-server-2',
config: createSSEConfig('Test Server'),
author: authorId,
});
await MCPServer.create({
serverName: 'test-server-3',
config: createSSEConfig('Test Server'),
author: authorId,
});
const server = await methods.createMCPServer({
config: createSSEConfig('Test Server'),
author: authorId,
});
expect(server.serverName).toBe('test-server-4');
});
test('should handle gaps in sequence', async () => {
// Create servers with gaps: test, test-2, test-5
await MCPServer.create({
serverName: 'test-server',
config: createSSEConfig('Test Server'),
author: authorId,
});
await MCPServer.create({
serverName: 'test-server-2',
config: createSSEConfig('Test Server'),
author: authorId,
});
await MCPServer.create({
serverName: 'test-server-5',
config: createSSEConfig('Test Server'),
author: authorId,
});
const server = await methods.createMCPServer({
config: createSSEConfig('Test Server'),
author: authorId,
});
// Should append -6 (max + 1)
expect(server.serverName).toBe('test-server-6');
});
test('should not match partial names', async () => {
// Create 'test-server-extra' which shouldn't affect 'test-server' sequence
await MCPServer.create({
serverName: 'test-server-extra',
config: createSSEConfig('Test Server Extra'),
author: authorId,
});
const server = await methods.createMCPServer({
config: createSSEConfig('Test Server'),
author: authorId,
});
// 'test-server' is available, so should use it
expect(server.serverName).toBe('test-server');
});
test('should handle special regex characters in base name', async () => {
// The slug generation removes special characters, but test the regex escaping
await MCPServer.create({
serverName: 'test-server',
config: createSSEConfig('Test Server'),
author: authorId,
});
const server = await methods.createMCPServer({
config: createSSEConfig('Test Server'),
author: authorId2,
});
expect(server.serverName).toBe('test-server-2');
});
});
describe('findMCPServerById', () => {
test('should find server by serverName', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Find By Id Test'),
author: authorId,
});
const found = await methods.findMCPServerById(created.serverName);
expect(found).toBeDefined();
expect(found?.serverName).toBe('find-by-id-test');
expect(found?.config.title).toBe('Find By Id Test');
});
test('should return null when server not found', async () => {
const found = await methods.findMCPServerById('non-existent-server');
expect(found).toBeNull();
});
test('should return lean document', async () => {
await methods.createMCPServer({
config: createSSEConfig('Lean Test'),
author: authorId,
});
const found = await methods.findMCPServerById('lean-test');
// Lean documents don't have mongoose methods
expect(found).toBeDefined();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect(typeof (found as any).save).toBe('undefined');
});
});
describe('findMCPServerByObjectId', () => {
test('should find server by MongoDB ObjectId', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Object Id Test'),
author: authorId,
});
const found = await methods.findMCPServerByObjectId(created._id);
expect(found).toBeDefined();
expect(found?.serverName).toBe('object-id-test');
expect(found?._id.toString()).toBe(created._id.toString());
});
test('should find server by string ObjectId', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('String Object Id Test'),
author: authorId,
});
const found = await methods.findMCPServerByObjectId(created._id.toString());
expect(found).toBeDefined();
expect(found?.serverName).toBe('string-object-id-test');
});
test('should return null when ObjectId not found', async () => {
const randomId = new mongoose.Types.ObjectId();
const found = await methods.findMCPServerByObjectId(randomId);
expect(found).toBeNull();
});
test('should return null for invalid ObjectId string', async () => {
await expect(methods.findMCPServerByObjectId('invalid-id')).rejects.toThrow();
});
});
describe('findMCPServersByAuthor', () => {
test('should find all servers by author', async () => {
await methods.createMCPServer({
config: createSSEConfig('Author Server 1'),
author: authorId,
});
await methods.createMCPServer({
config: createSSEConfig('Author Server 2'),
author: authorId,
});
await methods.createMCPServer({
config: createSSEConfig('Other Author Server'),
author: authorId2,
});
const servers = await methods.findMCPServersByAuthor(authorId);
expect(servers).toHaveLength(2);
expect(servers.every((s) => s.author.toString() === authorId.toString())).toBe(true);
});
test('should return empty array when author has no servers', async () => {
const servers = await methods.findMCPServersByAuthor(new mongoose.Types.ObjectId());
expect(servers).toEqual([]);
});
test('should sort by updatedAt descending', async () => {
// Create servers with slight delay to ensure different timestamps
const server1 = await methods.createMCPServer({
config: createSSEConfig('First Created'),
author: authorId,
});
// Update first server to make it most recently updated
await MCPServer.findByIdAndUpdate(server1._id, {
$set: { 'config.description': 'Updated' },
});
await methods.createMCPServer({
config: createSSEConfig('Second Created'),
author: authorId,
});
const servers = await methods.findMCPServersByAuthor(authorId);
expect(servers).toHaveLength(2);
// Most recently updated should come first
expect(servers[0].serverName).toBe('second-created');
});
test('should handle string author ID', async () => {
await methods.createMCPServer({
config: createSSEConfig('String Author Server'),
author: authorId,
});
const servers = await methods.findMCPServersByAuthor(authorId.toString());
expect(servers).toHaveLength(1);
});
});
describe('getListMCPServersByIds', () => {
let server1: t.MCPServerDocument;
let server2: t.MCPServerDocument;
let server3: t.MCPServerDocument;
beforeEach(async () => {
server1 = await methods.createMCPServer({
config: createSSEConfig('Server One'),
author: authorId,
});
server2 = await methods.createMCPServer({
config: createSSEConfig('Server Two'),
author: authorId,
});
server3 = await methods.createMCPServer({
config: createSSEConfig('Server Three'),
author: authorId,
});
});
test('should return servers matching provided IDs', async () => {
const result = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id],
});
expect(result.data).toHaveLength(2);
expect(result.has_more).toBe(false);
expect(result.after).toBeNull();
});
test('should return empty data for empty IDs array', async () => {
const result = await methods.getListMCPServersByIds({ ids: [] });
expect(result.data).toEqual([]);
expect(result.has_more).toBe(false);
expect(result.after).toBeNull();
});
test('should handle pagination with limit', async () => {
const result = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
limit: 2,
});
expect(result.data).toHaveLength(2);
expect(result.has_more).toBe(true);
expect(result.after).not.toBeNull();
});
test('should paginate using cursor', async () => {
// Get first page
const firstPage = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
limit: 2,
});
expect(firstPage.has_more).toBe(true);
expect(firstPage.after).not.toBeNull();
// Get second page using cursor
const secondPage = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
limit: 2,
after: firstPage.after,
});
expect(secondPage.data).toHaveLength(1);
expect(secondPage.has_more).toBe(false);
expect(secondPage.after).toBeNull();
// Ensure no duplicates between pages
const firstPageIds = firstPage.data.map((s) => s._id.toString());
const secondPageIds = secondPage.data.map((s) => s._id.toString());
const intersection = firstPageIds.filter((id) => secondPageIds.includes(id));
expect(intersection).toHaveLength(0);
});
test('should handle invalid cursor gracefully', async () => {
const result = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id],
after: 'invalid-cursor',
});
// Should still return results, ignoring invalid cursor
expect(result.data).toHaveLength(2);
});
test('should return all when limit is null', async () => {
const result = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
limit: null,
});
expect(result.data).toHaveLength(3);
expect(result.has_more).toBe(false);
expect(result.after).toBeNull();
});
test('should apply additional filters via otherParams', async () => {
// Create a server with different config
const serverWithDesc = await methods.createMCPServer({
config: createSSEConfig('Filtered Server', 'Has description'),
author: authorId,
});
const result = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, serverWithDesc._id],
otherParams: { 'config.description': 'Has description' },
});
expect(result.data).toHaveLength(1);
expect(result.data[0].serverName).toBe('filtered-server');
});
test('should normalize limit to valid range', async () => {
// Limit should be clamped to 1-100
const resultLow = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
limit: 0,
});
expect(resultLow.data.length).toBeGreaterThanOrEqual(1);
const resultHigh = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
limit: 200,
});
expect(resultHigh.data).toHaveLength(3); // All 3 servers (less than 100)
});
test('should sort by updatedAt descending, _id ascending', async () => {
const result = await methods.getListMCPServersByIds({
ids: [server1._id, server2._id, server3._id],
});
expect(result.data).toHaveLength(3);
// Most recently created/updated should come first
for (let i = 0; i < result.data.length - 1; i++) {
const current = new Date(result.data[i].updatedAt!).getTime();
const next = new Date(result.data[i + 1].updatedAt!).getTime();
expect(current).toBeGreaterThanOrEqual(next);
}
});
});
describe('updateMCPServer', () => {
test('should update server config', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Update Test', 'Original description'),
author: authorId,
});
const updated = await methods.updateMCPServer(created.serverName, {
config: createSSEConfig('Update Test', 'Updated description'),
});
expect(updated).toBeDefined();
expect(updated?.config.description).toBe('Updated description');
expect(updated?.serverName).toBe('update-test'); // serverName shouldn't change
});
test('should return null when server not found', async () => {
const updated = await methods.updateMCPServer('non-existent', {
config: createSSEConfig('Test'),
});
expect(updated).toBeNull();
});
test('should return updated document (new: true)', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Return Test'),
author: authorId,
});
const updated = await methods.updateMCPServer(created.serverName, {
config: createSSEConfig('Return Test', 'New description'),
});
expect(updated?.config.description).toBe('New description');
});
test('should run validators on update', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Validation Test'),
author: authorId,
});
// The update should succeed with valid config
const updated = await methods.updateMCPServer(created.serverName, {
config: createSSEConfig('Validation Test', 'Valid config'),
});
expect(updated).toBeDefined();
});
test('should update timestamps', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Timestamp Test'),
author: authorId,
});
const originalUpdatedAt = created.updatedAt;
// Wait a bit to ensure timestamp difference
await new Promise((resolve) => setTimeout(resolve, 10));
const updated = await methods.updateMCPServer(created.serverName, {
config: createSSEConfig('Timestamp Test', 'Updated'),
});
expect(updated?.updatedAt).toBeDefined();
expect(new Date(updated!.updatedAt!).getTime()).toBeGreaterThan(
new Date(originalUpdatedAt!).getTime(),
);
});
test('should handle partial config updates', async () => {
const created = await methods.createMCPServer({
config: {
type: 'sse',
url: 'https://example.com/mcp',
title: 'Partial Update Test',
description: 'Original',
},
author: authorId,
});
const updated = await methods.updateMCPServer(created.serverName, {
config: {
type: 'sse',
url: 'https://example.com/mcp',
title: 'Partial Update Test',
description: 'New description',
iconPath: '/icons/new-icon.png',
},
});
expect(updated?.config.description).toBe('New description');
expect(updated?.config.iconPath).toBe('/icons/new-icon.png');
});
});
describe('deleteMCPServer', () => {
test('should delete existing server', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Delete Test'),
author: authorId,
});
const deleted = await methods.deleteMCPServer(created.serverName);
expect(deleted).toBeDefined();
expect(deleted?.serverName).toBe('delete-test');
// Verify it's actually deleted
const found = await methods.findMCPServerById('delete-test');
expect(found).toBeNull();
});
test('should return null when server does not exist', async () => {
const deleted = await methods.deleteMCPServer('non-existent-server');
expect(deleted).toBeNull();
});
test('should return the deleted document', async () => {
const created = await methods.createMCPServer({
config: createSSEConfig('Delete Return Test', 'Will be deleted'),
author: authorId,
});
const deleted = await methods.deleteMCPServer(created.serverName);
expect(deleted?.config.description).toBe('Will be deleted');
});
});
describe('getListMCPServersByNames', () => {
test('should return empty data for empty names array', async () => {
const result = await methods.getListMCPServersByNames({ names: [] });
expect(result.data).toEqual([]);
});
test('should find servers by serverName strings', async () => {
await methods.createMCPServer({
config: createSSEConfig('Name Query One'),
author: authorId,
});
await methods.createMCPServer({
config: createSSEConfig('Name Query Two'),
author: authorId,
});
await methods.createMCPServer({
config: createSSEConfig('Name Query Three'),
author: authorId,
});
const result = await methods.getListMCPServersByNames({
names: ['name-query-one', 'name-query-two'],
});
expect(result.data).toHaveLength(2);
const serverNames = result.data.map((s) => s.serverName);
expect(serverNames).toContain('name-query-one');
expect(serverNames).toContain('name-query-two');
expect(serverNames).not.toContain('name-query-three');
});
test('should handle non-existent names gracefully', async () => {
await methods.createMCPServer({
config: createSSEConfig('Existing Server'),
author: authorId,
});
const result = await methods.getListMCPServersByNames({
names: ['existing-server', 'non-existent-1', 'non-existent-2'],
});
expect(result.data).toHaveLength(1);
expect(result.data[0].serverName).toBe('existing-server');
});
test('should return all matching servers for multiple names', async () => {
const server1 = await methods.createMCPServer({
config: createSSEConfig('Multi Name 1'),
author: authorId,
});
const server2 = await methods.createMCPServer({
config: createSSEConfig('Multi Name 2'),
author: authorId,
});
const server3 = await methods.createMCPServer({
config: createSSEConfig('Multi Name 3'),
author: authorId,
});
const result = await methods.getListMCPServersByNames({
names: [server1.serverName, server2.serverName, server3.serverName],
});
expect(result.data).toHaveLength(3);
});
test('should handle duplicate names in input', async () => {
await methods.createMCPServer({
config: createSSEConfig('Duplicate Test'),
author: authorId,
});
const result = await methods.getListMCPServersByNames({
names: ['duplicate-test', 'duplicate-test', 'duplicate-test'],
});
// Should only return one server (unique by serverName)
expect(result.data).toHaveLength(1);
});
});
describe('Edge Cases', () => {
test('should handle concurrent creation with retry logic for race conditions', async () => {
// Ensure indexes are created before concurrent test
await MCPServer.ensureIndexes();
// Create multiple servers with same title concurrently
// The retry logic handles TOCTOU race conditions by retrying with
// exponential backoff when duplicate key errors occur
const promises = Array.from({ length: 5 }, () =>
methods.createMCPServer({
config: createSSEConfig('Concurrent Test'),
author: authorId,
}),
);
const results = await Promise.allSettled(promises);
const successes = results.filter(
(r): r is PromiseFulfilledResult<t.MCPServerDocument> => r.status === 'fulfilled',
);
const failures = results.filter((r): r is PromiseRejectedResult => r.status === 'rejected');
// With retry logic, all concurrent requests should succeed
// Each will get a unique serverName (concurrent-test, concurrent-test-2, etc.)
expect(successes.length).toBe(5);
expect(failures.length).toBe(0);
// Verify all servers have unique names
const serverNames = successes.map((s) => s.value.serverName);
const uniqueNames = new Set(serverNames);
expect(uniqueNames.size).toBe(5);
// Verify all servers exist in the database
const dbServers = await MCPServer.find({
serverName: { $regex: /^concurrent-test/ },
}).lean();
expect(dbServers.length).toBe(5);
});
test('should handle sequential creation with same title - no race condition', async () => {
// Create multiple servers with same title sequentially
// Each creation completes before the next one starts, so no race condition
const results: t.MCPServerDocument[] = [];
for (let i = 0; i < 5; i++) {
const server = await methods.createMCPServer({
config: createSSEConfig('Sequential Test'),
author: authorId,
});
results.push(server);
}
// All should succeed with unique serverNames
const serverNames = results.map((r) => r.serverName);
const uniqueNames = new Set(serverNames);
expect(uniqueNames.size).toBe(5);
expect(serverNames).toContain('sequential-test');
expect(serverNames).toContain('sequential-test-2');
expect(serverNames).toContain('sequential-test-3');
expect(serverNames).toContain('sequential-test-4');
expect(serverNames).toContain('sequential-test-5');
});
test('should handle very long titles', async () => {
const longTitle = 'A'.repeat(200) + ' Server';
const config = createSSEConfig(longTitle);
const server = await methods.createMCPServer({ config, author: authorId });
expect(server).toBeDefined();
expect(server.serverName).toBe('a'.repeat(200) + '-server');
});
test('should handle unicode in title', async () => {
// Unicode characters should be stripped, leaving only alphanumeric
const config = createSSEConfig('Serveur Français 日本語');
const server = await methods.createMCPServer({ config, author: authorId });
expect(server.serverName).toBe('serveur-franais');
});
test('should handle empty string title', async () => {
const config: MCPOptions = {
type: 'sse',
url: 'https://example.com/mcp',
title: '',
};
const server = await methods.createMCPServer({ config, author: authorId });
// Empty title should fallback to nanoid
expect(server.serverName).toMatch(/^mcp-[a-zA-Z0-9_-]{16}$/);
});
test('should handle whitespace-only title', async () => {
const config = createSSEConfig(' ');
const server = await methods.createMCPServer({ config, author: authorId });
// Whitespace-only title after trimming results in fallback
expect(server.serverName).toBe('mcp-server');
});
});
});