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