Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -1177,3 +1177,4 @@ Legend: `shipped` ≥95% checked · `in-flight` 1–94% · `drafted` 0% · `—`
| [118-auto-rescan-on-change](./specs/118-auto-rescan-on-change/) | — | — |
| [118-pin-visibility-boundary](./specs/118-pin-visibility-boundary/) | — | — |
| [119-scan-default-trust](./specs/119-scan-default-trust/) | — | — |
| [120-mcp-router-import](./specs/120-mcp-router-import/) | `drafted` | 0/66 (0%) |
34 changes: 34 additions & 0 deletions specs/120-mcp-router-import/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Specification Quality Checklist: MCP Router Importer

**Purpose**: Validate specification completeness and quality before planning
**Created**: 2026-10-20
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs) beyond the existing CLI/REST surfaces the feature must reuse
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders (the mapping table and `research.md` carry the technical detail)
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain (decisions self-answered, see Clarifications)
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- The one real technical decision (a read-only SQLite reader, since `go.mod` has none) is deliberately left to plan.md; the spec only requires "read a live database without modifying it" (FR-004, FR-005).
60 changes: 60 additions & 0 deletions specs/120-mcp-router-import/contracts/cli.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# CLI contract: `mcpproxy import`

```text
mcpproxy import --from <source> [--path <path>] [--dry-run] [--workspace <name-or-id>]
[--yes] [--secrets-inline] [-o table|json|yaml]
```

| Flag | Meaning |
|------|---------|
| `--from` | required; `mcp-router` (unknown values: error listing supported sources) |
| `--path` | data directory, `mcprouter.db`, or an exported `mcp-servers-*.json`; default: auto-detect per OS |
| `--dry-run` | plan only: no add, no keyring write, no telemetry |
| `--workspace` | limit to one local workspace |
| `--yes` | skip the single confirmation prompt (never implies `--secrets-inline`) |
| `--secrets-inline` | allow secret-like values and inline credentials to stay in the config file when the keyring is unavailable or cannot hold them |
| `--help-json` | machine-readable help (existing mechanism) |

No `--no-quarantine`.

## Default locations checked (and printed when none exist)

| OS | Path |
|----|------|
| macOS | `~/Library/Application Support/MCP Router` |
| Windows | `%APPDATA%\MCP Router` |
| Linux | `~/.config/MCP Router` (best effort) |

## Output (`-o json`; the table shows the same content)

```json
{
"source": "mcp-router",
"input": {"kind": "data_dir", "path": "...", "workspaces": ["Local"]},
"dry_run": true,
"summary": {"total": 12, "to_import": 8, "needs_configuration": 1, "already_present": 1,
"conflict": 0, "collision": 0, "unmappable": 2, "skipped": 0, "added": 0, "failed": 0},
"servers": [
{"name": "fs", "source_name": "fs", "workspace": "Local", "class": "to_import",
"kind": "stdio", "summary": "npx -y @modelcontextprotocol/server-filesystem /tmp",
"enabled": true, "hidden_tools": ["write_file"],
"secrets": [{"field": "env:API_KEY", "secret_like": true, "handling": "keyring"}],
"not_carried_over": ["auto_start", "project"], "warnings": []}
],
"not_imported": {"workflows": true, "hooks": false, "skills": true, "logs": true, "cloud_workspaces": 0},
"reconnect": [{"client": "cursor", "config_path": "~/.cursor/mcp.json", "command": "mcpproxy connect cursor"},
{"client": "cline", "note": "MCPProxy has no Cline connector; re-point Cline's MCP settings manually"}],
"warnings": [],
"exit_class": "success"
}
```

Never present: any env/header/token/arg value, any `MCPR_TOKEN`. `servers[].summary` is the importer-owned summary (plan D4): fixed `‹redacted›` masks for sensitive argument values and for every URL query value and fragment; no tail bytes, no lengths.

## Exit codes

0 success · 20 partial · 21 nothing added · 22 source not found · 23 source unreadable/inconsistent · 24 refused by secret rules. (Plan D8.)

## Interaction

Interactive run: print the plan, then `Add N servers (X secrets to keyring, Y inline)? [y/N]`. Non-interactive without `--yes`: exit 24 with the message. After a real run: print the reconnect list and `mcpproxy upstream list`/review hints.
20 changes: 20 additions & 0 deletions specs/120-mcp-router-import/contracts/rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# REST contract additions

## `POST /api/v1/servers/import/report` (new)

Auth: API key + `requireServerOp(auth.ServerOpAdd, ...)`.

Request: `{"source": "mcp-router"}` — `source` must be in the closed enum (`mcp-router`; extensible). No count is sent.
Responses: `204` for a valid request whether or not an event was recorded (the response never reveals opt-out state); `400` for an unknown `source`.
Effect: `Service.RecordImportRun(source)` evaluates event-time telemetry eligibility under the service's own synchronisation (config plus env overrides) and, if eligible, increments the run counter for that source.

