ungated-global: system project whose servers ride every project, pre-gate
Some checks failed
CI/CD / lint (pull_request) Successful in 1m12s
CI/CD / test (pull_request) Successful in 1m26s
CI/CD / typecheck (pull_request) Successful in 3m22s
CI/CD / smoke (pull_request) Failing after 1m57s
CI/CD / build (pull_request) Successful in 5m15s
CI/CD / publish (pull_request) Has been skipped

New seed-once system project `ungated-global`: every server attached to it
is merged into every project's MCP endpoint in front of the gate (listed
and callable before begin_session), and its project prompt is prepended to
every project's instructions.

- mcpd: bootstrapUngatedGlobal seeds the project once-ever (marker key
  `ungatedGlobalSeeded` in mcpctl-system-settings), with default server
  `calcmcp` (patched CalcMCP fork, mysources.co.uk/michal/calcmcp:1.1.0)
  and a "never do math yourself, always use calcmcp" prompt. Deletions are
  durable: `mcpctl delete server calcmcp -p ungated-global` stays deleted.
- mcpd: GET /projects/:id/servers appends global servers flagged
  `ungatedGlobal: true` (direct attachment wins, no self-merge);
  GET /projects/:id/instructions prepends the global prompt and lists the
  global servers.
- mcplocal: discovery surfaces the flagged names; the gate plugin exempts
  them — gated tools/list returns begin_session + exempt tools, calls to
  them pass through without ungating, and initialize instructions list
  them under "Available immediately (no begin_session required)".
- templates/calcmcp.yaml so a deleted default can be re-created by hand.
- docs/ungated-global.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-10-03 01:45:44 +01:00
parent 23c8218811
commit 7b70c91c41
18 changed files with 584 additions and 27 deletions

View File

@@ -0,0 +1,112 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import {
bootstrapUngatedGlobal,
UNGATED_GLOBAL_PROJECT_NAME,
SETTING_UNGATED_GLOBAL_SEEDED,
} from '../src/bootstrap/ungated-global.js';
import { SYSTEM_SETTINGS_SECRET } from '../src/bootstrap/system-settings.js';
import type { PrismaClient } from '@prisma/client';
function mockPrisma(opts: { settingsData?: Record<string, unknown> | null } = {}): PrismaClient {
const secretRow = opts.settingsData
? { id: 'secret-1', name: SYSTEM_SETTINGS_SECRET, data: opts.settingsData, version: 3 }
: null;
return {
secret: {
findUnique: vi.fn(async () => secretRow),
update: vi.fn(async () => ({})),
create: vi.fn(async () => ({})),
},
user: {
upsert: vi.fn(async () => ({ id: 'sys-user-id', email: 'system@mcpctl.local' })),
},
project: {
upsert: vi.fn(async (args: { where: { name: string } }) => ({ id: 'proj-ug', name: args.where.name })),
},
mcpServer: {
upsert: vi.fn(async (args: { where: { name: string } }) => ({ id: 'srv-calc', name: args.where.name })),
},
projectServer: {
upsert: vi.fn(async () => ({})),
},
} as unknown as PrismaClient;
}
describe('bootstrapUngatedGlobal', () => {
let prisma: PrismaClient;
beforeEach(() => {
prisma = mockPrisma();
});
it('seeds the ungated-global project with the calcmcp server and attaches it', async () => {
await bootstrapUngatedGlobal(prisma);
expect(prisma.project.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { name: UNGATED_GLOBAL_PROJECT_NAME },
create: expect.objectContaining({ gated: false, prompt: expect.stringContaining('calcmcp') }),
}),
);
expect(prisma.mcpServer.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { name: 'calcmcp' },
create: expect.objectContaining({
dockerImage: expect.stringContaining('calcmcp'),
transport: 'STDIO',
}),
}),
);
expect(prisma.projectServer.upsert).toHaveBeenCalledWith(
expect.objectContaining({
where: { projectId_serverId: { projectId: 'proj-ug', serverId: 'srv-calc' } },
}),
);
});
it('marks the seed as done in the system settings secret', async () => {
await bootstrapUngatedGlobal(prisma);
// Settings secret didn't exist → created with the marker key
expect(prisma.secret.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
name: SYSTEM_SETTINGS_SECRET,
data: expect.objectContaining({ [SETTING_UNGATED_GLOBAL_SEEDED]: true }),
}),
}),
);
});
it('does NOT re-seed when the marker is set (deletions stay deleted)', async () => {
prisma = mockPrisma({ settingsData: { [SETTING_UNGATED_GLOBAL_SEEDED]: true } });
await bootstrapUngatedGlobal(prisma);
expect(prisma.project.upsert).not.toHaveBeenCalled();
expect(prisma.mcpServer.upsert).not.toHaveBeenCalled();
expect(prisma.projectServer.upsert).not.toHaveBeenCalled();
expect(prisma.secret.update).not.toHaveBeenCalled();
expect(prisma.secret.create).not.toHaveBeenCalled();
});
it('preserves existing settings keys when writing the marker', async () => {
prisma = mockPrisma({ settingsData: { allowSelfPasswordChange: false } });
await bootstrapUngatedGlobal(prisma);
expect(prisma.secret.update).toHaveBeenCalledWith(
expect.objectContaining({
where: { name: SYSTEM_SETTINGS_SECRET },
data: expect.objectContaining({
data: {
allowSelfPasswordChange: false,
[SETTING_UNGATED_GLOBAL_SEEDED]: true,
},
version: 4,
}),
}),
);
});
});