fix(servers): persist secretDelivery and entrypoint #118

Merged
michal merged 1 commits from fix/server-repo-field-mapping into main 2026-08-20 22:41:13 +00:00
2 changed files with 52 additions and 0 deletions

View File

@@ -34,6 +34,8 @@ export class McpServerRepository implements IMcpServerRepository {
env: data.env, env: data.env,
healthCheck: (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue, healthCheck: (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue,
volumes: data.volumes, 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.env !== undefined) updateData['env'] = data.env;
if (data.healthCheck !== undefined) updateData['healthCheck'] = (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue; if (data.healthCheck !== undefined) updateData['healthCheck'] = (data.healthCheck ?? Prisma.JsonNull) as Prisma.InputJsonValue;
if (data.volumes !== undefined) updateData['volumes'] = data.volumes; 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 }); return this.prisma.mcpServer.update({ where: { id }, data: updateData });
} }

View File

@@ -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<string, unknown> }) => data);
const create = vi.fn(async ({ data }: { data: Record<string, unknown> }) => 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' });
});
});