# Multi-Client Install & HTTP Transport — Design Spec

**Date:** 2026-03-15
**Version:** v0.3
**Status:** Approved

---

## Problem

Teukhos currently only installs to Claude Desktop via `teukhos install`. Users can't easily register their MCP servers with other agentic clients (Cursor, GitHub Copilot, Gemini CLI, etc.), and the HTTP transport mode — while functional — lacks polish, proper auth key resolution, and documentation for remote deployment. This makes Teukhos harder to adopt than it should be.

## Goals

1. Support installing Teukhos-generated MCP servers into all major agentic clients
2. Support both local (stdio) and remote (HTTP) server registration
3. Implement flexible API key resolution (`env:` prefix convention)
4. Polish the HTTP/Streamable HTTP transport for production remote use
5. Document everything thoroughly

## Non-Goals (this version)

- OAuth 2.0 / mTLS authentication
- REST or Shell adapters
- Web UI for server management
- Auto-discovery (`teukhos discover`) from CLI `--help` output

---

## Architecture

### Installer Plugin System (Strategy Pattern)

Each agentic client gets its own installer class under `teukhos/installers/`. This pattern was chosen over a data-driven registry because:
- Each client is self-contained and easy to add or remove
- Deprecated clients can be removed by deleting a single file
- Clients with quirky config formats get custom logic without hacks
- Each installer is independently testable

#### Directory Structure

```
teukhos/installers/
├── __init__.py          # Registry: discover_clients(), get_installer(), all_installers()
├── base.py              # Abstract BaseInstaller + InstallScope enum
├── claude_desktop.py    # Claude Desktop (migrated from cli.py)
├── claude_code.py       # Claude Code CLI
├── cursor.py            # Cursor
├── github_copilot.py    # GitHub Copilot / VS Code
├── gemini_cli.py        # Gemini CLI
├── codex.py             # OpenAI Codex CLI
├── windsurf.py          # Windsurf / Codeium
├── cline.py             # Cline
├── roo_code.py          # Roo Code
├── continue_dev.py      # Continue.dev
├── kiro.py              # Kiro
├── auggie.py            # Auggie
├── codebuddy.py         # CodeBuddy
├── opencode.py          # OpenCode
├── trae.py              # Trae
└── ... (more as needed)
```

#### BaseInstaller Interface

```python
from abc import ABC, abstractmethod
from enum import Enum
from pathlib import Path


class InstallScope(Enum):
    global_ = "global"    # User-level config (~/.config/..., %APPDATA%/...)
    project = "project"   # Project-level config (./.vscode/, ./.cursor/, etc.)


class BaseInstaller(ABC):
    name: str                              # Human-readable, e.g., "Claude Code"
    slug: str                              # CLI flag value, e.g., "claude-code"
    supported_scopes: list[InstallScope]   # What this client supports

    @abstractmethod
    def detect(self) -> bool:
        """Return True if this client is installed on the system."""

    @abstractmethod
    def config_path(self, scope: InstallScope) -> Path:
        """Return config path for the given scope.

        If scope is project but not supported, returns global path.
        Callers (install_stdio/install_http) are responsible for printing
        the scope fallback info message.
        For project scope, resolves relative to Path.cwd(). Tests should
        inject cwd via an optional constructor parameter on BaseInstaller.
        """

    @abstractmethod
    def install_stdio(self, server_name: str, teukhos_config_path: Path,
                      scope: InstallScope = InstallScope.global_) -> None:
        """Register as stdio MCP server.

        Args:
            server_name: MCP server name (from forge.name).
            teukhos_config_path: Path to the teukhos.yaml config file.
            scope: Install scope. Falls back to global if project unsupported.

        The client config path is determined internally via self.config_path(scope).
        """

    @abstractmethod
    def install_http(self, server_name: str, url: str, api_key: str | None,
                     scope: InstallScope = InstallScope.global_) -> None:
        """Register as HTTP MCP server.

        Args:
            server_name: MCP server name (from forge.name).
            url: Remote server URL (e.g., http://host:8765/mcp).
            api_key: API key value or env: reference. None if no auth.
            scope: Install scope. Falls back to global if project unsupported.

        The client config path is determined internally via self.config_path(scope).
        """

    @abstractmethod
    def uninstall(self, server_name: str,
                  scope: InstallScope = InstallScope.global_) -> None:
        """Remove registration."""
```

**Scope fallback rule:** If `--project` is specified but the client only supports global, it falls back to global and prints an info message: `"ℹ {client.name} does not support project-level config; installing globally."` This avoids silent surprises while keeping the flow non-blocking.

#### Registry (`__init__.py`)

All installer classes are imported and collected. Three functions exposed:

- `all_installers() -> list[BaseInstaller]` — every known client
- `discover_clients() -> list[BaseInstaller]` — only clients where `detect()` returns True
- `get_installer(slug: str) -> BaseInstaller` — lookup by slug, raises if unknown

