Files
mcpctl/.taskmaster/tasks/tasks.json

2564 lines
273 KiB
JSON
Raw Normal View History

2026-02-21 03:10:39 +00:00
{
"master": {
"tasks": [
{
"id": "1",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Verify project builds successfully with `pnpm build`. Ensure all packages compile without errors. Test workspace linking works correctly between packages.",
"priority": "high",
"dependencies": [],
"status": "done",
"subtasks": [
{
"id": 1,
"title": "Initialize pnpm workspace monorepo with future-proof directory structure",
"description": "Create the complete monorepo directory structure using pnpm workspaces that accommodates all 18 planned tasks without requiring future refactoring.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Configure TypeScript with strict mode and project references",
"description": "Set up TypeScript configuration with strict mode, ES2022 target, and proper project references for monorepo build orchestration.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 3,
"title": "Set up Vitest testing framework with workspace configuration",
"description": "Configure Vitest as the test framework across all packages with proper workspace setup, coverage reporting, and test-driven development infrastructure.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 4,
"title": "Configure ESLint with TypeScript rules and docker-compose for local development",
"description": "Set up shared ESLint configuration with TypeScript-aware rules, Prettier integration, and docker-compose.yml for local PostgreSQL database.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 5,
"title": "Install core dependencies and perform security/architecture review",
"description": "Install all required production dependencies across packages, run security audit, and validate the directory structure supports all 18 planned tasks.",
"dependencies": [
1,
3,
4
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
]
},
{
"id": "2",
2026-02-21 03:10:39 +00:00
"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).",
"testStrategy": "Run Prisma migrations against test database. Verify all relations work correctly with seed data. Test CRUD operations for each model using Prisma client.",
"priority": "high",
"dependencies": [
"1"
],
"status": "done",
2026-02-21 03:10:39 +00:00
"subtasks": [
{
"id": 1,
"title": "Set up Prisma ORM and PostgreSQL test infrastructure with docker-compose",
"description": "Initialize Prisma in the db package with PostgreSQL configuration, create docker-compose.yml for local development with separate test database.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Write TDD tests for all Prisma models before implementing schema",
"description": "Create comprehensive Vitest test suites for all 8 models testing CRUD operations, relations, constraints, and edge cases.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 3,
"title": "Implement Prisma schema with all models and security considerations",
"description": "Create the complete Prisma schema with all 8 models, proper relations, indexes for audit queries, and security-conscious field design.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 4,
"title": "Create seed data functions with unit tests for common MCP servers",
"description": "Implement seed functions for common MCP server configurations (Slack, Jira, GitHub, Terraform) with comprehensive unit tests.",
"dependencies": [
3
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 5,
"title": "Create database migrations and perform security/architecture review",
"description": "Generate initial Prisma migration, create migration helper utilities with tests, and conduct comprehensive security and architecture review.",
"dependencies": [
3,
4
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
],
"updatedAt": "2026-02-21T04:10:25.433Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "3",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Unit test middleware functions. Integration test health endpoint. Load test with multiple concurrent requests. Verify statelessness by running two instances.",
"priority": "high",
"dependencies": [
"1",
"2"
],
"status": "done",
2026-02-21 03:10:39 +00:00
"subtasks": [
{
"id": 1,
"title": "Set up mcpd package structure with clean architecture layers and TDD infrastructure",
"description": "Create the src/mcpd directory structure following clean architecture principles with separate layers for routes, controllers, services, and repositories.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Implement Fastify server core with health endpoint and database connectivity verification",
"description": "Create the core Fastify server with health check endpoint that verifies PostgreSQL database connectivity.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 3,
"title": "Implement authentication middleware with JWT validation and session management",
"description": "Create authentication preHandler hook that validates Bearer tokens against the Session table in PostgreSQL.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 4,
"title": "Implement security middleware stack with CORS, Helmet, rate limiting, and input sanitization",
"description": "Configure and register security middleware including CORS policy, Helmet security headers, rate limiting.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 5,
"title": "Implement error handling, audit logging middleware, and graceful shutdown",
"description": "Create global error handler, audit logging onResponse hook, and graceful shutdown handling with connection draining.",
"dependencies": [
2,
3,
4
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
],
"updatedAt": "2026-02-21T04:21:50.389Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "4",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test CRUD operations for servers and profiles. Verify profile inheritance works. Test that invalid configurations are rejected by Zod validation.",
"priority": "high",
"dependencies": [
"3"
],
"status": "done",
2026-02-21 03:10:39 +00:00
"subtasks": [
{
"id": 1,
"title": "Create Zod validation schemas with comprehensive TDD test coverage",
"description": "Define and test Zod schemas for MCP server registration, profile management, and configuration templates before implementing any routes.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Implement repository pattern for MCP server and profile data access",
"description": "Create injectable repository classes for McpServer and McpProfile data access with Prisma, following dependency injection patterns.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 3,
"title": "Implement MCP server service layer with business logic and authorization",
"description": "Create McpServerService and McpProfileService with business logic, authorization checks, and validation orchestration.",
"dependencies": [
1,
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 4,
"title": "Implement REST API routes for MCP servers and profiles with request validation",
"description": "Create Fastify route handlers for MCP server and profile CRUD operations using the service layer.",
"dependencies": [
3
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 5,
"title": "Create seed data for pre-configured MCP servers and perform security review",
"description": "Implement seed data for Slack, Jira, GitHub, and Terraform MCP servers with default profiles, plus security review.",
"dependencies": [
4
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
],
"updatedAt": "2026-02-21T04:26:06.239Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "5",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test project CRUD operations. Verify profile associations work correctly. Test MCP config generation produces valid .mcp.json format.",
"priority": "high",
"dependencies": [
"4"
],
"status": "done",
2026-02-21 03:10:39 +00:00
"subtasks": [
{
"id": 1,
"title": "Write TDD tests for project Zod validation schemas and generateMcpConfig function",
"description": "Create comprehensive Vitest test suites for project validation schemas and generateMcpConfig function BEFORE implementing any code.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Implement project repository and generateMcpConfig service with security filtering",
"description": "Create the project repository and generateMcpConfig function that strips sensitive credentials from output.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 3,
"title": "Implement project service layer with authorization and profile validation",
"description": "Create ProjectService with business logic including authorization checks and profile existence validation.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 4,
"title": "Implement REST API routes for project CRUD and mcp-config endpoint",
"description": "Create Fastify route handlers for all project management endpoints including /api/projects/:name/mcp-config.",
"dependencies": [
3
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 5,
"title": "Create integration tests and security review for project APIs",
"description": "Write comprehensive integration tests and security review documenting credential handling.",
"dependencies": [
4
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
],
"updatedAt": "2026-02-21T04:30:43.622Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "6",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test container creation, start, stop, and removal. Integration test with actual Docker daemon. Verify network isolation.",
"priority": "high",
"dependencies": [
"3",
"4"
],
"status": "done",
2026-02-21 03:10:39 +00:00
"subtasks": [
{
"id": 1,
"title": "Define McpOrchestrator interface and write TDD tests for ContainerManager",
"description": "Define the McpOrchestrator abstraction interface for Docker and Kubernetes orchestrators. Write comprehensive unit tests.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Implement ContainerManager class with DockerOrchestrator strategy pattern",
"description": "Implement the ContainerManager class as a DockerOrchestrator implementation using dockerode.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 3,
"title": "Create docker-compose.yml template with mcpd, PostgreSQL, and test MCP server",
"description": "Create the production-ready deploy/docker-compose.yml template for local development.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 4,
"title": "Write integration tests with real Docker daemon",
"description": "Create integration test suite that tests ContainerManager against a real Docker daemon.",
"dependencies": [
2,
3
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 5,
"title": "Implement container network isolation and resource management",
"description": "Add network segmentation utilities and resource management capabilities for container isolation.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 6,
"title": "Conduct security review of Docker socket access and container configuration",
"description": "Perform comprehensive security review of all Docker-related code with security controls documentation.",
"dependencies": [
2,
3,
5
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 7,
"title": "Implement container logs streaming and health monitoring",
"description": "Add log streaming capabilities and health monitoring to ContainerManager for observability.",
"dependencies": [
2
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
],
"updatedAt": "2026-02-21T04:52:51.544Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "7",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test CLI argument parsing. Test configuration persistence. Mock API calls and verify request formatting.",
"priority": "high",
"dependencies": [
"1"
],
"status": "done",
2026-02-21 03:10:39 +00:00
"subtasks": [
{
"id": 1,
"title": "Set up CLI package structure with TDD infrastructure and command registry pattern",
"description": "Create src/cli directory structure with Commander.js foundation, Vitest test configuration, and extensible command registry.",
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
},
{
"id": 2,
"title": "Implement secure configuration management with encrypted credential storage",
"description": "Create configuration loader/saver with ~/.mcpctl/config.json and encrypted credentials storage.",
"dependencies": [
1
],
"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.",
"parentId": "undefined"
2026-02-21 03:10:39 +00:00
}
],
"updatedAt": "2026-02-21T04:17:17.744Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "8",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test output formatting for each resource type. Test filtering and sorting options.",
"priority": "medium",
"dependencies": [
"7"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T04:55:53.675Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "9",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test YAML/JSON parsing. Test interactive prompts with mock inputs. Verify credentials are stored securely.",
"priority": "medium",
"dependencies": [
"7",
"4"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:14:48.368Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "10",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test .mcp.json file generation. Test project switching. Verify file permissions are correct.",
"priority": "medium",
"dependencies": [
"7",
"5"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:17:02.390Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "11",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Architecture review. Document security considerations. Create proof-of-concept for MCP protocol handling.",
"priority": "medium",
"dependencies": [
"1"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:00:28.388Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "12",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test MCP protocol parsing. Test request routing. Integration test with actual MCP server.",
"priority": "medium",
"dependencies": [
"11"
],
"status": "done",
"subtasks": [
{
"id": 1,
"title": "Create main.ts entry point with configuration loading",
"description": "Implement the main.ts entry point that reads proxy configuration from file or CLI arguments, initializes upstreams based on config, and boots the StdioProxyServer.",
"dependencies": [],
"details": "Create src/local-proxy/src/main.ts that: 1) Parses command-line arguments (--config flag for JSON config path, or individual --upstream flags), 2) Loads ProxyConfig from JSON file if specified, 3) Instantiates StdioUpstream or HttpUpstream for each UpstreamConfig based on transport type, 4) Calls start() on each StdioUpstream to spawn child processes, 5) Adds all upstreams to McpRouter via addUpstream(), 6) Creates StdioProxyServer with the router and calls start(), 7) Handles SIGTERM/SIGINT for graceful shutdown calling router.closeAll(). Use a simple arg parser or process.argv directly. Export a main() function and call it when run directly.",
"status": "done",
"testStrategy": "Test config file loading with valid/invalid JSON. Test CLI argument parsing. Integration test: spawn proxy with mock upstream config and verify it starts and responds to initialize request.",
"parentId": "undefined",
"updatedAt": "2026-02-21T05:05:48.624Z"
},
{
"id": 2,
"title": "Add resource forwarding support to McpRouter",
"description": "Extend McpRouter to handle resources/list and resources/read methods, forwarding them to upstream servers with proper namespacing similar to tools.",
"dependencies": [
1
],
"details": "Modify src/local-proxy/src/router.ts to: 1) Add a resourceToServer Map similar to toolToServer, 2) Create discoverResources() method that calls resources/list on each upstream and aggregates results with namespaced URIs (e.g., 'servername://resource'), 3) Add 'resources' to capabilities in initialize response, 4) Handle 'resources/list' in route() by calling discoverResources(), 5) Handle 'resources/read' by parsing the namespaced URI, extracting server name, stripping prefix, and forwarding to correct upstream, 6) Handle 'resources/subscribe' and 'resources/unsubscribe' if needed for completeness. Update types.ts if additional resource-related types are needed.",
"status": "done",
"testStrategy": "Unit test discoverResources() with mocked upstreams returning different resources. Test resources/read routing extracts correct server and forwards properly. Test error handling when resource URI has unknown server prefix.",
"parentId": "undefined",
"updatedAt": "2026-02-21T05:05:48.626Z"
},
{
"id": 3,
"title": "Add prompt forwarding support to McpRouter",
"description": "Extend McpRouter to handle prompts/list and prompts/get methods, forwarding them to upstream servers with proper namespacing.",
"dependencies": [
1
],
"details": "Modify src/local-proxy/src/router.ts to: 1) Add a promptToServer Map for tracking prompt origins, 2) Create discoverPrompts() method that calls prompts/list on each upstream and aggregates with namespaced names (e.g., 'servername/prompt-name'), 3) Add 'prompts' to capabilities in initialize response, 4) Handle 'prompts/list' in route() by calling discoverPrompts(), 5) Handle 'prompts/get' by parsing namespaced prompt name, extracting server, stripping prefix, and forwarding to correct upstream. Follow same pattern as tools for consistency.",
"status": "done",
"testStrategy": "Unit test discoverPrompts() with mocked upstreams. Test prompts/get routing correctly forwards to upstream. Test error handling for unknown prompt names.",
"parentId": "undefined",
"updatedAt": "2026-02-21T05:05:48.638Z"
},
{
"id": 4,
"title": "Implement notification forwarding from upstreams to client",
"description": "Add support for forwarding JSON-RPC notifications from upstream servers to the proxy client, enabling real-time updates like progress notifications.",
"dependencies": [
1
],
"details": "Modify upstream classes and server: 1) Add onNotification callback to UpstreamConnection interface in types.ts, 2) Update StdioUpstream to detect notifications (messages without 'id' field) in stdout handler and invoke onNotification callback with namespaced method if needed, 3) Update HttpUpstream if SSE support is needed (may require EventSource or SSE client for true streaming), 4) Add setNotificationHandler(callback) method to McpRouter that registers handler and wires it to all upstreams, 5) Update StdioProxyServer to call router.setNotificationHandler() with a function that writes notification JSON to stdout, 6) Consider namespacing notification params to indicate source server.",
"status": "done",
"testStrategy": "Test StdioUpstream correctly identifies and forwards notifications. Integration test: upstream sends progress notification, verify proxy forwards it to stdout. Test notifications are properly namespaced with source server name.",
"parentId": "undefined",
"updatedAt": "2026-02-21T05:05:48.641Z"
},
{
"id": 5,
"title": "Implement connection health monitoring with reconnection",
"description": "Add health monitoring for upstream connections with automatic status tracking, health check pings, and reconnection logic for failed STDIO upstreams.",
"dependencies": [
1,
4
],
"details": "Create src/local-proxy/src/health.ts with HealthMonitor class: 1) Track connection state for each upstream (healthy, degraded, disconnected), 2) Implement periodic health checks using ping/pong or a lightweight method like calling initialize, 3) Emit health status change events via EventEmitter pattern, 4) Add reconnection logic for StdioUpstream: detect process exit, attempt restart with exponential backoff (1s, 2s, 4s... max 30s), 5) Update McpRouter to accept HealthMonitor instance and use it to filter available upstreams, 6) Add health status to proxy logs/stderr for debugging, 7) Optionally expose health status via a special proxy method (e.g., 'proxy/health'). Update main.ts to instantiate and wire HealthMonitor.",
"status": "done",
"testStrategy": "Test health check detects unresponsive upstream. Test reconnection attempts with mocked process that fails then succeeds. Test exponential backoff timing. Test degraded upstream is excluded from tool discovery until healthy.",
"parentId": "undefined",
"updatedAt": "2026-02-21T05:05:48.643Z"
}
],
"updatedAt": "2026-02-21T05:05:48.643Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "13",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test each provider adapter. Test provider switching. Mock API responses for testing.",
"priority": "low",
"dependencies": [
"12"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:22:44.011Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "14",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test audit log creation. Test query APIs. Verify log retention works correctly.",
"priority": "medium",
"dependencies": [
"3"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:09:18.694Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "15",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test profile templates are valid. Test profile application. Document each profile's use case.",
"priority": "low",
"dependencies": [
"4"
],
"status": "done",
"subtasks": [
{
"id": 1,
"title": "Define Profile Template Types and Schemas",
"description": "Create TypeScript interfaces and Zod validation schemas for profile templates that extend the existing McpProfile type.",
"dependencies": [],
"details": "Create src/shared/src/profiles/types.ts with ProfileTemplate interface containing: id, serverType, name, displayName, description, category (filesystem/database/integration/etc), command, args, requiredEnvVars (with EnvTemplateEntry array), optionalEnvVars, defaultPermissions, setupInstructions, and documentationUrl. Also create profileTemplateSchema.ts with Zod schemas for validation. The templates should be immutable definitions that can be instantiated into actual profiles.",
"status": "pending",
"testStrategy": "Unit test Zod schemas with valid and invalid template data. Verify type compatibility with existing McpServerConfig and McpProfile types.",
"parentId": "undefined"
},
{
"id": 2,
"title": "Implement Common MCP Server Profile Templates",
"description": "Create profile template definitions for common MCP servers including filesystem, github, postgres, slack, and other popular integrations.",
"dependencies": [
1
],
"details": "Create src/shared/src/profiles/templates/ directory with individual template files: filesystem.ts (npx @modelcontextprotocol/server-filesystem with path args), github.ts (npx @modelcontextprotocol/server-github with GITHUB_TOKEN env), postgres.ts (npx @modelcontextprotocol/server-postgres with DATABASE_URL), slack.ts (npx @modelcontextprotocol/server-slack with SLACK_TOKEN), memory.ts, and fetch.ts. Each template exports a ProfileTemplate constant with pre-configured best-practice settings. Include clear descriptions and setup guides for each.",
"status": "pending",
"testStrategy": "Validate each template against the ProfileTemplate Zod schema. Verify all required fields are populated. Test that commands and args are syntactically correct.",
"parentId": "undefined"
},
{
"id": 3,
"title": "Build Profile Registry with Lookup and Filtering",
"description": "Create a profile registry that aggregates all templates and provides lookup, filtering, and search capabilities.",
"dependencies": [
1,
2
],
"details": "Create src/shared/src/profiles/registry.ts implementing a ProfileRegistry class with methods: getAll(), getById(id), getByCategory(category), getByServerType(type), search(query), and getCategories(). The registry should be a singleton that lazily loads all templates from the templates directory. Export a default registry instance. Also create src/shared/src/profiles/index.ts to export all profile-related types, templates, and the registry.",
"status": "pending",
"testStrategy": "Test registry initialization loads all templates. Test each lookup method returns correct results. Test search functionality with partial matches. Verify no duplicate IDs across templates.",
"parentId": "undefined"
},
{
"id": 4,
"title": "Add Profile Validation and Instantiation Utilities",
"description": "Create utility functions to validate profile templates and instantiate them into concrete profile configurations.",
"dependencies": [
1,
3
],
"details": "Create src/shared/src/profiles/utils.ts with functions: validateTemplate(template) - validates a ProfileTemplate against schema, instantiateProfile(templateId, envValues) - creates a concrete profile config from a template by filling in env vars, validateEnvValues(template, envValues) - checks if all required env vars are provided, getMissingEnvVars(template, envValues) - returns list of missing required env vars, and generateMcpJsonEntry(profile) - converts instantiated profile to .mcp.json format entry.",
"status": "pending",
"testStrategy": "Test validateTemplate with valid and invalid templates. Test instantiateProfile produces correct configs. Test env validation catches missing required vars. Test .mcp.json output matches expected format.",
"parentId": "undefined"
},
{
"id": 5,
"title": "Export Profiles Module and Add Integration Tests",
"description": "Export the profiles module from shared package main entry and create comprehensive integration tests.",
"dependencies": [
3,
4
],
"details": "Update src/shared/src/index.ts to add 'export * from ./profiles/index.js'. Create src/shared/src/profiles/__tests__/profiles.test.ts with tests covering: all templates are valid, registry contains expected templates, instantiation works for each template type, .mcp.json generation produces valid output, and round-trip validation (instantiate then validate). Also add documentation comments to all exported functions and types.",
"status": "pending",
"testStrategy": "Run full test suite with vitest. Verify exports are accessible from @mcpctl/shared. Integration test the full workflow: lookup template, validate, instantiate with env vars, generate .mcp.json entry.",
"parentId": "undefined"
}
],
"updatedAt": "2026-02-21T05:26:02.010Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "16",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test instance state transitions. Test concurrent instance management. Integration test with Docker.",
"priority": "medium",
"dependencies": [
"6"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:11:52.795Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "17",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test Kubernetes manifest generation. Test with kind/minikube. Verify resource limits and security contexts.",
"priority": "low",
"dependencies": [
"6"
],
"status": "done",
"subtasks": [
{
"id": 1,
"title": "Create K8s API HTTP client and connection handling",
"description": "Implement a Kubernetes API client using node:http/https to communicate with the K8s API server, including authentication, TLS handling, and base request/response utilities.",
"dependencies": [],
"details": "Create src/mcpd/src/services/k8s/k8s-client.ts with: 1) K8sClientConfig interface supporting kubeconfig file parsing, in-cluster config detection, and direct API server URL/token config. 2) HTTP client wrapper using node:http/https that handles TLS certificates, bearer token auth, and API versioning. 3) Base request methods (get, post, delete, patch) with proper error handling and response parsing. 4) Support for watching resources with streaming responses. Reference the Docker container-manager.ts pattern for constructor options and ping() implementation.",
"status": "pending",
"testStrategy": "Unit tests with mocked HTTP responses for successful API calls, auth failures, connection errors. Test kubeconfig parsing with sample config files. Test in-cluster config detection by mocking environment variables and service account token file.",
"parentId": "undefined"
},
{
"id": 2,
"title": "Implement K8s manifest generation for MCP servers",
"description": "Create manifest generator that converts ContainerSpec to Kubernetes Pod and Deployment YAML/JSON specifications with proper resource limits and security contexts.",
"dependencies": [
1
],
"details": "Create src/mcpd/src/services/k8s/manifest-generator.ts with: 1) generatePodSpec(spec: ContainerSpec, namespace: string) that creates a Pod manifest with container image, env vars, resource limits (CPU/memory from spec.nanoCpus and spec.memoryLimit), and labels including mcpctl.managed=true. 2) generateDeploymentSpec() for replicated deployments with selector labels. 3) generateServiceSpec() for exposing container ports. 4) Security context configuration (non-root user, read-only root filesystem, drop capabilities). 5) Map ContainerSpec fields to K8s equivalents (memoryLimit to resources.limits.memory, nanoCpus to resources.limits.cpu, etc.).",
"status": "pending",
"testStrategy": "Unit tests validating generated manifests match expected K8s spec structure. Test resource limit conversion (bytes to Ki/Mi/Gi, nanoCPUs to millicores). Test label propagation from ContainerSpec.labels. Validate manifests against K8s API schema if possible.",
"parentId": "undefined"
},
{
"id": 3,
"title": "Implement KubernetesOrchestrator class with McpOrchestrator interface",
"description": "Create the main KubernetesOrchestrator class that implements the McpOrchestrator interface using the K8s client and manifest generator.",
"dependencies": [
1,
2
],
"details": "Create src/mcpd/src/services/k8s/kubernetes-orchestrator.ts implementing McpOrchestrator interface: 1) Constructor accepting K8sClientConfig and default namespace. 2) ping() - call /api/v1 endpoint to verify cluster connectivity. 3) pullImage() - no-op for K8s (images pulled on pod schedule) or optionally create a pre-pull DaemonSet. 4) createContainer(spec) - generate Pod/Deployment manifest, POST to K8s API, wait for pod Ready condition, return ContainerInfo with pod name as containerId. 5) stopContainer(containerId) - scale deployment to 0 or delete pod. 6) removeContainer(containerId) - DELETE the pod/deployment resource. 7) inspectContainer(containerId) - GET pod status, map phase to ContainerInfo state (Running→running, Pending→starting, Failed→error, etc.). 8) getContainerLogs(containerId) - GET /api/v1/namespaces/{ns}/pods/{name}/log endpoint.",
"status": "pending",
"testStrategy": "Integration tests with mocked K8s API responses for each method. Test createContainer returns valid ContainerInfo with mapped state. Test state mapping from K8s pod phases. Test log retrieval with tail and since parameters. Test error handling when pod not found or API errors.",
"parentId": "undefined"
},
{
"id": 4,
"title": "Add namespace and multi-namespace support",
"description": "Extend KubernetesOrchestrator to support configurable namespaces, namespace creation, and querying resources across namespaces.",
"dependencies": [
3
],
"details": "Enhance src/mcpd/src/services/k8s/kubernetes-orchestrator.ts with: 1) Add namespace parameter to ContainerSpec or use labels to specify target namespace. 2) ensureNamespace(name) method that creates namespace if not exists (POST /api/v1/namespaces). 3) listContainers(namespace?: string) method to list all mcpctl-managed pods in a namespace or all namespaces. 4) Add namespace to ContainerInfo response. 5) Support 'default' namespace fallback and configurable default namespace in constructor. 6) Add namespace label to generated manifests for filtering. 7) Validate namespace names (DNS-1123 label format).",
"status": "pending",
"testStrategy": "Test namespace creation with mocked API. Test namespace validation for invalid names. Test listing pods across namespaces. Test ContainerInfo includes correct namespace. Test default namespace fallback behavior.",
"parentId": "undefined"
},
{
"id": 5,
"title": "Add comprehensive tests and module exports",
"description": "Create unit tests with mocked K8s API responses, integration test utilities, and export the KubernetesOrchestrator from the services module.",
"dependencies": [
3,
4
],
"details": "1) Create src/mcpd/src/services/k8s/index.ts exporting KubernetesOrchestrator, K8sClientConfig, and helper types. 2) Update src/mcpd/src/services/index.ts to export k8s module. 3) Create src/mcpd/src/services/k8s/__tests__/kubernetes-orchestrator.test.ts with mocked HTTP responses using vitest's mock system. 4) Create mock-k8s-api.ts helper that simulates K8s API responses (pod list, pod status, logs, errors). 5) Test all McpOrchestrator interface methods with success and error cases. 6) Add tests for resource limit edge cases (0 memory, very high CPU). 7) Document usage examples in code comments showing how to switch from DockerContainerManager to KubernetesOrchestrator.",
"status": "pending",
"testStrategy": "Ensure all tests pass with mocked responses. Verify test coverage for all public methods. Test error scenarios (404 pod not found, 403 forbidden, 500 server error). Optional: Add integration test script that runs against kind/minikube if available.",
"parentId": "undefined"
}
],
"updatedAt": "2026-02-21T05:30:53.921Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "18",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Review documentation for completeness. Run e2e test suite. Test installation instructions.",
"priority": "medium",
"dependencies": [
"7",
"8",
"9",
"10"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:19:02.525Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "19",
2026-02-21 03:10:39 +00:00
"title": "CANCELLED - Auth middleware",
"description": "Merged into Task 3 subtasks",
"details": null,
"testStrategy": null,
"priority": "low",
2026-02-21 03:10:39 +00:00
"dependencies": [],
"status": "cancelled",
"subtasks": [],
2026-02-21 03:10:39 +00:00
"updatedAt": "2026-02-21T02:21:03.958Z"
},
{
"id": "20",
2026-02-21 03:10:39 +00:00
"title": "CANCELLED - Duplicate project management",
"description": "Merged into Task 5",
"details": null,
"testStrategy": null,
"priority": "low",
2026-02-21 03:10:39 +00:00
"dependencies": [],
"status": "cancelled",
"subtasks": [],
2026-02-21 03:10:39 +00:00
"updatedAt": "2026-02-21T02:21:03.966Z"
},
{
"id": "21",
2026-02-21 03:10:39 +00:00
"title": "CANCELLED - Duplicate audit logging",
"description": "Merged into Task 14",
"details": null,
"testStrategy": null,
"priority": "low",
2026-02-21 03:10:39 +00:00
"dependencies": [],
"status": "cancelled",
"subtasks": [],
2026-02-21 03:10:39 +00:00
"updatedAt": "2026-02-21T02:21:03.972Z"
},
{
"id": "22",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test health check endpoints. Test metrics collection. Verify dashboard displays correct data.",
"priority": "low",
"dependencies": [
"6",
"14"
],
"status": "done",
"subtasks": [
{
"id": 1,
"title": "Create MetricsCollector Service",
"description": "Implement a MetricsCollector service in src/mcpd/src/services/metrics-collector.ts that tracks instance health metrics, uptime, request counts, error rates, and resource usage data.",
"dependencies": [],
"details": "Create MetricsCollector class with methods: recordRequest(), recordError(), updateInstanceMetrics(), getMetrics(). Store metrics in-memory using Map<instanceId, InstanceMetrics>. Define InstanceMetrics interface with fields: instanceId, status, uptime, requestCount, errorCount, lastRequestAt, memoryUsage, cpuUsage. Inject IMcpInstanceRepository and McpOrchestrator dependencies to gather real-time instance status from containers. Export service from src/mcpd/src/services/index.ts.",
"status": "pending",
"testStrategy": "Unit tests with mocked repository and orchestrator dependencies. Test metric recording, aggregation, and retrieval. Verify error rate calculations and uptime tracking accuracy.",
"parentId": "undefined"
},
{
"id": 2,
"title": "Implement Health Aggregation Service",
"description": "Create a HealthAggregator service that computes overall system health by aggregating health status across all MCP server instances.",
"dependencies": [
1
],
"details": "Add HealthAggregator class in src/mcpd/src/services/health-aggregator.ts. Methods: getOverview() returns SystemHealth with totalInstances, healthyCount, unhealthyCount, errorCount, and overallStatus (healthy/degraded/unhealthy). Use MetricsCollector to gather per-instance metrics. Include orchestrator.ping() check for runtime availability. Compute aggregate error rate and average uptime. Export from services/index.ts.",
"status": "pending",
"testStrategy": "Unit tests with mocked MetricsCollector. Test aggregation logic for various instance states. Verify overall status determination rules (e.g., >50% unhealthy = degraded).",
"parentId": "undefined"
},
{
"id": 3,
"title": "Create Health Monitoring REST Endpoints",
"description": "Implement REST endpoints for health monitoring: GET /api/v1/health/overview, GET /api/v1/health/instances/:id, and GET /api/v1/metrics in src/mcpd/src/routes/health-monitoring.ts.",
"dependencies": [
1,
2
],
"details": "Create registerHealthMonitoringRoutes(app, deps) function. GET /api/v1/health/overview returns SystemHealth from HealthAggregator.getOverview(). GET /api/v1/health/instances/:id returns InstanceMetrics for specific instance from MetricsCollector. GET /api/v1/metrics returns all metrics in Prometheus-compatible format or JSON. Add proper error handling for 404 when instance not found. Register routes in src/mcpd/src/routes/index.ts and wire up in server.ts.",
"status": "pending",
"testStrategy": "Integration tests using Fastify inject(). Test all three endpoints with mocked services. Verify 200 responses with correct payload structure, 404 for missing instances.",
"parentId": "undefined"
},
{
"id": 4,
"title": "Add Request/Error Metrics Middleware",
"description": "Create middleware in src/mcpd/src/middleware/metrics.ts that intercepts requests to record metrics for request counts and error rates per instance.",
"dependencies": [
1
],
"details": "Implement Fastify preHandler hook that extracts instance ID from request params/query where applicable. Record request start time. Use onResponse hook to record completion and calculate latency. Use onError hook to record errors with MetricsCollector.recordError(). Track metrics per-route and per-instance. Register middleware in src/mcpd/src/middleware/index.ts. Apply to instance-related routes (/api/v1/instances/*) to track per-instance metrics.",
"status": "pending",
"testStrategy": "Unit tests verifying hooks call MetricsCollector methods. Integration tests confirming request/error counts increment correctly after API calls.",
"parentId": "undefined"
},
{
"id": 5,
"title": "Write Comprehensive Health Monitoring Tests",
"description": "Create test suite in src/mcpd/tests/health-monitoring.test.ts covering MetricsCollector, HealthAggregator, health monitoring routes, and metrics middleware.",
"dependencies": [
1,
2,
3,
4
],
"details": "Write tests for: MetricsCollector - test recordRequest(), recordError(), getMetrics(), concurrent access safety. HealthAggregator - test getOverview() with various instance states, edge cases (no instances, all unhealthy). Routes - test /api/v1/health/overview, /api/v1/health/instances/:id, /api/v1/metrics endpoints with mocked dependencies. Middleware - test request counting, error tracking, latency recording. Use vi.mock() for dependencies following existing test patterns in the codebase.",
"status": "pending",
"testStrategy": "Self-referential - this subtask IS the test implementation. Verify all tests pass with `npm test`. Aim for >80% coverage on new health monitoring code.",
"parentId": "undefined"
}
],
"updatedAt": "2026-02-21T05:34:25.289Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "23",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test backup creation. Test restore from backup. Verify secrets are encrypted.",
"priority": "low",
"dependencies": [
"2",
"5"
],
"status": "done",
"subtasks": [
{
"id": 1,
"title": "Implement BackupService for JSON export",
"description": "Create BackupService in src/mcpd/src/services/backup/ that exports servers, profiles, and projects from repositories to a structured JSON bundle.",
"dependencies": [],
"details": "Create BackupService class that uses IMcpServerRepository, IMcpProfileRepository, and IProjectRepository to fetch all data. Define a BackupBundle interface with metadata (version, timestamp, mcpctlVersion), servers array, profiles array, and projects array. Implement createBackup() method that aggregates all data into the bundle format. Add optional filtering by resource type (e.g., only servers, or only specific profiles). Export via services/index.ts following existing patterns.",
"status": "pending",
"testStrategy": "Unit test BackupService with mocked repositories. Verify bundle structure includes all expected fields. Test filtering options. Test handling of empty repositories.",
"parentId": "undefined"
},
{
"id": 2,
"title": "Add secrets encryption using Node crypto",
"description": "Implement AES-256-GCM encryption for sensitive data in backup bundles using password-derived keys via scrypt.",
"dependencies": [
1
],
"details": "Create crypto utility module in src/mcpd/src/services/backup/crypto.ts using Node's built-in crypto module. Implement deriveKey() using scrypt with configurable salt length and key length. Implement encrypt() that creates IV, encrypts data with AES-256-GCM, and returns base64-encoded result with IV and auth tag prepended. Implement decrypt() that reverses the process. In BackupService, detect fields containing secrets (env vars with sensitive patterns like *_KEY, *_SECRET, *_TOKEN, PASSWORD) and encrypt them. Store encryption metadata (algorithm, salt) in bundle header.",
"status": "pending",
"testStrategy": "Test encryption/decryption round-trip with various data sizes. Verify wrong password fails decryption. Test key derivation produces consistent results with same inputs. Test detection of sensitive field patterns.",
"parentId": "undefined"
},
{
"id": 3,
"title": "Implement RestoreService for JSON import",
"description": "Create RestoreService that imports a backup bundle back into the system, handling decryption and conflict resolution.",
"dependencies": [
1,
2
],
"details": "Create RestoreService class in src/mcpd/src/services/backup/. Implement restore() method that parses JSON bundle, validates version compatibility, decrypts encrypted fields using provided password, and imports data using repositories. Support conflict resolution strategies: 'skip' (ignore existing), 'overwrite' (replace existing), 'fail' (abort on conflict). Implement validateBundle() for schema validation before import. Handle partial failures with transaction-like rollback or detailed error reporting.",
"status": "pending",
"testStrategy": "Test restore with valid bundle creates expected resources. Test conflict resolution modes (skip, overwrite, fail). Test encrypted bundle restore with correct/incorrect passwords. Test invalid bundle rejection.",
"parentId": "undefined"
},
{
"id": 4,
"title": "Add REST endpoints for backup and restore",
"description": "Create REST API routes in src/mcpd/src/routes/ for triggering backup creation and restore operations.",
"dependencies": [
1,
2,
3
],
"details": "Create backup.ts routes file with: POST /api/v1/backup (create backup, optional password for encryption, returns JSON bundle), POST /api/v1/restore (accepts JSON bundle in body, password if encrypted, conflict strategy option, returns import summary). Register routes in routes/index.ts. Define BackupDeps interface following existing patterns. Add appropriate error handling for invalid bundles, decryption failures, and conflict errors. Include validation schemas for request bodies.",
"status": "pending",
"testStrategy": "Integration test backup endpoint returns valid JSON bundle. Test restore endpoint with valid/invalid bundles. Test encrypted backup/restore round-trip via API. Test error responses for various failure scenarios.",
"parentId": "undefined"
},
{
"id": 5,
"title": "Add CLI commands for backup and restore",
"description": "Implement CLI commands in src/cli/src/commands/ for backup export to file and restore from file.",
"dependencies": [
4
],
"details": "Create backup.ts commands file with: 'mcpctl backup' command with options --output/-o (file path), --encrypt (prompt for password), --resources (filter: servers,profiles,projects). Create 'mcpctl restore' command with options --input/-i (file path), --password (or prompt if encrypted), --conflict (skip|overwrite|fail). Commands should call the daemon API endpoints. Add progress output and summary of backed up/restored resources. Register commands in cli/src/index.ts following existing createXxxCommand pattern.",
"status": "pending",
"testStrategy": "Test backup command creates valid file. Test restore command from backup file. Test encryption password prompting. Test --resources filtering. Test various conflict resolution modes via CLI.",
"parentId": "undefined"
}
],
"updatedAt": "2026-02-21T05:40:51.787Z"
2026-02-21 03:10:39 +00:00
},
{
"id": "24",
2026-02-21 03:10:39 +00:00
"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.",
"testStrategy": "Test CI pipeline runs successfully. Test release automation. Verify artifacts are published.",
"priority": "medium",
"dependencies": [
"1"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T05:12:31.235Z"
},
{
"id": "25",
"title": "Rename local-proxy to mcplocal",
"description": "Rename the src/local-proxy directory to src/mcplocal and update all package references, imports, and build configurations throughout the monorepo.",
"details": "1. Rename directory: `mv src/local-proxy src/mcplocal`\n2. Update package.json name from `@mcpctl/local-proxy` to `@mcpctl/mcplocal`\n3. Update pnpm-workspace.yaml if needed\n4. Update all imports in other packages that reference local-proxy:\n - Search for `@mcpctl/local-proxy` and replace with `@mcpctl/mcplocal`\n - Check tsconfig references and path mappings\n5. Update any scripts in package.json root that reference local-proxy\n6. Update docker-compose files in deploy/ if they reference local-proxy\n7. Update documentation and README references\n8. Run `pnpm install` to regenerate lockfile with new package name\n9. Verify TypeScript compilation succeeds: `pnpm build`\n10. Run existing tests to ensure nothing broke: `pnpm test`",
"testStrategy": "1. Verify directory rename completed: `ls src/mcplocal`\n2. Verify package.json has correct name\n3. Run `pnpm install` - should complete without errors\n4. Run `pnpm build` - all packages should compile\n5. Run `pnpm test` - all existing tests should pass\n6. Grep codebase for 'local-proxy' - should find no stale references except git history",
"priority": "high",
"dependencies": [],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:04:17.018Z"
},
{
"id": "26",
"title": "Add HTTP REST server to mcplocal",
"description": "Add a Fastify HTTP server to mcplocal that runs alongside the existing stdio server, providing REST endpoints for mcpctl management commands.",
"details": "1. Add Fastify dependency to mcplocal package.json: `@fastify/cors`, `fastify`\n2. Create `src/mcplocal/src/http/server.ts` with Fastify app setup:\n ```typescript\n import Fastify from 'fastify';\n import cors from '@fastify/cors';\n \n export async function createHttpServer(config: HttpServerConfig) {\n const app = Fastify({ logger: true });\n await app.register(cors, { origin: true });\n // Register routes\n return app;\n }\n ```\n3. Create `src/mcplocal/src/http/routes/` directory structure\n4. Create health check endpoint: `GET /health`\n5. Create config types in `src/mcplocal/src/config.ts`:\n - `httpPort`: number (default 3200)\n - `httpHost`: string (default '127.0.0.1')\n - `mcpdUrl`: string (default 'http://localhost:3100')\n6. Update mcplocal entry point to start both servers:\n - stdio server for Claude MCP protocol\n - HTTP server for mcpctl REST API\n7. Add graceful shutdown handling for both servers",
"testStrategy": "1. Unit test: HTTP server starts on configured port\n2. Unit test: Health endpoint returns 200 OK\n3. Integration test: Both stdio and HTTP servers can run simultaneously\n4. Test graceful shutdown stops both servers cleanly\n5. Test CORS headers are present on responses\n6. Manual test: curl http://localhost:3200/health",
"priority": "high",
"dependencies": [
"25"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:09:26.322Z"
},
{
"id": "27",
"title": "Implement mcplocal management proxy routes",
"description": "Add REST endpoints to mcplocal that mirror mcpd's API and proxy management requests to mcpd without LLM processing. All requests must include proper authentication to mcpd using a Bearer token read from mcplocal config.",
"status": "done",
"dependencies": [
"26"
],
"priority": "high",
"details": "1. Create HTTP client for mcpd communication with auth: `src/local-proxy/src/http/mcpd-client.ts`\n ```typescript\n export class McpdClient {\n private token: string;\n \n constructor(private baseUrl: string, token: string) {\n this.token = token;\n }\n \n private getHeaders(): Record<string, string> {\n return {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${this.token}`\n };\n }\n \n async get<T>(path: string): Promise<T> {\n const response = await fetch(`${this.baseUrl}${path}`, {\n method: 'GET',\n headers: this.getHeaders()\n });\n await this.handleAuthError(response);\n return response.json();\n }\n \n async post<T>(path: string, body: unknown): Promise<T> {\n const response = await fetch(`${this.baseUrl}${path}`, {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify(body)\n });\n await this.handleAuthError(response);\n return response.json();\n }\n \n async put<T>(path: string, body: unknown): Promise<T> {\n const response = await fetch(`${this.baseUrl}${path}`, {\n method: 'PUT',\n headers: this.getHeaders(),\n body: JSON.stringify(body)\n });\n await this.handleAuthError(response);\n return response.json();\n }\n \n async delete<T>(path: string): Promise<T> {\n const response = await fetch(`${this.baseUrl}${path}`, {\n method: 'DELETE',\n headers: this.getHeaders()\n });\n await this.handleAuthError(response);\n return response.json();\n }\n \n private async handleAuthError(response: Response): Promise<void> {\n if (response.status === 401) {\n throw new AuthenticationError('Invalid or expired token. Please check mcplocal config.');\n }\n }\n }\n \n export class AuthenticationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AuthenticationError';\n }\n }\n ```\n2. Add token to mcplocal config type (extend ProxyConfig or similar):\n ```typescript\n export interface McpdAuthConfig {\n /** Bearer token for mcpd API authentication */\n mcpdToken: string;\n }\n ```\n3. Create proxy routes in `src/local-proxy/src/http/routes/`:\n - `servers.ts`: GET/POST /api/v1/servers, GET/PUT/DELETE /api/v1/servers/:id\n - `profiles.ts`: GET/POST /api/v1/profiles, GET/PUT/DELETE /api/v1/profiles/:id\n - `instances.ts`: GET/POST /api/v1/instances, GET/POST/DELETE /api/v1/instances/:id, etc.\n - `projects.ts`: GET/POST /api/v1/projects, etc.\n - `audit.ts`: GET /api/v1/audit-logs\n - `backup.ts`: POST /api/v1/backup, POST /api/v1/restore\n4. Each route handler forwards to mcpd with auth:\n ```typescript\n app.get('/api/v1/servers', async (req, reply) => {\n try {\n const result = await mcpdClient.get('/api/v1/servers');\n return result;\n } catch (error) {\n if (error instanceof AuthenticationError) {\n return reply.status(401).send({ error: error.message });\n }\n throw error;\n }\n });\n ```\n5. Add comprehensive error handling:\n - If mcpd is unreachable, return 503 Service Unavailable\n - If mcpd returns 401, return 401 with clear message about token configuration\n - Forward other HTTP errors from mcpd with appropriate status codes\n6. Add request/response logging for debugging",
"testStrategy": "1. Unit test: McpdClient attaches Authorization header to all request methods (GET, POST, PUT, DELETE)\n2. Unit test: McpdClient throws AuthenticationError on 401 response from mcpd\n3. Unit test: Each proxy route forwards requests correctly with auth headers\n4. Unit test: Error handling when mcpd is unreachable (503 response)\n5. Unit test: Error handling when mcpd returns 401 (clear error message returned)\n6. Integration test: Full request flow mcpctl -> mcplocal -> mcpd with valid token\n7. Integration test: Full request flow with invalid token returns 401\n8. Test query parameters are forwarded correctly\n9. Test request body is forwarded correctly for POST/PUT\n10. Test path parameters (:id) are passed through correctly\n11. Mock mcpd responses and verify mcplocal returns them unchanged\n12. Test token is read correctly from mcplocal config",
"subtasks": [],
"updatedAt": "2026-02-21T18:34:20.942Z"
},
{
"id": "28",
"title": "Add MCP proxy endpoint to mcpd",
"description": "Create a new endpoint in mcpd at /api/v1/mcp/proxy that accepts MCP tool call requests and executes them on managed MCP server instances. Also add authentication endpoints (login/logout) that mcpctl will use to authenticate users.",
"status": "done",
"dependencies": [],
"priority": "high",
"details": "## MCP Proxy Endpoint\n\n1. Create new route file: `src/mcpd/src/routes/mcp-proxy.ts`\n2. Define request schema:\n ```typescript\n interface McpProxyRequest {\n serverId: string; // or instanceId\n method: string; // e.g., 'tools/call', 'resources/read'\n params: Record<string, unknown>;\n }\n ```\n3. Create McpProxyService in `src/mcpd/src/services/mcp-proxy-service.ts`:\n - Look up instance by serverId (auto-start if profile allows)\n - Connect to the container via stdio or HTTP (depending on transport type)\n - Execute the MCP JSON-RPC call\n - Return the result\n4. Handle MCP JSON-RPC protocol:\n ```typescript\n async executeCall(instanceId: string, method: string, params: unknown) {\n const instance = await this.instanceService.getInstance(instanceId);\n const connection = await this.getOrCreateConnection(instance);\n const result = await connection.call(method, params);\n return result;\n }\n ```\n5. Connection pooling: maintain persistent connections to running instances\n6. Add route: `POST /api/v1/mcp/proxy` (must be behind auth middleware)\n7. Add audit logging for all MCP proxy calls - include authenticated userId from request.userId\n8. Handle errors: instance not found, instance not running, MCP call failed\n\n## Authentication Endpoints\n\n9. Create auth routes file: `src/mcpd/src/routes/auth.ts`\n10. Implement `POST /api/v1/auth/login`:\n - Request body: `{ username: string, password: string }`\n - Validate credentials against User table (use bcrypt for password comparison)\n - Create new Session record with token (use crypto.randomUUID or similar)\n - Response: `{ token: string, expiresAt: string }`\n11. Implement `POST /api/v1/auth/logout`:\n - Requires Bearer token in Authorization header\n - Delete/invalidate the Session record\n - Response: `{ success: true }`\n\n## Auth Integration Notes\n\n- Existing auth middleware in `src/mcpd/src/middleware/auth.ts` validates Bearer tokens against Session table\n- It sets `request.userId` on successful authentication\n- MCP proxy endpoint MUST use this auth middleware\n- Auth endpoints (login) should NOT require auth middleware\n- Logout endpoint SHOULD require auth middleware to validate the session being invalidated",
"testStrategy": "1. Unit test: Proxy service looks up correct instance\n2. Unit test: JSON-RPC call is formatted correctly\n3. Integration test: Full flow with a mock MCP server container\n4. Test error handling: non-existent server returns 404\n5. Test error handling: stopped instance returns appropriate error\n6. Test audit log entries include authenticated userId\n7. Test connection reuse for multiple calls to same instance\n8. Test login endpoint: valid credentials return session token\n9. Test login endpoint: invalid credentials return 401\n10. Test logout endpoint: valid session is invalidated\n11. Test logout endpoint: invalid/missing token returns 401\n12. Test MCP proxy endpoint without auth token returns 401\n13. Test MCP proxy endpoint with expired token returns 401\n14. Test MCP proxy endpoint with valid token succeeds and logs userId in audit",
"subtasks": [
{
"id": 1,
"title": "Create auth routes with login/logout endpoints",
"description": "Create src/mcpd/src/routes/auth.ts with POST /api/v1/auth/login and POST /api/v1/auth/logout endpoints for mcpctl authentication.",
"dependencies": [],
"details": "Implement login endpoint: validate username/password against User table using bcrypt, create Session record with generated token and expiry. Implement logout endpoint: require auth middleware, delete/invalidate Session record. Login does NOT require auth, logout DOES require auth. Export registerAuthRoutes function and update routes/index.ts.",
"status": "pending",
"testStrategy": "Test login with valid/invalid credentials. Test logout invalidates session. Test logout requires valid auth token. Test session token format and expiry.",
"parentId": "undefined"
},
{
"id": 2,
"title": "Create MCP proxy route file with auth middleware",
"description": "Create src/mcpd/src/routes/mcp-proxy.ts with POST /api/v1/mcp/proxy endpoint protected by auth middleware.",
"dependencies": [
1
],
"details": "Define McpProxyRequest interface (serverId, method, params). Register route handler that extracts userId from request.userId (set by auth middleware). Apply auth middleware using preHandler hook. Validate request body schema.",
"status": "pending",
"testStrategy": "Test endpoint returns 401 without auth token. Test endpoint returns 401 with invalid/expired token. Test valid auth token allows request through.",
"parentId": "undefined"
},
{
"id": 3,
"title": "Create McpProxyService for instance lookup and connection",
"description": "Create src/mcpd/src/services/mcp-proxy-service.ts to handle instance lookup, connection management, and MCP call execution.",
"dependencies": [],
"details": "Implement getInstance to look up by serverId, auto-start if profile allows. Implement getOrCreateConnection for connection pooling. Handle both stdio and HTTP transports. Implement executeCall method that formats JSON-RPC call and returns result.",
"status": "pending",
"testStrategy": "Unit test instance lookup. Unit test connection pooling reuses connections. Test auto-start behavior. Test both transport types.",
"parentId": "undefined"
},
{
"id": 4,
"title": "Implement MCP JSON-RPC call execution",
"description": "Implement the core JSON-RPC call logic in McpProxyService to execute tool calls on MCP server instances.",
"dependencies": [
3
],
"details": "Format JSON-RPC 2.0 request with method and params. Send request over established connection (stdio/HTTP). Parse JSON-RPC response and handle errors. Return result or throw appropriate error for failed calls.",
"status": "pending",
"testStrategy": "Unit test JSON-RPC request formatting. Test successful call returns result. Test JSON-RPC error responses are handled. Integration test with mock MCP server.",
"parentId": "undefined"
},
{
"id": 5,
"title": "Add audit logging with userId for MCP proxy calls",
"description": "Ensure all MCP proxy calls are logged to audit log including the authenticated userId from the session.",
"dependencies": [
2,
4
],
"details": "Use existing audit middleware/service. Include userId from request.userId in audit log entry. Log serverId, method, and outcome (success/failure). Log any errors that occur during MCP call execution.",
"status": "pending",
"testStrategy": "Test audit log entries contain userId. Test audit log entries contain serverId and method. Test failed calls are logged with error details.",
"parentId": "undefined"
},
{
"id": 6,
"title": "Integrate auth and proxy routes into server.ts",
"description": "Register the new auth and mcp-proxy routes in the Fastify server with proper auth middleware wiring.",
"dependencies": [
1,
2,
5
],
"details": "Update server.ts to register auth routes (no auth required for login). Register mcp-proxy routes with auth middleware. Ensure auth middleware is wired with findSession dependency from Prisma. Update routes/index.ts exports.",
"status": "pending",
"testStrategy": "Integration test full login -> proxy call flow. Test auth middleware correctly protects proxy endpoint. Test health endpoints remain unauthenticated.",
"parentId": "undefined"
}
],
"updatedAt": "2026-02-21T18:09:26.327Z"
},
{
"id": "29",
"title": "Implement LLM pre-processing pipeline in mcplocal",
"description": "Create the core LLM pre-processing pipeline that intercepts MCP tool calls, uses a local LLM to optimize requests before sending to mcpd, and filters responses before returning to Claude.",
"details": "1. Create `src/mcplocal/src/llm/processor.ts` - the core pipeline:\n ```typescript\n export class LlmProcessor {\n constructor(\n private providerRegistry: ProviderRegistry,\n private config: LlmProcessorConfig\n ) {}\n \n async preprocessRequest(toolName: string, params: unknown): Promise<ProcessedRequest> {\n // Use LLM to interpret and optimize the request\n const prompt = this.buildRequestPrompt(toolName, params);\n const result = await this.providerRegistry.getActiveProvider().complete({\n systemPrompt: REQUEST_OPTIMIZATION_SYSTEM_PROMPT,\n userPrompt: prompt\n });\n return this.parseOptimizedRequest(result);\n }\n \n async filterResponse(toolName: string, originalRequest: unknown, rawResponse: unknown): Promise<FilteredResponse> {\n // Use LLM to filter/summarize the response\n const prompt = this.buildFilterPrompt(toolName, originalRequest, rawResponse);\n const result = await this.providerRegistry.getActiveProvider().complete({\n systemPrompt: RESPONSE_FILTER_SYSTEM_PROMPT,\n userPrompt: prompt\n });\n return this.parseFilteredResponse(result);\n }\n }\n ```\n2. Create system prompts in `src/mcplocal/src/llm/prompts.ts`:\n - REQUEST_OPTIMIZATION_SYSTEM_PROMPT: instruct LLM to generate optimal queries\n - RESPONSE_FILTER_SYSTEM_PROMPT: instruct LLM to extract relevant information\n3. Integrate into router.ts - wrap tools/call handler:\n ```typescript\n async handleToolsCall(request: JsonRpcRequest) {\n if (this.shouldPreprocess(request.params.name)) {\n const processed = await this.llmProcessor.preprocessRequest(...);\n // Call mcpd with processed request\n const rawResponse = await this.callMcpd(processed);\n const filtered = await this.llmProcessor.filterResponse(...);\n return filtered;\n }\n return this.callMcpd(request.params);\n }\n ```\n4. Add configuration options:\n - `enablePreprocessing`: boolean\n - `preprocessingExclude`: string[] (tool names to skip)\n - `preferredProvider`: string (ollama, gemini, deepseek, etc.)\n5. Add bypass logic for simple operations (list, create, delete)",
"testStrategy": "1. Unit test: Request preprocessing generates optimized queries\n2. Unit test: Response filtering reduces data volume\n3. Unit test: Bypass logic works for excluded tools\n4. Integration test: Full pipeline with mock LLM provider\n5. Test error handling: LLM failure falls back to unfiltered pass-through\n6. Test configuration options are respected\n7. Measure: response size reduction percentage",
"priority": "high",
"dependencies": [
"25",
"27",
"28"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:41:26.539Z"
},
{
"id": "30",
"title": "Add Gemini CLI LLM provider",
"description": "Implement a new LLM provider that uses the Gemini CLI binary for local, free LLM inference as the preferred provider for pre-processing.",
"details": "1. Create `src/mcplocal/src/providers/gemini-cli.ts`:\n ```typescript\n import { spawn } from 'child_process';\n \n export class GeminiCliProvider implements LlmProvider {\n readonly name = 'gemini-cli';\n private binaryPath: string;\n \n constructor(config: GeminiCliConfig) {\n this.binaryPath = config.binaryPath || 'gemini';\n }\n \n async isAvailable(): Promise<boolean> {\n // Check if gemini binary exists and is executable\n try {\n await this.runCommand(['--version']);\n return true;\n } catch {\n return false;\n }\n }\n \n async complete(options: CompletionOptions): Promise<CompletionResult> {\n const input = this.formatPrompt(options);\n const output = await this.runCommand(['--prompt', input]);\n return { content: output, model: 'gemini-cli' };\n }\n \n private async runCommand(args: string[]): Promise<string> {\n // Spawn gemini CLI process and capture output\n }\n }\n ```\n2. Research actual Gemini CLI interface and adjust implementation\n3. Add to provider registry with high priority (prefer over API providers)\n4. Add configuration: `geminiCliBinaryPath`\n5. Handle timeout for slow inference\n6. Add fallback to next provider if Gemini CLI fails",
"testStrategy": "1. Unit test: Provider correctly detects CLI availability\n2. Unit test: Prompt formatting is correct\n3. Unit test: Output parsing handles various formats\n4. Integration test: Full completion with actual Gemini CLI (if available)\n5. Test timeout handling for slow responses\n6. Test fallback when CLI is not installed",
"priority": "medium",
"dependencies": [
"25"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:34:20.968Z"
},
{
"id": "31",
"title": "Add DeepSeek API LLM provider",
"description": "Implement DeepSeek API provider as a cheap cloud-based fallback when local LLMs are unavailable.",
"details": "1. Create `src/mcplocal/src/providers/deepseek.ts`:\n ```typescript\n export class DeepSeekProvider implements LlmProvider {\n readonly name = 'deepseek';\n private apiKey: string;\n private baseUrl = 'https://api.deepseek.com/v1';\n \n constructor(config: DeepSeekConfig) {\n this.apiKey = config.apiKey || process.env.DEEPSEEK_API_KEY;\n }\n \n async isAvailable(): Promise<boolean> {\n return !!this.apiKey;\n }\n \n async complete(options: CompletionOptions): Promise<CompletionResult> {\n // DeepSeek uses OpenAI-compatible API\n const response = await fetch(`${this.baseUrl}/chat/completions`, {\n method: 'POST',\n headers: {\n 'Authorization': `Bearer ${this.apiKey}`,\n 'Content-Type': 'application/json'\n },\n body: JSON.stringify({\n model: 'deepseek-chat',\n messages: [{ role: 'user', content: options.userPrompt }]\n })\n });\n // Parse and return\n }\n }\n ```\n2. Add DEEPSEEK_API_KEY to configuration\n3. Register in provider registry with medium priority\n4. Support both deepseek-chat and deepseek-coder models\n5. Add rate limiting handling",
"testStrategy": "1. Unit test: Provider correctly checks API key availability\n2. Unit test: Request formatting matches DeepSeek API spec\n3. Unit test: Response parsing handles all fields\n4. Integration test: Full completion with actual API (with valid key)\n5. Test error handling for rate limits\n6. Test error handling for invalid API key",
"priority": "medium",
"dependencies": [
"25"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:34:20.974Z"
},
{
"id": "32",
"title": "Implement smart context optimization",
"description": "Add token counting and decision logic to intelligently skip LLM filtering when responses are small enough, and cache filtering decisions for repeated queries.",
"details": "1. Create `src/mcplocal/src/llm/token-counter.ts`:\n ```typescript\n export function estimateTokens(text: string): number {\n // Simple estimation: ~4 chars per token for English\n // More accurate: use tiktoken or similar library\n return Math.ceil(text.length / 4);\n }\n ```\n2. Create `src/mcplocal/src/llm/filter-cache.ts`:\n ```typescript\n export class FilterCache {\n private cache: LRUCache<string, FilterDecision>;\n \n shouldFilter(toolName: string, params: unknown, responseSize: number): boolean {\n const key = this.computeKey(toolName, params);\n const cached = this.cache.get(key);\n if (cached) return cached.shouldFilter;\n // No cache hit - use default threshold logic\n return responseSize > this.tokenThreshold;\n }\n \n recordDecision(toolName: string, params: unknown, decision: FilterDecision): void {\n const key = this.computeKey(toolName, params);\n this.cache.set(key, decision);\n }\n }\n ```\n3. Add configuration options:\n - `tokenThreshold`: number (default 1000 tokens)\n - `filterCacheSize`: number (default 1000 entries)\n - `filterCacheTtl`: number (default 3600 seconds)\n4. Integrate into LlmProcessor:\n ```typescript\n async filterResponse(...) {\n const tokens = estimateTokens(JSON.stringify(rawResponse));\n if (tokens < this.config.tokenThreshold) {\n // Not worth filtering - return as-is\n return { filtered: false, response: rawResponse };\n }\n // Proceed with LLM filtering\n }\n ```\n5. Add metrics tracking:\n - Total tokens processed\n - Tokens saved by filtering\n - Filter cache hit rate\n - Average latency added by filtering",
"testStrategy": "1. Unit test: Token estimation is reasonably accurate\n2. Unit test: Cache correctly stores and retrieves decisions\n3. Unit test: Threshold logic skips filtering for small responses\n4. Unit test: Cache TTL expiration works correctly\n5. Integration test: Metrics are recorded accurately\n6. Performance test: Cache improves latency for repeated queries",
"priority": "medium",
"dependencies": [
"29"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:47:07.709Z"
},
{
"id": "33",
"title": "Update mcpctl to use mcplocal as daemon",
"description": "Modify mcpctl CLI to connect to mcplocal instead of mcpd directly, update configuration options, add dual connectivity status checking, and implement authentication commands (login/logout) with secure credential storage.",
"status": "done",
"dependencies": [
"27"
],
"priority": "high",
"details": "1. Update `src/cli/src/config/schema.ts`:\n ```typescript\n export interface McpctlConfig {\n mcplocalUrl: string; // NEW: default 'http://localhost:3200'\n mcpdUrl: string; // Keep for reference/direct access if needed\n // ... other fields\n }\n ```\n2. Update `src/cli/src/config/defaults.ts`:\n - Change default daemonUrl to http://localhost:3200 (mcplocal)\n3. Update `src/cli/src/api-client.ts`:\n - Default baseUrl now points to mcplocal\n4. Add new config commands in `src/cli/src/commands/config.ts`:\n ```typescript\n .command('set-mcplocal-url <url>')\n .command('set-mcpd-url <url>')\n .command('get-mcplocal-url')\n .command('get-mcpd-url')\n ```\n5. Update `src/cli/src/commands/status.ts` to show both connections and auth status:\n ```\n $ mcpctl status\n mcplocal: connected (localhost:3200)\n mcpd: connected (nas.local:3100) via mcplocal\n Auth: logged in as user@example.com\n LLM Provider: ollama (llama3.2)\n Token savings: 45% (last 24h)\n ```\n6. Update CLI --daemon-url flag to point to mcplocal\n7. Add --direct flag to bypass mcplocal and talk to mcpd directly (for debugging)\n8. Create `src/cli/src/commands/auth.ts` with login/logout commands:\n - `mcpctl login`: Prompt for mcpd URL (if not configured) and credentials\n - Call POST /api/v1/auth/login with { email, password }\n - Store session token in ~/.mcpctl/credentials with 0600 permissions\n - `mcpctl logout`: Invalidate session and delete stored token\n9. Create `src/cli/src/auth/credentials.ts` for secure token storage:\n - Use fs.chmod to set 0600 permissions on credentials file\n - Token format: { token: string, mcpdUrl: string, user: string, expiresAt?: string }\n10. Update api-client.ts to include stored token in requests to mcplocal\n - mcplocal passes this token to mcpd for authentication",
"testStrategy": "1. Unit test: Default config points to mcplocal URL\n2. Unit test: Config commands update correct fields\n3. Integration test: CLI commands work through mcplocal proxy\n4. Test status command shows both mcplocal and mcpd status\n5. Test --direct flag bypasses mcplocal\n6. Test backward compatibility with existing config files\n7. Unit test: login command stores token with correct permissions (0600)\n8. Unit test: logout command removes credentials file\n9. Integration test: login flow with POST /api/v1/auth/login\n10. Test status command shows auth status (logged in as user)\n11. Test token is passed to mcplocal in API requests\n12. Test invalid credentials return appropriate error message\n13. Test expired token handling",
"subtasks": [
{
"id": 1,
"title": "Update config schema for mcplocal and mcpd URLs",
"description": "Modify McpctlConfigSchema in src/cli/src/config/schema.ts to include separate mcplocalUrl and mcpdUrl fields with appropriate defaults.",
"dependencies": [],
"details": "Update the Zod schema to add mcplocalUrl (default: http://localhost:3200) and mcpdUrl (default: http://localhost:3100). Update DEFAULT_CONFIG and ensure backward compatibility with existing daemonUrl field by mapping it to mcplocalUrl.",
"status": "pending",
"testStrategy": "Unit test schema validation for new URL fields. Test default values are correct. Test backward compatibility mapping.",
"parentId": "undefined"
},
{
"id": 2,
"title": "Create auth credentials storage module",
"description": "Create src/cli/src/auth/credentials.ts to handle secure storage and retrieval of session tokens in ~/.mcpctl/credentials.",
"dependencies": [],
"details": "Implement saveCredentials(token, mcpdUrl, user), loadCredentials(), and deleteCredentials() functions. Use fs.chmod to set 0600 permissions. Store JSON format: { token, mcpdUrl, user, expiresAt }. Handle file not found gracefully in loadCredentials.",
"status": "pending",
"testStrategy": "Unit test credentials are saved with 0600 permissions. Test load returns null when file doesn't exist. Test delete removes the file.",
"parentId": "undefined"
},
{
"id": 3,
"title": "Implement login command",
"description": "Create src/cli/src/commands/auth.ts with mcpctl login command that prompts for mcpd URL and credentials, calls POST /api/v1/auth/login, and stores the session token.",
"dependencies": [
2
],
"details": "Use inquirer or prompts library for interactive credential input (email, password). If mcpdUrl not configured, prompt for it. Call POST /api/v1/auth/login with credentials. On success, save token using credentials module. Display 'Logged in as {user}' on success. Handle errors (invalid credentials, network errors) with clear messages.",
"status": "pending",
"testStrategy": "Test prompts collect correct input. Test successful login stores credentials. Test failed login shows error without storing token.",
"parentId": "undefined"
},
{
"id": 4,
"title": "Implement logout command",
"description": "Add mcpctl logout command to auth.ts that invalidates the session and removes stored credentials.",
"dependencies": [
2
],
"details": "Load stored credentials, optionally call a logout endpoint on mcpd to invalidate server-side session, then delete the local credentials file. Display 'Logged out successfully' or 'Not logged in' as appropriate.",
"status": "pending",
"testStrategy": "Test logout removes credentials file. Test logout when not logged in shows appropriate message.",
"parentId": "undefined"
},
{
"id": 5,
"title": "Update api-client to include auth token",
"description": "Modify src/cli/src/api-client.ts to load and include stored session token in Authorization header for requests to mcplocal.",
"dependencies": [
2
],
"details": "Import loadCredentials from auth module. Add Authorization: Bearer {token} header to requests when credentials exist. Handle expired token by returning appropriate error suggesting re-login.",
"status": "pending",
"testStrategy": "Test requests include Authorization header when logged in. Test requests work without token when not logged in.",
"parentId": "undefined"
},
{
"id": 6,
"title": "Update status command to show auth status",
"description": "Modify src/cli/src/commands/status.ts to display authentication status (logged in as user X or not logged in) along with mcplocal and mcpd connectivity.",
"dependencies": [
2,
5
],
"details": "Load credentials and display auth status line: 'Auth: logged in as {user}' or 'Auth: not logged in'. Update status output format to show mcplocal and mcpd status separately with the auth info.",
"status": "pending",
"testStrategy": "Test status shows 'logged in as user' when credentials exist. Test status shows 'not logged in' when no credentials.",
"parentId": "undefined"
},
{
"id": 7,
"title": "Add config commands for mcplocal and mcpd URLs",
"description": "Add set-mcplocal-url, set-mcpd-url, get-mcplocal-url, and get-mcpd-url commands to src/cli/src/commands/config.ts.",
"dependencies": [
1
],
"details": "Add four new subcommands to the config command for setting and getting the mcplocal and mcpd URLs independently. Update the generic 'set' command to handle these new schema fields.",
"status": "pending",
"testStrategy": "Test each command correctly reads/writes the appropriate config field.",
"parentId": "undefined"
},
{
"id": 8,
"title": "Add --direct flag for mcpd bypass",
"description": "Add --direct flag to CLI commands that bypasses mcplocal and connects directly to mcpd for debugging purposes.",
"dependencies": [
1,
5
],
"details": "Add global --direct option to the main CLI. When set, api-client uses mcpdUrl instead of mcplocalUrl. Useful for debugging connectivity issues between mcplocal and mcpd.",
"status": "pending",
"testStrategy": "Test --direct flag causes requests to use mcpdUrl. Test normal operation uses mcplocalUrl.",
"parentId": "undefined"
},
{
"id": 9,
"title": "Register auth commands in CLI entry point",
"description": "Import and register the login and logout commands in src/cli/src/index.ts.",
"dependencies": [
3,
4
],
"details": "Import createAuthCommand from commands/auth.ts and add it to the main program with program.addCommand(createAuthCommand()).",
"status": "pending",
"testStrategy": "Test mcpctl login and mcpctl logout are available as commands.",
"parentId": "undefined"
}
],
"updatedAt": "2026-02-21T18:39:11.345Z"
},
{
"id": "34",
"title": "Connect mcplocal MCP router to mcpd proxy endpoint",
"description": "Update mcplocal's MCP router to forward tool calls to mcpd's new /api/v1/mcp/proxy endpoint instead of connecting to MCP servers directly.",
"details": "1. Update `src/mcplocal/src/router.ts` to use mcpd proxy:\n ```typescript\n class Router {\n private mcpdClient: McpdClient;\n \n async handleToolsCall(request: JsonRpcRequest) {\n const { name, arguments: args } = request.params;\n const [serverName, toolName] = name.split('/');\n \n // Pre-process with LLM if enabled\n const processedArgs = this.config.enablePreprocessing\n ? await this.llmProcessor.preprocessRequest(toolName, args)\n : args;\n \n // Call mcpd proxy endpoint\n const result = await this.mcpdClient.post('/api/v1/mcp/proxy', {\n serverId: serverName,\n method: 'tools/call',\n params: { name: toolName, arguments: processedArgs }\n });\n \n // Post-process response with LLM if enabled\n return this.config.enablePreprocessing\n ? await this.llmProcessor.filterResponse(toolName, args, result)\n : result;\n }\n }\n ```\n2. Update upstream configuration:\n - Remove direct upstream connections for managed servers\n - Keep option for local/unmanaged upstreams\n3. Add server discovery from mcpd:\n ```typescript\n async refreshServerList() {\n const servers = await this.mcpdClient.get('/api/v1/servers');\n this.updateAvailableTools(servers);\n }\n ```\n4. Handle tools/list by aggregating from mcpd servers\n5. Handle resources/list and prompts/list similarly",
"testStrategy": "1. Unit test: Tool calls are forwarded to mcpd proxy correctly\n2. Unit test: Server name is extracted from namespaced tool name\n3. Integration test: Full flow Claude -> mcplocal -> mcpd -> container\n4. Test tools/list aggregates from all mcpd servers\n5. Test error handling when mcpd is unreachable\n6. Test LLM preprocessing is applied when enabled",
"priority": "high",
"dependencies": [
"28",
"29"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:43:14.673Z"
},
{
"id": "35",
"title": "Implement health monitoring across all tiers",
"description": "Extend health monitoring to track connectivity and status across mcplocal, mcpd, and individual MCP server instances.",
"details": "1. Update mcplocal health monitor in `src/mcplocal/src/health.ts`:\n ```typescript\n export class TieredHealthMonitor {\n async checkHealth(): Promise<TieredHealthStatus> {\n return {\n mcplocal: {\n status: 'healthy',\n llmProvider: await this.checkLlmProvider(),\n uptime: process.uptime()\n },\n mcpd: await this.checkMcpdHealth(),\n instances: await this.checkInstancesHealth()\n };\n }\n \n private async checkMcpdHealth(): Promise<McpdHealth> {\n try {\n const health = await this.mcpdClient.get('/api/v1/health');\n return { status: 'connected', ...health };\n } catch {\n return { status: 'disconnected' };\n }\n }\n \n private async checkInstancesHealth(): Promise<InstanceHealth[]> {\n const instances = await this.mcpdClient.get('/api/v1/instances');\n return instances.map(i => ({\n name: i.name,\n status: i.status,\n lastHealthCheck: i.lastHealthCheck\n }));\n }\n }\n ```\n2. Add health endpoint to mcplocal HTTP server: `GET /health`\n3. Update mcpctl status command to display tiered health\n4. Add degraded state detection:\n - LLM provider unavailable but mcpd reachable\n - Some instances down but others healthy\n5. Add health event notifications for state transitions\n6. Add configurable health check intervals",
"testStrategy": "1. Unit test: Health check correctly identifies all states\n2. Unit test: Degraded state is detected correctly\n3. Integration test: Full health check across all tiers\n4. Test health endpoint returns correct format\n5. Test mcpctl status displays health correctly\n6. Test state transition events are emitted",
"priority": "medium",
"dependencies": [
"33",
"34"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:46:07.885Z"
},
{
"id": "36",
"title": "End-to-end integration testing",
"description": "Create comprehensive integration tests that validate the full data flow from mcpctl through mcplocal to mcpd to MCP server containers and back.",
"details": "1. Create test fixtures in `src/mcplocal/test/fixtures/`:\n - Mock MCP server that returns predictable responses\n - Test configuration files\n - Sample tool call payloads\n2. Create integration test suite in `src/mcplocal/test/integration/`:\n ```typescript\n describe('End-to-end flow', () => {\n it('mcpctl -> mcplocal -> mcpd -> mcp_server', async () => {\n // Start mock MCP server\n // Start mcpd with test config\n // Start mcplocal pointing to mcpd\n // Execute mcpctl command\n // Verify response flows back correctly\n });\n \n it('LLM pre-processing reduces response size', async () => {\n // Send query that returns large dataset\n // Verify LLM filtering reduces token count\n // Verify relevant data is preserved\n });\n \n it('credentials never leave mcpd', async () => {\n // Monitor all traffic from mcplocal\n // Verify no credentials appear in requests/responses\n });\n });\n ```\n3. Test scenarios:\n - Management commands (get servers, instances, etc.)\n - MCP tool calls with LLM preprocessing\n - MCP tool calls without preprocessing\n - Error handling (mcpd down, instance down, LLM failure)\n - Health monitoring accuracy\n4. Add CI integration test workflow\n5. Create docker-compose.test.yml for test environment",
"testStrategy": "1. All integration tests pass in CI environment\n2. Test coverage includes happy path and error scenarios\n3. Performance benchmarks: measure latency at each tier\n4. Security test: verify credential isolation\n5. Load test: multiple concurrent requests\n6. Chaos test: random component failures",
"priority": "high",
"dependencies": [
"29",
"33",
"34",
"35"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-21T18:52:29.084Z"
},
{
"id": "37",
"title": "Add priority, summary, chapters, and linkTarget fields to Prompt schema",
"description": "Extend the Prisma schema for the Prompt model to include priority (integer 1-10, default 5), summary (nullable string), chapters (nullable JSON array), and linkTarget (nullable string for prompt links).",
"details": "1. Update `/src/db/prisma/schema.prisma` to add fields to the Prompt model:\n - `priority Int @default(5)` with check constraint 1-10\n - `summary String? @db.Text`\n - `chapters Json?` (stored as JSON array of strings)\n - `linkTarget String?` (format: `project/server:resource-uri`)\n\n2. Create Prisma migration:\n ```bash\n pnpm --filter db exec prisma migrate dev --name add-prompt-priority-summary-chapters-link\n ```\n\n3. Update TypeScript types in shared package to reflect new fields\n\n4. Add validation for priority range (1-10) at the database level if possible, otherwise enforce in application layer",
"testStrategy": "- Unit test: Verify migration creates columns with correct types and defaults\n- Unit test: Verify priority default is 5\n- Unit test: Verify nullable fields accept null\n- Unit test: Verify chapters stores/retrieves JSON arrays correctly\n- Integration test: Create prompt with all new fields, retrieve and verify values",
"priority": "high",
"dependencies": [],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:35:08.154Z"
},
{
"id": "38",
"title": "Add priority field to PromptRequest schema",
"description": "Extend the Prisma schema for the PromptRequest model to include the priority field (integer 1-10, default 5) to match the Prompt model.",
"details": "1. Update `/src/db/prisma/schema.prisma` to add to PromptRequest:\n - `priority Int @default(5)`\n\n2. Create Prisma migration:\n ```bash\n pnpm --filter db exec prisma migrate dev --name add-promptrequest-priority\n ```\n\n3. Update the `CreatePromptRequestSchema` in `/src/mcpd/src/validation/prompt.schema.ts` to include priority validation:\n ```typescript\n priority: z.number().int().min(1).max(10).default(5).optional(),\n ```\n\n4. Update TypeScript types in shared package",
"testStrategy": "- Unit test: Migration creates priority column with default 5\n- Unit test: PromptRequest creation with explicit priority\n- Unit test: PromptRequest creation uses default priority when not specified\n- Unit test: Validation rejects priority outside 1-10 range",
"priority": "high",
"dependencies": [
"37"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:35:08.160Z"
},
{
"id": "39",
"title": "Add gated field to Project schema",
"description": "Extend the Prisma schema for the Project model to include the gated boolean field (default true) that controls whether sessions go through the keyword-driven prompt retrieval flow.",
"details": "1. Update `/src/db/prisma/schema.prisma` to add to Project:\n - `gated Boolean @default(true)`\n\n2. Create Prisma migration:\n ```bash\n pnpm --filter db exec prisma migrate dev --name add-project-gated\n ```\n\n3. Update project-related TypeScript types\n\n4. Update project validation schemas to include gated field:\n ```typescript\n gated: z.boolean().default(true).optional(),\n ```\n\n5. Update project API routes to accept and return the gated field",
"testStrategy": "- Unit test: Migration creates gated column with default true\n- Unit test: Project creation with gated=false\n- Unit test: Project creation uses default gated=true when not specified\n- Unit test: Project update can toggle gated field\n- Integration test: GET /api/v1/projects/:name returns gated field",
"priority": "high",
"dependencies": [],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:35:08.165Z"
},
{
"id": "40",
"title": "Update Prompt CRUD API to handle priority and linkTarget",
"description": "Modify prompt API endpoints to accept, validate, and return the priority and linkTarget fields. LinkTarget should be immutable after creation.",
"details": "1. Update `/src/mcpd/src/validation/prompt.schema.ts`:\n ```typescript\n export const CreatePromptSchema = z.object({\n name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),\n content: z.string().min(1).max(50000),\n projectId: z.string().optional(),\n priority: z.number().int().min(1).max(10).default(5).optional(),\n linkTarget: z.string().regex(/^[a-z0-9-]+\\/[a-z0-9-]+:[\\S]+$/).optional(),\n });\n \n export const UpdatePromptSchema = z.object({\n content: z.string().min(1).max(50000).optional(),\n priority: z.number().int().min(1).max(10).optional(),\n // Note: linkTarget is NOT included - links are immutable\n });\n ```\n\n2. Update `/src/mcpd/src/routes/prompts.ts`:\n - POST /api/v1/prompts: Accept priority, linkTarget\n - PUT /api/v1/prompts/:id: Accept priority only (not linkTarget)\n - GET endpoints: Return priority, linkTarget in response\n\n3. Update repository layer to handle new fields\n\n4. Add linkTarget format validation: `project/server:resource-uri`",
"testStrategy": "- Unit test: POST /api/v1/prompts with priority creates prompt with correct priority\n- Unit test: POST /api/v1/prompts with linkTarget creates linked prompt\n- Unit test: PUT /api/v1/prompts/:id with priority updates priority\n- Unit test: PUT /api/v1/prompts/:id rejects linkTarget (immutable)\n- Unit test: GET /api/v1/prompts returns priority and linkTarget fields\n- Unit test: Invalid linkTarget format rejected (validation error)\n- Unit test: Priority outside 1-10 range rejected",
"priority": "high",
"dependencies": [
"37"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:37:17.506Z"
},
{
"id": "41",
"title": "Update PromptRequest API to handle priority",
"description": "Modify prompt request API endpoints to accept, validate, and return the priority field for proposed prompts.",
"details": "1. Update validation in `/src/mcpd/src/validation/prompt.schema.ts`:\n ```typescript\n export const CreatePromptRequestSchema = z.object({\n name: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),\n content: z.string().min(1).max(50000),\n projectId: z.string().optional(),\n createdBySession: z.string().optional(),\n createdByUserId: z.string().optional(),\n priority: z.number().int().min(1).max(10).default(5).optional(),\n });\n ```\n\n2. Update `/src/mcpd/src/routes/prompts.ts` for PromptRequest endpoints:\n - POST /api/v1/promptrequests: Accept priority\n - GET /api/v1/promptrequests: Return priority\n - POST /api/v1/promptrequests/:id/approve: Preserve priority when creating Prompt\n\n3. Update PromptService.approve() to copy priority from request to prompt\n\n4. Update repository layer",
"testStrategy": "- Unit test: POST /api/v1/promptrequests with priority creates request with correct priority\n- Unit test: POST /api/v1/promptrequests uses default priority 5 when not specified\n- Unit test: GET /api/v1/promptrequests returns priority field\n- Unit test: Approve preserves priority from request to created prompt\n- Unit test: Priority validation (1-10 range)",
"priority": "high",
"dependencies": [
"38"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:37:17.511Z"
},
{
"id": "42",
"title": "Implement prompt summary generation service",
"description": "Create a service that auto-generates summary (20 words) and chapters (key sections) for prompts, using fast LLM when available or regex fallback.",
"details": "1. Create `/src/mcpd/src/services/prompt-summary.service.ts`:\n ```typescript\n export class PromptSummaryService {\n constructor(\n private llmClient: LlmClient | null,\n private promptRepo: IPromptRepository\n ) {}\n \n async generateSummary(content: string): Promise<{ summary: string; chapters: string[] }> {\n if (this.llmClient) {\n return this.generateWithLlm(content);\n }\n return this.generateWithRegex(content);\n }\n \n private async generateWithLlm(content: string): Promise<...> {\n // Send content to fast LLM with prompt:\n // \"Generate a 20-word summary and extract key section topics...\"\n }\n \n private generateWithRegex(content: string): { summary: string; chapters: string[] } {\n // summary: first sentence of content (truncated to ~20 words)\n // chapters: extract markdown headings via regex /^#+\\s+(.+)$/gm\n }\n }\n ```\n\n2. Integrate with PromptService:\n - Call generateSummary on prompt create\n - Call generateSummary on prompt update (when content changes)\n - Cache results on the prompt record\n\n3. Handle LLM availability check via existing LlmConfig patterns",
"testStrategy": "- Unit test: generateWithRegex extracts first sentence as summary\n- Unit test: generateWithRegex extracts markdown headings as chapters\n- Unit test: generateWithLlm calls LLM with correct prompt (mock LLM)\n- Unit test: generateSummary uses LLM when available\n- Unit test: generateSummary falls back to regex when no LLM\n- Unit test: Empty content handled gracefully\n- Unit test: Content without headings returns empty chapters array\n- Integration test: Creating prompt triggers summary generation",
"priority": "high",
"dependencies": [
"37"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:39:28.196Z"
},
{
"id": "43",
"title": "Add regenerate-summary API endpoint",
"description": "Create POST /api/v1/prompts/:id/regenerate-summary endpoint to force re-generation of summary and chapters for a prompt.",
"details": "1. Add route in `/src/mcpd/src/routes/prompts.ts`:\n ```typescript\n fastify.post('/api/v1/prompts/:id/regenerate-summary', async (request, reply) => {\n const { id } = request.params as { id: string };\n const prompt = await promptService.findById(id);\n if (!prompt) {\n return reply.status(404).send({ error: 'Prompt not found' });\n }\n \n const { summary, chapters } = await summaryService.generateSummary(prompt.content);\n const updated = await promptService.updateSummary(id, summary, chapters);\n \n return reply.send(updated);\n });\n ```\n\n2. Add `updateSummary(id, summary, chapters)` method to PromptRepository and PromptService\n\n3. Return the updated prompt with new summary/chapters in response",
"testStrategy": "- Unit test: POST to valid prompt ID regenerates summary\n- Unit test: Returns updated prompt with new summary/chapters\n- Unit test: 404 for non-existent prompt ID\n- Unit test: Uses LLM when available, regex fallback otherwise\n- Integration test: End-to-end regeneration updates database",
"priority": "medium",
"dependencies": [
"42"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:39:28.201Z"
},
{
"id": "44",
"title": "Create prompt-index API endpoint",
"description": "Create GET /api/v1/projects/:name/prompt-index endpoint that returns a compact index of prompts (name, priority, summary, chapters) for a project.",
"details": "1. Add route in `/src/mcpd/src/routes/prompts.ts`:\n ```typescript\n fastify.get('/api/v1/projects/:name/prompt-index', async (request, reply) => {\n const { name } = request.params as { name: string };\n const project = await projectService.findByName(name);\n if (!project) {\n return reply.status(404).send({ error: 'Project not found' });\n }\n \n const prompts = await promptService.findByProject(project.id);\n const index = prompts.map(p => ({\n name: p.name,\n priority: p.priority,\n summary: p.summary,\n chapters: p.chapters,\n linkTarget: p.linkTarget,\n }));\n \n return reply.send({ prompts: index });\n });\n ```\n\n2. Consider adding global prompts to the index (inherited by all projects)\n\n3. Sort by priority descending in response",
"testStrategy": "- Unit test: Returns compact index for valid project\n- Unit test: Index contains name, priority, summary, chapters, linkTarget\n- Unit test: 404 for non-existent project\n- Unit test: Empty array for project with no prompts\n- Unit test: Results sorted by priority descending\n- Integration test: End-to-end retrieval matches database state",
"priority": "medium",
"dependencies": [
"42"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:39:28.208Z"
},
{
"id": "45",
"title": "Implement tag-matching algorithm for prompt selection",
"description": "Create a deterministic keyword-based tag matching algorithm as the no-LLM fallback for prompt selection, with byte-budget allocation and priority weighting.",
"details": "1. Create `/src/mcplocal/src/services/tag-matcher.service.ts`:\n ```typescript\n interface MatchedPrompt {\n prompt: PromptIndex;\n score: number;\n matchedTags: string[];\n }\n \n export class TagMatcherService {\n constructor(private byteBudget: number = 8192) {}\n \n matchPrompts(tags: string[], promptIndex: PromptIndex[]): {\n fullContent: PromptIndex[]; // Prompts to include in full\n indexOnly: PromptIndex[]; // Prompts to include as index entries\n remaining: PromptIndex[]; // Non-matched prompts (names only)\n } {\n // 1. Priority 10 prompts: always included (score = Infinity)\n // 2. For each prompt, compute score:\n // - Check tags against summary + chapters (case-insensitive substring)\n // - score = matching_tags_count * priority\n // 3. Sort by score descending\n // 4. Fill byte budget from top:\n // - Include full content until budget exhausted\n // - Remaining matched: include as index entries\n // - Non-matched: names only\n }\n \n private computeScore(tags: string[], prompt: PromptIndex): number {\n if (prompt.priority === 10) return Infinity;\n const matchingTags = tags.filter(tag => \n this.matchesPrompt(tag.toLowerCase(), prompt)\n );\n return matchingTags.length * prompt.priority;\n }\n \n private matchesPrompt(tag: string, prompt: PromptIndex): boolean {\n const searchText = [\n prompt.summary || '',\n ...(prompt.chapters || [])\n ].join(' ').toLowerCase();\n return searchText.includes(tag);\n }\n }\n ```\n\n2. Handle edge cases: empty tags, no prompts, all priority 10, etc.",
"testStrategy": "- Unit test: Priority 10 prompts always included regardless of tags\n- Unit test: Score calculation: matching_tags * priority\n- Unit test: Case-insensitive matching\n- Unit test: Substring matching in summary and chapters\n- Unit test: Byte budget exhaustion stops full content inclusion\n- Unit test: Matched prompts beyond budget become index entries\n- Unit test: Non-matched prompts listed as names only\n- Unit test: Sorting by score descending\n- Unit test: Empty tags returns priority 10 only\n- Unit test: No prompts returns empty result",
"priority": "high",
"dependencies": [
"44"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:40:47.570Z"
},
{
"id": "46",
"title": "Implement LLM-based prompt selection service",
"description": "Create a service that uses the heavy LLM to intelligently select relevant prompts based on tags and the full prompt index, understanding synonyms and context.",
"details": "1. Create `/src/mcplocal/src/services/llm-prompt-selector.service.ts`:\n ```typescript\n export class LlmPromptSelectorService {\n constructor(\n private llmClient: LlmClient,\n private fastLlmClient: LlmClient | null,\n private tagMatcher: TagMatcherService // fallback\n ) {}\n \n async selectPrompts(tags: string[], promptIndex: PromptIndex[]): Promise<{\n selected: Array<{ name: string; reason: string }>;\n priority10: PromptIndex[]; // Always included\n }> {\n // 1. Extract priority 10 prompts (always included)\n // 2. Generate missing summaries using fast LLM if needed\n // 3. Send to heavy LLM:\n const prompt = `\n Given these keywords: ${tags.join(', ')}\n And this prompt index:\n ${promptIndex.map(p => `- ${p.name}: ${p.summary}`).join('\\n')}\n \n Select the most relevant prompts for someone working on tasks\n related to these keywords. Consider synonyms and related concepts.\n Return a ranked JSON array: [{name: string, reason: string}]\n `;\n // 4. Parse LLM response\n // 5. On LLM error, fall back to tag matcher\n }\n }\n ```\n\n2. Handle LLM timeouts and errors gracefully with fallback\n\n3. Validate LLM response format",
"testStrategy": "- Unit test: Priority 10 prompts always returned regardless of LLM selection\n- Unit test: LLM called with correct prompt format (mock)\n- Unit test: LLM response parsed correctly\n- Unit test: Invalid LLM response falls back to tag matcher\n- Unit test: LLM timeout falls back to tag matcher\n- Unit test: Missing summaries trigger fast LLM generation\n- Unit test: No LLM available uses tag matcher directly\n- Integration test: End-to-end selection with mock LLM",
"priority": "high",
"dependencies": [
"45"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:45:57.158Z"
},
{
"id": "47",
"title": "Implement session state management for gating",
"description": "Extend the McpRouter to track per-session gating state including gated status, accumulated tags, and retrieved prompts set.",
"details": "1. Update `/src/mcplocal/src/router.ts` to add session state:\n ```typescript\n interface SessionState {\n gated: boolean; // starts true if project is gated\n tags: string[]; // accumulated from begin_session + read_prompts\n retrievedPrompts: Set<string>; // prompts already sent (avoid duplicates)\n }\n \n export class McpRouter {\n private sessionStates: Map<string, SessionState> = new Map();\n \n getSessionState(sessionId: string): SessionState {\n if (!this.sessionStates.has(sessionId)) {\n this.sessionStates.set(sessionId, {\n gated: this.projectConfig?.gated ?? true,\n tags: [],\n retrievedPrompts: new Set(),\n });\n }\n return this.sessionStates.get(sessionId)!;\n }\n \n ungateSession(sessionId: string): void {\n const state = this.getSessionState(sessionId);\n state.gated = false;\n }\n \n addRetrievedPrompts(sessionId: string, names: string[]): void {\n const state = this.getSessionState(sessionId);\n names.forEach(n => state.retrievedPrompts.add(n));\n }\n }\n ```\n\n2. Clean up session state when session closes\n\n3. Handle session state for non-gated projects (gated=false from start)",
"testStrategy": "- Unit test: New session starts with gated=true for gated project\n- Unit test: New session starts with gated=false for non-gated project\n- Unit test: ungateSession changes gated to false\n- Unit test: addRetrievedPrompts adds to set\n- Unit test: retrievedPrompts prevents duplicates\n- Unit test: Session state isolated per sessionId\n- Unit test: Session cleanup removes state",
"priority": "high",
"dependencies": [
"39"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:45:57.164Z"
},
{
"id": "48",
"title": "Implement begin_session tool for gated sessions",
"description": "Create the begin_session MCP tool that accepts 5 keywords, triggers prompt selection, returns matched content with encouragement, and ungates the session.",
"details": "1. Add begin_session tool definition in `/src/mcplocal/src/router.ts`:\n ```typescript\n private getBeginSessionTool(): Tool {\n return {\n name: 'begin_session',\n description: 'Start your session by providing 5 keywords that describe your current task. You\\'ll receive relevant project context, policies, and guidelines. Required before using other tools.',\n inputSchema: {\n type: 'object',\n properties: {\n tags: {\n type: 'array',\n items: { type: 'string' },\n maxItems: 10,\n description: '5 keywords describing your current task'\n }\n },\n required: ['tags']\n }\n };\n }\n ```\n\n2. Implement begin_session handler:\n - Validate tags array (1-10 items)\n - Call LlmPromptSelector or TagMatcher based on LLM availability\n - Fetch full content for selected prompts\n - Build response with matched content + index + encouragement\n - Ungate session\n - Send `notifications/tools/list_changed`\n\n3. Response format:\n ```\n [Priority 10 prompts - full content]\n \n [Tag-matched prompts - full content, priority-ordered]\n \n Other prompts available that may become relevant...\n - name: summary\n ...\n If any seem related, request them with read_prompts({ tags: [...] }).\n ```",
"testStrategy": "- Unit test: begin_session with valid tags returns matched prompts\n- Unit test: begin_session includes priority 10 prompts always\n- Unit test: begin_session response includes encouragement text\n- Unit test: begin_session response includes prompt index\n- Unit test: Session ungated after successful begin_session\n- Unit test: notifications/tools/list_changed sent after ungating\n- Unit test: Empty tags handled (returns priority 10 only)\n- Unit test: Invalid tags rejected with error\n- Unit test: begin_session while already ungated returns error",
"priority": "high",
"dependencies": [
"46",
"47"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:50:39.111Z"
},
{
"id": "49",
"title": "Implement read_prompts tool for ongoing retrieval",
"description": "Create the read_prompts MCP tool that allows clients to request additional context by keywords after the session is ungated.",
"details": "1. Add read_prompts tool definition:\n ```typescript\n private getReadPromptsTool(): Tool {\n return {\n name: 'read_prompts',\n description: 'Request additional project context by keywords. Use this whenever you need guidelines, policies, or conventions related to your current work.',\n inputSchema: {\n type: 'object',\n properties: {\n tags: {\n type: 'array',\n items: { type: 'string' },\n description: 'Keywords describing what context you need'\n }\n },\n required: ['tags']\n }\n };\n }\n ```\n\n2. Implement read_prompts handler:\n - Always use keyword matching (not LLM) for precision\n - Exclude already-retrieved prompts from response\n - Add newly retrieved prompts to session state\n - Include reminder about more prompts available\n\n3. Response format:\n ```\n [Matched prompt content - deduplicated]\n \n Remember: you can request more prompts at any time with read_prompts({ tags: [...] }).\n The project may have additional guidelines relevant to your current approach.\n ```",
"testStrategy": "- Unit test: read_prompts returns matched prompts by keyword\n- Unit test: Already retrieved prompts excluded from response\n- Unit test: Newly retrieved prompts added to session state\n- Unit test: Response includes reminder text\n- Unit test: read_prompts while gated returns error\n- Unit test: Empty tags returns empty response\n- Unit test: Uses keyword matching not LLM",
"priority": "high",
"dependencies": [
"48"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:50:39.115Z"
},
{
"id": "50",
"title": "Implement progressive tool exposure for gated sessions",
"description": "Modify tools/list behavior to only expose begin_session while gated, and expose all tools plus read_prompts after ungating.",
"details": "1. Update tools/list handling in `/src/mcplocal/src/router.ts`:\n ```typescript\n async handleToolsList(sessionId: string): Promise<Tool[]> {\n const state = this.getSessionState(sessionId);\n \n if (state.gated) {\n // Only show begin_session while gated\n return [this.getBeginSessionTool()];\n }\n \n // After ungating: all upstream tools + read_prompts\n const upstreamTools = await this.discoverTools();\n return [...upstreamTools, this.getReadPromptsTool()];\n }\n ```\n\n2. Block direct tool calls while gated:\n ```typescript\n async handleToolCall(sessionId: string, toolName: string, args: any): Promise<any> {\n const state = this.getSessionState(sessionId);\n \n if (state.gated && toolName !== 'begin_session') {\n // Intercept: extract keywords, match prompts, inject briefing\n return this.handleInterceptedCall(sessionId, toolName, args);\n }\n \n // Normal routing\n return this.routeToolCall(toolName, args);\n }\n ```\n\n3. Ensure notifications/tools/list_changed is sent after ungating",
"testStrategy": "- Unit test: tools/list while gated returns only begin_session\n- Unit test: tools/list after ungating returns all tools + read_prompts\n- Unit test: begin_session not visible after ungating\n- Unit test: Tool call while gated (not begin_session) triggers intercept\n- Unit test: Tool call after ungating routes normally\n- Unit test: notifications/tools/list_changed sent on ungate",
"priority": "high",
"dependencies": [
"48",
"49"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:50:39.120Z"
},
{
"id": "51",
"title": "Implement keyword extraction from tool calls",
"description": "Create a service that extracts keywords from tool names and arguments for the intercept fallback path when clients skip begin_session.",
"details": "1. Create `/src/mcplocal/src/services/keyword-extractor.service.ts`:\n ```typescript\n export class KeywordExtractorService {\n extractKeywords(toolName: string, args: Record<string, any>): string[] {\n const keywords: string[] = [];\n \n // Extract from tool name (split on / and -)\n // e.g., \"home-assistant/get_entities\" -> [\"home\", \"assistant\", \"get\", \"entities\"]\n keywords.push(...this.extractFromName(toolName));\n \n // Extract from argument values\n // e.g., { domain: \"light\", entity_id: \"light.kitchen\" } -> [\"light\", \"kitchen\"]\n keywords.push(...this.extractFromArgs(args));\n \n // Deduplicate and sanitize\n return [...new Set(keywords.map(k => this.sanitize(k)))];\n }\n \n private sanitize(keyword: string): string {\n // Remove special characters, lowercase, limit length\n return keyword.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0, 50);\n }\n }\n ```\n\n2. Handle various argument types: strings, arrays, nested objects\n\n3. Prevent injection by sanitizing extracted keywords",
"testStrategy": "- Unit test: Extracts keywords from tool name with /\n- Unit test: Extracts keywords from tool name with -\n- Unit test: Extracts keywords from string argument values\n- Unit test: Extracts keywords from array argument values\n- Unit test: Handles nested object arguments\n- Unit test: Sanitizes special characters\n- Unit test: Deduplicates keywords\n- Unit test: Handles empty arguments\n- Unit test: Limits keyword length to prevent abuse",
"priority": "medium",
"dependencies": [],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:40:47.575Z"
},
{
"id": "52",
"title": "Implement tool call intercept with briefing injection",
"description": "When a gated session calls a tool without first calling begin_session, intercept the call, extract keywords, match prompts, and inject the briefing alongside the real tool result.",
"details": "1. Implement handleInterceptedCall in `/src/mcplocal/src/router.ts`:\n ```typescript\n async handleInterceptedCall(\n sessionId: string,\n toolName: string,\n args: any\n ): Promise<ToolResult> {\n // 1. Extract keywords from tool call\n const keywords = this.keywordExtractor.extractKeywords(toolName, args);\n \n // 2. Match prompts using keywords\n const { fullContent, indexOnly, remaining } = \n await this.promptSelector.selectPrompts(keywords, this.promptIndex);\n \n // 3. Execute the actual tool call\n const actualResult = await this.routeToolCall(toolName, args);\n \n // 4. Build briefing with intercept preamble\n const briefing = this.buildBriefing(fullContent, indexOnly, remaining, 'intercept');\n \n // 5. Ungate session\n this.ungateSession(sessionId);\n \n // 6. Send notifications/tools/list_changed\n await this.sendToolsListChanged();\n \n // 7. Return combined result\n return {\n content: [{\n type: 'text',\n text: `${briefing}\\n\\n---\\n\\n${actualResult.content[0].text}`\n }]\n };\n }\n ```\n\n2. Use gate-intercept-preamble system prompt for the briefing prefix",
"testStrategy": "- Unit test: Tool call while gated triggers intercept\n- Unit test: Keywords extracted from tool name and args\n- Unit test: Prompts matched using extracted keywords\n- Unit test: Actual tool still executes and returns result\n- Unit test: Briefing prepended to tool result\n- Unit test: Session ungated after intercept\n- Unit test: notifications/tools/list_changed sent\n- Unit test: Intercept preamble included in briefing\n- Integration test: End-to-end intercept flow",
"priority": "high",
"dependencies": [
"50",
"51"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:51:03.822Z"
},
{
"id": "53",
"title": "Add prompt index to initialize instructions",
"description": "Modify the initialize handler to include the compact prompt index and gate message in instructions for gated projects.",
"details": "1. Update initialize handling in `/src/mcplocal/src/router.ts`:\n ```typescript\n async handleInitialize(sessionId: string): Promise<InitializeResult> {\n const state = this.getSessionState(sessionId);\n \n let instructions = this.projectConfig.prompt || '';\n \n if (state.gated) {\n // Add gate instructions\n const gateInstructions = await this.getSystemPrompt('gate-instructions');\n \n // Build prompt index (cap at 50, priority 7+ if over)\n const index = this.buildPromptIndex();\n \n instructions += `\\n\\n${gateInstructions.replace('{{prompt_index}}', index)}`;\n }\n \n return {\n protocolVersion: '2024-11-05',\n capabilities: { ... },\n serverInfo: { ... },\n instructions,\n };\n }\n ```\n\n2. Build prompt index with cap:\n - If <= 50 prompts: include all\n - If > 50 prompts: include only priority 7+\n - Format: `- <name>: <summary>` (~100 chars per entry)",
"testStrategy": "- Unit test: Gated project includes gate instructions in initialize\n- Unit test: Prompt index included in instructions\n- Unit test: Index capped at 50 entries\n- Unit test: Over 50 prompts shows priority 7+ only\n- Unit test: Non-gated project skips gate instructions\n- Unit test: {{prompt_index}} template replaced\n- Integration test: End-to-end initialize with gated project",
"priority": "high",
"dependencies": [
"47",
"44"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:52:13.697Z"
},
{
"id": "54",
"title": "Create mcpctl-system project with system prompts",
"description": "Implement bootstrap logic to create the mcpctl-system project and its required system prompts on first startup, with protection against deletion.",
"details": "1. Create seed migration or startup hook:\n ```typescript\n async function bootstrapSystemProject() {\n const systemProject = await projectRepo.findByName('mcpctl-system');\n if (systemProject) return; // Already exists\n \n // Create mcpctl-system project\n const project = await projectRepo.create({\n name: 'mcpctl-system',\n description: 'System prompts for mcpctl gating and encouragement',\n gated: false, // System project is not gated\n ownerId: SYSTEM_USER_ID,\n });\n \n // Create required system prompts\n const systemPrompts = [\n { name: 'gate-instructions', priority: 10, content: GATE_INSTRUCTIONS },\n { name: 'gate-encouragement', priority: 10, content: GATE_ENCOURAGEMENT },\n { name: 'read-prompts-reminder', priority: 10, content: READ_PROMPTS_REMINDER },\n { name: 'gate-intercept-preamble', priority: 10, content: GATE_INTERCEPT_PREAMBLE },\n { name: 'session-greeting', priority: 10, content: SESSION_GREETING },\n ];\n \n for (const p of systemPrompts) {\n await promptRepo.create({ ...p, projectId: project.id });\n }\n }\n ```\n\n2. Add delete protection in prompt delete endpoint:\n - Check if prompt belongs to mcpctl-system\n - Return 403 error if attempting to delete system prompt\n\n3. Define default content for each system prompt per PRD",
"testStrategy": "- Unit test: System project created on first startup\n- Unit test: All 5 system prompts created\n- Unit test: Subsequent startups don't duplicate\n- Unit test: Delete system prompt returns 403\n- Unit test: System prompts have priority 10\n- Unit test: mcpctl-system project has gated=false\n- Integration test: End-to-end bootstrap flow",
"priority": "high",
"dependencies": [
"40",
"39"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:56:12.064Z"
},
{
"id": "55",
"title": "Implement system prompt fetching and caching in mcplocal",
"description": "Add functionality to mcplocal router to fetch system prompts from mcpd and cache them with 60s TTL, supporting template variable replacement.",
"details": "1. Add system prompt fetching in `/src/mcplocal/src/router.ts`:\n ```typescript\n private systemPromptCache: Map<string, { content: string; expiresAt: number }> = new Map();\n \n async getSystemPrompt(name: string): Promise<string> {\n const cached = this.systemPromptCache.get(name);\n if (cached && cached.expiresAt > Date.now()) {\n return cached.content;\n }\n \n const prompts = await this.mcpdClient.fetch(\n '/api/v1/projects/mcpctl-system/prompts/visible'\n );\n const prompt = prompts.find(p => p.name === name);\n if (!prompt) {\n throw new Error(`System prompt not found: ${name}`);\n }\n \n this.systemPromptCache.set(name, {\n content: prompt.content,\n expiresAt: Date.now() + 60000, // 60s TTL\n });\n \n return prompt.content;\n }\n ```\n\n2. Add template variable replacement:\n ```typescript\n replaceTemplateVariables(content: string, vars: Record<string, string>): string {\n return content\n .replace(/\\{\\{prompt_index\\}\\}/g, vars.prompt_index || '')\n .replace(/\\{\\{project_name\\}\\}/g, vars.project_name || '')\n .replace(/\\{\\{matched_prompts\\}\\}/g, vars.matched_prompts || '')\n .replace(/\\{\\{remaining_prompts\\}\\}/g, vars.remaining_prompts || '');\n }\n ```",
"testStrategy": "- Unit test: System prompt fetched from mcpd\n- Unit test: Cached prompt returned within TTL\n- Unit test: Cache miss triggers fresh fetch\n- Unit test: Missing system prompt throws error\n- Unit test: Template variables replaced correctly\n- Unit test: Unknown template variables left as-is\n- Integration test: End-to-end fetch and cache",
"priority": "high",
"dependencies": [
"54"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:57:28.917Z"
},
{
"id": "56",
"title": "Implement prompt link resolution service",
"description": "Create a service that fetches linked prompt content from source MCP servers using the project's service account, with dead link detection.",
"details": "1. Create `/src/mcplocal/src/services/link-resolver.service.ts`:\n ```typescript\n export class LinkResolverService {\n constructor(private mcpdClient: McpdClient) {}\n \n async resolveLink(linkTarget: string): Promise<{\n content: string | null;\n status: 'alive' | 'dead' | 'unknown';\n error?: string;\n }> {\n // Parse linkTarget: project/server:resource-uri\n const { project, server, uri } = this.parseLink(linkTarget);\n \n try {\n // Use service account for source project\n const content = await this.fetchResource(project, server, uri);\n return { content, status: 'alive' };\n } catch (error) {\n this.logDeadLink(linkTarget, error);\n return { \n content: null, \n status: 'dead',\n error: error.message \n };\n }\n }\n \n private parseLink(linkTarget: string): { project: string; server: string; uri: string } {\n const match = linkTarget.match(/^([^/]+)\\/([^:]+):(.+)$/);\n if (!match) throw new Error('Invalid link format');\n return { project: match[1], server: match[2], uri: match[3] };\n }\n \n private async fetchResource(project: string, server: string, uri: string): Promise<string> {\n // Call mcpd to fetch resource via service account\n // mcpd routes to the source project's MCP server\n }\n }\n ```\n\n2. Log dead links as errors\n\n3. Cache resolution results",
"testStrategy": "- Unit test: Valid link parsed correctly\n- Unit test: Invalid link format throws error\n- Unit test: Successful resolution returns content and status='alive'\n- Unit test: Failed resolution returns status='dead' with error\n- Unit test: Dead link logged as error\n- Unit test: Service account header included in request\n- Integration test: End-to-end link resolution",
"priority": "medium",
"dependencies": [
"40"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:07:29.026Z"
},
{
"id": "57",
"title": "Add linkStatus to prompt GET responses",
"description": "Modify the GET /api/v1/prompts endpoint to include linkStatus (alive/dead/unknown) for linked prompts by checking link health.",
"details": "1. Update `/src/mcpd/src/routes/prompts.ts` GET endpoint:\n ```typescript\n fastify.get('/api/v1/prompts', async (request, reply) => {\n const prompts = await promptService.findAll(filter);\n \n // Check link status for linked prompts\n const promptsWithStatus = await Promise.all(\n prompts.map(async (p) => {\n if (!p.linkTarget) {\n return { ...p, linkStatus: null };\n }\n const status = await linkResolver.checkLinkHealth(p.linkTarget);\n return { ...p, linkStatus: status };\n })\n );\n \n return reply.send(promptsWithStatus);\n });\n ```\n\n2. Consider caching link health to avoid repeated checks\n\n3. Add `linkStatus` field to prompt response schema:\n - `null` for non-linked prompts\n - `'alive'` for working links\n - `'dead'` for broken links\n - `'unknown'` for unchecked links",
"testStrategy": "- Unit test: Non-linked prompt has linkStatus=null\n- Unit test: Linked prompt with working link has linkStatus='alive'\n- Unit test: Linked prompt with broken link has linkStatus='dead'\n- Unit test: Link health cached to avoid repeated checks\n- Unit test: All prompts in response have linkStatus field\n- Integration test: End-to-end GET with linked prompts",
"priority": "medium",
"dependencies": [
"56"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:09:07.078Z"
},
{
"id": "58",
"title": "Add RBAC for prompt link creation",
"description": "Implement RBAC checks requiring edit permission on the target project to create prompt links, and auto-create service account permission on the source project.",
"details": "1. Update prompt creation in `/src/mcpd/src/services/prompt.service.ts`:\n ```typescript\n async createPrompt(data: CreatePromptInput, userId: string): Promise<Prompt> {\n if (data.linkTarget) {\n // Verify user has edit permission on target project RBAC\n const hasPermission = await this.rbacService.checkPermission(\n userId, data.projectId, 'edit'\n );\n if (!hasPermission) {\n throw new ForbiddenError('Edit permission required to create prompt links');\n }\n \n // Parse link target\n const { project: sourceProject, server, uri } = this.parseLink(data.linkTarget);\n \n // Create service account permission on source project\n await this.rbacService.createServiceAccountPermission(\n data.projectId, // target project\n sourceProject, // source project\n server,\n uri,\n 'read'\n );\n }\n \n return this.promptRepo.create(data);\n }\n ```\n\n2. Clean up service account permission when link is deleted\n\n3. Handle permission denied from source project",
"testStrategy": "- Unit test: Link creation requires edit permission\n- Unit test: Link creation without permission throws 403\n- Unit test: Service account permission created on source project\n- Unit test: Service account permission deleted when link deleted\n- Unit test: Non-link prompts skip RBAC checks\n- Integration test: End-to-end link creation with RBAC",
"priority": "medium",
"dependencies": [
"56"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:09:07.081Z"
},
{
"id": "59",
"title": "Update CLI create prompt command for priority and link",
"description": "Extend the mcpctl create prompt command to accept --priority (1-10) and --link (project/server:uri) flags.",
"details": "1. Update `/src/cli/src/commands/create.ts` for prompt:\n ```typescript\n .command('prompt <name>')\n .description('Create a new prompt')\n .option('-p, --project <name>', 'Project to create prompt in')\n .option('--priority <number>', 'Priority level (1-10, default: 5)', '5')\n .option('--link <target>', 'Link to MCP resource (project/server:uri)')\n .option('-f, --file <path>', 'Read content from file')\n .action(async (name, options) => {\n const priority = parseInt(options.priority, 10);\n if (priority < 1 || priority > 10) {\n console.error('Priority must be between 1 and 10');\n process.exit(1);\n }\n \n let content = '';\n if (options.link) {\n // Linked prompts don't need content (fetched from source)\n content = `[Link: ${options.link}]`;\n } else if (options.file) {\n content = await fs.readFile(options.file, 'utf-8');\n } else {\n content = await promptForContent();\n }\n \n const body = {\n name,\n content,\n projectId: options.project,\n priority,\n linkTarget: options.link,\n };\n \n await api.post('/api/v1/prompts', body);\n });\n ```\n\n2. Validate link format: `project/server:resource-uri`\n\n3. Add shell completions for new flags",
"testStrategy": "- Unit test: --priority flag sets prompt priority\n- Unit test: --priority validation (1-10 range)\n- Unit test: --link flag sets linkTarget\n- Unit test: --link validation (format check)\n- Unit test: Linked prompt skips content prompt\n- Unit test: Default priority is 5\n- Integration test: End-to-end create with flags",
"priority": "medium",
"dependencies": [
"40"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:03:45.972Z"
},
{
"id": "60",
"title": "Update CLI get prompt command for -A flag and link columns",
"description": "Extend the mcpctl get prompt command with -A (all projects) flag and add link target and status columns to output.",
"details": "1. Update `/src/cli/src/commands/get.ts` for prompt:\n ```typescript\n .command('prompt [name]')\n .option('-A, --all-projects', 'Show prompts from all projects')\n .option('-p, --project <name>', 'Filter by project')\n .action(async (name, options) => {\n let url = '/api/v1/prompts';\n if (options.allProjects) {\n url += '?all=true';\n } else if (options.project) {\n url += `?project=${options.project}`;\n }\n \n const prompts = await api.get(url);\n \n // Format table with new columns\n formatPromptsTable(prompts, {\n columns: ['PROJECT', 'NAME', 'PRIORITY', 'LINK', 'STATUS']\n });\n });\n ```\n\n2. Update table formatter to handle link columns:\n ```\n PROJECT NAME PRIORITY LINK STATUS\n homeautomation security-policies 8 - -\n homeautomation architecture-adr 6 system-public/docmost-mcp:docmost://pages/a1 alive\n ```\n\n3. Add shell completions for -A flag",
"testStrategy": "- Unit test: -A flag shows all projects\n- Unit test: --project flag filters by project\n- Unit test: PRIORITY column displayed\n- Unit test: LINK column shows linkTarget or -\n- Unit test: STATUS column shows linkStatus or -\n- Unit test: Table formatted correctly\n- Integration test: End-to-end get with flags",
"priority": "medium",
"dependencies": [
"57",
"59"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:09:31.501Z"
},
{
"id": "61",
"title": "Update CLI describe project command for gated status",
"description": "Extend mcpctl describe project to show gated status, session greeting, and prompt table with priority and link information.",
"details": "1. Update `/src/cli/src/commands/get.ts` describe project:\n ```typescript\n async function describeProject(name: string) {\n const project = await api.get(`/api/v1/projects/${name}`);\n const prompts = await api.get(`/api/v1/projects/${name}/prompt-index`);\n const greeting = await getSessionGreeting(name);\n \n console.log(`Name: ${project.name}`);\n console.log(`Gated: ${project.gated}`);\n console.log(`LLM Provider: ${project.llmProvider || '-'}`);\n console.log(`...`);\n console.log();\n console.log(`Session greeting:`);\n console.log(` ${greeting}`);\n console.log();\n console.log(`Prompts:`);\n console.log(` NAME PRIORITY TYPE LINK`);\n for (const p of prompts) {\n const type = p.linkTarget ? 'link' : 'local';\n const link = p.linkTarget || '-';\n console.log(` ${p.name.padEnd(20)} ${p.priority.toString().padEnd(9)} ${type.padEnd(7)} ${link}`);\n }\n }\n ```\n\n2. Fetch session greeting from system prompts or project config",
"testStrategy": "- Unit test: Gated status displayed\n- Unit test: Session greeting displayed\n- Unit test: Prompt table with PRIORITY, TYPE, LINK columns\n- Unit test: TYPE shows 'local' or 'link'\n- Unit test: LINK shows target or -\n- Integration test: End-to-end describe project",
"priority": "medium",
"dependencies": [
"44",
"54"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:04:56.320Z"
},
{
"id": "62",
"title": "Update CLI edit project command for gated field",
"description": "Extend mcpctl edit project to allow editing the gated boolean field.",
"details": "1. Update `/src/cli/src/commands/edit.ts` for project:\n ```typescript\n async function editProject(name: string) {\n const project = await api.get(`/api/v1/projects/${name}`);\n \n // Add gated to editable fields\n const yaml = `\n name: ${project.name}\n description: ${project.description}\n gated: ${project.gated}\n llmProvider: ${project.llmProvider || ''}\n ...`;\n \n const edited = await openEditor(yaml);\n const parsed = YAML.parse(edited);\n \n // Validate gated is boolean\n if (typeof parsed.gated !== 'boolean') {\n console.error('gated must be true or false');\n process.exit(1);\n }\n \n await api.put(`/api/v1/projects/${name}`, parsed);\n }\n ```\n\n2. Update project validation schema to accept gated\n\n3. Handle conversion from string 'true'/'false' to boolean",
"testStrategy": "- Unit test: Gated field appears in editor YAML\n- Unit test: Gated field saved on edit\n- Unit test: Boolean validation (true/false only)\n- Unit test: String 'true'/'false' converted to boolean\n- Integration test: End-to-end edit project gated",
"priority": "medium",
"dependencies": [
"39"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:03:46.657Z"
},
{
"id": "63",
"title": "Add unit tests for prompt priority and link CRUD",
"description": "Create comprehensive unit tests for all prompt CRUD operations with the new priority and linkTarget fields.",
"details": "1. Add tests in `/src/mcpd/tests/services/prompt-service.test.ts`:\n ```typescript\n describe('Prompt Priority', () => {\n it('creates prompt with explicit priority', async () => {\n const prompt = await service.createPrompt({ ...data, priority: 8 });\n expect(prompt.priority).toBe(8);\n });\n \n it('uses default priority 5 when not specified', async () => {\n const prompt = await service.createPrompt(data);\n expect(prompt.priority).toBe(5);\n });\n \n it('validates priority range 1-10', async () => {\n await expect(service.createPrompt({ ...data, priority: 11 }))\n .rejects.toThrow();\n });\n \n it('updates priority', async () => {\n const updated = await service.updatePrompt(id, { priority: 3 });\n expect(updated.priority).toBe(3);\n });\n });\n \n describe('Prompt Links', () => {\n it('creates linked prompt', async () => {\n const prompt = await service.createPrompt({\n ...data,\n linkTarget: 'project/server:uri'\n });\n expect(prompt.linkTarget).toBe('project/server:uri');\n });\n \n it('rejects invalid link format', async () => {\n await expect(service.createPrompt({\n ...data,\n linkTarget: 'invalid'\n })).rejects.toThrow();\n });\n \n it('linkTarget is immutable on update', async () => {\n // linkTarget not accepted in update schema\n });\n });\n ```",
"testStrategy": "This task IS the test implementation. Verify:\n- All priority CRUD tests pass\n- All link CRUD tests pass\n- Validation tests cover edge cases\n- Tests use proper mocking patterns\n- Coverage meets project standards",
"priority": "high",
"dependencies": [
"40",
"41"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:52:53.091Z"
},
{
"id": "64",
"title": "Add unit tests for tag matching algorithm",
"description": "Create comprehensive unit tests for the deterministic tag matching algorithm covering score calculation, byte budget, and priority handling.",
"details": "1. Add tests in `/src/mcplocal/tests/services/tag-matcher.test.ts`:\n ```typescript\n describe('TagMatcherService', () => {\n describe('score calculation', () => {\n it('priority 10 prompts have infinite score', () => {\n const score = matcher.computeScore(['any'], { priority: 10, ... });\n expect(score).toBe(Infinity);\n });\n \n it('score = matching_tags * priority', () => {\n const score = matcher.computeScore(\n ['tag1', 'tag2'],\n { priority: 5, summary: 'tag1 tag2', chapters: [] }\n );\n expect(score).toBe(10); // 2 tags * 5 priority\n });\n });\n \n describe('matching', () => {\n it('matches case-insensitively', () => {\n const matches = matcher.matchesPrompt('ZIGBEE', { summary: 'zigbee setup' });\n expect(matches).toBe(true);\n });\n \n it('matches substring in summary', () => { ... });\n it('matches substring in chapters', () => { ... });\n });\n \n describe('byte budget', () => {\n it('includes full content until budget exhausted', () => { ... });\n it('matched prompts beyond budget become index entries', () => { ... });\n it('non-matched prompts listed as names only', () => { ... });\n });\n });\n ```",
"testStrategy": "This task IS the test implementation. Verify:\n- Score calculation tests pass\n- Matching tests cover all cases\n- Byte budget tests verify allocation\n- Edge cases handled (empty tags, no prompts, etc.)\n- Tests are deterministic",
"priority": "high",
"dependencies": [
"45"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:51:03.827Z"
},
{
"id": "65",
"title": "Add unit tests for gating state machine",
"description": "Create comprehensive unit tests for the session gating state machine covering all transitions and edge cases.",
"details": "1. Add tests in `/src/mcplocal/tests/router-gating.test.ts`:\n ```typescript\n describe('Gating State Machine', () => {\n describe('initial state', () => {\n it('starts gated for gated project', () => {\n const router = createRouter({ gated: true });\n const state = router.getSessionState('session1');\n expect(state.gated).toBe(true);\n });\n \n it('starts ungated for non-gated project', () => {\n const router = createRouter({ gated: false });\n const state = router.getSessionState('session1');\n expect(state.gated).toBe(false);\n });\n });\n \n describe('begin_session transition', () => {\n it('ungates session on successful begin_session', async () => {\n const router = createGatedRouter();\n await router.handleBeginSession('session1', { tags: ['test'] });\n expect(router.getSessionState('session1').gated).toBe(false);\n });\n \n it('returns matched prompts', async () => { ... });\n it('sends notifications/tools/list_changed', async () => { ... });\n });\n \n describe('intercept transition', () => {\n it('ungates session on tool call intercept', async () => { ... });\n it('extracts keywords from tool call', async () => { ... });\n it('injects briefing with tool result', async () => { ... });\n });\n \n describe('tools/list behavior', () => {\n it('returns only begin_session while gated', async () => { ... });\n it('returns all tools + read_prompts after ungating', async () => { ... });\n });\n });\n ```",
"testStrategy": "This task IS the test implementation. Verify:\n- Initial state tests pass\n- Transition tests cover happy paths\n- Edge case tests (already ungated, etc.)\n- Notification tests verify signals sent\n- Tests use proper mocking",
"priority": "high",
"dependencies": [
"50",
"52"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:51:03.832Z"
},
{
"id": "66",
"title": "Add unit tests for LLM prompt selection",
"description": "Create unit tests for the LLM-based prompt selection service covering LLM interactions, fallback behavior, and priority 10 handling.",
"details": "1. Add tests in `/src/mcplocal/tests/services/llm-prompt-selector.test.ts`:\n ```typescript\n describe('LlmPromptSelectorService', () => {\n describe('priority 10 handling', () => {\n it('always includes priority 10 prompts', async () => {\n const result = await selector.selectPrompts(['unrelated'], promptIndex);\n expect(result.priority10).toContain(priority10Prompt);\n });\n });\n \n describe('LLM selection', () => {\n it('sends tags and index to heavy LLM', async () => {\n await selector.selectPrompts(['zigbee', 'mqtt'], promptIndex);\n expect(mockLlm.complete).toHaveBeenCalledWith(\n expect.stringContaining('zigbee')\n );\n });\n \n it('parses LLM response correctly', async () => {\n mockLlm.complete.mockResolvedValue(\n '[{\"name\": \"prompt1\", \"reason\": \"relevant\"}]'\n );\n const result = await selector.selectPrompts(['test'], promptIndex);\n expect(result.selected[0].name).toBe('prompt1');\n });\n });\n \n describe('fallback behavior', () => {\n it('falls back to tag matcher on LLM error', async () => { ... });\n it('falls back on LLM timeout', async () => { ... });\n it('falls back when no LLM available', async () => { ... });\n });\n \n describe('summary generation', () => {\n it('generates missing summaries with fast LLM', async () => { ... });\n });\n });\n ```",
"testStrategy": "This task IS the test implementation. Verify:\n- Priority 10 tests pass\n- LLM interaction tests use proper mocks\n- Fallback tests cover all error scenarios\n- Summary generation tests pass\n- Response parsing handles edge cases",
"priority": "high",
"dependencies": [
"46"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:51:03.836Z"
},
{
"id": "67",
"title": "Add integration tests for gated session flow",
"description": "Create end-to-end integration tests for the complete gated session flow including connect, begin_session, tool calls, and read_prompts.",
"details": "1. Add tests in `/src/mcplocal/tests/integration/gated-flow.test.ts`:\n ```typescript\n describe('Gated Session Flow Integration', () => {\n let app: FastifyInstance;\n let mcpClient: McpClient;\n \n beforeAll(async () => {\n app = await createTestApp();\n // Seed test project with gated=true and test prompts\n });\n \n describe('end-to-end gated flow', () => {\n it('connect → begin_session with tags → tools available → correct prompts', async () => {\n // 1. Connect to MCP endpoint\n const session = await mcpClient.connect(app, 'test-project');\n \n // 2. Verify only begin_session available\n const toolsBefore = await session.listTools();\n expect(toolsBefore.map(t => t.name)).toEqual(['begin_session']);\n \n // 3. Call begin_session\n const briefing = await session.callTool('begin_session', {\n tags: ['test', 'integration']\n });\n expect(briefing).toContain('matched prompt content');\n \n // 4. Verify all tools now available\n const toolsAfter = await session.listTools();\n expect(toolsAfter.map(t => t.name)).toContain('read_prompts');\n });\n });\n \n describe('end-to-end intercept flow', () => {\n it('connect → skip begin_session → call tool → keywords extracted → briefing injected', async () => { ... });\n });\n \n describe('end-to-end read_prompts', () => {\n it('after ungating → request more context → additional prompts → no duplicates', async () => { ... });\n });\n });\n ```",
"testStrategy": "This task IS the test implementation. Verify:\n- Happy path tests pass\n- Intercept path tests pass\n- read_prompts deduplication works\n- Tests use realistic data\n- Tests clean up properly",
"priority": "high",
"dependencies": [
"65"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T22:51:03.840Z"
},
{
"id": "68",
"title": "Add integration tests for prompt links",
"description": "Create end-to-end integration tests for prompt link creation, resolution, and dead link detection.",
"details": "1. Add tests in `/src/mcplocal/tests/integration/prompt-links.test.ts`:\n ```typescript\n describe('Prompt Links Integration', () => {\n describe('link creation', () => {\n it('creates link with RBAC permission', async () => {\n // Setup: user with edit permission on target project\n const prompt = await api.post('/api/v1/prompts', {\n name: 'linked-prompt',\n content: '[Link]',\n projectId: targetProject.id,\n linkTarget: 'source-project/server:uri'\n });\n expect(prompt.linkTarget).toBe('source-project/server:uri');\n });\n \n it('rejects link creation without RBAC permission', async () => { ... });\n });\n \n describe('link resolution', () => {\n it('fetches content from source server', async () => { ... });\n it('uses service account for RBAC', async () => { ... });\n });\n \n describe('dead link lifecycle', () => {\n it('detects dead link when source unavailable', async () => {\n // Kill source server\n const prompts = await api.get('/api/v1/prompts');\n const linked = prompts.find(p => p.linkTarget);\n expect(linked.linkStatus).toBe('dead');\n });\n \n it('recovers when source restored', async () => { ... });\n });\n });\n ```",
"testStrategy": "This task IS the test implementation. Verify:\n- RBAC tests cover permission scenarios\n- Resolution tests verify content fetched\n- Dead link tests cover full lifecycle\n- Tests properly mock/control source servers\n- Tests clean up resources",
"priority": "medium",
"dependencies": [
"57",
"58"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:12:22.348Z"
},
{
"id": "69",
"title": "Add CLI unit tests for new prompt and project flags",
"description": "Create unit tests for the new CLI flags: --priority, --link for prompts, -A for get, and gated field for projects.",
"details": "1. Add tests in `/src/cli/tests/commands/prompt.test.ts`:\n ```typescript\n describe('create prompt command', () => {\n it('--priority sets prompt priority', async () => {\n await cli('create prompt test --priority 8');\n expect(mockApi.post).toHaveBeenCalledWith(\n '/api/v1/prompts',\n expect.objectContaining({ priority: 8 })\n );\n });\n \n it('--priority validates range 1-10', async () => {\n await expect(cli('create prompt test --priority 15'))\n .rejects.toThrow('Priority must be between 1 and 10');\n });\n \n it('--link sets linkTarget', async () => {\n await cli('create prompt test --link proj/srv:uri');\n expect(mockApi.post).toHaveBeenCalledWith(\n '/api/v1/prompts',\n expect.objectContaining({ linkTarget: 'proj/srv:uri' })\n );\n });\n });\n \n describe('get prompt command', () => {\n it('-A shows all projects', async () => {\n await cli('get prompt -A');\n expect(mockApi.get).toHaveBeenCalledWith('/api/v1/prompts?all=true');\n });\n });\n ```\n\n2. Add tests for project gated field editing\n\n3. Add tests for describe project output",
"testStrategy": "This task IS the test implementation. Verify:\n- Flag parsing tests pass\n- Validation tests cover edge cases\n- API call tests verify correct parameters\n- Output formatting tests verify columns\n- Tests mock API properly",
"priority": "medium",
"dependencies": [
"59",
"60",
"61",
"62"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:12:22.352Z"
},
{
"id": "70",
"title": "Add shell completions for new CLI flags",
"description": "Update shell completion scripts (bash, zsh, fish) to include completions for new flags: --priority, --link, -A, and gated values.",
"details": "1. Update `/completions/mcpctl.fish`:\n ```fish\n # create prompt completions\n complete -c mcpctl -n '__fish_seen_subcommand_from create; and __fish_seen_subcommand_from prompt' -l priority -d 'Priority level (1-10)' -a '(seq 1 10)'\n complete -c mcpctl -n '__fish_seen_subcommand_from create; and __fish_seen_subcommand_from prompt' -l link -d 'Link to MCP resource (project/server:uri)'\n \n # get prompt completions \n complete -c mcpctl -n '__fish_seen_subcommand_from get; and __fish_seen_subcommand_from prompt' -s A -l all-projects -d 'Show prompts from all projects'\n ```\n\n2. Update bash completions similarly\n\n3. Update zsh completions similarly\n\n4. Add dynamic completion for priority values (1-10)",
"testStrategy": "- Manual test: Fish completions suggest --priority with values 1-10\n- Manual test: Fish completions suggest --link flag\n- Manual test: Fish completions suggest -A/--all-projects\n- Manual test: Bash completions work similarly\n- Manual test: Zsh completions work similarly",
"priority": "low",
"dependencies": [
"59",
"60"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-25T23:12:22.363Z"
},
{
"id": "71",
"title": "Define ProxyModel Public Type Contract",
"description": "Create the core TypeScript types for the ProxyModel framework that stages will import from `mcpctl/proxymodel`. This establishes the public API contract that stage authors write against.",
"details": "Create `src/mcplocal/src/proxymodel/types.ts` with:\n\n```typescript\nexport interface StageHandler {\n (content: string, ctx: StageContext): Promise<StageResult>;\n}\n\nexport interface StageContext {\n contentType: 'prompt' | 'toolResult' | 'resource';\n sourceName: string;\n projectName: string;\n sessionId: string;\n originalContent: string;\n llm: LLMProvider;\n cache: CacheProvider;\n log: Logger;\n config: Record<string, unknown>;\n}\n\nexport interface StageResult {\n content: string;\n sections?: Section[];\n metadata?: Record<string, unknown>;\n}\n\nexport interface Section {\n id: string;\n title: string;\n content: string;\n}\n\nexport interface LLMProvider {\n complete(prompt: string, options?: { system?: string; maxTokens?: number }): Promise<string>;\n available(): boolean;\n}\n\nexport interface CacheProvider {\n getOrCompute(key: string, compute: () => Promise<string>): Promise<string>;\n hash(content: string): string;\n get(key: string): Promise<string | null>;\n set(key: string, value: string): Promise<void>;\n}\n\nexport interface Logger {\n debug(msg: string): void;\n info(msg: string): void;\n warn(msg: string): void;\n error(msg: string): void;\n}\n```\n\nAlso create `src/mcplocal/src/proxymodel/index.ts` as the public entrypoint that re-exports these types. Update `package.json` exports to expose `mcpctl/proxymodel`.",
"testStrategy": "Unit tests verifying type exports are accessible from the public entrypoint. Create a sample stage file that imports from `mcpctl/proxymodel` and verify it compiles without errors.",
"priority": "high",
"dependencies": [],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:50:07.620Z"
},
{
"id": "72",
"title": "Implement LLMProvider Adapter",
"description": "Create an adapter that wraps the existing ProviderRegistry to implement the StageContext.llm interface, providing stages with a simplified LLM access API.",
"details": "Create `src/mcplocal/src/proxymodel/llm-adapter.ts`:\n\n```typescript\nimport type { LLMProvider } from './types';\nimport type { ProviderRegistry } from '../providers/registry';\n\nexport function createLLMAdapter(registry: ProviderRegistry, projectName: string): LLMProvider {\n return {\n async complete(prompt: string, options?: { system?: string; maxTokens?: number }): Promise<string> {\n const provider = registry.getProvider('heavy');\n if (!provider) throw new Error('No LLM provider configured');\n \n const messages = options?.system \n ? [{ role: 'system', content: options.system }, { role: 'user', content: prompt }]\n : [{ role: 'user', content: prompt }];\n \n const result = await provider.complete({\n messages,\n maxTokens: options?.maxTokens ?? 1000,\n });\n return result.content;\n },\n \n available(): boolean {\n return registry.getProvider('heavy') !== null;\n }\n };\n}\n```\n\nThis adapter uses the 'heavy' tier from the existing registry, preserving the project-level LLM configuration.",
"testStrategy": "Unit test with mocked ProviderRegistry verifying complete() calls are delegated correctly. Test available() returns false when no provider is configured. Integration test with a real provider.",
"priority": "high",
"dependencies": [
"71"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:50:07.628Z"
},
{
"id": "73",
"title": "Implement In-Memory CacheProvider",
"description": "Create the CacheProvider implementation that stages use for caching expensive computations. Start with in-memory cache for Phase 1, with content-addressed keys.",
"details": "Create `src/mcplocal/src/proxymodel/cache-provider.ts`:\n\n```typescript\nimport { createHash } from 'crypto';\nimport type { CacheProvider } from './types';\n\nexport class InMemoryCacheProvider implements CacheProvider {\n private cache = new Map<string, { value: string; timestamp: number }>();\n private maxSize: number;\n private ttlMs: number;\n\n constructor(options: { maxSize?: number; ttlMs?: number } = {}) {\n this.maxSize = options.maxSize ?? 1000;\n this.ttlMs = options.ttlMs ?? 3600000; // 1 hour default\n }\n\n hash(content: string): string {\n return createHash('sha256').update(content).digest('hex').slice(0, 16);\n }\n\n async get(key: string): Promise<string | null> {\n const entry = this.cache.get(key);\n if (!entry) return null;\n if (Date.now() - entry.timestamp > this.ttlMs) {\n this.cache.delete(key);\n return null;\n }\n return entry.value;\n }\n\n async set(key: string, value: string): Promise<void> {\n if (this.cache.size >= this.maxSize) this.evictOldest();\n this.cache.set(key, { value, timestamp: Date.now() });\n }\n\n async getOrCompute(key: string, compute: () => Promise<string>): Promise<string> {\n const cached = await this.get(key);\n if (cached !== null) return cached;\n const value = await compute();\n await this.set(key, value);\n return value;\n }\n\n private evictOldest(): void {\n const oldest = [...this.cache.entries()].sort((a, b) => a[1].timestamp - b[1].timestamp)[0];\n if (oldest) this.cache.delete(oldest[0]);\n }\n}\n```",
"testStrategy": "Unit tests for: hash() produces consistent output, get() returns null for missing keys, set()/get() round-trip works, TTL expiration works, LRU eviction triggers at maxSize, getOrCompute() caches and returns cached values.",
"priority": "high",
"dependencies": [
"71"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:50:07.634Z"
},
{
"id": "74",
"title": "Implement Content Type Detection",
"description": "Create a utility that detects content type (JSON, YAML, XML, code, prose) for structural splitting in the section-split stage.",
"details": "Create `src/mcplocal/src/proxymodel/content-detection.ts`:\n\n```typescript\nexport type ContentType = 'json' | 'yaml' | 'xml' | 'code' | 'prose';\n\nexport function detectContentType(content: string): ContentType {\n const trimmed = content.trimStart();\n \n // JSON detection\n if (trimmed.startsWith('{') || trimmed.startsWith('[')) {\n try {\n JSON.parse(content);\n return 'json';\n } catch { /* not valid JSON, continue */ }\n }\n \n // XML detection\n if (trimmed.startsWith('<?xml') || /^<[a-zA-Z][^>]*>/.test(trimmed)) {\n return 'xml';\n }\n \n // YAML detection (key: value at start of lines)\n if (/^[a-zA-Z_][a-zA-Z0-9_]*:\\s/m.test(trimmed) && !trimmed.includes('{')) {\n return 'yaml';\n }\n \n // Code detection (common patterns)\n const codePatterns = [\n /^(function |class |def |const |let |var |import |export |package |pub fn |fn |impl )/m,\n /^#include\\s+[<\"]/m,\n /^(public |private |protected )?(static )?(void |int |string |bool )/m,\n ];\n if (codePatterns.some(p => p.test(trimmed))) {\n return 'code';\n }\n \n return 'prose';\n}\n```",
"testStrategy": "Unit tests with sample content for each type: valid JSON objects/arrays, XML documents, YAML configs, code snippets in multiple languages (JS, Python, Rust, Go, Java), and prose markdown. Edge cases: JSON-like strings that aren't valid JSON, mixed content.",
"priority": "high",
"dependencies": [],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:50:07.640Z"
},
{
"id": "75",
"title": "Implement section-split Stage",
"description": "Create the built-in section-split stage that splits content based on detected content type, using structural boundaries for JSON/YAML/XML and headers for prose.",
"details": "Create `src/mcplocal/src/proxymodel/stages/section-split.ts`:\n\n```typescript\nimport type { StageHandler, Section } from '../types';\nimport { detectContentType } from '../content-detection';\n\nconst handler: StageHandler = async (content, ctx) => {\n const minSize = (ctx.config.minSectionSize as number) ?? 2000;\n const maxSize = (ctx.config.maxSectionSize as number) ?? 15000;\n const contentType = detectContentType(content);\n \n let sections: Section[];\n \n switch (contentType) {\n case 'json':\n sections = splitJson(content, minSize, maxSize);\n break;\n case 'yaml':\n sections = splitYaml(content, minSize, maxSize);\n break;\n case 'xml':\n sections = splitXml(content, minSize, maxSize);\n break;\n case 'code':\n sections = splitCode(content, minSize);\n break;\n default:\n sections = splitProse(content, minSize);\n }\n \n if (sections.length === 0) {\n return { content, sections: [{ id: 'main', title: 'Content', content }] };\n }\n \n const toc = sections.map((s, i) => `[${s.id}] ${s.title}`).join('\\n');\n return {\n content: `${sections.length} sections (${contentType}):\\n${toc}`,\n sections,\n };\n};\n\nfunction splitJson(content: string, minSize: number, maxSize: number): Section[] {\n const parsed = JSON.parse(content);\n if (Array.isArray(parsed)) {\n return parsed.map((item, i) => ({\n id: item.id ?? item.name ?? `item-${i}`,\n title: item.label ?? item.title ?? item.name ?? `Item ${i}`,\n content: JSON.stringify(item, null, 2),\n }));\n }\n return Object.entries(parsed).map(([key, value]) => ({\n id: key,\n title: key,\n content: JSON.stringify(value, null, 2),\n }));\n}\n\n// Similar implementations for splitYaml, splitXml, splitCode, splitProse\n```",
"testStrategy": "Unit tests for each content type: JSON arrays split by element, JSON objects split by key, YAML split by top-level keys, XML split by elements, prose split by markdown headers. Test minSize/maxSize thresholds. Test fallback when content can't be parsed.",
"priority": "high",
"dependencies": [
"71",
"74"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:55:47.712Z"
},
{
"id": "76",
"title": "Implement summarize-tree Stage",
"description": "Create the built-in summarize-tree stage that recursively summarizes sections, using structural summaries for programmatic content and LLM summaries for prose.",
"details": "Create `src/mcplocal/src/proxymodel/stages/summarize-tree.ts`:\n\n```typescript\nimport type { StageHandler, Section, StageContext } from '../types';\nimport { detectContentType } from '../content-detection';\n\nconst handler: StageHandler = async (content, ctx) => {\n const maxTokens = (ctx.config.maxSummaryTokens as number) ?? 200;\n const maxGroup = (ctx.config.maxGroupSize as number) ?? 5;\n const maxDepth = (ctx.config.maxDepth as number) ?? 3;\n \n // Parse sections from previous stage or create single section\n const inputSections = parseSectionsFromContent(content);\n \n const tree = await buildTree(inputSections, ctx, { maxTokens, maxGroup, maxDepth, depth: 0 });\n \n const toc = tree.map(s => \n `[${s.id}] ${s.title} — ${s.metadata?.summary ?? ''}` +\n (s.sections?.length ? `\\n → ${s.sections.length} sub-sections` : '')\n ).join('\\n');\n \n return {\n content: `${tree.length} sections:\\n${toc}\\n\\nUse section parameter to read details.`,\n sections: tree,\n };\n};\n\nasync function buildTree(\n sections: Section[], \n ctx: StageContext, \n opts: { maxTokens: number; maxGroup: number; maxDepth: number; depth: number }\n): Promise<Section[]> {\n for (const section of sections) {\n const contentType = detectContentType(section.content);\n \n // Structural summary for programmatic content (no LLM needed)\n if (contentType !== 'prose') {\n section.metadata = { summary: generateStructuralSummary(section.content, contentType) };\n } else {\n // LLM summary for prose (cached)\n const cacheKey = `summary:${ctx.cache.hash(section.content)}:${opts.maxTokens}`;\n const summary = await ctx.cache.getOrCompute(cacheKey, () =>\n ctx.llm.complete(\n `Summarize in ${opts.maxTokens} tokens, preserve MUST/REQUIRED items:\\n\\n${section.content}`\n )\n );\n section.metadata = { summary };\n }\n \n // Recurse if large and not at max depth\n if (section.content.length > 5000 && opts.depth < opts.maxDepth) {\n section.sections = await buildTree(\n splitContent(section.content),\n ctx,\n { ...opts, depth: opts.depth + 1 }\n );\n }\n }\n return sections;\n}\n\nfunction generateStructuralSummary(content: string, type: string): string {\n // Generate summary from structure: key names, array lengths, types\n // No LLM needed for JSON/YAML/XML/code\n}\n```",
"testStrategy": "Unit tests: prose content gets LLM summary (mock LLM), JSON content gets structural summary without LLM call, recursive splitting triggers at 5000 chars, maxDepth is respected, cache is used for repeated content. Integration test with real LLM provider.",
"priority": "high",
"dependencies": [
"71",
"72",
"73",
"74"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:55:47.719Z"
},
{
"id": "77",
"title": "Implement passthrough and paginate Stages",
"description": "Create the built-in passthrough (no-op) and paginate (large response splitting) stages that form the default proxymodel.",
"details": "Create `src/mcplocal/src/proxymodel/stages/passthrough.ts`:\n\n```typescript\nimport type { StageHandler } from '../types';\n\nconst handler: StageHandler = async (content, ctx) => {\n return { content };\n};\nexport default handler;\n```\n\nCreate `src/mcplocal/src/proxymodel/stages/paginate.ts`:\n\n```typescript\nimport type { StageHandler, Section } from '../types';\n\nconst handler: StageHandler = async (content, ctx) => {\n const pageSize = (ctx.config.pageSize as number) ?? 8000;\n \n if (content.length <= pageSize) {\n return { content };\n }\n \n const pages: Section[] = [];\n let offset = 0;\n let pageNum = 1;\n \n while (offset < content.length) {\n const pageContent = content.slice(offset, offset + pageSize);\n pages.push({\n id: `page-${pageNum}`,\n title: `Page ${pageNum}`,\n content: pageContent,\n });\n offset += pageSize;\n pageNum++;\n }\n \n return {\n content: `Content split into ${pages.length} pages (${content.length} chars total). Use section parameter to read specific pages.`,\n sections: pages,\n };\n};\nexport default handler;\n```",
"testStrategy": "passthrough: verify content returned unchanged. paginate: verify content under threshold returns unchanged, content over threshold splits correctly, page boundaries are correct, section IDs are sequential.",
"priority": "high",
"dependencies": [
"71"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T17:55:47.725Z"
},
{
"id": "78",
"title": "Create ProxyModel YAML Schema and Loader",
"description": "Define the YAML schema for proxymodel definitions and implement the loader that reads from ~/.mcpctl/proxymodels/ and merges with built-ins.",
"details": "Create `src/mcplocal/src/proxymodel/schema.ts`:\n\n```typescript\nimport { z } from 'zod';\n\nexport const ProxyModelSchema = z.object({\n kind: z.literal('ProxyModel'),\n metadata: z.object({\n name: z.string(),\n }),\n spec: z.object({\n controller: z.string().optional().default('gate'),\n controllerConfig: z.record(z.unknown()).optional(),\n stages: z.array(z.object({\n type: z.string(),\n config: z.record(z.unknown()).optional(),\n })),\n appliesTo: z.array(z.enum(['prompts', 'toolResults', 'resource'])).optional(),\n cacheable: z.boolean().optional().default(true),\n }),\n});\n\nexport type ProxyModelDefinition = z.infer<typeof ProxyModelSchema>;\n```\n\nCreate `src/mcplocal/src/proxymodel/loader.ts`:\n\n```typescript\nimport { readdir, readFile } from 'fs/promises';\nimport { join } from 'path';\nimport { parse as parseYaml } from 'yaml';\nimport { ProxyModelSchema, type ProxyModelDefinition } from './schema';\nimport { getBuiltInProxyModels } from './built-in-models';\n\nconst PROXYMODELS_DIR = join(process.env.HOME ?? '', '.mcpctl', 'proxymodels');\n\nexport async function loadProxyModels(): Promise<Map<string, ProxyModelDefinition>> {\n const models = new Map<string, ProxyModelDefinition>();\n \n // Load built-ins first\n for (const [name, model] of getBuiltInProxyModels()) {\n models.set(name, model);\n }\n \n // Load local (overrides built-ins)\n try {\n const files = await readdir(PROXYMODELS_DIR);\n for (const file of files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'))) {\n const content = await readFile(join(PROXYMODELS_DIR, file), 'utf-8');\n const parsed = parseYaml(content);\n const validated = ProxyModelSchema.parse(parsed);\n models.set(validated.metadata.name, validated);\n }\n } catch (e) {\n // Directory doesn't exist or can't be read - use built-ins only\n }\n \n return models;\n}\n```",
"testStrategy": "Unit tests: valid YAML parses correctly, invalid YAML throws validation error, local models override built-ins with same name, missing directory doesn't throw. Create test fixtures for various YAML configurations.",
"priority": "high",
"dependencies": [
"71"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T18:02:37.061Z"
},
{
"id": "79",
"title": "Implement Stage Registry and Dynamic Loader",
"description": "Create the stage registry that resolves stage names to handlers, loading from ~/.mcpctl/stages/ for custom stages and falling back to built-ins.",
"details": "Create `src/mcplocal/src/proxymodel/stage-registry.ts`:\n\n```typescript\nimport { readdir, stat } from 'fs/promises';\nimport { join } from 'path';\nimport type { StageHandler } from './types';\n\nconst STAGES_DIR = join(process.env.HOME ?? '', '.mcpctl', 'stages');\n\nconst builtInStages: Map<string, StageHandler> = new Map();\nconst customStages: Map<string, StageHandler> = new Map();\n\n// Register built-ins at module load\nimport passthrough from './stages/passthrough';\nimport paginate from './stages/paginate';\nimport sectionSplit from './stages/section-split';\nimport summarizeTree from './stages/summarize-tree';\n\nbuiltInStages.set('passthrough', passthrough);\nbuiltInStages.set('paginate', paginate);\nbuiltInStages.set('section-split', sectionSplit);\nbuiltInStages.set('summarize-tree', summarizeTree);\n\nexport async function loadCustomStages(): Promise<void> {\n customStages.clear();\n try {\n const files = await readdir(STAGES_DIR);\n for (const file of files.filter(f => f.endsWith('.ts') || f.endsWith('.js'))) {\n const name = file.replace(/\\.(ts|js)$/, '');\n const module = await import(join(STAGES_DIR, file));\n customStages.set(name, module.default);\n }\n } catch { /* directory doesn't exist */ }\n}\n\nexport function getStage(name: string): StageHandler | null {\n return customStages.get(name) ?? builtInStages.get(name) ?? null;\n}\n\nexport function listStages(): { name: string; source: 'built-in' | 'local' }[] {\n const result: { name: string; source: 'built-in' | 'local' }[] = [];\n for (const name of builtInStages.keys()) {\n result.push({ name, source: customStages.has(name) ? 'local' : 'built-in' });\n }\n for (const name of customStages.keys()) {\n if (!builtInStages.has(name)) result.push({ name, source: 'local' });\n }\n return result;\n}\n```",
"testStrategy": "Unit tests: built-in stages are registered, getStage() returns correct handler, custom stages override built-ins, listStages() shows correct sources, missing stages return null. Integration test with actual stage files in temp directory.",
"priority": "high",
"dependencies": [
"71",
"75",
"76",
"77"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T18:02:37.068Z"
},
{
"id": "80",
"title": "Implement Pipeline Executor",
"description": "Create the pipeline executor that runs content through a sequence of stages, managing context, caching, and error handling.",
"details": "Create `src/mcplocal/src/proxymodel/executor.ts`:\n\n```typescript\nimport type { StageContext, StageResult, Section } from './types';\nimport type { ProxyModelDefinition } from './schema';\nimport { getStage } from './stage-registry';\nimport { createLLMAdapter } from './llm-adapter';\nimport { InMemoryCacheProvider } from './cache-provider';\nimport type { ProviderRegistry } from '../providers/registry';\n\nexport interface ExecuteOptions {\n content: string;\n contentType: 'prompt' | 'toolResult' | 'resource';\n sourceName: string;\n projectName: string;\n sessionId: string;\n proxyModel: ProxyModelDefinition;\n providerRegistry: ProviderRegistry;\n cache?: InMemoryCacheProvider;\n}\n\nexport async function executePipeline(opts: ExecuteOptions): Promise<StageResult> {\n const { content, proxyModel, providerRegistry } = opts;\n const cache = opts.cache ?? new InMemoryCacheProvider();\n const llm = createLLMAdapter(providerRegistry, opts.projectName);\n \n let currentContent = content;\n let sections: Section[] | undefined;\n let metadata: Record<string, unknown> = {};\n \n for (const stageConfig of proxyModel.spec.stages) {\n const handler = getStage(stageConfig.type);\n if (!handler) {\n console.warn(`Stage '${stageConfig.type}' not found, skipping`);\n continue;\n }\n \n const ctx: StageContext = {\n contentType: opts.contentType,\n sourceName: opts.sourceName,\n projectName: opts.projectName,\n sessionId: opts.sessionId,\n originalContent: content,\n llm,\n cache,\n log: createLogger(stageConfig.type),\n config: stageConfig.config ?? {},\n };\n \n try {\n const result = await handler(currentContent, ctx);\n currentContent = result.content;\n if (result.sections) sections = result.sections;\n if (result.metadata) metadata = { ...metadata, ...result.metadata };\n } catch (err) {\n console.error(`Stage '${stageConfig.type}' failed:`, err);\n // Continue with previous content on error\n }\n }\n \n return { content: currentContent, sections, metadata };\n}\n\nfunction createLogger(stageName: string) {\n return {\n debug: (msg: string) => console.debug(`[${stageName}] ${msg}`),\n info: (msg: string) => console.info(`[${stageName}] ${msg}`),\n warn: (msg: string) => console.warn(`[${stageName}] ${msg}`),\n error: (msg: string) => console.error(`[${stageName}] ${msg}`),\n };\n}\n```",
"testStrategy": "Unit tests: single stage executes correctly, multiple stages chain output to input, originalContent preserved across stages, missing stage logs warning and continues, stage error doesn't break pipeline, sections/metadata accumulate correctly.",
"priority": "high",
"dependencies": [
"71",
"72",
"73",
"79"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T18:03:47.548Z"
},
{
"id": "81",
"title": "Define Built-in ProxyModels (default, subindex)",
"description": "Create the built-in proxymodel definitions for 'default' (current behavior) and 'subindex' (hierarchical navigation).",
"details": "Create `src/mcplocal/src/proxymodel/built-in-models.ts`:\n\n```typescript\nimport type { ProxyModelDefinition } from './schema';\n\nexport function getBuiltInProxyModels(): Map<string, ProxyModelDefinition> {\n const models = new Map<string, ProxyModelDefinition>();\n \n models.set('default', {\n kind: 'ProxyModel',\n metadata: { name: 'default' },\n spec: {\n controller: 'gate',\n controllerConfig: { byteBudget: 8192 },\n stages: [\n { type: 'passthrough' },\n { type: 'paginate', config: { pageSize: 8000 } },\n ],\n appliesTo: ['prompts', 'toolResults'],\n cacheable: false,\n },\n });\n \n models.set('subindex', {\n kind: 'ProxyModel',\n metadata: { name: 'subindex' },\n spec: {\n controller: 'gate',\n controllerConfig: { byteBudget: 8192 },\n stages: [\n { type: 'section-split', config: { minSectionSize: 2000, maxSectionSize: 15000 } },\n { type: 'summarize-tree', config: { maxSummaryTokens: 200, maxGroupSize: 5, maxDepth: 3 } },\n ],\n appliesTo: ['prompts', 'toolResults'],\n cacheable: true,\n },\n });\n \n return models;\n}\n```",
"testStrategy": "Unit tests: both models are returned by getBuiltInProxyModels(), 'default' has passthrough+paginate stages, 'subindex' has section-split+summarize-tree stages, both schemas validate correctly.",
"priority": "high",
"dependencies": [
"78"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T18:02:37.075Z"
},
{
"id": "82",
"title": "Integrate Pipeline Executor into Router",
"description": "Modify the McpRouter to route content through the proxymodel pipeline, keeping the gating logic cleanly separated from content processing.",
"details": "Modify `src/mcplocal/src/router.ts` to:\n\n1. Add proxyModel resolution during router creation:\n```typescript\nimport { loadProxyModels } from './proxymodel/loader';\nimport { executePipeline } from './proxymodel/executor';\n\ninterface RouterOptions {\n proxyModelName?: string;\n // ... existing options\n}\n\nasync function createRouter(opts: RouterOptions): Promise<McpRouter> {\n const proxyModels = await loadProxyModels();\n const proxyModel = proxyModels.get(opts.proxyModelName ?? 'default');\n // ...\n}\n```\n\n2. Add content processing method:\n```typescript\nasync processContent(\n content: string,\n type: 'prompt' | 'toolResult',\n sourceName: string,\n sessionId: string\n): Promise<StageResult> {\n if (!this.proxyModel) return { content };\n \n const appliesTo = this.proxyModel.spec.appliesTo ?? ['prompts', 'toolResults'];\n if (!appliesTo.includes(type === 'prompt' ? 'prompts' : 'toolResults')) {\n return { content };\n }\n \n return executePipeline({\n content,\n contentType: type,\n sourceName,\n projectName: this.projectName,\n sessionId,\n proxyModel: this.proxyModel,\n providerRegistry: this.providerRegistry,\n cache: this.cache,\n });\n}\n```\n\n3. Call processContent at the appropriate points in the request flow (prompt serving, tool result handling) WITHOUT interweaving with gating logic.",
"testStrategy": "Integration tests: default proxymodel passes content through unchanged, subindex proxymodel produces summaries, appliesTo filtering works correctly, gating still works as before with proxymodel processing happening at the right stage.",
"priority": "high",
"dependencies": [
"80",
"81"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T18:06:18.464Z"
},
{
"id": "83",
"title": "Implement Section Drill-Down for Prompts",
"description": "Extend read_prompts to support section parameter for drilling into specific sections produced by proxymodel stages.",
"details": "Modify the read_prompts handler in `src/mcplocal/src/router.ts`:\n\n```typescript\n// In the read_prompts tool handler\nif (args.section) {\n // Look up section in the processed result\n const sectionId = args.section;\n const cachedResult = this.sectionCache.get(promptName);\n if (cachedResult?.sections) {\n const section = findSection(cachedResult.sections, sectionId);\n if (section) {\n return { content: [{ type: 'text', text: section.content }] };\n }\n return { content: [{ type: 'text', text: `Section '${sectionId}' not found` }], isError: true };\n }\n}\n\n// Helper to find section by ID (supports nested sections)\nfunction findSection(sections: Section[], id: string): Section | null {\n for (const s of sections) {\n if (s.id === id) return s;\n if (s.sections) {\n const nested = findSection(s.sections, id);\n if (nested) return nested;\n }\n }\n return null;\n}\n```\n\nAlso add a sectionCache Map to store processed results with their sections for drill-down.",
"testStrategy": "Integration tests: read_prompts with section parameter returns correct section content, nested section lookup works, missing section returns error, section cache populated after initial processing.",
"priority": "high",
"dependencies": [
"82"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.554Z"
},
{
"id": "84",
"title": "Implement Section Drill-Down for Tool Results",
"description": "Extend tool result handling to support _section parameter for drilling into specific sections of large tool responses.",
"details": "Modify tool call handling in `src/mcplocal/src/router.ts`:\n\n```typescript\n// When processing tool calls\nif (args._section) {\n const sectionId = args._section;\n delete args._section; // Don't pass to upstream\n \n // Check cache for previous full result\n const cacheKey = `tool:${serverName}/${toolName}:${JSON.stringify(args)}`;\n const cachedResult = this.toolResultCache.get(cacheKey);\n \n if (cachedResult?.sections) {\n const section = findSection(cachedResult.sections, sectionId);\n if (section) {\n return { content: [{ type: 'text', text: section.content }] };\n }\n }\n // If no cache, make the full call and process, then serve section\n}\n\n// After receiving tool result, process through pipeline\nconst processed = await this.processContent(result, 'toolResult', `${serverName}/${toolName}`, sessionId);\nif (processed.sections) {\n this.toolResultCache.set(cacheKey, processed);\n}\n```\n\nAdd a toolResultCache Map with appropriate TTL.",
"testStrategy": "Integration tests: large tool result gets processed into sections, _section parameter returns specific section, _section removed before upstream call, cache hit serves from cache, cache miss processes and caches.",
"priority": "medium",
"dependencies": [
"82"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-02-27T18:06:37.590Z"
},
{
"id": "85",
"title": "Implement Hot-Reload for Stages",
"description": "Add file watching for ~/.mcpctl/stages/ to automatically reload custom stages when they change without restarting mcplocal.",
"details": "Modify `src/mcplocal/src/proxymodel/stage-registry.ts`:\n\n```typescript\nimport { watch, FSWatcher } from 'fs';\nimport { join, basename } from 'path';\n\nlet watcher: FSWatcher | null = null;\nconst stageFileHashes: Map<string, string> = new Map();\n\nexport function startStageWatcher(): void {\n if (watcher) return;\n \n try {\n watcher = watch(STAGES_DIR, async (eventType, filename) => {\n if (!filename || (!filename.endsWith('.ts') && !filename.endsWith('.js'))) return;\n \n const name = filename.replace(/\\.(ts|js)$/, '');\n const fullPath = join(STAGES_DIR, filename);\n \n if (eventType === 'rename') {\n // File added or removed\n await loadCustomStages();\n console.info(`[proxymodel] Stages reloaded due to ${filename} change`);\n } else if (eventType === 'change') {\n // File modified - invalidate module cache and reload\n delete require.cache[require.resolve(fullPath)];\n try {\n const module = await import(fullPath + '?t=' + Date.now());\n customStages.set(name, module.default);\n console.info(`[proxymodel] Stage '${name}' hot-reloaded`);\n } catch (err) {\n console.error(`[proxymodel] Failed to reload stage '${name}':`, err);\n }\n }\n });\n } catch {\n // Directory doesn't exist - no watching needed\n }\n}\n\nexport function stopStageWatcher(): void {\n watcher?.close();\n watcher = null;\n}\n```\n\nCall startStageWatcher() during mcplocal initialization.",
"testStrategy": "Integration tests: modify a stage file and verify the new version is loaded without restart, add a new stage file and verify it becomes available, remove a stage file and verify it's no longer available, syntax errors in stage file don't crash the watcher.",
"priority": "medium",
"dependencies": [
"79"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:38:57.221Z"
},
{
"id": "86",
"title": "Implement Hot-Reload for ProxyModels",
"description": "Add file watching for ~/.mcpctl/proxymodels/ to automatically reload proxymodel definitions when they change.",
"details": "Create `src/mcplocal/src/proxymodel/model-watcher.ts`:\n\n```typescript\nimport { watch, FSWatcher } from 'fs';\nimport { join } from 'path';\nimport { readFile } from 'fs/promises';\nimport { parse as parseYaml } from 'yaml';\nimport { ProxyModelSchema } from './schema';\n\nconst PROXYMODELS_DIR = join(process.env.HOME ?? '', '.mcpctl', 'proxymodels');\nlet watcher: FSWatcher | null = null;\nconst modelUpdateCallbacks: Set<() => void> = new Set();\n\nexport function onModelUpdate(callback: () => void): () => void {\n modelUpdateCallbacks.add(callback);\n return () => modelUpdateCallbacks.delete(callback);\n}\n\nexport function startModelWatcher(): void {\n if (watcher) return;\n \n try {\n watcher = watch(PROXYMODELS_DIR, async (eventType, filename) => {\n if (!filename || (!filename.endsWith('.yaml') && !filename.endsWith('.yml'))) return;\n \n console.info(`[proxymodel] Model file ${filename} changed, reloading...`);\n \n // Notify all subscribers to reload their models\n for (const cb of modelUpdateCallbacks) {\n try { cb(); } catch (err) { console.error('Model update callback failed:', err); }\n }\n });\n } catch {\n // Directory doesn't exist\n }\n}\n```\n\nIntegrate with router to reload proxymodels when files change.",
"testStrategy": "Integration tests: modify a proxymodel YAML and verify changes take effect, add a new proxymodel and verify it becomes available, invalid YAML logs error but doesn't crash.",
"priority": "medium",
"dependencies": [
"78"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:38:57.226Z"
},
{
"id": "87",
"title": "Add proxyModel Field to Project Schema",
"description": "Extend the Project database schema and API to support proxyModel field and proxyModelOverrides for per-content-type configuration.",
"details": "Update `src/db/prisma/schema.prisma`:\n\n```prisma\nmodel Project {\n // ... existing fields\n proxyModel String? @default(\"default\")\n proxyModelOverrides Json? // { prompts: { \"prompt-name\": \"model\" }, toolResults: { \"server/tool\": \"model\" } }\n}\n```\n\nRun `npx prisma migrate dev --name add_proxymodel_field`.\n\nUpdate `src/mcpd/src/routes/projects.ts` to include the new fields in CRUD operations.\n\nUpdate `src/cli/src/commands/get.ts` and `describe.ts` to display proxyModel.\n\nUpdate `src/cli/src/commands/patch.ts` to support `--set proxyModel=<name>`.",
"testStrategy": "Database migration test: verify migration applies cleanly. API tests: verify proxyModel field is returned in project GET, can be updated via PATCH. CLI tests: verify `mcpctl describe project <name>` shows proxyModel.",
"priority": "high",
"dependencies": [],
"status": "cancelled",
"subtasks": [
{
"id": 1,
"title": "Minimal placeholder subtask",
"description": "This task requires complete rewrite before expansion.",
"dependencies": [],
"details": "Task 87 has been marked as DO NOT EXPAND and needs to be completely rewritten first. No subtasks should be generated until the task is properly redefined.",
"status": "pending",
"testStrategy": null,
"parentId": "undefined"
}
],
"updatedAt": "2026-03-07T01:27:15.571Z"
},
{
"id": "88",
"title": "Rename proxyMode: filtered to proxyMode: proxy",
"description": "Rename the existing proxyMode value 'filtered' to 'proxy' for clarity, with backwards compatibility for existing configs.",
"details": "Update `src/db/prisma/schema.prisma`:\n\n```prisma\nenum ProxyMode {\n direct\n proxy // renamed from 'filtered'\n}\n```\n\nCreate migration that updates existing 'filtered' values to 'proxy':\n```sql\nUPDATE Project SET proxyMode = 'proxy' WHERE proxyMode = 'filtered';\n```\n\nUpdate all code references from 'filtered' to 'proxy':\n- `src/mcplocal/src/http/project-mcp-endpoint.ts`\n- `src/cli/src/commands/create.ts`\n- Documentation and help text\n\nFor backwards compatibility in config files, add a normalization step that treats 'filtered' as 'proxy'.",
"testStrategy": "Migration test: existing projects with proxyMode='filtered' are updated to 'proxy'. Config parsing test: both 'filtered' and 'proxy' values work. CLI test: help text shows 'proxy' not 'filtered'.",
"priority": "low",
"dependencies": [
"87"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T23:36:15.209Z"
},
{
"id": "89",
"title": "Implement mcpctl get proxymodels Command",
"description": "Add CLI command to list all available proxymodels (built-in + local) with source, stages, and requirements.",
"details": "Create `src/cli/src/commands/get-proxymodels.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { loadProxyModels } from 'mcplocal/proxymodel/loader';\nimport { listStages } from 'mcplocal/proxymodel/stage-registry';\nimport Table from 'cli-table3';\n\nexport function registerGetProxymodels(program: Command): void {\n program\n .command('get proxymodels')\n .description('List all available proxymodels')\n .action(async () => {\n const models = await loadProxyModels();\n const stageInfo = new Map(listStages().map(s => [s.name, s]));\n \n const table = new Table({\n head: ['NAME', 'SOURCE', 'STAGES', 'REQUIRES-LLM', 'CACHEABLE'],\n });\n \n for (const [name, model] of models) {\n const source = isBuiltIn(name) ? 'built-in' : 'local';\n const stages = model.spec.stages.map(s => s.type).join(',');\n const requiresLlm = model.spec.stages.some(s => stageRequiresLlm(s.type));\n const cacheable = model.spec.cacheable ? 'yes' : 'no';\n \n table.push([name, source, stages, requiresLlm ? 'yes' : 'no', cacheable]);\n }\n \n console.log(table.toString());\n });\n}\n```\n\nRegister in `src/cli/src/commands/get.ts` as a subcommand.",
"testStrategy": "CLI test: `mcpctl get proxymodels` outputs table with expected columns. Test with only built-ins, test with local overrides, verify correct source detection.",
"priority": "medium",
"dependencies": [
"78",
"79"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.577Z"
},
{
"id": "90",
"title": "Implement mcpctl get stages Command",
"description": "Add CLI command to list all available stages (built-in + custom) with source and LLM requirements.",
"details": "Create `src/cli/src/commands/get-stages.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { listStages } from 'mcplocal/proxymodel/stage-registry';\nimport Table from 'cli-table3';\n\nconst LLM_REQUIRING_STAGES = ['summarize', 'summarize-tree', 'enhance', 'compress'];\n\nexport function registerGetStages(program: Command): void {\n program\n .command('get stages')\n .description('List all available stages')\n .action(async () => {\n const stages = listStages();\n \n const table = new Table({\n head: ['NAME', 'SOURCE', 'REQUIRES-LLM'],\n });\n \n for (const stage of stages) {\n const requiresLlm = LLM_REQUIRING_STAGES.includes(stage.name);\n table.push([stage.name, stage.source, requiresLlm ? 'yes' : 'no']);\n }\n \n console.log(table.toString());\n });\n}\n```",
"testStrategy": "CLI test: `mcpctl get stages` outputs table with expected columns. Test with only built-ins, test with custom stages in ~/.mcpctl/stages/, verify custom overrides show 'local' source.",
"priority": "medium",
"dependencies": [
"79"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.582Z"
},
{
"id": "91",
"title": "Implement mcpctl describe proxymodel Command",
"description": "Add CLI command to show detailed information about a specific proxymodel including full stage configuration.",
"details": "Create `src/cli/src/commands/describe-proxymodel.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { loadProxyModels } from 'mcplocal/proxymodel/loader';\nimport { stringify as yamlStringify } from 'yaml';\n\nexport function registerDescribeProxymodel(program: Command): void {\n program\n .command('describe proxymodel <name>')\n .description('Show detailed information about a proxymodel')\n .action(async (name: string) => {\n const models = await loadProxyModels();\n const model = models.get(name);\n \n if (!model) {\n console.error(`Proxymodel '${name}' not found`);\n process.exit(1);\n }\n \n console.log(`Name: ${model.metadata.name}`);\n console.log(`Source: ${isBuiltIn(name) ? 'built-in' : 'local'}`);\n console.log(`Controller: ${model.spec.controller ?? 'gate'}`);\n console.log(`Cacheable: ${model.spec.cacheable ? 'yes' : 'no'}`);\n console.log(`Applies to: ${(model.spec.appliesTo ?? ['prompts', 'toolResults']).join(', ')}`);\n console.log('');\n console.log('Stages:');\n for (const stage of model.spec.stages) {\n console.log(` - ${stage.type}`);\n if (stage.config) {\n console.log(` config:`);\n for (const [k, v] of Object.entries(stage.config)) {\n console.log(` ${k}: ${JSON.stringify(v)}`);\n }\n }\n }\n });\n}\n```",
"testStrategy": "CLI test: `mcpctl describe proxymodel default` shows expected output. Test with proxymodel that has stage configs, verify all fields displayed correctly.",
"priority": "medium",
"dependencies": [
"78"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.587Z"
},
{
"id": "92",
"title": "Implement mcpctl describe stage Command",
"description": "Add CLI command to show detailed information about a specific stage including its source location.",
"details": "Create `src/cli/src/commands/describe-stage.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { listStages, getStage } from 'mcplocal/proxymodel/stage-registry';\nimport { join } from 'path';\n\nconst STAGES_DIR = join(process.env.HOME ?? '', '.mcpctl', 'stages');\n\nconst STAGE_DESCRIPTIONS: Record<string, string> = {\n 'passthrough': 'Returns content unchanged. No processing.',\n 'paginate': 'Splits large content into pages with navigation.',\n 'section-split': 'Splits content on structural boundaries (headers, JSON keys, etc.).',\n 'summarize-tree': 'Recursively summarizes sections with hierarchical navigation.',\n};\n\nexport function registerDescribeStage(program: Command): void {\n program\n .command('describe stage <name>')\n .description('Show detailed information about a stage')\n .action(async (name: string) => {\n const stages = listStages();\n const stageInfo = stages.find(s => s.name === name);\n \n if (!stageInfo) {\n console.error(`Stage '${name}' not found`);\n process.exit(1);\n }\n \n console.log(`Name: ${name}`);\n console.log(`Source: ${stageInfo.source}`);\n if (stageInfo.source === 'local') {\n console.log(`Path: ${join(STAGES_DIR, name + '.ts')}`);\n }\n console.log(`Description: ${STAGE_DESCRIPTIONS[name] ?? 'Custom stage'}`);\n console.log(`Requires LLM: ${requiresLlm(name) ? 'yes' : 'no'}`);\n });\n}\n```",
"testStrategy": "CLI test: `mcpctl describe stage passthrough` shows expected output. Test with custom stage, verify path is shown correctly.",
"priority": "medium",
"dependencies": [
"79"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.592Z"
},
{
"id": "93",
"title": "Implement mcpctl create stage Command",
"description": "Add CLI command to scaffold a new custom stage with boilerplate TypeScript code.",
"details": "Create `src/cli/src/commands/create-stage.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { mkdir, writeFile, access } from 'fs/promises';\nimport { join } from 'path';\n\nconst STAGES_DIR = join(process.env.HOME ?? '', '.mcpctl', 'stages');\n\nconst STAGE_TEMPLATE = `import type { StageHandler } from 'mcpctl/proxymodel';\n\n/**\n * Custom stage: {{name}}\n * \n * Modify this handler to transform content as needed.\n * Available in ctx:\n * - ctx.llm.complete(prompt) - call the configured LLM\n * - ctx.cache.getOrCompute(key, fn) - cache expensive computations\n * - ctx.config - stage configuration from proxymodel YAML\n * - ctx.originalContent - raw content before any stage processing\n * - ctx.log - structured logging\n */\nconst handler: StageHandler = async (content, ctx) => {\n // TODO: Implement your transformation\n return { content };\n};\n\nexport default handler;\n`;\n\nexport function registerCreateStage(program: Command): void {\n program\n .command('create stage <name>')\n .description('Create a new custom stage')\n .action(async (name: string) => {\n await mkdir(STAGES_DIR, { recursive: true });\n \n const filePath = join(STAGES_DIR, `${name}.ts`);\n \n try {\n await access(filePath);\n console.error(`Stage '${name}' already exists at ${filePath}`);\n process.exit(1);\n } catch {\n // File doesn't exist, good\n }\n \n const code = STAGE_TEMPLATE.replace(/\\{\\{name\\}\\}/g, name);\n await writeFile(filePath, code);\n \n console.log(`Created ${filePath}`);\n console.log('Edit the file to implement your stage logic.');\n });\n}\n```",
"testStrategy": "CLI test: `mcpctl create stage my-filter` creates file at expected path with correct template. Test error when stage already exists. Verify generated code compiles.",
"priority": "medium",
"dependencies": [
"71"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.598Z"
},
{
"id": "94",
"title": "Implement mcpctl create proxymodel Command",
"description": "Add CLI command to scaffold a new proxymodel YAML file with specified stages.",
"details": "Create `src/cli/src/commands/create-proxymodel.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { mkdir, writeFile, access } from 'fs/promises';\nimport { join } from 'path';\nimport { stringify as yamlStringify } from 'yaml';\n\nconst PROXYMODELS_DIR = join(process.env.HOME ?? '', '.mcpctl', 'proxymodels');\n\nexport function registerCreateProxymodel(program: Command): void {\n program\n .command('create proxymodel <name>')\n .description('Create a new proxymodel')\n .option('--stages <stages>', 'Comma-separated list of stage names', 'passthrough')\n .option('--controller <controller>', 'Session controller (gate or none)', 'gate')\n .action(async (name: string, opts) => {\n await mkdir(PROXYMODELS_DIR, { recursive: true });\n \n const filePath = join(PROXYMODELS_DIR, `${name}.yaml`);\n \n try {\n await access(filePath);\n console.error(`Proxymodel '${name}' already exists at ${filePath}`);\n process.exit(1);\n } catch {\n // File doesn't exist, good\n }\n \n const stages = opts.stages.split(',').map((s: string) => ({ type: s.trim() }));\n \n const model = {\n kind: 'ProxyModel',\n metadata: { name },\n spec: {\n controller: opts.controller,\n stages,\n appliesTo: ['prompts', 'toolResults'],\n cacheable: true,\n },\n };\n \n await writeFile(filePath, yamlStringify(model));\n \n console.log(`Created ${filePath}`);\n });\n}\n```",
"testStrategy": "CLI test: `mcpctl create proxymodel my-pipeline --stages summarize,compress` creates valid YAML. Test default values. Verify generated YAML validates against schema.",
"priority": "medium",
"dependencies": [
"78"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.605Z"
},
{
"id": "95",
"title": "Implement mcpctl proxymodel validate Command",
"description": "Add CLI command to validate a proxymodel definition, checking that all stages resolve and config is valid.",
"details": "Create `src/cli/src/commands/proxymodel-validate.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { loadProxyModels } from 'mcplocal/proxymodel/loader';\nimport { getStage, loadCustomStages } from 'mcplocal/proxymodel/stage-registry';\n\nexport function registerProxymodelValidate(program: Command): void {\n program\n .command('proxymodel validate <name>')\n .description('Validate a proxymodel definition')\n .action(async (name: string) => {\n await loadCustomStages();\n const models = await loadProxyModels();\n const model = models.get(name);\n \n if (!model) {\n console.error(`Proxymodel '${name}' not found`);\n process.exit(1);\n }\n \n let valid = true;\n const errors: string[] = [];\n \n // Check all stages resolve\n for (const stageConfig of model.spec.stages) {\n const stage = getStage(stageConfig.type);\n if (!stage) {\n errors.push(`Stage '${stageConfig.type}' not found`);\n valid = false;\n }\n }\n \n // Check controller is valid\n const validControllers = ['gate', 'none'];\n if (model.spec.controller && !validControllers.includes(model.spec.controller)) {\n errors.push(`Unknown controller '${model.spec.controller}'`);\n valid = false;\n }\n \n if (valid) {\n console.log(`✓ Proxymodel '${name}' is valid`);\n } else {\n console.error(`✗ Proxymodel '${name}' has errors:`);\n for (const err of errors) {\n console.error(` - ${err}`);\n }\n process.exit(1);\n }\n });\n}\n```",
"testStrategy": "CLI test: valid proxymodel passes, proxymodel with unknown stage fails with clear error, proxymodel with unknown controller fails. Test with both built-in and custom stages.",
"priority": "medium",
"dependencies": [
"78",
"79"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.613Z"
},
{
"id": "96",
"title": "Implement mcpctl delete stage Command",
"description": "Add CLI command to delete a custom stage file (cannot delete built-ins).",
"details": "Create `src/cli/src/commands/delete-stage.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { unlink, access } from 'fs/promises';\nimport { join } from 'path';\nimport { listStages } from 'mcplocal/proxymodel/stage-registry';\n\nconst STAGES_DIR = join(process.env.HOME ?? '', '.mcpctl', 'stages');\n\nexport function registerDeleteStage(program: Command): void {\n program\n .command('delete stage <name>')\n .description('Delete a custom stage')\n .action(async (name: string) => {\n const stages = listStages();\n const stageInfo = stages.find(s => s.name === name);\n \n if (!stageInfo) {\n console.error(`Stage '${name}' not found`);\n process.exit(1);\n }\n \n if (stageInfo.source === 'built-in') {\n console.error(`Cannot delete built-in stage '${name}'`);\n process.exit(1);\n }\n \n const filePath = join(STAGES_DIR, `${name}.ts`);\n await unlink(filePath);\n \n console.log(`Deleted ${filePath}`);\n });\n}\n```",
"testStrategy": "CLI test: can delete custom stage, cannot delete built-in stage (error message), deleting non-existent stage shows error.",
"priority": "low",
"dependencies": [
"79"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.619Z"
},
{
"id": "97",
"title": "Implement mcpctl delete proxymodel Command",
"description": "Add CLI command to delete a local proxymodel YAML file (cannot delete built-ins).",
"details": "Create `src/cli/src/commands/delete-proxymodel.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { unlink, access } from 'fs/promises';\nimport { join } from 'path';\nimport { loadProxyModels } from 'mcplocal/proxymodel/loader';\nimport { getBuiltInProxyModels } from 'mcplocal/proxymodel/built-in-models';\n\nconst PROXYMODELS_DIR = join(process.env.HOME ?? '', '.mcpctl', 'proxymodels');\n\nexport function registerDeleteProxymodel(program: Command): void {\n program\n .command('delete proxymodel <name>')\n .description('Delete a local proxymodel')\n .action(async (name: string) => {\n const models = await loadProxyModels();\n const builtIns = getBuiltInProxyModels();\n \n if (!models.has(name)) {\n console.error(`Proxymodel '${name}' not found`);\n process.exit(1);\n }\n \n const filePath = join(PROXYMODELS_DIR, `${name}.yaml`);\n \n try {\n await access(filePath);\n } catch {\n if (builtIns.has(name)) {\n console.error(`Cannot delete built-in proxymodel '${name}'`);\n } else {\n console.error(`Proxymodel '${name}' file not found at ${filePath}`);\n }\n process.exit(1);\n }\n \n await unlink(filePath);\n console.log(`Deleted ${filePath}`);\n \n if (builtIns.has(name)) {\n console.log(`Note: Built-in '${name}' will still be available`);\n }\n });\n}\n```",
"testStrategy": "CLI test: can delete local proxymodel, cannot delete built-in (error message), deleting local override shows note about built-in fallback.",
"priority": "low",
"dependencies": [
"78"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.625Z"
},
{
"id": "98",
"title": "Implement Persistent File Cache for Stages",
"description": "Extend CacheProvider with file-based persistence in ~/.mcpctl/cache/proxymodel/ for cross-session caching.",
"details": "Create `src/mcplocal/src/proxymodel/file-cache.ts`:\n\n```typescript\nimport { mkdir, readFile, writeFile, readdir, stat, unlink } from 'fs/promises';\nimport { join } from 'path';\nimport { createHash } from 'crypto';\nimport type { CacheProvider } from './types';\n\nconst CACHE_DIR = join(process.env.HOME ?? '', '.mcpctl', 'cache', 'proxymodel');\n\nexport class FileCacheProvider implements CacheProvider {\n private memCache = new Map<string, string>();\n private maxSizeBytes: number;\n \n constructor(options: { maxSizeBytes?: number } = {}) {\n this.maxSizeBytes = options.maxSizeBytes ?? 100 * 1024 * 1024; // 100MB default\n }\n \n hash(content: string): string {\n return createHash('sha256').update(content).digest('hex').slice(0, 16);\n }\n \n private keyToPath(key: string): string {\n const safeKey = key.replace(/[^a-zA-Z0-9-_]/g, '_');\n return join(CACHE_DIR, safeKey);\n }\n \n async get(key: string): Promise<string | null> {\n // Check memory first\n if (this.memCache.has(key)) return this.memCache.get(key)!;\n \n // Check file\n try {\n const content = await readFile(this.keyToPath(key), 'utf-8');\n this.memCache.set(key, content); // Warm memory cache\n return content;\n } catch {\n return null;\n }\n }\n \n async set(key: string, value: string): Promise<void> {\n await mkdir(CACHE_DIR, { recursive: true });\n this.memCache.set(key, value);\n await writeFile(this.keyToPath(key), value);\n await this.enforceMaxSize();\n }\n \n async getOrCompute(key: string, compute: () => Promise<string>): Promise<string> {\n const cached = await this.get(key);\n if (cached !== null) return cached;\n const value = await compute();\n await this.set(key, value);\n return value;\n }\n \n private async enforceMaxSize(): Promise<void> {\n // LRU eviction based on file mtime when cache exceeds maxSizeBytes\n }\n}\n```",
"testStrategy": "Unit tests: file-based persistence survives process restart, memory cache is warmed on file read, LRU eviction works when size exceeded, concurrent access is safe. Integration test with real filesystem.",
"priority": "high",
"dependencies": [
"73"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T23:36:15.877Z"
},
{
"id": "99",
"title": "Add Cache Key with Stage File Hash",
"description": "Include the stage file hash in cache keys so cached artifacts are automatically invalidated when stage code changes.",
"details": "Modify `src/mcplocal/src/proxymodel/executor.ts`:\n\n```typescript\nimport { readFile, stat } from 'fs/promises';\nimport { createHash } from 'crypto';\nimport { join } from 'path';\n\nconst STAGES_DIR = join(process.env.HOME ?? '', '.mcpctl', 'stages');\nconst stageFileHashes: Map<string, string> = new Map();\n\nasync function getStageFileHash(stageName: string): Promise<string> {\n // Check if custom stage file exists\n const filePath = join(STAGES_DIR, `${stageName}.ts`);\n try {\n const content = await readFile(filePath, 'utf-8');\n const hash = createHash('sha256').update(content).digest('hex').slice(0, 8);\n stageFileHashes.set(stageName, hash);\n return hash;\n } catch {\n // Built-in stage, use version-based hash or fixed value\n return 'builtin-v1';\n }\n}\n\n// In executePipeline, compute cache key:\nconst stageHash = await getStageFileHash(stageConfig.type);\nconst cacheKey = [\n 'stage',\n stageConfig.type,\n stageHash,\n cache.hash(currentContent),\n cache.hash(JSON.stringify(stageConfig.config ?? {})),\n].join(':');\n\n// Use pipeline-level cache wrapping:\nif (proxyModel.spec.cacheable) {\n const cached = await cache.get(cacheKey);\n if (cached) {\n currentContent = cached;\n continue; // Skip stage execution\n }\n}\n```",
"testStrategy": "Unit tests: changing stage file content changes the hash, built-in stages have stable hash, cache miss when stage file changes, cache hit when stage file unchanged.",
"priority": "medium",
"dependencies": [
"98"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T23:36:15.892Z"
},
{
"id": "100",
"title": "Implement mcpctl cache list Command",
"description": "Add CLI command to list cached proxymodel artifacts with size and age information.",
"details": "Create `src/cli/src/commands/cache-list.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { readdir, stat } from 'fs/promises';\nimport { join } from 'path';\nimport Table from 'cli-table3';\n\nconst CACHE_DIR = join(process.env.HOME ?? '', '.mcpctl', 'cache', 'proxymodel');\n\nexport function registerCacheList(program: Command): void {\n program\n .command('cache list')\n .description('List cached proxymodel artifacts')\n .option('--project <name>', 'Filter by project')\n .action(async (opts) => {\n try {\n const files = await readdir(CACHE_DIR);\n \n const table = new Table({\n head: ['KEY', 'SIZE', 'AGE'],\n });\n \n let totalSize = 0;\n \n for (const file of files) {\n const filePath = join(CACHE_DIR, file);\n const stats = await stat(filePath);\n const age = formatAge(Date.now() - stats.mtimeMs);\n const size = formatSize(stats.size);\n totalSize += stats.size;\n \n if (opts.project && !file.includes(opts.project)) continue;\n \n table.push([file, size, age]);\n }\n \n console.log(table.toString());\n console.log(`Total: ${formatSize(totalSize)}`);\n } catch {\n console.log('No cache entries found');\n }\n });\n}\n\nfunction formatSize(bytes: number): string {\n if (bytes < 1024) return `${bytes}B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;\n return `${(bytes / 1024 / 1024).toFixed(1)}MB`;\n}\n\nfunction formatAge(ms: number): string {\n const mins = Math.floor(ms / 60000);\n if (mins < 60) return `${mins}m`;\n const hours = Math.floor(mins / 60);\n if (hours < 24) return `${hours}h`;\n return `${Math.floor(hours / 24)}d`;\n}\n```",
"testStrategy": "CLI test: list shows cache entries with correct format, --project filter works, empty cache shows appropriate message, size/age formatting is correct.",
"priority": "low",
"dependencies": [
"98"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T23:36:15.902Z"
},
{
"id": "101",
"title": "Implement mcpctl cache clear Command",
"description": "Add CLI command to clear the proxymodel cache, optionally filtered by project.",
"details": "Create `src/cli/src/commands/cache-clear.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { readdir, unlink, rmdir } from 'fs/promises';\nimport { join } from 'path';\n\nconst CACHE_DIR = join(process.env.HOME ?? '', '.mcpctl', 'cache', 'proxymodel');\n\nexport function registerCacheClear(program: Command): void {\n program\n .command('cache clear')\n .description('Clear the proxymodel cache')\n .option('--project <name>', 'Clear only cache for a specific project')\n .option('--force', 'Skip confirmation', false)\n .action(async (opts) => {\n try {\n const files = await readdir(CACHE_DIR);\n const toDelete = opts.project \n ? files.filter(f => f.includes(opts.project))\n : files;\n \n if (toDelete.length === 0) {\n console.log('No cache entries to clear');\n return;\n }\n \n if (!opts.force) {\n console.log(`This will delete ${toDelete.length} cache entries.`);\n // Add confirmation prompt\n }\n \n for (const file of toDelete) {\n await unlink(join(CACHE_DIR, file));\n }\n \n console.log(`Cleared ${toDelete.length} cache entries`);\n } catch {\n console.log('Cache directory does not exist');\n }\n });\n}\n```",
"testStrategy": "CLI test: clears all entries without --project, clears filtered entries with --project, confirmation required without --force, --force skips confirmation.",
"priority": "low",
"dependencies": [
"98"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T23:36:15.957Z"
},
{
"id": "102",
"title": "Implement mcpctl cache stats Command",
"description": "Add CLI command to show cache statistics including hit rates, total size, and entry counts.",
"details": "Create `src/cli/src/commands/cache-stats.ts`:\n\n```typescript\nimport { Command } from 'commander';\nimport { readdir, stat } from 'fs/promises';\nimport { join } from 'path';\n\nconst CACHE_DIR = join(process.env.HOME ?? '', '.mcpctl', 'cache', 'proxymodel');\n\nexport function registerCacheStats(program: Command): void {\n program\n .command('cache stats')\n .description('Show cache statistics')\n .action(async () => {\n try {\n const files = await readdir(CACHE_DIR);\n \n let totalSize = 0;\n let oldest = Date.now();\n let newest = 0;\n \n for (const file of files) {\n const filePath = join(CACHE_DIR, file);\n const stats = await stat(filePath);\n totalSize += stats.size;\n oldest = Math.min(oldest, stats.mtimeMs);\n newest = Math.max(newest, stats.mtimeMs);\n }\n \n console.log(`Entries: ${files.length}`);\n console.log(`Total size: ${formatSize(totalSize)}`);\n console.log(`Oldest entry: ${formatAge(Date.now() - oldest)} ago`);\n console.log(`Newest entry: ${formatAge(Date.now() - newest)} ago`);\n \n // Note: hit rate tracking would require runtime instrumentation\n console.log('\\nNote: Hit rate statistics require runtime instrumentation.');\n } catch {\n console.log('No cache data available');\n }\n });\n}\n```",
"testStrategy": "CLI test: shows correct stats for populated cache, handles empty cache gracefully, size formatting is correct.",
"priority": "low",
"dependencies": [
"98"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T23:36:15.981Z"
},
{
"id": "103",
"title": "Add Shell Completions for ProxyModel Commands",
"description": "Extend shell completions to include all new proxymodel-related commands, resources, and flags.",
"details": "Update `src/cli/src/completions.ts` to add completions for:\n\n```typescript\n// Resource types\nconst RESOURCE_TYPES = [...existing, 'proxymodels', 'stages'];\n\n// Command completions\nconst COMMANDS = {\n 'get': ['proxymodels', 'stages', ...existing],\n 'describe': ['proxymodel', 'stage', ...existing],\n 'create': ['proxymodel', 'stage', ...existing],\n 'delete': ['proxymodel', 'stage', ...existing],\n 'proxymodel': ['validate'],\n 'cache': ['list', 'clear', 'stats'],\n};\n\n// Dynamic completions for proxymodel/stage names\nasync function completeProxymodelName(partial: string): Promise<string[]> {\n const models = await loadProxyModels();\n return [...models.keys()].filter(n => n.startsWith(partial));\n}\n\nasync function completeStageName(partial: string): Promise<string[]> {\n const stages = listStages();\n return stages.map(s => s.name).filter(n => n.startsWith(partial));\n}\n```\n\nGenerate completion scripts for bash, zsh, and fish.",
"testStrategy": "Manual test: completions work in bash/zsh/fish for all new commands. Test proxymodel name completion, stage name completion, subcommand completion.",
"priority": "low",
"dependencies": [
"89",
"90",
"91",
"92",
"93",
"94",
"95",
"96",
"97",
"100",
"101",
"102"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.630Z"
},
{
"id": "104",
"title": "Extend Traffic Events for ProxyModel Processing",
"description": "Add new traffic event types for proxymodel processing: content_original, content_transformed, stage timing, cache hits/misses.",
"details": "Modify `src/mcplocal/src/http/traffic.ts`:\n\n```typescript\nexport type TrafficEventType = \n | 'client_request'\n | 'client_response'\n | 'upstream_request'\n | 'upstream_response'\n | 'client_notification'\n // New proxymodel events:\n | 'content_original'\n | 'content_transformed'\n | 'stage_executed'\n | 'stage_cache_hit'\n | 'stage_cache_miss';\n\nexport interface ContentOriginalEvent {\n eventType: 'content_original';\n sessionId: string;\n contentType: 'prompt' | 'toolResult';\n sourceName: string;\n content: string;\n charCount: number;\n}\n\nexport interface ContentTransformedEvent {\n eventType: 'content_transformed';\n sessionId: string;\n contentType: 'prompt' | 'toolResult';\n sourceName: string;\n content: string;\n charCount: number;\n proxyModel: string;\n stages: string[];\n durationMs: number;\n}\n\nexport interface StageExecutedEvent {\n eventType: 'stage_executed';\n sessionId: string;\n stageName: string;\n inputChars: number;\n outputChars: number;\n durationMs: number;\n cacheHit: boolean;\n}\n```\n\nEmit these events from the pipeline executor.",
"testStrategy": "Unit tests: events emitted at correct points in pipeline execution, event payloads contain correct data, cache hit/miss events distinguish correctly. Integration test with inspector showing new events.",
"priority": "medium",
"dependencies": [
"80"
],
"status": "cancelled",
"subtasks": [],
"updatedAt": "2026-03-07T01:27:15.636Z"
},
{
"id": "105",
"title": "Implement Model Studio TUI Base",
"description": "Create the base TUI for mcpctl console --model-studio that extends --inspect with original vs transformed view.",
"details": "Create `src/cli/src/commands/console/model-studio.tsx`:\n\n```typescript\nimport React, { useState, useEffect } from 'react';\nimport { Box, Text, useInput } from 'ink';\nimport { TrafficEvent } from './types';\n\ninterface ModelStudioProps {\n projectName: string;\n events: TrafficEvent[];\n}\n\nexport function ModelStudio({ projectName, events }: ModelStudioProps) {\n const [selectedIdx, setSelectedIdx] = useState(0);\n const [viewMode, setViewMode] = useState<'original' | 'transformed' | 'diff'>('transformed');\n const [pauseMode, setPauseMode] = useState(false);\n \n useInput((input, key) => {\n if (input === 'j') setSelectedIdx(i => Math.min(i + 1, events.length - 1));\n if (input === 'k') setSelectedIdx(i => Math.max(i - 1, 0));\n if (input === 'o') setViewMode(m => m === 'original' ? 'transformed' : m === 'transformed' ? 'diff' : 'original');\n if (input === 'p') setPauseMode(p => !p);\n if (input === 'G') setSelectedIdx(events.length - 1);\n });\n \n const selected = events[selectedIdx];\n const isContentEvent = selected?.eventType === 'content_original' || selected?.eventType === 'content_transformed';\n \n return (\n <Box flexDirection=\"column\" height=\"100%\">\n <Box borderStyle=\"single\" padding={1}>\n <Text>Model Studio: {projectName}</Text>\n <Text> | </Text>\n <Text>View: {viewMode}</Text>\n <Text> | </Text>\n <Text color={pauseMode ? 'red' : 'green'}>{pauseMode ? '⏸ PAUSED' : '▶ LIVE'}</Text>\n </Box>\n \n <Box flexGrow={1} flexDirection=\"row\">\n {/* Event list sidebar */}\n <Box width=\"30%\" borderStyle=\"single\">\n {events.map((e, i) => (\n <Text key={i} inverse={i === selectedIdx}>\n {formatEventLine(e)}\n </Text>\n ))}\n </Box>\n \n {/* Content view */}\n <Box width=\"70%\" borderStyle=\"single\">\n {isContentEvent && (\n <ContentView event={selected} mode={viewMode} />\n )}\n </Box>\n </Box>\n \n <Box borderStyle=\"single\">\n <Text>[o] toggle view [p] pause [j/k] navigate [G] latest [q] quit</Text>\n </Box>\n </Box>\n );\n}\n```\n\nAdd --model-studio flag to console command.",
"testStrategy": "Manual test: TUI renders correctly, keyboard navigation works, original/transformed/diff views switch correctly, pause indicator shows correctly.",
"priority": "medium",
"dependencies": [
"104"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:22:03.301Z"
},
{
"id": "106",
"title": "Implement Pause Queue for Model Studio",
"description": "Add a pause queue in mcplocal that holds outgoing responses when model studio pause mode is active.",
"details": "Create `src/mcplocal/src/proxymodel/pause-queue.ts`:\n\n```typescript\ninterface PausedResponse {\n id: string;\n sessionId: string;\n contentType: 'prompt' | 'toolResult';\n sourceName: string;\n original: string;\n transformed: string;\n resolve: (content: string) => void;\n timestamp: number;\n}\n\nclass PauseQueue {\n private paused = false;\n private queue: PausedResponse[] = [];\n private listeners = new Set<(items: PausedResponse[]) => void>();\n \n setPaused(paused: boolean): void {\n this.paused = paused;\n if (!paused) {\n // Release all paused items with their transformed content\n for (const item of this.queue) {\n item.resolve(item.transformed);\n }\n this.queue = [];\n }\n this.notifyListeners();\n }\n \n isPaused(): boolean {\n return this.paused;\n }\n \n async enqueue(item: Omit<PausedResponse, 'resolve' | 'id' | 'timestamp'>): Promise<string> {\n if (!this.paused) return item.transformed;\n \n return new Promise(resolve => {\n this.queue.push({\n ...item,\n id: crypto.randomUUID(),\n timestamp: Date.now(),\n resolve,\n });\n this.notifyListeners();\n });\n }\n \n editAndRelease(id: string, editedContent: string): void {\n const idx = this.queue.findIndex(q => q.id === id);\n if (idx >= 0) {\n const item = this.queue.splice(idx, 1)[0];\n item.resolve(editedContent);\n this.notifyListeners();\n }\n }\n \n releaseOne(id: string): void {\n const idx = this.queue.findIndex(q => q.id === id);\n if (idx >= 0) {\n const item = this.queue.splice(idx, 1)[0];\n item.resolve(item.transformed);\n this.notifyListeners();\n }\n }\n \n dropOne(id: string): void {\n const idx = this.queue.findIndex(q => q.id === id);\n if (idx >= 0) {\n const item = this.queue.splice(idx, 1)[0];\n item.resolve(''); // Empty response\n this.notifyListeners();\n }\n }\n}\n\nexport const pauseQueue = new PauseQueue();\n```",
"testStrategy": "Unit tests: enqueue returns immediately when not paused, enqueue blocks when paused, releaseOne/editAndRelease/dropOne work correctly, setPaused(false) releases all.",
"priority": "medium",
"dependencies": [
"105"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:32:05.366Z"
},
{
"id": "107",
"title": "Implement Edit Mode for Model Studio",
"description": "Add inline editing capability to model studio for modifying paused responses before release.",
"details": "Extend `src/cli/src/commands/console/model-studio.tsx`:\n\n```typescript\nimport { spawn } from 'child_process';\nimport { writeFileSync, readFileSync, unlinkSync } from 'fs';\nimport { tmpdir } from 'os';\nimport { join } from 'path';\n\nasync function editContent(original: string): Promise<string> {\n const editor = process.env.EDITOR ?? 'vim';\n const tmpFile = join(tmpdir(), `mcpctl-edit-${Date.now()}.txt`);\n \n writeFileSync(tmpFile, original);\n \n return new Promise((resolve, reject) => {\n const proc = spawn(editor, [tmpFile], {\n stdio: 'inherit',\n });\n \n proc.on('close', (code) => {\n if (code === 0) {\n const edited = readFileSync(tmpFile, 'utf-8');\n unlinkSync(tmpFile);\n resolve(edited);\n } else {\n unlinkSync(tmpFile);\n reject(new Error(`Editor exited with code ${code}`));\n }\n });\n });\n}\n\n// In the TUI component:\nuseInput(async (input, key) => {\n if (input === 'e' && pauseMode && selectedPausedItem) {\n const edited = await editContent(selectedPausedItem.transformed);\n pauseQueue.editAndRelease(selectedPausedItem.id, edited);\n \n // Emit correction event\n trafficCapture.emit({\n eventType: 'content_edited',\n sessionId: selectedPausedItem.sessionId,\n contentType: selectedPausedItem.contentType,\n sourceName: selectedPausedItem.sourceName,\n original: selectedPausedItem.original,\n transformed: selectedPausedItem.transformed,\n edited,\n timestamp: Date.now(),\n });\n }\n});\n```",
"testStrategy": "Integration test: pressing 'e' opens editor with content, saving and closing applies edit, edit event is emitted with correct before/after content.",
"priority": "medium",
"dependencies": [
"106"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:32:05.373Z"
},
{
"id": "108",
"title": "Implement Model Switch for Model Studio",
"description": "Add ability to switch the active proxymodel for a project mid-session from model studio.",
"details": "Extend `src/cli/src/commands/console/model-studio.tsx`:\n\n```typescript\nfunction ModelPicker({ models, current, onSelect }: {\n models: string[];\n current: string;\n onSelect: (name: string) => void;\n}) {\n const [selectedIdx, setSelectedIdx] = useState(models.indexOf(current));\n \n useInput((input, key) => {\n if (key.upArrow) setSelectedIdx(i => Math.max(0, i - 1));\n if (key.downArrow) setSelectedIdx(i => Math.min(models.length - 1, i + 1));\n if (key.return) onSelect(models[selectedIdx]);\n });\n \n return (\n <Box flexDirection=\"column\" borderStyle=\"single\">\n <Text bold>Select ProxyModel:</Text>\n {models.map((m, i) => (\n <Text key={m} inverse={i === selectedIdx}>\n {m === current ? '✓ ' : ' '}{m}\n </Text>\n ))}\n </Box>\n );\n}\n\n// Add to main component:\nconst [showModelPicker, setShowModelPicker] = useState(false);\nconst [activeModel, setActiveModel] = useState('default');\n\nuseInput((input) => {\n if (input === 'm') setShowModelPicker(true);\n});\n\nasync function switchModel(name: string) {\n // Call mcplocal API to switch model\n await fetch(`http://localhost:${port}/projects/${projectName}/proxymodel`, {\n method: 'PUT',\n body: JSON.stringify({ proxyModel: name }),\n });\n setActiveModel(name);\n setShowModelPicker(false);\n \n // Emit model_switched event\n trafficCapture.emit({\n eventType: 'model_switched',\n projectName,\n previousModel: activeModel,\n newModel: name,\n timestamp: Date.now(),\n });\n}\n```\n\nAdd PUT endpoint to mcplocal for switching proxymodel.",
"testStrategy": "Integration test: 'm' opens model picker, selecting a model updates the active model, subsequent content flows through new model, model_switched event is emitted.",
"priority": "medium",
"dependencies": [
"105",
"82"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:22:03.308Z"
},
{
"id": "109",
"title": "Implement Studio MCP Server Tools",
"description": "Create MCP tools for Claude Monitor to observe traffic, get corrections, switch models, and modify stages.",
"details": "Extend `src/cli/src/commands/console/inspect-mcp.ts` with studio tools:\n\n```typescript\nconst studioTools: Tool[] = [\n {\n name: 'get_content_diff',\n description: 'Get original vs transformed vs edited content for a specific event',\n inputSchema: {\n type: 'object',\n properties: {\n eventId: { type: 'string', description: 'Event ID' },\n },\n required: ['eventId'],\n },\n },\n {\n name: 'get_corrections',\n description: 'Get all user corrections (edits) in a session',\n inputSchema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string', description: 'Optional session filter' },\n },\n },\n },\n {\n name: 'get_active_model',\n description: 'Get current proxymodel name and stage list for a project',\n inputSchema: {\n type: 'object',\n properties: {\n project: { type: 'string' },\n },\n required: ['project'],\n },\n },\n {\n name: 'switch_model',\n description: 'Hot-swap the active proxymodel on a project',\n inputSchema: {\n type: 'object',\n properties: {\n project: { type: 'string' },\n model: { type: 'string' },\n },\n required: ['project', 'model'],\n },\n },\n {\n name: 'reload_stages',\n description: 'Force reload all stages from ~/.mcpctl/stages/',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'get_stage_source',\n description: 'Read the source code of a stage file',\n inputSchema: {\n type: 'object',\n properties: {\n name: { type: 'string' },\n },\n required: ['name'],\n },\n },\n {\n name: 'list_models',\n description: 'List available proxymodels',\n inputSchema: { type: 'object', properties: {} },\n },\n {\n name: 'list_stages',\n description: 'List available stages',\n inputSchema: { type: 'object', properties: {} },\n },\n];\n```",
"testStrategy": "Integration test with MCP client: each tool returns expected data format, switch_model actually changes the model, reload_stages picks up file changes, get_corrections returns user edits.",
"priority": "medium",
"dependencies": [
"104",
"106",
"107",
"108"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:32:05.393Z"
},
{
"id": "110",
"title": "Implement RBAC for ProxyModels",
"description": "Add 'run' permission on proxymodels resource controlling which proxymodels users can activate on projects.",
"details": "Update RBAC schema and enforcement:\n\n1. Add to `src/db/prisma/schema.prisma`:\n```prisma\n// Extend existing RbacBinding or Permission model\nenum RbacResource {\n // ... existing\n proxymodels\n}\n\nenum RbacPermission {\n // ... existing\n run // permission to use a proxymodel\n cache // permission to push to shared cache\n}\n```\n\n2. Add enforcement in `src/mcplocal/src/router.ts`:\n```typescript\nasync function resolveProxyModel(\n requestedModel: string,\n projectName: string,\n userId: string\n): Promise<ProxyModelDefinition> {\n const models = await loadProxyModels();\n const model = models.get(requestedModel);\n \n if (!model) {\n console.warn(`Proxymodel '${requestedModel}' not found, using default`);\n return models.get('default')!;\n }\n \n // Check RBAC permission\n const hasPermission = await checkPermission(userId, 'run', 'proxymodels', requestedModel);\n if (!hasPermission) {\n console.warn(`User lacks 'run' permission for proxymodel '${requestedModel}', using default`);\n return models.get('default')!;\n }\n \n return model;\n}\n```\n\n3. 'default' proxymodel requires no permission (always allowed).",
"testStrategy": "Integration test: user with 'run' permission can use proxymodel, user without permission falls back to default, 'default' always works, permission check logs reason for fallback.",
"priority": "low",
"dependencies": [
"87"
],
"status": "deferred",
"subtasks": [],
"updatedAt": "2026-02-28T01:07:00.152Z"
},
{
"id": "111",
"title": "Write Integration Tests for subindex Model",
"description": "Create comprehensive integration tests for the subindex proxymodel processing real content through section-split and summarize-tree.",
"details": "Create `src/mcplocal/tests/proxymodel/subindex.test.ts`:\n\n```typescript\nimport { describe, it, expect, beforeAll } from 'vitest';\nimport { executePipeline } from '../../src/proxymodel/executor';\nimport { loadProxyModels } from '../../src/proxymodel/loader';\nimport { createMockProviderRegistry } from '../mocks/providers';\n\ndescribe('subindex proxymodel', () => {\n let proxyModel;\n let mockRegistry;\n \n beforeAll(async () => {\n const models = await loadProxyModels();\n proxyModel = models.get('subindex');\n mockRegistry = createMockProviderRegistry({\n complete: async (prompt) => 'Mock summary of the content',\n });\n });\n \n it('splits JSON array into sections', async () => {\n const content = JSON.stringify([\n { id: 'flow1', label: 'Thermostat', nodes: [] },\n { id: 'flow2', label: 'Lighting', nodes: [] },\n ]);\n \n const result = await executePipeline({\n content,\n contentType: 'toolResult',\n sourceName: 'test/get_flows',\n projectName: 'test',\n sessionId: 'test-session',\n proxyModel,\n providerRegistry: mockRegistry,\n });\n \n expect(result.sections).toHaveLength(2);\n expect(result.sections[0].id).toBe('flow1');\n expect(result.content).toContain('2 sections');\n });\n \n it('provides drill-down to exact JSON content', async () => {\n // Test that drilling into a section returns exact original JSON\n });\n \n it('uses structural summaries for JSON (no LLM call)', async () => {\n // Verify LLM not called for JSON content\n });\n \n it('uses LLM summaries for prose content', async () => {\n // Verify LLM called for markdown content\n });\n \n it('caches summaries across requests', async () => {\n // Verify cache hit on second request with same content\n });\n});\n```",
"testStrategy": "Run with vitest, verify all test cases pass, check LLM call counts are as expected (structural vs prose), verify cache behavior.",
"priority": "high",
"dependencies": [
"75",
"76",
"82",
"83"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T03:02:47.422Z"
},
{
"id": "112",
"title": "Write Documentation for ProxyModel Authoring",
"description": "Create comprehensive documentation for users wanting to create custom stages and proxymodels.",
"details": "Create documentation covering:\n\n1. `docs/proxymodels/authoring-guide.md` - Complete guide from PRD's \"Authoring Guide\" section:\n - Concepts: stages, proxymodels, framework\n - File locations\n - Step-by-step stage creation\n - Step-by-step proxymodel creation\n - Testing with mcpctl proxymodel validate\n - Section drill-down\n - Cache usage\n - Error handling\n - Full example\n\n2. `docs/proxymodels/built-in-stages.md` - Reference for all built-in stages:\n - passthrough\n - paginate\n - section-split\n - summarize-tree\n - Config options for each\n\n3. `docs/proxymodels/api-reference.md` - Type reference:\n - StageHandler\n - StageContext\n - StageResult\n - Section\n - LLMProvider\n - CacheProvider\n\n4. Update main README with proxymodels overview.",
"testStrategy": "Review documentation for completeness, verify all code examples compile, test example stage from documentation works end-to-end.",
"priority": "low",
"dependencies": [
"71",
"78",
"93",
"94"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T03:02:47.439Z"
},
{
"id": "113",
"title": "Write Documentation for Model Studio",
"description": "Create documentation for using model studio for live proxymodel development and debugging.",
"details": "Create `docs/proxymodels/model-studio.md` covering:\n\n1. Overview: Three-window setup (Claude Client, Model Studio, Claude Monitor)\n2. Starting Model Studio: `mcpctl console --model-studio <project>`\n3. Keyboard shortcuts reference\n4. Viewing original vs transformed content\n5. Pause mode: when and why to use it\n6. Editing paused responses\n7. Switching proxymodels mid-session\n8. Using Claude Monitor to observe and modify stages\n9. The correction workflow: edit → observe → adjust stage → verify\n10. MCP tools available to Claude Monitor\n11. Troubleshooting common issues",
"testStrategy": "Review documentation for completeness, verify all described features work as documented.",
"priority": "low",
"dependencies": [
"105",
"106",
"107",
"108",
"109"
],
"status": "done",
"subtasks": [],
"updatedAt": "2026-03-07T02:22:17.268Z"
},
{
"id": "114",
"title": "ProxyModel v2: Code-based MCP middleware plugin system",
"description": "Redesign the ProxyModel framework from a YAML-configured content transformation pipeline into a full code-based MCP middleware plugin system. Proxy models become TypeScript files that can intercept any MCP request/response, create synthetic tools, maintain per-session state, and compose via multiple inheritance with compile-time conflict detection. The existing gate functionality (begin_session, tools/list filtering, prompt scoring, ungating) becomes the first proxy model implementation, proving the framework works by implementing gate entirely as a plugin with zero gate-specific code in router.ts.",
"details": "## Vision\n\nA proxy model is a TypeScript code file (not YAML) that acts as full MCP middleware. It can:\n- Intercept any MCP request (initialize, tools/list, tools/call, resources/*, prompts/*)\n- Modify any response before it reaches the client\n- Create synthetic tools (e.g. begin_session doesn't exist upstream)\n- Maintain per-session state (gated/ungated, accumulated tags, etc.)\n- Access project resources (prompts, servers, config)\n- Transform content (what stages do today: paginate, section-split, etc.)\n\n## Key design decisions\n\n1. Code not YAML: Proxy models live as .ts files in a known directory (e.g. ~/.mcpctl/proxymodels/). File exists = model exists. No create/delete via CLI.\n2. Stages deprecated: No separate stage resource. Content transformation is just code inside the proxy model.\n3. Multiple inheritance: A model can extend [gate, subindex] to compose behaviors from multiple parents. Conflicts (two parents intercepting the same method incompatibly) detected at load/compile time, not runtime.\n4. Gate is just a proxy model: The ~300 lines of gate logic in router.ts move into a gate.ts proxy model file. Router becomes thin plumbing (~100 lines).\n5. gated:true replaced by proxyModel field: Projects get a proxyModel: gate field. If the assigned model implements gating, the project is gated. No separate boolean.\n6. Discoverable as resources: mcpctl get proxymodels lists available models (discovered from files). mcpctl describe proxymodel gate shows details. But no create/delete commands.\n7. Attached to projects: mcpctl edit project foo --proxyModel gate or via apply YAML.\n\n## Framework interface (sketch)\n\nexport interface ProxyModelContext {\n session: SessionState;\n project: ProjectConfig;\n upstream: UpstreamClient;\n llm?: LLMProvider;\n cache?: CacheProvider;\n}\n\nexport interface ProxyModel {\n name: string;\n extends?: string[];\n onInitialize?(ctx, request): Promise<InitializeResult>;\n onToolsList?(ctx): Promise<Tool[]>;\n onToolCall?(ctx, name, args): Promise<ToolResult | null>;\n onResourceRead?(ctx, uri): Promise<ResourceContent | null>;\n transformContent?(ctx, content, contentType): Promise<string>;\n createSessionState?(): Record<string, unknown>;\n}\n\n## Migration path\n\n1. Define the ProxyModel TypeScript interface\n2. Implement the plugin loader (discover .ts files, compile, validate inheritance, detect conflicts)\n3. Implement the router integration (router delegates to loaded proxy model)\n4. Extract gate logic from router.ts into gate.ts proxy model\n5. Extract content pipeline (passthrough, paginate, section-split) into proxy model code\n6. Add proxyModel field to Project schema (replaces gated boolean)\n7. Add CLI: get proxymodels, describe proxymodel, edit project --proxyModel\n8. Add smoke tests: gate proxy model produces identical behavior to current hardcoded gate\n9. Deprecate gated field (backward compat: gated:true maps to proxyModel:gate)\n\n## Supersedes\n\nThis task supersedes deferred tasks 83, 85-97, 98-99, 103, 104, 110, 111-112 which assumed the old YAML/stage architecture.",
"status": "done",
"priority": "high",
"dependencies": [],
"testStrategy": "1. Gate proxy model smoke test: identical behavior to current hardcoded gate (begin_session, tools/list filtering, ungating). 2. Composition test: model extending [gate, paginate] inherits both behaviors. 3. Conflict detection test: two parents intercepting same hook differently = compile-time error. 4. Discovery test: drop a .ts file in proxymodels dir, mcpctl get proxymodels shows it. 5. Existing smoke tests (proxy-pipeline.test.ts) pass unchanged after migration.",
"subtasks": [],
"updatedAt": "2026-03-07T01:26:57.383Z"
2026-02-21 03:10:39 +00:00
}
],
"metadata": {
"version": "1.0.0",
"lastModified": "2026-03-07T23:36:15.981Z",
"taskCount": 114,
"completedCount": 96,
"tags": [
"master"
]
2026-02-21 03:10:39 +00:00
}
}
}