feat: remove proxyMode — all traffic goes through mcplocal proxy

proxyMode "direct" was a security hole (leaked secrets as plaintext env
vars in .mcp.json) and bypassed all mcplocal features (gating, audit,
RBAC, content pipeline, namespacing). Removed from schema, API, CLI,
and all tests. Old configs with proxyMode are accepted but silently
stripped via Zod .transform() for backward compatibility.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michal
2026-03-07 23:36:36 +00:00
parent d9d0a7a374
commit 0995851810
28 changed files with 69 additions and 221 deletions

View File

@@ -125,7 +125,6 @@ const ProjectSpecSchema = z.object({
name: z.string().min(1),
description: z.string().default(''),
prompt: z.string().max(10000).default(''),
proxyMode: z.enum(['direct', 'filtered']).default('direct'),
proxyModel: z.string().optional(),
gated: z.boolean().optional(),
llmProvider: z.string().optional(),

View File

@@ -225,7 +225,6 @@ export function createCreateCommand(deps: CreateCommandDeps): Command {
.description('Create a project')
.argument('<name>', 'Project name')
.option('-d, --description <text>', 'Project description', '')
.option('--proxy-mode <mode>', 'Proxy mode (direct, filtered)')
.option('--proxy-model <name>', 'Plugin name (default, content-pipeline, gate, none)')
.option('--prompt <text>', 'Project-level prompt / instructions for the LLM')
.option('--gated', '[deprecated: use --proxy-model default]')
@@ -236,7 +235,6 @@ export function createCreateCommand(deps: CreateCommandDeps): Command {
const body: Record<string, unknown> = {
name,
description: opts.description,
proxyMode: opts.proxyMode ?? 'direct',
};
if (opts.prompt) body.prompt = opts.prompt;
if (opts.proxyModel) {

View File

@@ -143,8 +143,7 @@ function formatProjectDetail(
lines.push(`${pad('Name:')}${project.name}`);
if (project.description) lines.push(`${pad('Description:')}${project.description}`);
// Plugin & proxy config
const proxyMode = project.proxyMode as string | undefined;
// Plugin config
const proxyModel = (project.proxyModel as string | undefined) || 'default';
const llmProvider = project.llmProvider as string | undefined;
const llmModel = project.llmModel as string | undefined;
@@ -152,7 +151,6 @@ function formatProjectDetail(
lines.push('');
lines.push('Plugin Config:');
lines.push(` ${pad('Plugin:', 18)}${proxyModel}`);
lines.push(` ${pad('Mode:', 18)}${proxyMode ?? 'direct'}`);
if (llmProvider) lines.push(` ${pad('LLM Provider:', 18)}${llmProvider}`);
if (llmModel) lines.push(` ${pad('LLM Model:', 18)}${llmModel}`);

View File

@@ -23,7 +23,6 @@ interface ProjectRow {
id: string;
name: string;
description: string;
proxyMode: string;
proxyModel: string;
gated?: boolean;
ownerId: string;
@@ -86,7 +85,6 @@ interface RbacRow {
const projectColumns: Column<ProjectRow>[] = [
{ header: 'NAME', key: 'name' },
{ header: 'MODE', key: (r) => r.proxyMode ?? 'direct', width: 10 },
{ header: 'PLUGIN', key: (r) => r.proxyModel || 'default', width: 18 },
{ header: 'SERVERS', key: (r) => r.servers ? String(r.servers.length) : '0', width: 8 },
{ header: 'DESCRIPTION', key: 'description', width: 30 },

View File

@@ -332,7 +332,6 @@ rbacBindings:
projects:
- name: smart-home
description: Home automation
proxyMode: filtered
llmProvider: gemini-cli
llmModel: gemini-2.0-flash
servers:
@@ -345,7 +344,6 @@ projects:
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
name: 'smart-home',
proxyMode: 'filtered',
llmProvider: 'gemini-cli',
llmModel: 'gemini-2.0-flash',
servers: ['my-grafana', 'my-ha'],

View File

@@ -175,7 +175,6 @@ describe('create command', () => {
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', {
name: 'my-project',
description: 'A test project',
proxyMode: 'direct',
});
expect(output.join('\n')).toContain("project 'test' created");
});
@@ -186,7 +185,6 @@ describe('create command', () => {
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', {
name: 'minimal',
description: '',
proxyMode: 'direct',
});
});
@@ -195,7 +193,7 @@ describe('create command', () => {
vi.mocked(client.get).mockResolvedValueOnce([{ id: 'proj-1', name: 'my-proj' }] as never);
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['project', 'my-proj', '-d', 'updated', '--force'], { from: 'user' });
expect(client.put).toHaveBeenCalledWith('/api/v1/projects/proj-1', { description: 'updated', proxyMode: 'direct' });
expect(client.put).toHaveBeenCalledWith('/api/v1/projects/proj-1', { description: 'updated' });
expect(output.join('\n')).toContain("project 'my-proj' updated");
});
});

View File

@@ -96,7 +96,6 @@ describe('describe command', () => {
description: 'A gated project',
ownerId: 'user-1',
proxyModel: 'default',
proxyMode: 'direct',
createdAt: '2025-01-01',
});
const cmd = createDescribeCommand(deps);

View File

@@ -179,7 +179,6 @@ describe('get command', () => {
id: 'proj-1',
name: 'smart-home',
description: 'Home automation',
proxyMode: 'filtered',
ownerId: 'usr-1',
servers: [{ server: { name: 'grafana' } }],
}]);
@@ -187,10 +186,8 @@ describe('get command', () => {
await cmd.parseAsync(['node', 'test', 'projects']);
const text = deps.output.join('\n');
expect(text).toContain('MODE');
expect(text).toContain('SERVERS');
expect(text).toContain('smart-home');
expect(text).toContain('filtered');
expect(text).toContain('1');
});
@@ -341,7 +338,6 @@ describe('get command', () => {
id: 'proj-1',
name: 'home',
description: '',
proxyMode: 'direct',
proxyModel: '',
gated: true,
ownerId: 'usr-1',
@@ -362,7 +358,6 @@ describe('get command', () => {
id: 'proj-1',
name: 'home',
description: '',
proxyMode: 'direct',
proxyModel: '',
gated: true,
ownerId: 'usr-1',
@@ -381,7 +376,6 @@ describe('get command', () => {
id: 'proj-1',
name: 'tools',
description: '',
proxyMode: 'direct',
proxyModel: '',
gated: false,
ownerId: 'usr-1',
@@ -400,7 +394,6 @@ describe('get command', () => {
id: 'proj-1',
name: 'custom',
description: '',
proxyMode: 'direct',
proxyModel: 'gate',
gated: true,
ownerId: 'usr-1',

View File

@@ -24,12 +24,11 @@ describe('project with new fields', () => {
});
describe('create project with enhanced options', () => {
it('creates project with proxy mode and servers', async () => {
it('creates project with servers', async () => {
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync([
'project', 'smart-home',
'-d', 'Smart home project',
'--proxy-mode', 'filtered',
'--server', 'my-grafana',
'--server', 'my-ha',
], { from: 'user' });
@@ -37,30 +36,19 @@ describe('project with new fields', () => {
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
name: 'smart-home',
description: 'Smart home project',
proxyMode: 'filtered',
servers: ['my-grafana', 'my-ha'],
}));
});
it('defaults proxy mode to direct', async () => {
const cmd = createCreateCommand({ client, log });
await cmd.parseAsync(['project', 'basic'], { from: 'user' });
expect(client.post).toHaveBeenCalledWith('/api/v1/projects', expect.objectContaining({
proxyMode: 'direct',
}));
});
});
describe('get projects shows new columns', () => {
it('shows MODE and SERVERS columns', async () => {
it('shows SERVERS column', async () => {
const deps = {
output: [] as string[],
fetchResource: vi.fn(async () => [{
id: 'proj-1',
name: 'smart-home',
description: 'Test',
proxyMode: 'filtered',
ownerId: 'user-1',
servers: [{ server: { name: 'grafana' } }, { server: { name: 'ha' } }],
}]),
@@ -70,14 +58,13 @@ describe('project with new fields', () => {
await cmd.parseAsync(['node', 'test', 'projects']);
const text = deps.output.join('\n');
expect(text).toContain('MODE');
expect(text).toContain('SERVERS');
expect(text).toContain('smart-home');
});
});
describe('describe project shows full detail', () => {
it('shows servers and proxy config', async () => {
it('shows servers and LLM config', async () => {
const deps = {
output: [] as string[],
client: mockClient(),
@@ -85,7 +72,6 @@ describe('project with new fields', () => {
id: 'proj-1',
name: 'smart-home',
description: 'Smart home',
proxyMode: 'filtered',
llmProvider: 'gemini-cli',
llmModel: 'gemini-2.0-flash',
ownerId: 'user-1',
@@ -103,7 +89,6 @@ describe('project with new fields', () => {
const text = deps.output.join('\n');
expect(text).toContain('=== Project: smart-home ===');
expect(text).toContain('filtered');
expect(text).toContain('gemini-cli');
expect(text).toContain('my-grafana');
expect(text).toContain('my-ha');