diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index b47d9b6..3ff34e4 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -2,7 +2,7 @@ "master": { "tasks": [ { - "id": 1, + "id": "1", "title": "Initialize Project Structure and Core Dependencies", "description": "Set up the monorepo structure for mcpctl with CLI client, mcpd server, and shared libraries. Configure TypeScript, ESLint, and build tooling.", "details": "Create a monorepo using pnpm workspaces or npm workspaces with the following structure:\n\n```\nmcpctl/\n├── src/\n│ ├── cli/ # mcpctl CLI tool\n│ ├── mcpd/ # Backend daemon server\n│ ├── shared/ # Shared types, utilities, constants\n│ └── local-proxy/ # Local LLM proxy component\n├── deploy/\n│ └── docker-compose.yml\n├── package.json\n├── tsconfig.base.json\n└── pnpm-workspace.yaml\n```\n\nDependencies to install:\n- TypeScript 5.x\n- Commander.js for CLI\n- Express/Fastify for mcpd HTTP server\n- Zod for schema validation\n- Winston/Pino for logging\n- Prisma or Drizzle for database ORM\n\nCreate base tsconfig.json with strict mode, ES2022 target, and module resolution settings. Set up shared ESLint config with TypeScript rules.", @@ -18,7 +18,8 @@ "dependencies": [], "details": "Create root package.json with pnpm workspaces configuration. Create pnpm-workspace.yaml defining all workspace packages. Initialize the following directory structure:\n\n```\nmcpctl/\n├── src/\n│ ├── cli/ # mcpctl CLI tool (Task 7-10)\n│ │ ├── src/\n│ │ ├── tests/\n│ │ └── package.json\n│ ├── mcpd/ # Backend daemon server (Task 3-6, 14, 16)\n│ │ ├── src/\n│ │ ├── tests/\n│ │ └── package.json\n│ ├── shared/ # Shared types, utils, constants, validation\n│ │ ├── src/\n│ │ │ ├── types/ # TypeScript interfaces/types\n│ │ │ ├── utils/ # Utility functions\n│ │ │ ├── constants/# Shared constants\n│ │ │ ├── validation/ # Zod schemas\n│ │ │ └── index.ts # Barrel export\n│ │ ├── tests/\n│ │ └── package.json\n│ ├── local-proxy/ # Local LLM proxy (Task 11-13)\n│ │ ├── src/\n│ │ ├── tests/\n│ │ └── package.json\n│ └── db/ # Database package (Task 2)\n│ ├── src/\n│ ├── prisma/ # Schema and migrations\n│ ├── seed/ # Seed data\n│ ├── tests/\n│ └── package.json\n├── deploy/\n│ └── docker-compose.yml # Local dev services (postgres)\n├── tests/\n│ ├── e2e/ # End-to-end tests (Task 18)\n│ └── integration/ # Integration tests\n├── docs/ # Documentation (Task 18)\n├── package.json # Root workspace config\n├── pnpm-workspace.yaml\n└── turbo.json # Optional: Turborepo for build orchestration\n```\n\nThe pnpm-workspace.yaml should contain: `packages: [\"src/*\"]`", "status": "done", - "testStrategy": "Write Vitest tests that verify: (1) All expected directories exist, (2) All package.json files are valid JSON with correct workspace protocol dependencies, (3) pnpm-workspace.yaml correctly includes all packages, (4) Running 'pnpm install' succeeds and creates correct node_modules symlinks between packages. Run 'pnpm ls' to verify workspace linking." + "testStrategy": "Write Vitest tests that verify: (1) All expected directories exist, (2) All package.json files are valid JSON with correct workspace protocol dependencies, (3) pnpm-workspace.yaml correctly includes all packages, (4) Running 'pnpm install' succeeds and creates correct node_modules symlinks between packages. Run 'pnpm ls' to verify workspace linking.", + "parentId": "undefined" }, { "id": 2, @@ -29,7 +30,8 @@ ], "details": "Create root tsconfig.base.json with shared compiler options. Create package-specific tsconfig.json in each package that extends the base and sets appropriate paths.", "status": "done", - "testStrategy": "Write Vitest tests that verify tsconfig.base.json exists and has strict: true, each package tsconfig.json extends base correctly." + "testStrategy": "Write Vitest tests that verify tsconfig.base.json exists and has strict: true, each package tsconfig.json extends base correctly.", + "parentId": "undefined" }, { "id": 3, @@ -40,7 +42,8 @@ ], "details": "Install Vitest and related packages at root level. Create root vitest.config.ts and vitest.workspace.ts for workspace-aware testing pointing to src/cli, src/mcpd, src/shared, src/local-proxy, src/db.", "status": "done", - "testStrategy": "Run 'pnpm test:run' and verify Vitest discovers and runs tests, coverage report is generated." + "testStrategy": "Run 'pnpm test:run' and verify Vitest discovers and runs tests, coverage report is generated.", + "parentId": "undefined" }, { "id": 4, @@ -51,7 +54,8 @@ ], "details": "Install ESLint and plugins at root. Create eslint.config.js (flat config, ESLint 9+). Create deploy/docker-compose.yml for local development with PostgreSQL service.", "status": "done", - "testStrategy": "Write Vitest tests that verify eslint.config.js exists and exports valid config, deploy/docker-compose.yml is valid YAML and defines postgres service." + "testStrategy": "Write Vitest tests that verify eslint.config.js exists and exports valid config, deploy/docker-compose.yml is valid YAML and defines postgres service.", + "parentId": "undefined" }, { "id": 5, @@ -64,12 +68,13 @@ ], "details": "Install dependencies per package in src/cli, src/mcpd, src/shared, src/db, src/local-proxy. Perform security and architecture review.", "status": "done", - "testStrategy": "Verify each package.json has required dependencies, run pnpm audit, verify .gitignore contains required patterns." + "testStrategy": "Verify each package.json has required dependencies, run pnpm audit, verify .gitignore contains required patterns.", + "parentId": "undefined" } ] }, { - "id": 2, + "id": "2", "title": "Design and Implement Database Schema", "description": "Create the database schema for storing MCP server configurations, projects, profiles, user sessions, and audit logs. Use PostgreSQL for production readiness.", "details": "Design PostgreSQL schema using Prisma ORM with models: User, McpServer, McpProfile, Project, ProjectMcpProfile, McpInstance, AuditLog, Session. Create migrations and seed data for common MCP servers (slack, jira, github, terraform).", @@ -78,7 +83,7 @@ "dependencies": [ "1" ], - "status": "pending", + "status": "done", "subtasks": [ { "id": 1, @@ -87,7 +92,8 @@ "dependencies": [], "details": "Create src/db/prisma directory structure. Install Prisma dependencies. Configure deploy/docker-compose.yml with two PostgreSQL services: mcpctl-postgres (port 5432) for development and mcpctl-postgres-test (port 5433) for testing.", "status": "pending", - "testStrategy": "Write Vitest tests that verify docker-compose creates both postgres services, setupTestDb() successfully connects and pushes schema." + "testStrategy": "Write Vitest tests that verify docker-compose creates both postgres services, setupTestDb() successfully connects and pushes schema.", + "parentId": "undefined" }, { "id": 2, @@ -98,7 +104,8 @@ ], "details": "Create src/db/tests/models directory with separate test files for each model. Tests will initially fail (TDD red phase) until schema is implemented.", "status": "pending", - "testStrategy": "Tests are expected to fail initially (TDD red phase). After schema implementation, all tests should pass." + "testStrategy": "Tests are expected to fail initially (TDD red phase). After schema implementation, all tests should pass.", + "parentId": "undefined" }, { "id": 3, @@ -109,7 +116,8 @@ ], "details": "Implement src/db/prisma/schema.prisma with all models. Add version Int field and updatedAt DateTime for git-based backup support.", "status": "pending", - "testStrategy": "Run TDD tests from subtask 2 - all should now pass (TDD green phase). Verify npx prisma validate passes." + "testStrategy": "Run TDD tests from subtask 2 - all should now pass (TDD green phase). Verify npx prisma validate passes.", + "parentId": "undefined" }, { "id": 4, @@ -120,7 +128,8 @@ ], "details": "Create src/db/seed directory with server definitions and seeding functions for Slack, Jira, GitHub, Terraform MCP servers.", "status": "pending", - "testStrategy": "Write unit tests BEFORE implementing seed functions (TDD). Verify seedMcpServers() creates exactly 4 servers with idempotent behavior." + "testStrategy": "Write unit tests BEFORE implementing seed functions (TDD). Verify seedMcpServers() creates exactly 4 servers with idempotent behavior.", + "parentId": "undefined" }, { "id": 5, @@ -132,12 +141,14 @@ ], "details": "Run npx prisma migrate dev --name init. Create src/db/src/migration-helpers.ts. Document security and architecture findings.", "status": "pending", - "testStrategy": "Verify migration files exist, migration helper tests pass, SECURITY_REVIEW.md covers all security checkpoints." + "testStrategy": "Verify migration files exist, migration helper tests pass, SECURITY_REVIEW.md covers all security checkpoints.", + "parentId": "undefined" } - ] + ], + "updatedAt": "2026-02-21T04:10:25.433Z" }, { - "id": 3, + "id": "3", "title": "Implement mcpd Core Server Framework", "description": "Build the mcpd daemon server with Express/Fastify, including middleware for authentication, logging, and error handling. Design for horizontal scalability.", "details": "Create mcpd server in src/mcpd/src/ with Fastify, health check endpoint, auth middleware, and audit logging. Design for statelessness and scalability.", @@ -156,7 +167,8 @@ "dependencies": [], "details": "Create src/mcpd/src/ with routes/, controllers/, services/, repositories/, middleware/, config/, types/, utils/ directories.", "status": "pending", - "testStrategy": "Write initial Vitest tests that verify all required directories exist, package.json has required dependencies." + "testStrategy": "Write initial Vitest tests that verify all required directories exist, package.json has required dependencies.", + "parentId": "undefined" }, { "id": 2, @@ -167,7 +179,8 @@ ], "details": "Create src/mcpd/src/server.ts with Fastify instance factory function. Implement config validation with Zod and health endpoint.", "status": "pending", - "testStrategy": "TDD approach - write tests first for config validation, health endpoint returns correct structure." + "testStrategy": "TDD approach - write tests first for config validation, health endpoint returns correct structure.", + "parentId": "undefined" }, { "id": 3, @@ -178,7 +191,8 @@ ], "details": "Create src/mcpd/src/middleware/auth.ts with authMiddleware factory function using dependency injection.", "status": "pending", - "testStrategy": "TDD - write all tests before implementation for 401 responses, token validation, request decoration." + "testStrategy": "TDD - write all tests before implementation for 401 responses, token validation, request decoration.", + "parentId": "undefined" }, { "id": 4, @@ -189,7 +203,8 @@ ], "details": "Create src/mcpd/src/middleware/security.ts with registerSecurityPlugins function. Create sanitization and validation utilities.", "status": "pending", - "testStrategy": "TDD tests for CORS headers, Helmet security headers, rate limiting returns 429, input validation." + "testStrategy": "TDD tests for CORS headers, Helmet security headers, rate limiting returns 429, input validation.", + "parentId": "undefined" }, { "id": 5, @@ -202,12 +217,13 @@ ], "details": "Create error-handler.ts, audit.ts middleware, and shutdown.ts utilities in src/mcpd/src/.", "status": "pending", - "testStrategy": "TDD for all components: error handler HTTP codes, audit middleware creates records, graceful shutdown handles SIGTERM." + "testStrategy": "TDD for all components: error handler HTTP codes, audit middleware creates records, graceful shutdown handles SIGTERM.", + "parentId": "undefined" } ] }, { - "id": 4, + "id": "4", "title": "Implement MCP Server Registry and Profile Management", "description": "Create APIs for registering MCP servers, managing profiles with different permission levels, and storing configuration templates.", "details": "Create REST API endpoints in mcpd for MCP server and profile CRUD operations with seed data for common servers.", @@ -225,7 +241,8 @@ "dependencies": [], "details": "Create src/mcpd/src/validation/mcp-server.schema.ts with CreateMcpServerSchema, UpdateMcpServerSchema, CreateMcpProfileSchema.", "status": "pending", - "testStrategy": "TDD approach - write all tests first, then implement schemas. Tests verify valid inputs pass, invalid inputs fail." + "testStrategy": "TDD approach - write all tests first, then implement schemas. Tests verify valid inputs pass, invalid inputs fail.", + "parentId": "undefined" }, { "id": 2, @@ -236,7 +253,8 @@ ], "details": "Create src/mcpd/src/repositories/interfaces.ts with IMcpServerRepository and IMcpProfileRepository interfaces.", "status": "pending", - "testStrategy": "TDD - write tests before implementation with mocked PrismaClient. Verify all repository methods are covered." + "testStrategy": "TDD - write tests before implementation with mocked PrismaClient. Verify all repository methods are covered.", + "parentId": "undefined" }, { "id": 3, @@ -248,7 +266,8 @@ ], "details": "Create src/mcpd/src/services/mcp-server.service.ts and mcp-profile.service.ts with DI and authorization checks.", "status": "pending", - "testStrategy": "TDD - write tests first mocking repositories and authorization. Verify authorization checks are called for every method." + "testStrategy": "TDD - write tests first mocking repositories and authorization. Verify authorization checks are called for every method.", + "parentId": "undefined" }, { "id": 4, @@ -259,7 +278,8 @@ ], "details": "Create src/mcpd/src/routes/mcp-servers.ts and mcp-profiles.ts with all CRUD endpoints.", "status": "pending", - "testStrategy": "Write integration tests before implementation using Fastify.inject(). Test with docker-compose postgres." + "testStrategy": "Write integration tests before implementation using Fastify.inject(). Test with docker-compose postgres.", + "parentId": "undefined" }, { "id": 5, @@ -270,12 +290,13 @@ ], "details": "Create src/mcpd/src/seed/mcp-servers.seed.ts with seedMcpServers() function and SECURITY_REVIEW.md.", "status": "pending", - "testStrategy": "Write unit tests for seed functions. Security tests for injection prevention, authorization checks." + "testStrategy": "Write unit tests for seed functions. Security tests for injection prevention, authorization checks.", + "parentId": "undefined" } ] }, { - "id": 5, + "id": "5", "title": "Implement Project Management APIs", "description": "Create APIs for managing MCP projects that group multiple MCP profiles together for easy assignment to Claude sessions.", "details": "Create project management endpoints with generateMcpConfig function for .mcp.json format output.", @@ -293,7 +314,8 @@ "dependencies": [], "details": "Create tests for CreateProjectSchema, UpdateProjectSchema, UpdateProjectProfilesSchema, and generateMcpConfig with security tests.", "status": "pending", - "testStrategy": "TDD red phase - all tests should fail initially. Verify generateMcpConfig security tests check secret env vars are excluded." + "testStrategy": "TDD red phase - all tests should fail initially. Verify generateMcpConfig security tests check secret env vars are excluded.", + "parentId": "undefined" }, { "id": 2, @@ -304,7 +326,8 @@ ], "details": "Create src/mcpd/src/repositories/project.repository.ts and src/mcpd/src/services/mcp-config-generator.ts.", "status": "pending", - "testStrategy": "Run TDD tests from subtask 1. Verify output must NOT contain secret values." + "testStrategy": "Run TDD tests from subtask 1. Verify output must NOT contain secret values.", + "parentId": "undefined" }, { "id": 3, @@ -315,7 +338,8 @@ ], "details": "Create src/mcpd/src/services/project.service.ts with DI accepting IProjectRepository and IMcpProfileRepository.", "status": "pending", - "testStrategy": "TDD - write tests before implementation. Verify authorization and profile validation." + "testStrategy": "TDD - write tests before implementation. Verify authorization and profile validation.", + "parentId": "undefined" }, { "id": 4, @@ -326,7 +350,8 @@ ], "details": "Create src/mcpd/src/routes/projects.ts with all CRUD routes and mcp-config endpoint.", "status": "pending", - "testStrategy": "Integration tests using Fastify.inject(). Verify mcp-config returns valid structure WITHOUT secret env vars." + "testStrategy": "Integration tests using Fastify.inject(). Verify mcp-config returns valid structure WITHOUT secret env vars.", + "parentId": "undefined" }, { "id": 5, @@ -337,12 +362,13 @@ ], "details": "Create src/mcpd/tests/integration/projects.test.ts with end-to-end scenarios and SECURITY_REVIEW.md section.", "status": "pending", - "testStrategy": "Run full integration test suite. Verify coverage >85% for project-related files." + "testStrategy": "Run full integration test suite. Verify coverage >85% for project-related files.", + "parentId": "undefined" } ] }, { - "id": 6, + "id": "6", "title": "Implement Docker Container Management for MCP Servers", "description": "Create the container orchestration layer for running MCP servers as Docker containers, with support for docker-compose deployment.", "details": "Create Docker management module with ContainerManager class using dockerode. Create deploy/docker-compose.yml template.", @@ -361,7 +387,8 @@ "dependencies": [], "details": "Create src/mcpd/src/services/orchestrator.ts interface and TDD tests for ContainerManager methods.", "status": "pending", - "testStrategy": "Run tests to verify they exist and fail with expected errors. Coverage target: 100% of interface methods." + "testStrategy": "Run tests to verify they exist and fail with expected errors. Coverage target: 100% of interface methods.", + "parentId": "undefined" }, { "id": 2, @@ -372,7 +399,8 @@ ], "details": "Create src/mcpd/src/services/docker/container-manager.ts implementing McpOrchestrator interface.", "status": "pending", - "testStrategy": "Run unit tests from subtask 1. Verify TypeScript compilation and resource limits." + "testStrategy": "Run unit tests from subtask 1. Verify TypeScript compilation and resource limits.", + "parentId": "undefined" }, { "id": 3, @@ -381,7 +409,8 @@ "dependencies": [], "details": "Create deploy/docker-compose.yml with mcpd, postgres, and test-mcp-server services with proper networking.", "status": "pending", - "testStrategy": "Validate with docker-compose config. Run docker-compose up -d and verify all services start." + "testStrategy": "Validate with docker-compose config. Run docker-compose up -d and verify all services start.", + "parentId": "undefined" }, { "id": 4, @@ -393,7 +422,8 @@ ], "details": "Create src/mcpd/src/services/docker/__tests__/container-manager.integration.test.ts.", "status": "pending", - "testStrategy": "Run integration tests with pnpm --filter @mcpctl/mcpd test:integration. Verify containers are created/destroyed." + "testStrategy": "Run integration tests with pnpm --filter @mcpctl/mcpd test:integration. Verify containers are created/destroyed.", + "parentId": "undefined" }, { "id": 5, @@ -404,7 +434,8 @@ ], "details": "Create src/mcpd/src/services/docker/network-manager.ts with network isolation and resource management.", "status": "pending", - "testStrategy": "Unit tests for network creation. Integration test: verify container network isolation." + "testStrategy": "Unit tests for network creation. Integration test: verify container network isolation.", + "parentId": "undefined" }, { "id": 6, @@ -417,7 +448,8 @@ ], "details": "Create src/mcpd/docs/DOCKER_SECURITY_REVIEW.md documenting risks and mitigations.", "status": "pending", - "testStrategy": "Review DOCKER_SECURITY_REVIEW.md covers all 6 security areas. Run security unit tests." + "testStrategy": "Review DOCKER_SECURITY_REVIEW.md covers all 6 security areas. Run security unit tests.", + "parentId": "undefined" }, { "id": 7, @@ -428,12 +460,13 @@ ], "details": "Extend ContainerManager with getLogs, getHealthStatus, attachToContainer, and event subscriptions.", "status": "pending", - "testStrategy": "Unit tests for getLogs. Integration test: run container, tail logs, verify output." + "testStrategy": "Unit tests for getLogs. Integration test: run container, tail logs, verify output.", + "parentId": "undefined" } ] }, { - "id": 7, + "id": "7", "title": "Build mcpctl CLI Core Framework", "description": "Create the CLI tool foundation using Commander.js with kubectl-inspired command structure, configuration management, and server communication.", "details": "Create CLI in src/cli/src/ with Commander.js, configuration management at ~/.mcpctl/config.json, and API client for mcpd.", @@ -451,7 +484,8 @@ "dependencies": [], "details": "Create src/cli/src/ with commands/, config/, client/, formatters/, utils/, types/ directories and registry pattern.", "status": "pending", - "testStrategy": "TDD approach - write tests first. Tests verify CLI shows version, help, CommandRegistry works." + "testStrategy": "TDD approach - write tests first. Tests verify CLI shows version, help, CommandRegistry works.", + "parentId": "undefined" }, { "id": 2, @@ -462,12 +496,13 @@ ], "details": "Implement config management with proxy settings, custom CA certificates support, and Zod validation.", "status": "pending", - "testStrategy": "TDD tests for config loading, saving, validation, and credential encryption." + "testStrategy": "TDD tests for config loading, saving, validation, and credential encryption.", + "parentId": "undefined" } ] }, { - "id": 8, + "id": "8", "title": "Implement mcpctl get and describe Commands", "description": "Create kubectl-style get and describe commands for viewing MCP servers, profiles, projects, and instances.", "details": "Implement get command with table/json/yaml output formats and describe command for detailed views.", @@ -477,10 +512,10 @@ "7" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 9, + "id": "9", "title": "Implement mcpctl apply and setup Commands", "description": "Create apply command for declarative configuration and setup wizard for interactive MCP server configuration.", "details": "Implement apply command for YAML/JSON config files and interactive setup wizard with credential prompts.", @@ -491,10 +526,10 @@ "4" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 10, + "id": "10", "title": "Implement mcpctl claude and project Commands", "description": "Create commands for managing Claude MCP configuration and project assignments.", "details": "Implement claude command for managing .mcp.json files and project command for project management.", @@ -505,10 +540,10 @@ "5" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 11, + "id": "11", "title": "Design Local LLM Proxy Architecture", "description": "Design the architecture for the local LLM proxy that enables Claude to use MCP servers through a local intermediary.", "details": "Design proxy architecture in src/local-proxy/ with MCP protocol handling and request/response transformation.", @@ -518,10 +553,10 @@ "1" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 12, + "id": "12", "title": "Implement Local LLM Proxy Core", "description": "Build the core local proxy server that handles MCP protocol communication between Claude and MCP servers.", "details": "Implement proxy server in src/local-proxy/src/ with MCP SDK integration and request routing.", @@ -531,10 +566,10 @@ "11" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 13, + "id": "13", "title": "Implement LLM Provider Strategy Pattern", "description": "Create pluggable LLM provider support with strategy pattern for different providers (OpenAI, Anthropic, local models).", "details": "Implement provider strategy pattern in src/local-proxy/src/providers/ with adapters for different LLM APIs.", @@ -544,10 +579,10 @@ "12" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 14, + "id": "14", "title": "Implement Audit Logging and Compliance", "description": "Create comprehensive audit logging system for tracking all MCP operations for compliance and debugging.", "details": "Implement audit logging in src/mcpd/src/services/ with structured logging, retention policies, and query APIs.", @@ -557,10 +592,10 @@ "3" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 15, + "id": "15", "title": "Create MCP Profiles Library", "description": "Build a library of pre-configured MCP profiles for common use cases with best practices baked in.", "details": "Create profile library in src/shared/src/profiles/ with templates for common MCP server configurations.", @@ -570,10 +605,10 @@ "4" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 16, + "id": "16", "title": "Implement MCP Instance Lifecycle Management", "description": "Create APIs and CLI commands for managing the full lifecycle of MCP server instances.", "details": "Implement instance lifecycle management in src/mcpd/src/services/ with start, stop, restart, logs commands.", @@ -583,10 +618,10 @@ "6" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 17, + "id": "17", "title": "Add Kubernetes Deployment Support", "description": "Extend the orchestration layer to support Kubernetes deployments for production environments.", "details": "Implement KubernetesOrchestrator in src/mcpd/src/services/k8s/ implementing McpOrchestrator interface.", @@ -596,10 +631,10 @@ "6" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 18, + "id": "18", "title": "Documentation and Testing", "description": "Create comprehensive documentation and end-to-end test suite for the entire mcpctl system.", "details": "Create documentation in docs/ and e2e tests in tests/e2e/ covering all major workflows.", @@ -612,10 +647,10 @@ "10" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 19, + "id": "19", "title": "CANCELLED - Auth middleware", "description": "Merged into Task 3 subtasks", "details": null, @@ -623,11 +658,11 @@ "priority": null, "dependencies": [], "status": "cancelled", - "subtasks": null, + "subtasks": [], "updatedAt": "2026-02-21T02:21:03.958Z" }, { - "id": 20, + "id": "20", "title": "CANCELLED - Duplicate project management", "description": "Merged into Task 5", "details": null, @@ -635,11 +670,11 @@ "priority": null, "dependencies": [], "status": "cancelled", - "subtasks": null, + "subtasks": [], "updatedAt": "2026-02-21T02:21:03.966Z" }, { - "id": 21, + "id": "21", "title": "CANCELLED - Duplicate audit logging", "description": "Merged into Task 14", "details": null, @@ -647,11 +682,11 @@ "priority": null, "dependencies": [], "status": "cancelled", - "subtasks": null, + "subtasks": [], "updatedAt": "2026-02-21T02:21:03.972Z" }, { - "id": 22, + "id": "22", "title": "Implement Health Monitoring Dashboard", "description": "Create a monitoring dashboard for tracking MCP server health, resource usage, and system metrics.", "details": "Implement health monitoring endpoints in src/mcpd/src/routes/ and optional web dashboard.", @@ -662,10 +697,10 @@ "14" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 23, + "id": "23", "title": "Implement Backup and Restore", "description": "Create backup and restore functionality for mcpctl configuration and state.", "details": "Implement git-based backup in src/mcpd/src/services/backup/ with encrypted secrets and restore capability.", @@ -676,10 +711,10 @@ "5" ], "status": "pending", - "subtasks": null + "subtasks": [] }, { - "id": 24, + "id": "24", "title": "CI/CD Pipeline Setup", "description": "Set up continuous integration and deployment pipelines for the mcpctl project.", "details": "Create GitHub Actions workflows in .github/workflows/ for testing, building, and releasing.", @@ -689,13 +724,17 @@ "1" ], "status": "pending", - "subtasks": null + "subtasks": [] } ], "metadata": { - "created": "2026-02-21T02:23:17.813Z", - "updated": "2026-02-21T02:23:17.813Z", - "description": "Tasks for master context" + "version": "1.0.0", + "lastModified": "2026-02-21T04:10:25.433Z", + "taskCount": 24, + "completedCount": 2, + "tags": [ + "master" + ] } } } \ No newline at end of file diff --git a/src/db/prisma/schema.prisma b/src/db/prisma/schema.prisma new file mode 100644 index 0000000..a445a89 --- /dev/null +++ b/src/db/prisma/schema.prisma @@ -0,0 +1,172 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +// ── Users ── + +model User { + id String @id @default(cuid()) + email String @unique + name String? + role Role @default(USER) + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + sessions Session[] + auditLogs AuditLog[] + projects Project[] + + @@index([email]) +} + +enum Role { + USER + ADMIN +} + +// ── Sessions ── + +model Session { + id String @id @default(cuid()) + token String @unique + userId String + expiresAt DateTime + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([token]) + @@index([userId]) + @@index([expiresAt]) +} + +// ── MCP Servers ── + +model McpServer { + id String @id @default(cuid()) + name String @unique + description String @default("") + packageName String? + dockerImage String? + transport Transport @default(STDIO) + repositoryUrl String? + envTemplate Json @default("[]") + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + profiles McpProfile[] + instances McpInstance[] + + @@index([name]) +} + +enum Transport { + STDIO + SSE + STREAMABLE_HTTP +} + +// ── MCP Profiles ── + +model McpProfile { + id String @id @default(cuid()) + name String + serverId String + permissions Json @default("[]") + envOverrides Json @default("{}") + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + server McpServer @relation(fields: [serverId], references: [id], onDelete: Cascade) + projects ProjectMcpProfile[] + + @@unique([name, serverId]) + @@index([serverId]) +} + +// ── Projects ── + +model Project { + id String @id @default(cuid()) + name String @unique + description String @default("") + ownerId String + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + profiles ProjectMcpProfile[] + + @@index([name]) + @@index([ownerId]) +} + +// ── Project <-> Profile join table ── + +model ProjectMcpProfile { + id String @id @default(cuid()) + projectId String + profileId String + + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + profile McpProfile @relation(fields: [profileId], references: [id], onDelete: Cascade) + + @@unique([projectId, profileId]) + @@index([projectId]) + @@index([profileId]) +} + +// ── MCP Instances (running containers) ── + +model McpInstance { + id String @id @default(cuid()) + serverId String + containerId String? + status InstanceStatus @default(STOPPED) + port Int? + metadata Json @default("{}") + version Int @default(1) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + server McpServer @relation(fields: [serverId], references: [id], onDelete: Cascade) + + @@index([serverId]) + @@index([status]) +} + +enum InstanceStatus { + STARTING + RUNNING + STOPPING + STOPPED + ERROR +} + +// ── Audit Logs ── + +model AuditLog { + id String @id @default(cuid()) + userId String + action String + resource String + resourceId String? + details Json @default("{}") + createdAt DateTime @default(now()) + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([action]) + @@index([resource]) + @@index([createdAt]) +} diff --git a/src/db/src/index.ts b/src/db/src/index.ts index 0140567..2312a36 100644 --- a/src/db/src/index.ts +++ b/src/db/src/index.ts @@ -1,2 +1,18 @@ // Database package - Prisma client and utilities -// Will be implemented in Task 2 +export { PrismaClient } from '@prisma/client'; +export type { + User, + Session, + McpServer, + McpProfile, + Project, + ProjectMcpProfile, + McpInstance, + AuditLog, + Role, + Transport, + InstanceStatus, +} from '@prisma/client'; + +export { seedMcpServers, defaultServers } from './seed/index.js'; +export type { SeedServer } from './seed/index.js'; diff --git a/src/db/src/seed/index.ts b/src/db/src/seed/index.ts new file mode 100644 index 0000000..8840535 --- /dev/null +++ b/src/db/src/seed/index.ts @@ -0,0 +1,131 @@ +import { PrismaClient } from '@prisma/client'; + +export interface SeedServer { + name: string; + description: string; + packageName: string; + transport: 'STDIO' | 'SSE' | 'STREAMABLE_HTTP'; + repositoryUrl: string; + envTemplate: Array<{ + name: string; + description: string; + isSecret: boolean; + setupUrl?: string; + }>; +} + +export const defaultServers: SeedServer[] = [ + { + name: 'slack', + description: 'Slack MCP server for reading channels, messages, and user info', + packageName: '@anthropic/slack-mcp', + transport: 'STDIO', + repositoryUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/slack', + envTemplate: [ + { + name: 'SLACK_BOT_TOKEN', + description: 'Slack Bot User OAuth Token (xoxb-...)', + isSecret: true, + setupUrl: 'https://api.slack.com/apps', + }, + { + name: 'SLACK_TEAM_ID', + description: 'Slack Workspace Team ID', + isSecret: false, + }, + ], + }, + { + name: 'jira', + description: 'Jira MCP server for issues, projects, and boards', + packageName: '@anthropic/jira-mcp', + transport: 'STDIO', + repositoryUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/jira', + envTemplate: [ + { + name: 'JIRA_URL', + description: 'Jira instance URL (e.g., https://company.atlassian.net)', + isSecret: false, + }, + { + name: 'JIRA_EMAIL', + description: 'Jira account email', + isSecret: false, + }, + { + name: 'JIRA_API_TOKEN', + description: 'Jira API token', + isSecret: true, + setupUrl: 'https://id.atlassian.com/manage-profile/security/api-tokens', + }, + ], + }, + { + name: 'github', + description: 'GitHub MCP server for repos, issues, PRs, and code search', + packageName: '@anthropic/github-mcp', + transport: 'STDIO', + repositoryUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/github', + envTemplate: [ + { + name: 'GITHUB_TOKEN', + description: 'GitHub Personal Access Token', + isSecret: true, + setupUrl: 'https://github.com/settings/tokens', + }, + ], + }, + { + name: 'terraform', + description: 'Terraform MCP server for infrastructure documentation and state', + packageName: '@anthropic/terraform-mcp', + transport: 'STDIO', + repositoryUrl: 'https://github.com/modelcontextprotocol/servers/tree/main/src/terraform', + envTemplate: [], + }, +]; + +export async function seedMcpServers( + prisma: PrismaClient, + servers: SeedServer[] = defaultServers, +): Promise { + let created = 0; + + for (const server of servers) { + await prisma.mcpServer.upsert({ + where: { name: server.name }, + update: { + description: server.description, + packageName: server.packageName, + transport: server.transport, + repositoryUrl: server.repositoryUrl, + envTemplate: server.envTemplate, + }, + create: { + name: server.name, + description: server.description, + packageName: server.packageName, + transport: server.transport, + repositoryUrl: server.repositoryUrl, + envTemplate: server.envTemplate, + }, + }); + created++; + } + + return created; +} + +// CLI entry point +if (import.meta.url === `file://${process.argv[1]}`) { + const prisma = new PrismaClient(); + seedMcpServers(prisma) + .then((count) => { + console.log(`Seeded ${count} MCP servers`); + return prisma.$disconnect(); + }) + .catch((e) => { + console.error(e); + return prisma.$disconnect().then(() => process.exit(1)); + }); +} diff --git a/src/db/tests/helpers.ts b/src/db/tests/helpers.ts new file mode 100644 index 0000000..7ef2c06 --- /dev/null +++ b/src/db/tests/helpers.ts @@ -0,0 +1,58 @@ +import { PrismaClient } from '@prisma/client'; +import { execSync } from 'node:child_process'; + +const TEST_DATABASE_URL = process.env['DATABASE_URL'] ?? + 'postgresql://mcpctl:mcpctl_test@localhost:5433/mcpctl_test'; + +let prisma: PrismaClient | undefined; +let schemaReady = false; + +export function getTestClient(): PrismaClient { + if (!prisma) { + prisma = new PrismaClient({ + datasources: { db: { url: TEST_DATABASE_URL } }, + }); + } + return prisma; +} + +export async function setupTestDb(): Promise { + const client = getTestClient(); + + // Only push schema once per process (multiple test files share the worker) + if (!schemaReady) { + execSync('npx prisma db push --force-reset --skip-generate', { + cwd: new URL('..', import.meta.url).pathname, + env: { + ...process.env, + DATABASE_URL: TEST_DATABASE_URL, + // Consent required when Prisma detects AI agent context. + // This targets the ephemeral test database (tmpfs-backed, port 5433). + PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION: 'yes', + }, + stdio: 'pipe', + }); + schemaReady = true; + } + + return client; +} + +export async function cleanupTestDb(): Promise { + if (prisma) { + await prisma.$disconnect(); + prisma = undefined; + } +} + +export async function clearAllTables(client: PrismaClient): Promise { + // Delete in order respecting foreign keys + await client.auditLog.deleteMany(); + await client.projectMcpProfile.deleteMany(); + await client.mcpInstance.deleteMany(); + await client.mcpProfile.deleteMany(); + await client.session.deleteMany(); + await client.project.deleteMany(); + await client.mcpServer.deleteMany(); + await client.user.deleteMany(); +} diff --git a/src/db/tests/models.test.ts b/src/db/tests/models.test.ts new file mode 100644 index 0000000..7e01abf --- /dev/null +++ b/src/db/tests/models.test.ts @@ -0,0 +1,364 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import type { PrismaClient } from '@prisma/client'; +import { setupTestDb, cleanupTestDb, clearAllTables, getTestClient } from './helpers.js'; + +let prisma: PrismaClient; + +beforeAll(async () => { + prisma = await setupTestDb(); +}, 30_000); + +afterAll(async () => { + await cleanupTestDb(); +}); + +beforeEach(async () => { + await clearAllTables(prisma); +}); + +// ── Helper factories ── + +async function createUser(overrides: { email?: string; name?: string; role?: 'USER' | 'ADMIN' } = {}) { + return prisma.user.create({ + data: { + email: overrides.email ?? `test-${Date.now()}@example.com`, + name: overrides.name ?? 'Test User', + role: overrides.role ?? 'USER', + }, + }); +} + +async function createServer(overrides: { name?: string; transport?: 'STDIO' | 'SSE' | 'STREAMABLE_HTTP' } = {}) { + return prisma.mcpServer.create({ + data: { + name: overrides.name ?? `server-${Date.now()}`, + description: 'Test server', + packageName: '@test/mcp-server', + transport: overrides.transport ?? 'STDIO', + }, + }); +} + +// ── User model ── + +describe('User', () => { + it('creates a user with defaults', async () => { + const user = await createUser(); + expect(user.id).toBeDefined(); + expect(user.role).toBe('USER'); + expect(user.version).toBe(1); + expect(user.createdAt).toBeInstanceOf(Date); + expect(user.updatedAt).toBeInstanceOf(Date); + }); + + it('enforces unique email', async () => { + await createUser({ email: 'dup@test.com' }); + await expect(createUser({ email: 'dup@test.com' })).rejects.toThrow(); + }); + + it('allows ADMIN role', async () => { + const admin = await createUser({ role: 'ADMIN' }); + expect(admin.role).toBe('ADMIN'); + }); + + it('updates updatedAt on change', async () => { + const user = await createUser(); + const original = user.updatedAt; + // Small delay to ensure different timestamp + await new Promise((r) => setTimeout(r, 50)); + const updated = await prisma.user.update({ + where: { id: user.id }, + data: { name: 'Updated' }, + }); + expect(updated.updatedAt.getTime()).toBeGreaterThanOrEqual(original.getTime()); + }); +}); + +// ── Session model ── + +describe('Session', () => { + it('creates a session linked to user', async () => { + const user = await createUser(); + const session = await prisma.session.create({ + data: { + token: 'test-token-123', + userId: user.id, + expiresAt: new Date(Date.now() + 86400_000), + }, + }); + expect(session.token).toBe('test-token-123'); + expect(session.userId).toBe(user.id); + }); + + it('enforces unique token', async () => { + const user = await createUser(); + const data = { + token: 'unique-token', + userId: user.id, + expiresAt: new Date(Date.now() + 86400_000), + }; + await prisma.session.create({ data }); + await expect(prisma.session.create({ data })).rejects.toThrow(); + }); + + it('cascades delete when user is deleted', async () => { + const user = await createUser(); + await prisma.session.create({ + data: { + token: 'cascade-token', + userId: user.id, + expiresAt: new Date(Date.now() + 86400_000), + }, + }); + await prisma.user.delete({ where: { id: user.id } }); + const sessions = await prisma.session.findMany({ where: { userId: user.id } }); + expect(sessions).toHaveLength(0); + }); +}); + +// ── McpServer model ── + +describe('McpServer', () => { + it('creates a server with defaults', async () => { + const server = await createServer(); + expect(server.transport).toBe('STDIO'); + expect(server.version).toBe(1); + expect(server.envTemplate).toEqual([]); + }); + + it('enforces unique name', async () => { + await createServer({ name: 'slack' }); + await expect(createServer({ name: 'slack' })).rejects.toThrow(); + }); + + it('stores envTemplate as JSON', async () => { + const server = await prisma.mcpServer.create({ + data: { + name: 'with-env', + envTemplate: [ + { name: 'API_KEY', description: 'Key', isSecret: true }, + ], + }, + }); + const envTemplate = server.envTemplate as Array<{ name: string }>; + expect(envTemplate).toHaveLength(1); + expect(envTemplate[0].name).toBe('API_KEY'); + }); + + it('supports SSE transport', async () => { + const server = await createServer({ transport: 'SSE' }); + expect(server.transport).toBe('SSE'); + }); +}); + +// ── McpProfile model ── + +describe('McpProfile', () => { + it('creates a profile linked to server', async () => { + const server = await createServer(); + const profile = await prisma.mcpProfile.create({ + data: { + name: 'readonly', + serverId: server.id, + permissions: ['read'], + }, + }); + expect(profile.name).toBe('readonly'); + expect(profile.serverId).toBe(server.id); + }); + + it('enforces unique name per server', async () => { + const server = await createServer(); + const data = { name: 'default', serverId: server.id }; + await prisma.mcpProfile.create({ data }); + await expect(prisma.mcpProfile.create({ data })).rejects.toThrow(); + }); + + it('allows same profile name on different servers', async () => { + const server1 = await createServer({ name: 'server-1' }); + const server2 = await createServer({ name: 'server-2' }); + await prisma.mcpProfile.create({ data: { name: 'default', serverId: server1.id } }); + const profile2 = await prisma.mcpProfile.create({ data: { name: 'default', serverId: server2.id } }); + expect(profile2.name).toBe('default'); + }); + + it('cascades delete when server is deleted', async () => { + const server = await createServer(); + await prisma.mcpProfile.create({ data: { name: 'test', serverId: server.id } }); + await prisma.mcpServer.delete({ where: { id: server.id } }); + const profiles = await prisma.mcpProfile.findMany({ where: { serverId: server.id } }); + expect(profiles).toHaveLength(0); + }); +}); + +// ── Project model ── + +describe('Project', () => { + it('creates a project with owner', async () => { + const user = await createUser(); + const project = await prisma.project.create({ + data: { name: 'weekly-reports', ownerId: user.id }, + }); + expect(project.name).toBe('weekly-reports'); + expect(project.ownerId).toBe(user.id); + }); + + it('enforces unique project name', async () => { + const user = await createUser(); + await prisma.project.create({ data: { name: 'dup', ownerId: user.id } }); + await expect( + prisma.project.create({ data: { name: 'dup', ownerId: user.id } }), + ).rejects.toThrow(); + }); + + it('cascades delete when owner is deleted', async () => { + const user = await createUser(); + await prisma.project.create({ data: { name: 'orphan', ownerId: user.id } }); + await prisma.user.delete({ where: { id: user.id } }); + const projects = await prisma.project.findMany({ where: { ownerId: user.id } }); + expect(projects).toHaveLength(0); + }); +}); + +// ── ProjectMcpProfile (join table) ── + +describe('ProjectMcpProfile', () => { + it('links project to profile', async () => { + const user = await createUser(); + const server = await createServer(); + const profile = await prisma.mcpProfile.create({ + data: { name: 'default', serverId: server.id }, + }); + const project = await prisma.project.create({ + data: { name: 'test-project', ownerId: user.id }, + }); + + const link = await prisma.projectMcpProfile.create({ + data: { projectId: project.id, profileId: profile.id }, + }); + expect(link.projectId).toBe(project.id); + expect(link.profileId).toBe(profile.id); + }); + + it('enforces unique project+profile combination', async () => { + const user = await createUser(); + const server = await createServer(); + const profile = await prisma.mcpProfile.create({ + data: { name: 'default', serverId: server.id }, + }); + const project = await prisma.project.create({ + data: { name: 'test-project', ownerId: user.id }, + }); + + const data = { projectId: project.id, profileId: profile.id }; + await prisma.projectMcpProfile.create({ data }); + await expect(prisma.projectMcpProfile.create({ data })).rejects.toThrow(); + }); + + it('loads profiles through project include', async () => { + const user = await createUser(); + const server = await createServer(); + const profile = await prisma.mcpProfile.create({ + data: { name: 'slack-ro', serverId: server.id }, + }); + const project = await prisma.project.create({ + data: { name: 'reports', ownerId: user.id }, + }); + await prisma.projectMcpProfile.create({ + data: { projectId: project.id, profileId: profile.id }, + }); + + const loaded = await prisma.project.findUnique({ + where: { id: project.id }, + include: { profiles: { include: { profile: true } } }, + }); + expect(loaded!.profiles).toHaveLength(1); + expect(loaded!.profiles[0].profile.name).toBe('slack-ro'); + }); +}); + +// ── McpInstance model ── + +describe('McpInstance', () => { + it('creates an instance linked to server', async () => { + const server = await createServer(); + const instance = await prisma.mcpInstance.create({ + data: { serverId: server.id }, + }); + expect(instance.status).toBe('STOPPED'); + expect(instance.serverId).toBe(server.id); + }); + + it('tracks instance status transitions', async () => { + const server = await createServer(); + const instance = await prisma.mcpInstance.create({ + data: { serverId: server.id, status: 'STARTING' }, + }); + const running = await prisma.mcpInstance.update({ + where: { id: instance.id }, + data: { status: 'RUNNING', containerId: 'abc123', port: 8080 }, + }); + expect(running.status).toBe('RUNNING'); + expect(running.containerId).toBe('abc123'); + expect(running.port).toBe(8080); + }); + + it('cascades delete when server is deleted', async () => { + const server = await createServer(); + await prisma.mcpInstance.create({ data: { serverId: server.id } }); + await prisma.mcpServer.delete({ where: { id: server.id } }); + const instances = await prisma.mcpInstance.findMany({ where: { serverId: server.id } }); + expect(instances).toHaveLength(0); + }); +}); + +// ── AuditLog model ── + +describe('AuditLog', () => { + it('creates an audit log entry', async () => { + const user = await createUser(); + const log = await prisma.auditLog.create({ + data: { + userId: user.id, + action: 'CREATE', + resource: 'McpServer', + resourceId: 'server-123', + details: { name: 'slack' }, + }, + }); + expect(log.action).toBe('CREATE'); + expect(log.resource).toBe('McpServer'); + expect(log.createdAt).toBeInstanceOf(Date); + }); + + it('supports querying by action and resource', async () => { + const user = await createUser(); + await prisma.auditLog.createMany({ + data: [ + { userId: user.id, action: 'CREATE', resource: 'McpServer' }, + { userId: user.id, action: 'UPDATE', resource: 'McpServer' }, + { userId: user.id, action: 'CREATE', resource: 'Project' }, + ], + }); + + const creates = await prisma.auditLog.findMany({ + where: { action: 'CREATE' }, + }); + expect(creates).toHaveLength(2); + + const serverLogs = await prisma.auditLog.findMany({ + where: { resource: 'McpServer' }, + }); + expect(serverLogs).toHaveLength(2); + }); + + it('cascades delete when user is deleted', async () => { + const user = await createUser(); + await prisma.auditLog.create({ + data: { userId: user.id, action: 'TEST', resource: 'Test' }, + }); + await prisma.user.delete({ where: { id: user.id } }); + const logs = await prisma.auditLog.findMany({ where: { userId: user.id } }); + expect(logs).toHaveLength(0); + }); +}); diff --git a/src/db/tests/seed.test.ts b/src/db/tests/seed.test.ts new file mode 100644 index 0000000..41ddbd5 --- /dev/null +++ b/src/db/tests/seed.test.ts @@ -0,0 +1,71 @@ +import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; +import type { PrismaClient } from '@prisma/client'; +import { setupTestDb, cleanupTestDb, clearAllTables } from './helpers.js'; +import { seedMcpServers, defaultServers } from '../src/seed/index.js'; + +let prisma: PrismaClient; + +beforeAll(async () => { + prisma = await setupTestDb(); +}, 30_000); + +afterAll(async () => { + await cleanupTestDb(); +}); + +beforeEach(async () => { + await clearAllTables(prisma); +}); + +describe('seedMcpServers', () => { + it('seeds all default servers', async () => { + const count = await seedMcpServers(prisma); + expect(count).toBe(defaultServers.length); + + const servers = await prisma.mcpServer.findMany({ orderBy: { name: 'asc' } }); + expect(servers).toHaveLength(defaultServers.length); + + const names = servers.map((s) => s.name); + expect(names).toContain('slack'); + expect(names).toContain('github'); + expect(names).toContain('jira'); + expect(names).toContain('terraform'); + }); + + it('is idempotent (upsert)', async () => { + await seedMcpServers(prisma); + const count = await seedMcpServers(prisma); + expect(count).toBe(defaultServers.length); + + const servers = await prisma.mcpServer.findMany(); + expect(servers).toHaveLength(defaultServers.length); + }); + + it('seeds envTemplate correctly', async () => { + await seedMcpServers(prisma); + const slack = await prisma.mcpServer.findUnique({ where: { name: 'slack' } }); + const envTemplate = slack!.envTemplate as Array<{ name: string; isSecret: boolean }>; + expect(envTemplate).toHaveLength(2); + expect(envTemplate[0].name).toBe('SLACK_BOT_TOKEN'); + expect(envTemplate[0].isSecret).toBe(true); + }); + + it('accepts custom server list', async () => { + const custom = [ + { + name: 'custom-server', + description: 'Custom test server', + packageName: '@test/custom', + transport: 'STDIO' as const, + repositoryUrl: 'https://example.com', + envTemplate: [], + }, + ]; + const count = await seedMcpServers(prisma, custom); + expect(count).toBe(1); + + const servers = await prisma.mcpServer.findMany(); + expect(servers).toHaveLength(1); + expect(servers[0].name).toBe('custom-server'); + }); +}); diff --git a/src/db/vitest.config.ts b/src/db/vitest.config.ts index 8400038..9393b93 100644 --- a/src/db/vitest.config.ts +++ b/src/db/vitest.config.ts @@ -4,5 +4,7 @@ export default defineProject({ test: { name: 'db', include: ['tests/**/*.test.ts'], + // Test files share the same database — run sequentially + fileParallelism: false, }, });