From 0995851810551276de3af17d92a103837c9c457a Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 7 Mar 2026 23:36:36 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20remove=20proxyMode=20=E2=80=94=20all=20?= =?UTF-8?q?traffic=20goes=20through=20mcplocal=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- completions/mcpctl.bash | 22 +++- completions/mcpctl.fish | 13 ++- src/cli/src/commands/apply.ts | 1 - src/cli/src/commands/create.ts | 2 - src/cli/src/commands/describe.ts | 4 +- src/cli/src/commands/get.ts | 2 - src/cli/tests/commands/apply.test.ts | 2 - src/cli/tests/commands/create.test.ts | 4 +- src/cli/tests/commands/describe.test.ts | 1 - src/cli/tests/commands/get.test.ts | 7 -- src/cli/tests/commands/project.test.ts | 21 +--- .../migration.sql | 2 + src/db/prisma/schema.prisma | 1 - src/db/tests/models.test.ts | 9 +- src/mcpd/src/bootstrap/system-project.ts | 1 - src/mcpd/src/main.ts | 2 +- .../src/repositories/project.repository.ts | 5 +- .../src/services/backup/backup-service.ts | 1 - .../src/services/backup/restore-service.ts | 4 +- src/mcpd/src/services/project.service.ts | 37 ++---- src/mcpd/src/validation/project.schema.ts | 13 +-- src/mcpd/tests/backup.test.ts | 6 +- src/mcpd/tests/project-routes.test.ts | 17 +-- src/mcpd/tests/project-service.test.ts | 109 ++---------------- src/mcpd/tests/prompt-routes.test.ts | 1 - .../tests/services/prompt-service.test.ts | 1 - .../tests/system-prompt-validation.test.ts | 1 - .../tests/smoke/fixtures/smoke-data.yaml | 1 - 28 files changed, 69 insertions(+), 221 deletions(-) create mode 100644 src/db/prisma/migrations/20260307120000_remove_proxy_mode/migration.sql diff --git a/completions/mcpctl.bash b/completions/mcpctl.bash index f35cb50..4c89749 100644 --- a/completions/mcpctl.bash +++ b/completions/mcpctl.bash @@ -5,7 +5,7 @@ _mcpctl() { local cur prev words cword _init_completion || return - local commands="status login logout config get describe delete logs create edit apply patch backup restore approve console" + local commands="status login logout config get describe delete logs create edit apply patch backup restore approve console cache" local project_commands="get describe delete logs create edit attach-server detach-server" local global_opts="-v --version --daemon-url --direct -p --project -h --help" local resources="servers instances secrets templates projects users groups rbac prompts promptrequests serverattachments proxymodels all" @@ -185,7 +185,7 @@ _mcpctl() { COMPREPLY=($(compgen -W "--data --force -h --help" -- "$cur")) ;; project) - COMPREPLY=($(compgen -W "-d --description --proxy-mode --proxy-model --prompt --gated --no-gated --server --force -h --help" -- "$cur")) + COMPREPLY=($(compgen -W "-d --description --proxy-model --prompt --gated --no-gated --server --force -h --help" -- "$cur")) ;; user) COMPREPLY=($(compgen -W "--password --name --force -h --help" -- "$cur")) @@ -281,6 +281,24 @@ _mcpctl() { COMPREPLY=($(compgen -W "--stdin-mcp --audit -h --help" -- "$cur")) fi return ;; + cache) + local cache_sub=$(_mcpctl_get_subcmd $subcmd_pos) + if [[ -z "$cache_sub" ]]; then + COMPREPLY=($(compgen -W "stats clear help" -- "$cur")) + else + case "$cache_sub" in + stats) + COMPREPLY=($(compgen -W "-h --help" -- "$cur")) + ;; + clear) + COMPREPLY=($(compgen -W "--older-than -y --yes -h --help" -- "$cur")) + ;; + *) + COMPREPLY=($(compgen -W "-h --help" -- "$cur")) + ;; + esac + fi + return ;; help) COMPREPLY=($(compgen -W "$commands" -- "$cur")) return ;; diff --git a/completions/mcpctl.fish b/completions/mcpctl.fish index 0a6334f..96ec444 100644 --- a/completions/mcpctl.fish +++ b/completions/mcpctl.fish @@ -4,7 +4,7 @@ # Erase any stale completions from previous versions complete -c mcpctl -e -set -l commands status login logout config get describe delete logs create edit apply patch backup restore approve console +set -l commands status login logout config get describe delete logs create edit apply patch backup restore approve console cache set -l project_commands get describe delete logs create edit attach-server detach-server # Disable file completions by default @@ -231,6 +231,7 @@ complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_ complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a restore -d 'Restore mcpctl configuration from a backup file' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a approve -d 'Approve a pending prompt request (atomic: delete request, create prompt)' complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a console -d 'Interactive MCP console — unified timeline with tools, provenance, and lab replay' +complete -c mcpctl -n "not __mcpctl_has_project; and not __fish_seen_subcommand_from $commands" -a cache -d 'Manage ProxyModel pipeline cache' # Project-scoped commands (with --project) complete -c mcpctl -n "__mcpctl_has_project; and not __fish_seen_subcommand_from $project_commands" -a get -d 'List resources (servers, projects, instances, all)' @@ -313,7 +314,6 @@ complete -c mcpctl -n "__mcpctl_subcmd_active create secret" -l force -d 'Update # create project options complete -c mcpctl -n "__mcpctl_subcmd_active create project" -s d -l description -d 'Project description' -x -complete -c mcpctl -n "__mcpctl_subcmd_active create project" -l proxy-mode -d 'Proxy mode (direct, filtered)' -x complete -c mcpctl -n "__mcpctl_subcmd_active create project" -l proxy-model -d 'Plugin name (default, content-pipeline, gate, none)' -x complete -c mcpctl -n "__mcpctl_subcmd_active create project" -l prompt -d 'Project-level prompt / instructions for the LLM' -x complete -c mcpctl -n "__mcpctl_subcmd_active create project" -l gated -d '[deprecated: use --proxy-model default]' @@ -353,6 +353,15 @@ complete -c mcpctl -n "__mcpctl_subcmd_active create promptrequest" -l content - complete -c mcpctl -n "__mcpctl_subcmd_active create promptrequest" -l content-file -d 'Read prompt content from file' -rF complete -c mcpctl -n "__mcpctl_subcmd_active create promptrequest" -l priority -d 'Priority 1-10 (default: 5, higher = more important)' -x +# cache subcommands +set -l cache_cmds stats clear +complete -c mcpctl -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from $cache_cmds" -a stats -d 'Show cache statistics' +complete -c mcpctl -n "__fish_seen_subcommand_from cache; and not __fish_seen_subcommand_from $cache_cmds" -a clear -d 'Clear cache entries' + +# cache clear options +complete -c mcpctl -n "__mcpctl_subcmd_active cache clear" -l older-than -d 'Clear entries older than N days' -x +complete -c mcpctl -n "__mcpctl_subcmd_active cache clear" -s y -l yes -d 'Skip confirmation' + # status options complete -c mcpctl -n "__fish_seen_subcommand_from status" -s o -l output -d 'output format (table, json, yaml)' -x diff --git a/src/cli/src/commands/apply.ts b/src/cli/src/commands/apply.ts index 137fe92..973157d 100644 --- a/src/cli/src/commands/apply.ts +++ b/src/cli/src/commands/apply.ts @@ -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(), diff --git a/src/cli/src/commands/create.ts b/src/cli/src/commands/create.ts index 2327c86..7954884 100644 --- a/src/cli/src/commands/create.ts +++ b/src/cli/src/commands/create.ts @@ -225,7 +225,6 @@ export function createCreateCommand(deps: CreateCommandDeps): Command { .description('Create a project') .argument('', 'Project name') .option('-d, --description ', 'Project description', '') - .option('--proxy-mode ', 'Proxy mode (direct, filtered)') .option('--proxy-model ', 'Plugin name (default, content-pipeline, gate, none)') .option('--prompt ', '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 = { name, description: opts.description, - proxyMode: opts.proxyMode ?? 'direct', }; if (opts.prompt) body.prompt = opts.prompt; if (opts.proxyModel) { diff --git a/src/cli/src/commands/describe.ts b/src/cli/src/commands/describe.ts index 887dbeb..9fc1d24 100644 --- a/src/cli/src/commands/describe.ts +++ b/src/cli/src/commands/describe.ts @@ -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}`); diff --git a/src/cli/src/commands/get.ts b/src/cli/src/commands/get.ts index f22609c..af82a06 100644 --- a/src/cli/src/commands/get.ts +++ b/src/cli/src/commands/get.ts @@ -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[] = [ { 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 }, diff --git a/src/cli/tests/commands/apply.test.ts b/src/cli/tests/commands/apply.test.ts index b12ae87..962700d 100644 --- a/src/cli/tests/commands/apply.test.ts +++ b/src/cli/tests/commands/apply.test.ts @@ -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'], diff --git a/src/cli/tests/commands/create.test.ts b/src/cli/tests/commands/create.test.ts index 081d6ce..fe8eb0f 100644 --- a/src/cli/tests/commands/create.test.ts +++ b/src/cli/tests/commands/create.test.ts @@ -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"); }); }); diff --git a/src/cli/tests/commands/describe.test.ts b/src/cli/tests/commands/describe.test.ts index e1d6e68..979ddba 100644 --- a/src/cli/tests/commands/describe.test.ts +++ b/src/cli/tests/commands/describe.test.ts @@ -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); diff --git a/src/cli/tests/commands/get.test.ts b/src/cli/tests/commands/get.test.ts index 71865a6..23912df 100644 --- a/src/cli/tests/commands/get.test.ts +++ b/src/cli/tests/commands/get.test.ts @@ -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', diff --git a/src/cli/tests/commands/project.test.ts b/src/cli/tests/commands/project.test.ts index b6713a9..f09a771 100644 --- a/src/cli/tests/commands/project.test.ts +++ b/src/cli/tests/commands/project.test.ts @@ -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'); diff --git a/src/db/prisma/migrations/20260307120000_remove_proxy_mode/migration.sql b/src/db/prisma/migrations/20260307120000_remove_proxy_mode/migration.sql new file mode 100644 index 0000000..d2088b9 --- /dev/null +++ b/src/db/prisma/migrations/20260307120000_remove_proxy_mode/migration.sql @@ -0,0 +1,2 @@ +-- Remove proxyMode column (redundant — all traffic goes through mcplocal proxy) +ALTER TABLE "Project" DROP COLUMN IF EXISTS "proxyMode"; diff --git a/src/db/prisma/schema.prisma b/src/db/prisma/schema.prisma index 6e49e61..e12acff 100644 --- a/src/db/prisma/schema.prisma +++ b/src/db/prisma/schema.prisma @@ -173,7 +173,6 @@ model Project { name String @unique description String @default("") prompt String @default("") - proxyMode String @default("direct") proxyModel String @default("") gated Boolean @default(true) llmProvider String? diff --git a/src/db/tests/models.test.ts b/src/db/tests/models.test.ts index 6915cad..dbedd36 100644 --- a/src/db/tests/models.test.ts +++ b/src/db/tests/models.test.ts @@ -506,23 +506,16 @@ describe('ProjectServer', () => { // ── Project new fields ── describe('Project new fields', () => { - it('defaults proxyMode to direct', async () => { - const project = await createProject(); - expect(project.proxyMode).toBe('direct'); - }); - - it('stores proxyMode, llmProvider, llmModel', async () => { + it('stores llmProvider, llmModel', async () => { const user = await createUser(); const project = await prisma.project.create({ data: { name: 'filtered-project', ownerId: user.id, - proxyMode: 'filtered', llmProvider: 'gemini-cli', llmModel: 'gemini-2.0-flash', }, }); - expect(project.proxyMode).toBe('filtered'); expect(project.llmProvider).toBe('gemini-cli'); expect(project.llmModel).toBe('gemini-2.0-flash'); }); diff --git a/src/mcpd/src/bootstrap/system-project.ts b/src/mcpd/src/bootstrap/system-project.ts index 5530490..27b08d9 100644 --- a/src/mcpd/src/bootstrap/system-project.ts +++ b/src/mcpd/src/bootstrap/system-project.ts @@ -144,7 +144,6 @@ export async function bootstrapSystemProject(prisma: PrismaClient): Promise { const instanceService = new InstanceService(instanceRepo, serverRepo, orchestrator, secretRepo); serverService.setInstanceService(instanceService); const secretService = new SecretService(secretRepo); - const projectService = new ProjectService(projectRepo, serverRepo, secretRepo); + const projectService = new ProjectService(projectRepo, serverRepo); const auditLogService = new AuditLogService(auditLogRepo); const auditEventService = new AuditEventService(auditEventRepo); const metricsCollector = new MetricsCollector(); diff --git a/src/mcpd/src/repositories/project.repository.ts b/src/mcpd/src/repositories/project.repository.ts index 343d046..fd086fd 100644 --- a/src/mcpd/src/repositories/project.repository.ts +++ b/src/mcpd/src/repositories/project.repository.ts @@ -12,7 +12,7 @@ export interface IProjectRepository { findAll(ownerId?: string): Promise; findById(id: string): Promise; findByName(name: string): Promise; - create(data: { name: string; description: string; prompt?: string; ownerId: string; proxyMode: string; proxyModel?: string; gated?: boolean; llmProvider?: string; llmModel?: string; serverOverrides?: Record }): Promise; + create(data: { name: string; description: string; prompt?: string; ownerId: string; proxyModel?: string; gated?: boolean; llmProvider?: string; llmModel?: string; serverOverrides?: Record }): Promise; update(id: string, data: Record): Promise; delete(id: string): Promise; setServers(projectId: string, serverIds: string[]): Promise; @@ -36,12 +36,11 @@ export class ProjectRepository implements IProjectRepository { return this.prisma.project.findUnique({ where: { name }, include: PROJECT_INCLUDE }) as unknown as Promise; } - async create(data: { name: string; description: string; prompt?: string; ownerId: string; proxyMode: string; proxyModel?: string; gated?: boolean; llmProvider?: string; llmModel?: string; serverOverrides?: Record }): Promise { + async create(data: { name: string; description: string; prompt?: string; ownerId: string; proxyModel?: string; gated?: boolean; llmProvider?: string; llmModel?: string; serverOverrides?: Record }): Promise { const createData: Record = { name: data.name, description: data.description, ownerId: data.ownerId, - proxyMode: data.proxyMode, }; if (data.prompt !== undefined) createData['prompt'] = data.prompt; if (data.proxyModel !== undefined) createData['proxyModel'] = data.proxyModel; diff --git a/src/mcpd/src/services/backup/backup-service.ts b/src/mcpd/src/services/backup/backup-service.ts index 6ee0db6..c4a468a 100644 --- a/src/mcpd/src/services/backup/backup-service.ts +++ b/src/mcpd/src/services/backup/backup-service.ts @@ -116,7 +116,6 @@ export class BackupService { projects = allProjects.map((proj) => ({ name: proj.name, description: proj.description, - proxyMode: proj.proxyMode, proxyModel: proj.proxyModel, llmProvider: proj.llmProvider, llmModel: proj.llmModel, diff --git a/src/mcpd/src/services/backup/restore-service.ts b/src/mcpd/src/services/backup/restore-service.ts index 3ba1639..a1b82cb 100644 --- a/src/mcpd/src/services/backup/restore-service.ts +++ b/src/mcpd/src/services/backup/restore-service.ts @@ -255,7 +255,6 @@ export class RestoreService { } // overwrite const updateData: Record = { description: project.description }; - if (project.proxyMode) updateData['proxyMode'] = project.proxyMode; if (project.proxyModel) updateData['proxyModel'] = project.proxyModel; if (project.llmProvider !== undefined) updateData['llmProvider'] = project.llmProvider; if (project.llmModel !== undefined) updateData['llmModel'] = project.llmModel; @@ -271,11 +270,10 @@ export class RestoreService { continue; } - const projectCreateData: { name: string; description: string; ownerId: string; proxyMode: string; proxyModel?: string; llmProvider?: string; llmModel?: string } = { + const projectCreateData: { name: string; description: string; ownerId: string; proxyModel?: string; llmProvider?: string; llmModel?: string } = { name: project.name, description: project.description, ownerId: 'system', - proxyMode: project.proxyMode ?? 'direct', }; if (project.proxyModel) projectCreateData.proxyModel = project.proxyModel; if (project.llmProvider != null) projectCreateData.llmProvider = project.llmProvider; diff --git a/src/mcpd/src/services/project.service.ts b/src/mcpd/src/services/project.service.ts index fce11da..b464e44 100644 --- a/src/mcpd/src/services/project.service.ts +++ b/src/mcpd/src/services/project.service.ts @@ -1,17 +1,13 @@ -import type { McpServer } from '@prisma/client'; import type { IProjectRepository, ProjectWithRelations } from '../repositories/project.repository.js'; -import type { IMcpServerRepository, ISecretRepository } from '../repositories/interfaces.js'; +import type { IMcpServerRepository } from '../repositories/interfaces.js'; import { CreateProjectSchema, UpdateProjectSchema } from '../validation/project.schema.js'; import { NotFoundError, ConflictError } from './mcp-server.service.js'; -import { resolveServerEnv } from './env-resolver.js'; -import { generateMcpConfig } from './mcp-config-generator.js'; import type { McpConfig } from './mcp-config-generator.js'; export class ProjectService { constructor( private readonly projectRepo: IProjectRepository, private readonly serverRepo: IMcpServerRepository, - private readonly secretRepo: ISecretRepository, ) {} async list(ownerId?: string): Promise { @@ -55,7 +51,6 @@ export class ProjectService { description: data.description, prompt: data.prompt, ownerId, - proxyMode: data.proxyMode, proxyModel: data.proxyModel, gated: data.gated, ...(data.llmProvider !== undefined ? { llmProvider: data.llmProvider } : {}), @@ -80,7 +75,6 @@ export class ProjectService { const updateData: Record = {}; if (data.description !== undefined) updateData['description'] = data.description; if (data.prompt !== undefined) updateData['prompt'] = data.prompt; - if (data.proxyMode !== undefined) updateData['proxyMode'] = data.proxyMode; if (data.proxyModel !== undefined) updateData['proxyModel'] = data.proxyModel; if (data.llmProvider !== undefined) updateData['llmProvider'] = data.llmProvider; if (data.llmModel !== undefined) updateData['llmModel'] = data.llmModel; @@ -110,29 +104,14 @@ export class ProjectService { async generateMcpConfig(idOrName: string): Promise { const project = await this.resolveAndGet(idOrName); - if (project.proxyMode === 'filtered') { - // Single entry pointing at mcplocal proxy - return { - mcpServers: { - [project.name]: { - url: `http://localhost:3100/api/v1/mcp/proxy/project/${project.name}`, - }, + // All traffic goes through mcplocal proxy — single entry + return { + mcpServers: { + [project.name]: { + url: `http://localhost:3100/api/v1/mcp/proxy/project/${project.name}`, }, - }; - } - - // Direct mode: fetch full servers and resolve env - const serverEntries: Array<{ server: McpServer; resolvedEnv: Record }> = []; - - for (const ps of project.servers) { - const server = await this.serverRepo.findById(ps.server.id); - if (server === null) continue; - - const resolvedEnv = await resolveServerEnv(server, this.secretRepo); - serverEntries.push({ server, resolvedEnv }); - } - - return generateMcpConfig(serverEntries); + }, + }; } async addServer(idOrName: string, serverName: string): Promise { diff --git a/src/mcpd/src/validation/project.schema.ts b/src/mcpd/src/validation/project.schema.ts index b355d38..382a2d6 100644 --- a/src/mcpd/src/validation/project.schema.ts +++ b/src/mcpd/src/validation/project.schema.ts @@ -4,7 +4,6 @@ export const CreateProjectSchema = z.object({ name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/, 'Name must be lowercase alphanumeric with hyphens'), description: z.string().max(1000).default(''), prompt: z.string().max(10000).default(''), - proxyMode: z.enum(['direct', 'filtered']).default('direct'), proxyModel: z.string().max(100).default(''), gated: z.boolean().default(true), llmProvider: z.string().max(100).optional(), @@ -13,15 +12,13 @@ export const CreateProjectSchema = z.object({ serverOverrides: z.record(z.string(), z.object({ proxyModel: z.string().optional(), })).optional(), -}).refine( - (d) => d.proxyMode !== 'filtered' || d.llmProvider, - { message: 'llmProvider is required when proxyMode is "filtered"' }, -); + // Backward compat: accept but ignore proxyMode from old configs + proxyMode: z.string().optional(), +}).transform(({ proxyMode: _ignored, ...rest }) => rest); export const UpdateProjectSchema = z.object({ description: z.string().max(1000).optional(), prompt: z.string().max(10000).optional(), - proxyMode: z.enum(['direct', 'filtered']).optional(), proxyModel: z.string().max(100).optional(), gated: z.boolean().optional(), llmProvider: z.string().max(100).nullable().optional(), @@ -30,7 +27,9 @@ export const UpdateProjectSchema = z.object({ serverOverrides: z.record(z.string(), z.object({ proxyModel: z.string().optional(), })).optional(), -}); + // Backward compat: accept but ignore proxyMode from old configs + proxyMode: z.string().optional(), +}).transform(({ proxyMode: _ignored, ...rest }) => rest); export type CreateProjectInput = z.infer; export type UpdateProjectInput = z.infer; diff --git a/src/mcpd/tests/backup.test.ts b/src/mcpd/tests/backup.test.ts index 9b08060..3ea1553 100644 --- a/src/mcpd/tests/backup.test.ts +++ b/src/mcpd/tests/backup.test.ts @@ -34,7 +34,7 @@ const mockSecrets = [ const mockProjects = [ { - id: 'proj1', name: 'my-project', description: 'Test project', proxyMode: 'direct', proxyModel: '', llmProvider: null, llmModel: null, + id: 'proj1', name: 'my-project', description: 'Test project', proxyModel: '', llmProvider: null, llmModel: null, ownerId: 'user1', version: 1, createdAt: new Date(), updatedAt: new Date(), servers: [{ id: 'ps1', server: { id: 's1', name: 'github' } }], }, @@ -217,7 +217,6 @@ describe('BackupService', () => { it('includes enriched projects with server names', async () => { const bundle = await backupService.createBackup(); const proj = bundle.projects[0]!; - expect(proj.proxyMode).toBe('direct'); expect(proj.serverNames).toEqual(['github']); }); @@ -322,7 +321,7 @@ describe('RestoreService', () => { { name: 'admins', subjects: [{ kind: 'User', name: 'alice@test.com' }], roleBindings: [{ role: 'edit', resource: '*' }] }, ], projects: [ - { name: 'test-proj', description: 'Test', proxyMode: 'filtered', llmProvider: 'openai', llmModel: 'gpt-4', serverNames: ['github'], members: ['alice@test.com'] }, + { name: 'test-proj', description: 'Test', llmProvider: 'openai', llmModel: 'gpt-4', serverNames: ['github'], members: ['alice@test.com'] }, ], }; @@ -423,7 +422,6 @@ describe('RestoreService', () => { expect(result.projectsCreated).toBe(1); expect(projectRepo.create).toHaveBeenCalledWith(expect.objectContaining({ name: 'test-proj', - proxyMode: 'filtered', llmProvider: 'openai', llmModel: 'gpt-4', })); diff --git a/src/mcpd/tests/project-routes.test.ts b/src/mcpd/tests/project-routes.test.ts index 438bac6..c21757e 100644 --- a/src/mcpd/tests/project-routes.test.ts +++ b/src/mcpd/tests/project-routes.test.ts @@ -5,7 +5,7 @@ import { registerProjectRoutes } from '../src/routes/projects.js'; import { ProjectService } from '../src/services/project.service.js'; import { errorHandler } from '../src/middleware/error-handler.js'; import type { IProjectRepository, ProjectWithRelations } from '../src/repositories/project.repository.js'; -import type { IMcpServerRepository, ISecretRepository } from '../src/repositories/interfaces.js'; +import type { IMcpServerRepository } from '../src/repositories/interfaces.js'; let app: FastifyInstance; @@ -15,7 +15,6 @@ function makeProject(overrides: Partial = {}): ProjectWith name: 'test-project', description: '', ownerId: 'user-1', - proxyMode: 'direct', prompt: '', proxyModel: '', gated: true, @@ -39,7 +38,6 @@ function mockProjectRepo(): IProjectRepository { name: data.name, description: data.description, ownerId: data.ownerId, - proxyMode: data.proxyMode, })), update: vi.fn(async (_id, data) => makeProject({ ...data as Partial })), delete: vi.fn(async () => {}), @@ -60,17 +58,6 @@ function mockServerRepo(): IMcpServerRepository { }; } -function mockSecretRepo(): ISecretRepository { - return { - findAll: vi.fn(async () => []), - findById: vi.fn(async () => null), - findByName: vi.fn(async () => null), - create: vi.fn(async () => ({} as never)), - update: vi.fn(async () => ({} as never)), - delete: vi.fn(async () => {}), - }; -} - afterEach(async () => { if (app) await app.close(); }); @@ -78,7 +65,7 @@ afterEach(async () => { function createApp(projectRepo: IProjectRepository, serverRepo?: IMcpServerRepository) { app = Fastify({ logger: false }); app.setErrorHandler(errorHandler); - const service = new ProjectService(projectRepo, serverRepo ?? mockServerRepo(), mockSecretRepo()); + const service = new ProjectService(projectRepo, serverRepo ?? mockServerRepo()); registerProjectRoutes(app, service); return app.ready(); } diff --git a/src/mcpd/tests/project-service.test.ts b/src/mcpd/tests/project-service.test.ts index 5b697f8..d369256 100644 --- a/src/mcpd/tests/project-service.test.ts +++ b/src/mcpd/tests/project-service.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { ProjectService } from '../src/services/project.service.js'; import { NotFoundError, ConflictError } from '../src/services/mcp-server.service.js'; import type { IProjectRepository, ProjectWithRelations } from '../src/repositories/project.repository.js'; -import type { IMcpServerRepository, ISecretRepository } from '../src/repositories/interfaces.js'; +import type { IMcpServerRepository } from '../src/repositories/interfaces.js'; import type { McpServer } from '@prisma/client'; function makeProject(overrides: Partial = {}): ProjectWithRelations { @@ -11,7 +11,6 @@ function makeProject(overrides: Partial = {}): ProjectWith name: 'test-project', description: '', ownerId: 'user-1', - proxyMode: 'direct', proxyModel: '', gated: true, llmProvider: null, @@ -57,7 +56,6 @@ function mockProjectRepo(): IProjectRepository { name: data.name, description: data.description, ownerId: data.ownerId, - proxyMode: data.proxyMode, llmProvider: data.llmProvider ?? null, llmModel: data.llmModel ?? null, })), @@ -80,28 +78,15 @@ function mockServerRepo(): IMcpServerRepository { }; } -function mockSecretRepo(): ISecretRepository { - return { - findAll: vi.fn(async () => []), - findById: vi.fn(async () => null), - findByName: vi.fn(async () => null), - create: vi.fn(async () => ({ id: 'sec-1', name: 'test', data: {}, version: 1, createdAt: new Date(), updatedAt: new Date() })), - update: vi.fn(async () => ({ id: 'sec-1', name: 'test', data: {}, version: 1, createdAt: new Date(), updatedAt: new Date() })), - delete: vi.fn(async () => {}), - }; -} - describe('ProjectService', () => { let projectRepo: ReturnType; let serverRepo: ReturnType; - let secretRepo: ReturnType; let service: ProjectService; beforeEach(() => { projectRepo = mockProjectRepo(); serverRepo = mockServerRepo(); - secretRepo = mockSecretRepo(); - service = new ProjectService(projectRepo, serverRepo, secretRepo); + service = new ProjectService(projectRepo, serverRepo); }); describe('create', () => { @@ -149,27 +134,6 @@ describe('ProjectService', () => { expect(result.servers).toHaveLength(2); }); - it('creates project with proxyMode and llmProvider', async () => { - const created = makeProject({ id: 'proj-filtered', proxyMode: 'filtered', llmProvider: 'openai' }); - vi.mocked(projectRepo.create).mockResolvedValue(created); - vi.mocked(projectRepo.findById).mockResolvedValue(created); - - const result = await service.create({ - name: 'filtered-proj', - proxyMode: 'filtered', - llmProvider: 'openai', - }, 'user-1'); - - expect(result.proxyMode).toBe('filtered'); - expect(result.llmProvider).toBe('openai'); - }); - - it('rejects filtered project without llmProvider', async () => { - await expect( - service.create({ name: 'bad-proj', proxyMode: 'filtered' }, 'user-1'), - ).rejects.toThrow(); - }); - it('throws NotFoundError when server name resolution fails', async () => { vi.mocked(serverRepo.findByName).mockResolvedValue(null); @@ -226,13 +190,12 @@ describe('ProjectService', () => { expect(projectRepo.setServers).toHaveBeenCalledWith('proj-1', ['srv-new']); }); - it('updates proxyMode', async () => { + it('updates llmProvider', async () => { const existing = makeProject({ id: 'proj-1' }); vi.mocked(projectRepo.findById).mockResolvedValue(existing); - await service.update('proj-1', { proxyMode: 'filtered', llmProvider: 'anthropic' }); + await service.update('proj-1', { llmProvider: 'anthropic' }); expect(projectRepo.update).toHaveBeenCalledWith('proj-1', { - proxyMode: 'filtered', llmProvider: 'anthropic', }); }); @@ -297,46 +260,10 @@ describe('ProjectService', () => { }); describe('generateMcpConfig', () => { - it('generates direct mode config with STDIO servers', async () => { - const srv = makeServer({ id: 'srv-1', name: 'github', packageName: '@mcp/github', transport: 'STDIO' }); + it('generates single mcplocal proxy entry', async () => { const project = makeProject({ id: 'proj-1', name: 'my-proj', - proxyMode: 'direct', - servers: [{ id: 'ps-1', server: { id: 'srv-1', name: 'github' } }], - }); - - vi.mocked(projectRepo.findById).mockResolvedValue(project); - vi.mocked(serverRepo.findById).mockResolvedValue(srv); - - const config = await service.generateMcpConfig('proj-1'); - expect(config.mcpServers['github']).toBeDefined(); - expect(config.mcpServers['github']?.command).toBe('npx'); - expect(config.mcpServers['github']?.args).toEqual(['-y', '@mcp/github']); - }); - - it('generates direct mode config with SSE servers (URL-based)', async () => { - const srv = makeServer({ id: 'srv-2', name: 'sse-server', transport: 'SSE' }); - const project = makeProject({ - id: 'proj-1', - proxyMode: 'direct', - servers: [{ id: 'ps-1', server: { id: 'srv-2', name: 'sse-server' } }], - }); - - vi.mocked(projectRepo.findById).mockResolvedValue(project); - vi.mocked(serverRepo.findById).mockResolvedValue(srv); - - const config = await service.generateMcpConfig('proj-1'); - expect(config.mcpServers['sse-server']?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/sse-server'); - expect(config.mcpServers['sse-server']?.command).toBeUndefined(); - }); - - it('generates filtered mode config (single mcplocal entry)', async () => { - const project = makeProject({ - id: 'proj-1', - name: 'filtered-proj', - proxyMode: 'filtered', - llmProvider: 'openai', servers: [{ id: 'ps-1', server: { id: 'srv-1', name: 'github' } }], }); @@ -344,14 +271,13 @@ describe('ProjectService', () => { const config = await service.generateMcpConfig('proj-1'); expect(Object.keys(config.mcpServers)).toHaveLength(1); - expect(config.mcpServers['filtered-proj']?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/project/filtered-proj'); + expect(config.mcpServers['my-proj']?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/project/my-proj'); }); it('resolves by name for mcp-config', async () => { const project = makeProject({ id: 'proj-1', name: 'my-proj', - proxyMode: 'direct', servers: [], }); @@ -359,27 +285,8 @@ describe('ProjectService', () => { vi.mocked(projectRepo.findByName).mockResolvedValue(project); const config = await service.generateMcpConfig('my-proj'); - expect(config.mcpServers).toEqual({}); - }); - - it('includes env for STDIO servers', async () => { - const srv = makeServer({ - id: 'srv-1', - name: 'github', - transport: 'STDIO', - env: [{ name: 'GITHUB_TOKEN', value: 'tok123' }], - }); - const project = makeProject({ - id: 'proj-1', - proxyMode: 'direct', - servers: [{ id: 'ps-1', server: { id: 'srv-1', name: 'github' } }], - }); - - vi.mocked(projectRepo.findById).mockResolvedValue(project); - vi.mocked(serverRepo.findById).mockResolvedValue(srv); - - const config = await service.generateMcpConfig('proj-1'); - expect(config.mcpServers['github']?.env?.['GITHUB_TOKEN']).toBe('tok123'); + expect(Object.keys(config.mcpServers)).toHaveLength(1); + expect(config.mcpServers['my-proj']?.url).toBe('http://localhost:3100/api/v1/mcp/proxy/project/my-proj'); }); }); }); diff --git a/src/mcpd/tests/prompt-routes.test.ts b/src/mcpd/tests/prompt-routes.test.ts index ff21daf..42a483c 100644 --- a/src/mcpd/tests/prompt-routes.test.ts +++ b/src/mcpd/tests/prompt-routes.test.ts @@ -48,7 +48,6 @@ function makeProject(overrides: Partial = {}): Project { name: 'homeautomation', description: '', prompt: '', - proxyMode: 'direct', proxyModel: '', gated: true, llmProvider: null, diff --git a/src/mcpd/tests/services/prompt-service.test.ts b/src/mcpd/tests/services/prompt-service.test.ts index 80a2f9d..4bb550d 100644 --- a/src/mcpd/tests/services/prompt-service.test.ts +++ b/src/mcpd/tests/services/prompt-service.test.ts @@ -42,7 +42,6 @@ function makeProject(overrides: Partial = {}): Project { name: 'test-project', description: '', prompt: '', - proxyMode: 'direct', proxyModel: '', gated: true, llmProvider: null, diff --git a/src/mcpd/tests/system-prompt-validation.test.ts b/src/mcpd/tests/system-prompt-validation.test.ts index 95f96e5..ac23082 100644 --- a/src/mcpd/tests/system-prompt-validation.test.ts +++ b/src/mcpd/tests/system-prompt-validation.test.ts @@ -35,7 +35,6 @@ function makeProject(overrides: Partial = {}): Project { name: 'test-project', description: '', prompt: '', - proxyMode: 'direct', proxyModel: '', gated: true, llmProvider: null, diff --git a/src/mcplocal/tests/smoke/fixtures/smoke-data.yaml b/src/mcplocal/tests/smoke/fixtures/smoke-data.yaml index ea1b880..faf22e9 100644 --- a/src/mcplocal/tests/smoke/fixtures/smoke-data.yaml +++ b/src/mcplocal/tests/smoke/fixtures/smoke-data.yaml @@ -17,7 +17,6 @@ projects: - name: smoke-data description: "Smoke test project with 100 AWS documentation prompt links" gated: true - proxyMode: direct serverattachments: - server: smoke-aws-docs