From ef9ba6fb8d3ce457e1a0f4b6bba6872de569e16e Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 20 Aug 2026 23:40:59 +0100 Subject: [PATCH] fix(servers): persist secretDelivery and entrypoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `mcpctl patch server my-grafana secretDelivery=injector` printed "patched server 'my-grafana'" and changed nothing. The repository maps update/create fields explicitly, one by one, so a new column silently does nothing until it is added there — and the silence is total: the API returns 200, the CLI reports success, and `get -o yaml` still shows the old value. Caught by trying to migrate a real server, not by any test. Adds the two fields to both create and update, plus tests that assert the mapping directly. Those tests fail against the unfixed repository (3 of 4) — verified before keeping them. This class of bug will recur: the mapping is manual and nothing links a schema column to it. The tests at least make the next omission loud for these two fields. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vybEitX4FykeMatKe5Xki --- .../src/repositories/mcp-server.repository.ts | 4 ++ .../mcp-server-repository-fields.test.ts | 48 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/mcpd/tests/mcp-server-repository-fields.test.ts diff --git a/src/mcpd/src/repositories/mcp-server.repository.ts b/src/mcpd/src/repositories/mcp-server.repository.ts index 2e26621..731cdee 100644 --- a/src/mcpd/src/repositories/mcp-server.repository.ts +++ b/src/mcpd/src/repositories/mcp-server.repository.ts @@ -34,6 +34,8 @@ export class McpServerRepository implements IMcpServerRepository { env: data.env, healthCheck: (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue, volumes: data.volumes, + secretDelivery: data.secretDelivery, + entrypoint: (data.entrypoint ?? Prisma.DbNull) as Prisma.InputJsonValue, }, }); } @@ -53,6 +55,8 @@ export class McpServerRepository implements IMcpServerRepository { if (data.env !== undefined) updateData['env'] = data.env; if (data.healthCheck !== undefined) updateData['healthCheck'] = (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue; if (data.volumes !== undefined) updateData['volumes'] = data.volumes; + if (data.secretDelivery !== undefined) updateData['secretDelivery'] = data.secretDelivery; + if (data.entrypoint !== undefined) updateData['entrypoint'] = (data.entrypoint ?? Prisma.JsonNull) as Prisma.InputJsonValue; return this.prisma.mcpServer.update({ where: { id }, data: updateData }); } diff --git a/src/mcpd/tests/mcp-server-repository-fields.test.ts b/src/mcpd/tests/mcp-server-repository-fields.test.ts new file mode 100644 index 0000000..843abaf --- /dev/null +++ b/src/mcpd/tests/mcp-server-repository-fields.test.ts @@ -0,0 +1,48 @@ +/** + * The server repository maps update/create fields explicitly, field by field. + * That means a new column silently does nothing until it is added here — and + * the failure is invisible: `mcpctl patch server x secretDelivery=injector` + * returns "patched" while the value never changes. + * + * Caught exactly that way in production. These assert the mapping instead. + */ +import { describe, it, expect, vi } from 'vitest'; +import { McpServerRepository } from '../src/repositories/mcp-server.repository.js'; +import type { PrismaClient } from '@prisma/client'; + +function prismaSpy() { + const update = vi.fn(async ({ data }: { data: Record }) => data); + const create = vi.fn(async ({ data }: { data: Record }) => data); + return { spy: { mcpServer: { update, create } } as unknown as PrismaClient, update, create }; +} + +describe('McpServerRepository field mapping', () => { + it('persists secretDelivery on update', async () => { + const { spy, update } = prismaSpy(); + await new McpServerRepository(spy).update('id1', { secretDelivery: 'injector' }); + expect(update.mock.calls[0]?.[0].data).toMatchObject({ secretDelivery: 'injector' }); + }); + + it('persists entrypoint on update', async () => { + const { spy, update } = prismaSpy(); + await new McpServerRepository(spy).update('id1', { entrypoint: ['/bin/x', '--flag'] }); + expect(update.mock.calls[0]?.[0].data).toMatchObject({ entrypoint: ['/bin/x', '--flag'] }); + }); + + it('leaves both untouched when not supplied', async () => { + const { spy, update } = prismaSpy(); + await new McpServerRepository(spy).update('id1', { description: 'x' }); + const data = update.mock.calls[0]?.[0].data ?? {}; + expect(data).not.toHaveProperty('secretDelivery'); + expect(data).not.toHaveProperty('entrypoint'); + }); + + it('persists secretDelivery on create', async () => { + const { spy, create } = prismaSpy(); + await new McpServerRepository(spy).create({ + name: 'x', description: '', transport: 'STDIO', replicas: 1, env: [], volumes: [], + secretDelivery: 'injector', + } as never); + expect(create.mock.calls[0]?.[0].data).toMatchObject({ secretDelivery: 'injector' }); + }); +}); -- 2.49.1