feat: remove ProjectMember, add expose RBAC role, attach/detach-server commands

- Remove ProjectMember model entirely (RBAC manages project access)
- Add 'expose' RBAC role for /mcp-config endpoint access (edit implies expose)
- Rename CLI flags: --llm-provider → --proxy-mode-llm-provider, --llm-model → --proxy-mode-llm-model
- Add attach-server / detach-server CLI commands (mcpctl --project NAME attach-server SERVER)
- Add POST/DELETE /api/v1/projects/:id/servers endpoints for server attach/detach
- Remove members from backup/restore, apply, get, describe
- Prisma migration to drop ProjectMember table

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-02-23 17:50:01 +00:00
parent 1f628d39d2
commit 329315ec71
23 changed files with 283 additions and 219 deletions

View File

@@ -326,7 +326,7 @@ rbacBindings:
rmSync(tmpDir, { recursive: true, force: true });
});
it('applies projects with servers and members', async () => {
it('applies projects with servers', async () => {
const configPath = join(tmpDir, 'config.yaml');
writeFileSync(configPath, `
projects:
@@ -338,9 +338,6 @@ projects:
servers:
- my-grafana
- my-ha
members:
- alice@test.com
- bob@test.com
`);
const cmd = createApplyCommand({ client, log });
@@ -352,7 +349,6 @@ projects:
llmProvider: 'gemini-cli',
llmModel: 'gemini-2.0-flash',
servers: ['my-grafana', 'my-ha'],
members: ['alice@test.com', 'bob@test.com'],
}));
expect(output.join('\n')).toContain('Created project: smart-home');

View File

@@ -181,7 +181,6 @@ describe('get command', () => {
proxyMode: 'filtered',
ownerId: 'usr-1',
servers: [{ server: { name: 'grafana' } }],
members: [{ user: { email: 'a@b.com' }, role: 'admin' }, { user: { email: 'c@d.com' }, role: 'member' }],
}]);
const cmd = createGetCommand(deps);
await cmd.parseAsync(['node', 'test', 'projects']);
@@ -189,11 +188,9 @@ describe('get command', () => {
const text = deps.output.join('\n');
expect(text).toContain('MODE');
expect(text).toContain('SERVERS');
expect(text).toContain('MEMBERS');
expect(text).toContain('smart-home');
expect(text).toContain('filtered');
expect(text).toContain('1');
expect(text).toContain('2');
});
it('displays mixed resource and operation bindings', async () => {

View File

@@ -30,8 +30,8 @@ describe('project with new fields', () => {
'project', 'smart-home',
'-d', 'Smart home project',
'--proxy-mode', 'filtered',
'--llm-provider', 'gemini-cli',
'--llm-model', 'gemini-2.0-flash',
'--proxy-mode-llm-provider', 'gemini-cli',
'--proxy-mode-llm-model', 'gemini-2.0-flash',
'--server', 'my-grafana',
'--server', 'my-ha',
], { from: 'user' });
@@ -46,20 +46,6 @@ describe('project with new fields', () => {
}));
});
it('creates project with members', async () => {
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync([
'project', 'team-project',
'--member', 'alice@test.com',
'--member', 'bob@test.com',
], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
name: 'team-project',
members: ['alice@test.com', 'bob@test.com'],
}));
});
it('defaults proxy mode to direct', async () => {
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['project', 'basic'], { from: 'user' });
@@ -71,7 +57,7 @@ describe('project with new fields', () => {
});
describe('get projects shows new columns', () => {
it('shows MODE, SERVERS, MEMBERS columns', async () => {
it('shows MODE and SERVERS columns', async () => {
const deps = {
output: [] as string[],
fetchResource: vi.fn(async () => [{
@@ -81,7 +67,6 @@ describe('project with new fields', () => {
proxyMode: 'filtered',
ownerId: 'user-1',
servers: [{ server: { name: 'grafana' } }, { server: { name: 'ha' } }],
members: [{ user: { email: 'alice@test.com' } }],
}]),
log: (...args: string[]) => deps.output.push(args.join(' ')),
};
@@ -91,13 +76,12 @@ describe('project with new fields', () => {
const text = deps.output.join('\n');
expect(text).toContain('MODE');
expect(text).toContain('SERVERS');
expect(text).toContain('MEMBERS');
expect(text).toContain('smart-home');
});
});
describe('describe project shows full detail', () => {
it('shows servers and members', async () => {
it('shows servers and proxy config', async () => {
const deps = {
output: [] as string[],
client: mockClient(),
@@ -113,10 +97,6 @@ describe('project with new fields', () => {
{ server: { name: 'my-grafana' } },
{ server: { name: 'my-ha' } },
],
members: [
{ user: { email: 'alice@test.com' } },
{ user: { email: 'bob@test.com' } },
],
createdAt: '2025-01-01',
updatedAt: '2025-01-01',
})),
@@ -131,8 +111,6 @@ describe('project with new fields', () => {
expect(text).toContain('gemini-cli');
expect(text).toContain('my-grafana');
expect(text).toContain('my-ha');
expect(text).toContain('alice@test.com');
expect(text).toContain('bob@test.com');
});
});
});