---

### Supported Clients

#### Tier 1 (Priority)

| Client | Slug | Global Config Path | Project Config | Format |
|--------|------|--------------------|----------------|--------|
| Claude Desktop | `claude-desktop` | `%APPDATA%/Claude/claude_desktop_config.json` | — | JSON `mcpServers` |
| Claude Code | `claude-code` | `~/.claude.json` | `.claude/settings.json` | JSON `mcpServers` |
| Cursor | `cursor` | `~/.cursor/mcp.json` | `.cursor/mcp.json` | JSON `mcpServers` |
| GitHub Copilot / VS Code | `github-copilot` | `~/.vscode/mcp.json` | `.vscode/mcp.json` | JSON `servers` |
| Gemini CLI | `gemini-cli` | `~/.gemini/settings.json` | — | JSON `mcpServers` |
| Codex | `codex` | `~/.codex/config.json` | — | JSON `mcpServers` |

#### Tier 2 (Broader Ecosystem)

| Client | Slug | Global Config Path | Project Config | Format |
|--------|------|--------------------|----------------|--------|
| Windsurf | `windsurf` | `~/.codeium/windsurf/mcp_config.json` | `.windsurf/mcp.json` | JSON `mcpServers` |
| Cline | `cline` | `~/.vscode/settings.json` (key: `cline.mcpServers`) | `.vscode/mcp.json` | JSON (see Family D) |
| Roo Code | `roo-code` | `~/.roo-code/mcp.json` | `.roo-code/mcp.json` | JSON `mcpServers` |
| Continue.dev | `continue` | `~/.continue/config.json` | `.continue/config.json` | JSON `mcpServers` |
| Kiro | `kiro` | `~/.kiro/settings.json` | `.kiro/mcp.json` | JSON `mcpServers` |
| Auggie | `auggie` | `~/.auggie/mcp.json` | — | JSON `mcpServers` |
| CodeBuddy | `codebuddy` | `~/.codebuddy/mcp.json` | — | JSON `mcpServers` |
| OpenCode | `opencode` | `~/.opencode/config.json` | — | JSON `mcpServers` |
| Trae | `trae` | `~/.trae/mcp.json` | `.trae/mcp.json` | JSON `mcpServers` |

> **Implementation note:** All config paths will be verified against each client's current documentation during implementation. Clients whose paths cannot be confirmed will be deferred rather than guessed.

#### Client Config Format Families

Most clients share one of these JSON entry formats. Each installer generates the appropriate one.

**Family A — stdio (most clients):**
```json
{
  "mcpServers": {
    "teukhos-my-server": {
      "command": "teukhos",
      "args": ["serve", "/path/to/teukhos.yaml"]
    }
  }
}
```

**Family B — HTTP with url + headers (Claude Code, Cursor, most modern clients):**
```json
{
  "mcpServers": {
    "teukhos-my-server": {
      "url": "http://host:8765/mcp",
      "headers": {
        "Authorization": "Bearer ${TEUKHOS_API_KEY}"
      }
    }
  }
}
```

**Family C — VS Code / GitHub Copilot (uses `servers` key, slightly different shape):**
```json
{
  "servers": {
    "teukhos-my-server": {
      "type": "stdio",
      "command": "teukhos",
      "args": ["serve", "/path/to/teukhos.yaml"]
    }
  }
}
```
HTTP variant uses `"type": "http"` with `"url"` and `"headers"`.

**Family D — Cline (nested in VS Code settings.json):**
Cline stores config under `"cline.mcpServers"` key inside VS Code's `settings.json`. This requires reading and merging into a different JSON structure than the others. Implementation deferred to Tier 2 — the Cline installer will document this complexity.

> **Note:** Clients that don't support env var substitution in their config (e.g., `${VAR}`) will have the resolved literal key written instead, with a warning printed to the user.

---

### CLI Commands

#### `teukhos install [config]`

Register the MCP server with agentic clients.

**Options:**
- `--client / -c` — target a specific client by slug (e.g., `cursor`, `claude-code`)
- `--all` — install for all detected clients
- `--project` — use project-level config instead of global (falls back to global with info message if unsupported)
- `--url` — remote server URL; generates HTTP config instead of stdio
- `--key` — API key for HTTP mode (default: `"env:TEUKHOS_API_KEY"`)

**Behavior without flags:**
1. Run `discover_clients()` to find installed clients
2. Print table: client name, detected status, config path
3. Prompt user to pick one or more
4. For each selected: `install_stdio()` or `install_http()` depending on `--url`
5. Print success message per client

