fix(esmole): publish typed add_connection inputSchema

add_connection advertised an empty `{ properties: {} }` while the server
validated strictly, so clients could not see field types or the required
`type` literal and sent port/verifyTls as strings.

Cause: the connection schema was wrapped in `.superRefine`, producing a
ZodEffects with no `.shape`. The MCP SDK's normalizeObjectSchema returns
undefined for it and falls back to EMPTY_OBJECT_JSON_SCHEMA. update_connection
passed a raw shape, so it serialized fine - hence the asymmetry.

Split the schema: a strict z.object (connectionObjectSchema) is advertised as
the wire schema, and the refined connectionConfigSchema is passed as a new
optional parseSchema applied in the handler. Strict unknown-key rejection and
the apiKey-or-user+password check are both preserved. fullSchema is now typed
as a ZodObject so a ZodEffects can no longer be passed by mistake.
This commit is contained in:
admin committed 2026-06-16 23:15:08 +05:00
1 parent a34d759598
commit 9bbd179a40
4 files changed
+94 -12

No files matched your search

@@ -53,6 +53,69 @@ describe('createServer', () => {
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({