## `POST /api/v1/servers` (extended)

`AddServerRequest.disabled_tools []string` (create only), applied to the new server's `DisabledTools`. `enabled_tools` is not accepted on this surface. Documented in `docs/api/rest-api.md` and `oas/swagger.yaml`; no change for callers that omit it.

## Web UI phase (P5, non-gating)

New endpoints, so no existing contract changes:

- `GET /api/v1/servers/import/mcp-router` → `{found, input_kind, path, workspaces:[{id,name}], server_count}` (stat plus a snapshot count under the same rules).
- `POST /api/v1/servers/import/mcp-router` body `{path?, workspace?, apply:false|true, secrets:"keyring"|"inline", inline_credentials_ok:false|true, servers?:[names]}` → plan view (same `ItemView` DTO as the CLI JSON) when `apply:false`, outcomes when `apply:true`. Same authorization as other server-adding operations. Records telemetry in-process through `Service.RecordImportRun`. Never bypasses quarantine; placeholder rejection (FR-006b) applies before any destination reference is inserted.
58 changes: 58 additions & 0 deletions specs/120-mcp-router-import/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Data Model: MCP Router Importer

No new persisted state. All types are in-memory, in `internal/mcprouter`.

## Input

| Type | Fields | Notes |
|------|--------|-------|
| `Input` | `Kind` (`data_dir`\|`database`\|`export_file`), `Root`, `Files []string` | Output of `Locate`. `Files` are already containment-checked (plan D3). |
| `Workspace` | `ID`, `Name`, `Type` (`local`\|`remote`), `DBPath`, `Order` | `remote` workspaces have no rows; counted as "not imported". Default workspace (`local-default`) sorts first. |

## Source (immutable, one per `servers` row or export entry)

| Field | Source column | Missing-column default |
|-------|---------------|------------------------|
| `Workspace` | workspace id | `local-default` |
| `Name` | `name` | required, else row failed |
| `Kind` | `server_type` | `local` |
| `Command`, `Args` | `command`, `args` (JSON) | empty |
| `Env` | `env` (JSON object, plaintext) | `{}` |
| `RemoteURL` | `remote_url` | empty |
| `Bearer` | `bearer_token` | none; `BearerState` = `none`\|`plain`\|`encrypted` (prefix `mcpr-secret:v1:`) |
| `Disabled` | `disabled` | false |
| `AutoStart`, `ProjectID`, `Description`, `Version`, `Verification` | respective | unset (reported as not carried over when set) |
| `Params` | `input_params` (JSON) | none |
| `ToolOff []string` | keys of `tool_permissions` with `false` | none |
| `FromExport bool` | — | export entries lack Disabled/ToolOff/Bearer/Kind info |

## Plan (output of `BuildPlan`, pure, no side effects)

| Type | Fields |
|------|--------|
| `Plan` | `Input`, `Items []Item`, `NotImported` (workflows, hooks, skills, logs, cloud workspaces: booleans/counts from table presence only), `Clients []ClientAdvice`, `Warnings []string` |
| `Item` | `Source`, `Class`, `Reason`, `Server *config.ServerConfig` (nil unless addable), `Secrets []SecretPlan`, `NotCarried []string` (field names), `Differs []string` (conflict, field names only) |
| `Class` | `to_import`, `needs_configuration`, `already_present`, `conflict`, `collision`, `unmappable`, `skipped_self`, `filtered_out` |
| `SecretPlan` | `Field` (`env:NAME`\|`header:NAME`\|`arg:INDEX`), `Handling` (`keyring`\|`inline`\|`missing`\|`blocked_inline_credential`), `SecretLike bool` — never a value |
| `ClientAdvice` | `Client` (MCP Router id), `ConfigPath`, `Command` (`mcpproxy connect <id>` or empty), `Note` |

### State transitions of an Item

```text
read ─▶ resolve placeholders ─▶ validate (name, URL, command, `${`) ─▶ classify
unmappable ◀──────────── any failure
already_present / conflict / collision ◀── compare against existing servers (FR-016a)
needs_configuration ◀── encrypted bearer
to_import ◀── otherwise
apply: to_import|needs_configuration ─▶ added | failed(reason)
```

## Outcome (output of `Apply`)

`ServerOutcome{Name, Status(added|failed|uncertain), Reason, Enabled, Quarantined (read back, or `unknown`), OrphanRefs []string}`. `Classify(plan, outcomes)` gives the exit class; only `added` outcomes make a run telemetry-eligible.

`ItemView` is the only type that is ever rendered or serialised: it has no value-bearing fields; `Plan`/`Item` (which hold `*config.ServerConfig` with raw values) are never marshalled.

## Telemetry

`CounterRegistry.importCounts map[ImportSource]int64` (new), payload `import_source_counts: {"mcp-router": N}` — integer counts of runs, fixed key set, omitted when empty.
Loading
Loading