**Examples:**
```bash
# Auto-detect and pick
teukhos install

# Specific client, global
teukhos install --client cursor

# Project-level for current directory
teukhos install --client claude-code --project

# All detected clients
teukhos install --all

# Remote HTTP server
teukhos install --url http://myserver:8765/mcp --client cursor

# Remote with explicit key
teukhos install --url http://myserver:8765/mcp --key "env:MY_SECRET" --all
```

#### `teukhos uninstall [config]`

Remove MCP server registration from clients.

**Options:**
- `--client / -c` — target a specific client
- `--all` — remove from all detected clients
- `--project` — target project-level config

**Examples:**
```bash
teukhos uninstall --client cursor
teukhos uninstall --all
teukhos uninstall --client claude-code --project
```

#### `teukhos clients`

List all known clients and their status.

**Output example:**
```
╭─ Known MCP Clients ─────────────────────────────────────────────────╮
│ Client            │ Slug            │ Detected │ Config Path        │
├───────────────────┼─────────────────┼──────────┼────────────────────┤
│ Claude Code       │ claude-code     │ ✓        │ ~/.claude.json     │
│ Cursor            │ cursor          │ ✓        │ ~/.cursor/mcp.json │
│ GitHub Copilot    │ github-copilot  │ ✗        │ ~/.vscode/mcp.json │
│ Gemini CLI        │ gemini-cli      │ ✓        │ ~/.gemini/sett...  │
│ ...               │                 │          │                    │
╰───────────────────┴─────────────────┴──────────┴────────────────────╯
```

---

### API Key Resolution

#### The `env:` Prefix Convention

API keys in Teukhos config support two forms:

- `"env:VARIABLE_NAME"` — resolved from the named environment variable at runtime
- `"any-literal-string"` — used directly as the key value

**Default:** `"env:TEUKHOS_API_KEY"`

#### Resolution Logic

New utility in `teukhos/auth.py`:

```python
import os


def resolve_key(value: str) -> str:
    """Resolve an API key value.

    If the value starts with 'env:', read from the named environment variable.
    Otherwise, use the value as a literal key.
    """
    if value.startswith("env:"):
        env_var = value[4:]
        key = os.environ.get(env_var)
        if not key:
            raise ValueError(f"Environment variable '{env_var}' is not set")
        return key
    return value
```

#### Where It's Used

1. **Server-side** — when the MCP server starts with HTTP transport and `auth.mode: api_key`, each key in `auth.api_keys` is resolved through `resolve_key()` before comparing against incoming Bearer tokens.

2. **Install-side** — when `teukhos install --url` writes an HTTP config entry for a client:
   - Clients that support env var substitution in their config → write `${TEUKHOS_API_KEY}` or equivalent
   - Clients that don't → resolve the key and write the literal (with a warning)

#### Config Example

```yaml
auth:
  mode: api_key
  api_keys:
    - "env:TEUKHOS_API_KEY"       # default — reads $TEUKHOS_API_KEY
    - "env:BACKUP_KEY"            # reads $BACKUP_KEY
    - "hardcoded-key-abc123"      # literal, used as-is
```

---

### HTTP Transport Enhancements

#### What Already Works

FastMCP's `streamable-http` transport with `/mcp` and `/health` endpoints, and a security warning when binding to `0.0.0.0` without auth. The `AuthConfig` model exists in `config.py` but **auth enforcement (Bearer token validation middleware) has not been implemented yet** — it must be built as part of this work.

#### What We Add

**0. Auth Enforcement Middleware (Prerequisite)**

The `AuthConfig` model exists but is never consumed at runtime. We must implement Bearer token validation middleware that:
- Intercepts incoming HTTP requests to `/mcp`
- Extracts the `Authorization: Bearer <token>` header
- Compares against the resolved `auth.api_keys` list
- Returns 401 Unauthorized if no match
- Skips auth entirely when `auth.mode` is `none`
- Is wired into the FastMCP/Starlette app during `build_server()` or at serve time

This is the foundation for all HTTP auth — without it, `env:` key resolution and `install_http()` are meaningless.

**1. `env:` Key Resolution in Auth Config**

The `resolve_key()` function is applied to all `auth.api_keys` entries at server startup, before any request validation.

**2. CORS Configuration**

For browser-based or cross-origin MCP clients. New optional field in server config:

```yaml
server:
  transport: http
  host: 0.0.0.0
  port: 8765
  cors_origins: ["*"]    # or specific origins; default: not set (no CORS headers)
```

**3. Improved Startup Banner**

When running in HTTP mode, the banner clearly shows connection info:

```
╭─ Teukhos: git-tools ─────────────────────────────╮
│ Transport:  streamable-http                       │
│ Endpoint:   http://0.0.0.0:8765/mcp               │
│ Health:     http://0.0.0.0:8765/health             │
│ Auth:       api_key (1 key configured)             │
│                                                    │
│ Connect from a client:                             │
│   teukhos install --url http://HOST:8765/mcp       │
╰────────────────────────────────────────────────────╯
```

