From 3ee0dbe58ea6b97aa799946b138be4cbef984d42 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 21 Feb 2026 05:18:57 +0000 Subject: [PATCH] docs: add architecture and getting-started docs, CLI e2e tests Architecture doc covers all packages, APIs, and design principles. Getting-started guide covers installation, quick start, and config. E2e tests verify all CLI commands are properly registered. Co-Authored-By: Claude Opus 4.6 --- docs/architecture.md | 149 ++++++++++++++++++++++++ docs/getting-started.md | 155 +++++++++++++++++++++++++ src/cli/tests/e2e/cli-commands.test.ts | 77 ++++++++++++ 3 files changed, 381 insertions(+) create mode 100644 docs/architecture.md create mode 100644 docs/getting-started.md create mode 100644 src/cli/tests/e2e/cli-commands.test.ts diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..80fd4ed --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,149 @@ +# mcpctl Architecture + +## Overview + +mcpctl is a kubectl-like management tool for MCP (Model Context Protocol) servers. It consists of a CLI, a daemon server, a database layer, a local proxy, and shared utilities. + +## Package Structure + +``` +src/ +├── cli/ @mcpctl/cli - Command-line interface +├── mcpd/ @mcpctl/mcpd - Daemon server (REST API) +├── db/ @mcpctl/db - Database layer (Prisma + PostgreSQL) +├── local-proxy/ @mcpctl/local-proxy - MCP protocol proxy +└── shared/ @mcpctl/shared - Shared constants and utilities +``` + +## Component Diagram + +``` +┌─────────────────┐ HTTP ┌──────────────┐ Prisma ┌────────────┐ +│ mcpctl CLI │ ──────────────│ mcpd │ ──────────────│ PostgreSQL │ +│ (Commander.js) │ │ (Fastify 5) │ │ │ +└─────────────────┘ └──────┬───────┘ └────────────┘ + │ + │ Docker/Podman API + ▼ + ┌──────────────┐ + │ Containers │ + │ (MCP servers)│ + └──────────────┘ + +┌─────────────────┐ STDIO ┌──────────────┐ STDIO/HTTP ┌────────────┐ +│ Claude / LLM │ ────────────│ local-proxy │ ──────────────│ MCP Servers│ +│ │ │ (McpRouter) │ │ │ +└─────────────────┘ └──────────────┘ └────────────┘ +``` + +## CLI (`@mcpctl/cli`) + +The CLI is built with Commander.js and communicates with mcpd via HTTP REST. + +### Commands + +| Command | Description | +|---------|-------------| +| `mcpctl get ` | List resources (servers, profiles, projects, instances) | +| `mcpctl describe ` | Show detailed resource info | +| `mcpctl apply ` | Apply declarative YAML/JSON configuration | +| `mcpctl setup [name]` | Interactive server setup wizard | +| `mcpctl instance list/start/stop/restart/remove/logs/inspect` | Manage instances | +| `mcpctl claude generate/show/add/remove` | Manage .mcp.json files | +| `mcpctl project list/create/delete/show/profiles/set-profiles` | Manage projects | +| `mcpctl config get/set/path` | Manage CLI configuration | +| `mcpctl status` | Check daemon connectivity | + +### Configuration + +CLI config is stored at `~/.config/mcpctl/config.json` with: +- `daemonUrl`: mcpd server URL (default: `http://localhost:4444`) + +## Daemon (`@mcpctl/mcpd`) + +Fastify 5-based REST API server that manages MCP server lifecycle. + +### Layers + +1. **Routes** - HTTP handlers, parameter extraction +2. **Services** - Business logic, validation (Zod schemas), error handling +3. **Repositories** - Data access via Prisma (interface-based for testability) + +### API Endpoints + +| Endpoint | Methods | Description | +|----------|---------|-------------| +| `/api/v1/servers` | GET, POST | MCP server definitions | +| `/api/v1/servers/:id` | GET, PUT, DELETE | Single server operations | +| `/api/v1/profiles` | GET, POST | Server configuration profiles | +| `/api/v1/profiles/:id` | GET, PUT, DELETE | Single profile operations | +| `/api/v1/projects` | GET, POST | Project management | +| `/api/v1/projects/:id` | GET, PUT, DELETE | Single project operations | +| `/api/v1/projects/:id/profiles` | GET, PUT | Project profile assignments | +| `/api/v1/projects/:id/mcp-config` | GET | Generate .mcp.json | +| `/api/v1/instances` | GET, POST | Instance lifecycle | +| `/api/v1/instances/:id` | GET, DELETE | Instance operations | +| `/api/v1/instances/:id/stop` | POST | Stop instance | +| `/api/v1/instances/:id/restart` | POST | Restart instance | +| `/api/v1/instances/:id/inspect` | GET | Container inspection | +| `/api/v1/instances/:id/logs` | GET | Container logs | +| `/api/v1/audit-logs` | GET | Query audit logs | +| `/api/v1/audit-logs/:id` | GET | Single audit log | +| `/api/v1/audit-logs/purge` | POST | Purge expired logs | +| `/health` | GET | Health check (detailed) | +| `/healthz` | GET | Liveness probe | + +### Container Orchestration + +The `McpOrchestrator` interface abstracts container management: +- `DockerContainerManager` - Docker/Podman implementation via dockerode +- Future: `KubernetesOrchestrator` for k8s deployments + +## Local Proxy (`@mcpctl/local-proxy`) + +Aggregates multiple MCP servers behind a single STDIO endpoint. + +### Features + +- **Tool namespacing**: `servername/toolname` routing +- **Resource forwarding**: `resources/list` and `resources/read` +- **Prompt forwarding**: `prompts/list` and `prompts/get` +- **Notification pass-through**: Upstream notifications forwarded to client +- **Health monitoring**: Periodic health checks with state tracking +- **Transport support**: STDIO (child process) and HTTP (SSE/Streamable HTTP) + +### Usage + +```bash +# Via config file +mcpctl-proxy --config proxy.json + +# Via CLI flags +mcpctl-proxy --upstream "slack:npx -y @anthropic/slack-mcp" \ + --upstream "github:npx -y @anthropic/github-mcp" +``` + +## Database (`@mcpctl/db`) + +Prisma ORM with PostgreSQL. Key models: + +- **User** / **Session** - Authentication +- **McpServer** - Server definitions (name, transport, package, docker image) +- **McpProfile** - Per-server configurations (env overrides, permissions) +- **Project** - Grouping of profiles for a workspace +- **McpInstance** - Running container instances with lifecycle state +- **AuditLog** - Immutable operation audit trail + +## Shared (`@mcpctl/shared`) + +Constants and utilities shared across packages: +- `APP_NAME`, `APP_VERSION` +- Common type definitions + +## Design Principles + +1. **Interface-based repositories** - All data access through interfaces for testability +2. **Dependency injection** - Services receive dependencies via constructor +3. **Zod validation** - All user input validated with Zod schemas +4. **Namespaced errors** - Custom error classes with HTTP status codes +5. **TypeScript strict mode** - `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..0b736b0 --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,155 @@ +# Getting Started with mcpctl + +## Prerequisites + +- Node.js >= 20.0.0 +- pnpm >= 9.0.0 +- PostgreSQL (for mcpd) +- Docker or Podman (for container management) + +## Installation + +```bash +# Clone the repository +git clone +cd mcpctl + +# Install dependencies +pnpm install + +# Generate Prisma client +pnpm --filter @mcpctl/db exec prisma generate + +# Build all packages +pnpm build +``` + +## Quick Start + +### 1. Start the Database + +```bash +# Start PostgreSQL via Docker Compose +pnpm db:up + +# Run database migrations +pnpm --filter @mcpctl/db exec prisma db push +``` + +### 2. Start the Daemon + +```bash +cd src/mcpd +pnpm dev +``` + +The daemon starts on `http://localhost:4444` by default. + +### 3. Use the CLI + +```bash +# Check daemon status +mcpctl status + +# Register an MCP server +mcpctl apply config.yaml + +# Or use the interactive wizard +mcpctl setup my-server + +# List registered servers +mcpctl get servers + +# Start an instance +mcpctl instance start + +# Check instance status +mcpctl instance list + +# View instance logs +mcpctl instance logs +``` + +### 4. Generate .mcp.json for Claude + +```bash +# Create a project +mcpctl project create my-workspace + +# Assign profiles to project +mcpctl project set-profiles + +# Generate .mcp.json +mcpctl claude generate + +# Or manually add servers +mcpctl claude add my-server -c npx -a -y @my/mcp-server +``` + +## Example Configuration + +Create a `config.yaml` file: + +```yaml +servers: + - name: slack + description: Slack MCP server + transport: STDIO + packageName: "@anthropic/slack-mcp" + envTemplate: + - name: SLACK_TOKEN + description: Slack bot token + isSecret: true + + - name: github + description: GitHub MCP server + transport: STDIO + packageName: "@anthropic/github-mcp" + +profiles: + - name: default + server: slack + envOverrides: + SLACK_TOKEN: "xoxb-your-token" + +projects: + - name: dev-workspace + description: Development workspace +``` + +Apply it: + +```bash +mcpctl apply config.yaml +``` + +## Running Tests + +```bash +# Run all tests +pnpm test:run + +# Run tests for a specific package +pnpm --filter @mcpctl/cli test:run +pnpm --filter @mcpctl/mcpd test:run +pnpm --filter @mcpctl/local-proxy test:run + +# Run tests with coverage +pnpm test:coverage + +# Typecheck +pnpm typecheck + +# Lint +pnpm lint +``` + +## Development + +```bash +# Watch mode for tests +pnpm test + +# Build in watch mode +cd src/cli && pnpm dev +``` diff --git a/src/cli/tests/e2e/cli-commands.test.ts b/src/cli/tests/e2e/cli-commands.test.ts new file mode 100644 index 0000000..9c86879 --- /dev/null +++ b/src/cli/tests/e2e/cli-commands.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from 'vitest'; +import { createProgram } from '../../src/index.js'; + +/** + * End-to-end tests that verify CLI command registration and help output + * without requiring a running daemon. + */ +describe('CLI command registration (e2e)', () => { + it('program has all expected commands', () => { + const program = createProgram(); + const commandNames = program.commands.map((c) => c.name()); + + expect(commandNames).toContain('config'); + expect(commandNames).toContain('status'); + expect(commandNames).toContain('get'); + expect(commandNames).toContain('describe'); + expect(commandNames).toContain('instance'); + expect(commandNames).toContain('apply'); + expect(commandNames).toContain('setup'); + expect(commandNames).toContain('claude'); + expect(commandNames).toContain('project'); + }); + + it('instance command has lifecycle subcommands', () => { + const program = createProgram(); + const instance = program.commands.find((c) => c.name() === 'instance'); + expect(instance).toBeDefined(); + + const subcommands = instance!.commands.map((c) => c.name()); + expect(subcommands).toContain('list'); + expect(subcommands).toContain('start'); + expect(subcommands).toContain('stop'); + expect(subcommands).toContain('restart'); + expect(subcommands).toContain('remove'); + expect(subcommands).toContain('logs'); + expect(subcommands).toContain('inspect'); + }); + + it('claude command has config management subcommands', () => { + const program = createProgram(); + const claude = program.commands.find((c) => c.name() === 'claude'); + expect(claude).toBeDefined(); + + const subcommands = claude!.commands.map((c) => c.name()); + expect(subcommands).toContain('generate'); + expect(subcommands).toContain('show'); + expect(subcommands).toContain('add'); + expect(subcommands).toContain('remove'); + }); + + it('project command has CRUD subcommands', () => { + const program = createProgram(); + const project = program.commands.find((c) => c.name() === 'project'); + expect(project).toBeDefined(); + + const subcommands = project!.commands.map((c) => c.name()); + expect(subcommands).toContain('list'); + expect(subcommands).toContain('create'); + expect(subcommands).toContain('delete'); + expect(subcommands).toContain('show'); + expect(subcommands).toContain('profiles'); + expect(subcommands).toContain('set-profiles'); + }); + + it('displays version', () => { + const program = createProgram(); + expect(program.version()).toBeDefined(); + expect(program.version()).toMatch(/^\d+\.\d+\.\d+$/); + }); + + it('displays help without error', () => { + const program = createProgram(); + const helpText = program.helpInformation(); + expect(helpText).toContain('mcpctl'); + expect(helpText).toContain('Manage MCP servers'); + }); +});