Files
dbmole-mcp/packages/esmole-mcp/test/unit/server.test.ts
T
admin 63bb2a5c99 fix: derive SERVER_VERSION from package.json
dbmole reported a hardcoded SERVER_VERSION of 0.1.0 while the package was
already published as 0.1.1: the constant had drifted because a bump has to
touch two places. Both servers now read the version from package.json
(resolveJsonModule + a JSON import attribute), so a release updates one
place and the reported version can no longer fall behind.

Add a dbmole guard test that add_connection publishes a non-empty
inputSchema with the expected required fields, mirroring esmole, so a future
`.superRefine` on the schema cannot silently empty it.
2026-06-16 23:49:05 +05:00

153 lines
6.2 KiB
TypeScript

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 pkg from '../../package.json' with { type: 'json' }
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<Client> => {
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('reports the package version over MCP', () => {
expect(SERVER_VERSION).toBe(pkg.version)
})
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()
})
})