import { mkdtempSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js' import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { createManager } from '../../src/manager.js' import { createRegistry } from '../../src/registry.js' import { createServer, SERVER_VERSION } from '../../src/server.js' const connectClient = async (server: McpServer): Promise => { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair() const client = new Client({ name: 'test-client', version: '0.0.0' }) await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]) return client } describe('createServer', () => { let dir: string beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'esmole-server-')) }) afterEach(() => { rmSync(dir, { recursive: true, force: true }) }) it('exports SERVER_VERSION', () => { expect(SERVER_VERSION).toBe('0.1.1') }) it('registers exactly the ten tools and names the server esmole-mcp', async () => { const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) const manager = createManager(registry) const server = createServer(registry, manager) const client = await connectClient(server) const { tools } = await client.listTools() expect(tools.map((tool) => tool.name).sort()).toEqual([ 'add_connection', 'es_cluster_health', 'es_get_mapping', 'es_list_indices', 'es_request', 'es_search', 'list_connections', 'remove_connection', 'test_connection', 'update_connection' ]) expect(client.getServerVersion()?.name).toBe('esmole-mcp') await client.close() }) it('publishes a typed inputSchema for add_connection, not an empty object', async () => { const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) const server = createServer(registry, createManager(registry)) const client = await connectClient(server) const { tools } = await client.listTools() const add = tools.find((tool) => tool.name === 'add_connection') const schema = add?.inputSchema expect(Object.keys(schema?.properties ?? {})).toEqual( expect.arrayContaining([ 'name', 'host', 'port', 'user', 'password', 'type', 'scheme', 'verifyTls', 'apiKey' ]) ) expect(schema?.required ?? []).toEqual(expect.arrayContaining(['name', 'host', 'type'])) expect(schema?.additionalProperties).toBe(false) await client.close() }) it('add_connection still enforces the apiKey-or-user+password cross-field check', async () => { const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) const server = createServer(registry, createManager(registry)) const client = await connectClient(server) const result = await client.callTool({ name: 'add_connection', arguments: { name: 'es-noauth', host: 'es.local', type: 'elasticsearch' } }) const content = result.content as Array<{ type: string; text: string }> expect(result.isError).toBe(true) expect(content[0].text).toContain('provide either apiKey or both user and password') await client.close() }) it('add_connection rejects unknown keys at the SDK boundary, not in the handler', async () => { const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) const server = createServer(registry, createManager(registry)) const client = await connectClient(server) const result = await client.callTool({ name: 'add_connection', arguments: { name: 'es-bogus', host: 'es.local', type: 'elasticsearch', apiKey: 'x', bogus: 1 } }) const content = result.content as Array<{ type: string; text: string }> expect(result.isError).toBe(true) // The SDK-level "Invalid arguments for tool" prefix proves the strict wire // schema rejected the unknown key before the handler ran, rather than the // key being silently stripped or caught only by the handler's parseSchema. expect(content[0].text).toContain('Invalid arguments for tool add_connection') expect(content[0].text).toContain('Unrecognized key') await client.close() }) it('list_connections renders the public view of a stored connection', async () => { const registry = createRegistry({ storePath: join(dir, 'connections.json'), env: {} }) registry.add({ name: 'prod', type: 'elasticsearch', host: 'es.example.com', apiKey: 'secret-key', scheme: 'https', verifyTls: true, readonly: false }) const server = createServer(registry, createManager(registry)) const client = await connectClient(server) const result = await client.callTool({ name: 'list_connections', arguments: {} }) const content = result.content as Array<{ type: string; text: string }> const view = JSON.parse(content[0].text) expect(view).toEqual([ { name: 'prod', type: 'elasticsearch', scheme: 'https', host: 'es.example.com', port: 9200, auth: 'apiKey', verifyTls: true, readonly: false, source: 'store', ssh: false } ]) await client.close() }) })