**4. Remote Server Example Config**

New file `examples/remote-server.yaml`:

```yaml
forge:
  name: remote-git-tools
  description: Git tools exposed over HTTP

server:
  transport: http
  host: 0.0.0.0
  port: 8765
  cors_origins: ["*"]

auth:
  mode: api_key
  api_keys:
    - "env:TEUKHOS_API_KEY"

tools:
  - name: git_log
    description: "Show recent git commits"
    adapter: cli
    cli:
      command: git
      subcommand: [log, --oneline]
    args:
      - name: count
        type: integer
        flag: "-n"
        default: 10
    output:
      type: stdout
```

---

### Documentation Plan

All new features are documented in the following places:

#### README.md Updates

- **Quick Start** — updated with the new install flow
- **CLI Reference** — full docs for `teukhos install`, `teukhos uninstall`, `teukhos clients` with all flags and examples
- **Supported Clients** — table listing all supported clients, their slugs, scopes (global/project)
- **Remote Server** — new section: starting with `--transport http`, connecting clients via `--url`, API key setup with `env:` convention
- **Authentication** — expanded section: `env:` prefix resolution, examples of env var vs literal keys
- **Deployment Guide** — local network and remote scenarios, reverse proxy examples (nginx/Caddy for TLS termination)

#### Inline CLI Help

- `teukhos install --help` — full usage with examples
- `teukhos uninstall --help` — usage with examples
- `teukhos clients --help` — explains output

#### Examples Directory

- New `examples/remote-server.yaml` with HTTP transport + auth
- Existing examples remain unchanged

---

### Config Model Changes

The following changes to `teukhos/config.py` are required:

- **`ServerConfig`**: Add `cors_origins: list[str] | None = None` field for CORS configuration

No other model changes needed — `AuthConfig` already has the right shape, it just needs to be consumed at runtime.

### Detection Strategy

All installers use a consistent detection approach: **check if the client config file's parent directory exists** (indicates the client has been run at least once). This is preferred over binary-in-PATH checks, which are fragile on Windows where GUI apps often aren't in PATH.

Each installer's `detect()` follows this pattern:
```python
def detect(self) -> bool:
    return self.config_path(InstallScope.global_).parent.exists()
```

> **Shared directory caveat:** For clients that share a config directory with another application (e.g., Cline and GitHub Copilot both use `~/.vscode/`), detection may require additional checks such as verifying the presence of extension-specific markers or config keys.

### File Write Safety

All config file writes use **atomic write** (write to temp file in the same directory, then `os.replace()`) to prevent corruption from concurrent access or crashes mid-write. Create parent directories (`os.makedirs` with `exist_ok=True`) before writing the temp file.

**Backup before modification:** Before modifying an existing config file, write a backup to `<config>.teukhos-backup`. This provides a recovery path if the merge logic produces incorrect JSON.

**Error handling for malformed configs:** If a client config file exists but contains invalid JSON, print a warning and skip that client. Do not overwrite or destroy user data.

**Best-effort multi-client installs:** When using `--all`, install is best-effort per client. Failures for one client do not affect others. All successes and failures are reported at the end.

### Migration from Current Code

The existing `install` command in `cli.py` (Claude Desktop only) is migrated to `teukhos/installers/claude_desktop.py`. The CLI command is rewritten to use the installer registry. This is a clean replacement, not a wrapper.

---

## Future Roadmap

Features deferred from this version but planned for future releases:

- **OAuth 2.0 / OIDC Authentication** — enterprise SSO, multi-user token-based auth with refresh flows
- **mTLS (Mutual TLS)** — certificate-based client authentication for zero-trust deployments
- **REST Adapter** — wrap REST APIs as MCP tools without CLI subprocess overhead
- **Shell Adapter** — run inline shell scripts instead of requiring a binary
- **Web UI** — browser-based dashboard for managing servers, viewing logs, testing tools
- **Auto-Discovery (`teukhos discover`)** — AI-powered config generation from a binary's `--help` output
- **OpenAPI Import** — generate Teukhos YAML from an OpenAPI/Swagger spec
- **Hot Reload** — watch the YAML config and restart the server on changes
- **Tool Composition** — pipe the output of one tool as input to another
- **Metrics & Observability** — Prometheus metrics endpoint, structured logging
- **Public Tool Registry** — share and discover community-built Teukhos configs

---

## Testing Strategy

- **Unit tests** for each installer: mock filesystem, verify correct JSON is written/merged
- **Unit tests** for `resolve_key()`: env var resolution, literal passthrough, missing var error
- **Unit tests** for registry: `discover_clients()`, `get_installer()`, unknown slug handling
- **Integration tests**: full `teukhos install` / `teukhos uninstall` cycle with temp config files
- **Existing tests** remain unchanged — no regressions
