ama-cli 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ama_cli-0.1.0/.gitignore +99 -0
- ama_cli-0.1.0/PKG-INFO +90 -0
- ama_cli-0.1.0/README.md +75 -0
- ama_cli-0.1.0/pyproject.toml +46 -0
- ama_cli-0.1.0/src/ama_cli/__init__.py +7 -0
- ama_cli-0.1.0/src/ama_cli/agents/__init__.py +25 -0
- ama_cli-0.1.0/src/ama_cli/agents/adapters/__init__.py +48 -0
- ama_cli-0.1.0/src/ama_cli/agents/adapters/cli_backed.py +271 -0
- ama_cli-0.1.0/src/ama_cli/agents/adapters/cursor.py +86 -0
- ama_cli-0.1.0/src/ama_cli/agents/base.py +206 -0
- ama_cli-0.1.0/src/ama_cli/agents/mcp.py +275 -0
- ama_cli-0.1.0/src/ama_cli/agents/registry.py +179 -0
- ama_cli-0.1.0/src/ama_cli/auth.py +292 -0
- ama_cli-0.1.0/src/ama_cli/cli.py +433 -0
- ama_cli-0.1.0/src/ama_cli/credentials.py +126 -0
- ama_cli-0.1.0/src/ama_cli/discovery.py +114 -0
- ama_cli-0.1.0/src/ama_cli/launch.py +82 -0
- ama_cli-0.1.0/src/ama_cli/onboard.py +421 -0
- ama_cli-0.1.0/src/ama_cli/prompts.py +53 -0
- ama_cli-0.1.0/src/ama_cli/verify.py +259 -0
- ama_cli-0.1.0/tests/__init__.py +0 -0
- ama_cli-0.1.0/tests/agents/__init__.py +0 -0
- ama_cli-0.1.0/tests/agents/test_detection.py +445 -0
- ama_cli-0.1.0/tests/agents/test_mcp_adapters.py +627 -0
- ama_cli-0.1.0/tests/test_auth.py +400 -0
- ama_cli-0.1.0/tests/test_discovery.py +168 -0
- ama_cli-0.1.0/tests/test_launch.py +106 -0
- ama_cli-0.1.0/tests/test_onboard.py +630 -0
- ama_cli-0.1.0/tests/test_verify.py +296 -0
- ama_cli-0.1.0/uv.lock +209 -0
ama_cli-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
build/
|
|
8
|
+
develop-eggs/
|
|
9
|
+
dist/
|
|
10
|
+
downloads/
|
|
11
|
+
eggs/
|
|
12
|
+
.eggs/
|
|
13
|
+
lib/
|
|
14
|
+
!packages/design-system/src/lib/
|
|
15
|
+
!apps/web/src/lib/
|
|
16
|
+
lib64/
|
|
17
|
+
parts/
|
|
18
|
+
sdist/
|
|
19
|
+
var/
|
|
20
|
+
wheels/
|
|
21
|
+
*.egg-info/
|
|
22
|
+
.installed.cfg
|
|
23
|
+
*.egg
|
|
24
|
+
|
|
25
|
+
# Virtual environments
|
|
26
|
+
.venv/
|
|
27
|
+
venv/
|
|
28
|
+
ENV/
|
|
29
|
+
|
|
30
|
+
# Node.js
|
|
31
|
+
node_modules/
|
|
32
|
+
.turbo/
|
|
33
|
+
.next/
|
|
34
|
+
|
|
35
|
+
# IDE
|
|
36
|
+
.idea/
|
|
37
|
+
.vscode/
|
|
38
|
+
*.swp
|
|
39
|
+
*.swo
|
|
40
|
+
|
|
41
|
+
# Environment
|
|
42
|
+
.env
|
|
43
|
+
.ama_state
|
|
44
|
+
|
|
45
|
+
# Data
|
|
46
|
+
data/
|
|
47
|
+
!data/podcast_demo/
|
|
48
|
+
!apps/web/src/routes/**/data/
|
|
49
|
+
/prompts/
|
|
50
|
+
|
|
51
|
+
# Testing
|
|
52
|
+
.pytest_cache/
|
|
53
|
+
.coverage
|
|
54
|
+
htmlcov/
|
|
55
|
+
ag_news_200.jsonl
|
|
56
|
+
|
|
57
|
+
# Node.js
|
|
58
|
+
node_modules/
|
|
59
|
+
# pnpm-lock.yaml is intentionally COMMITTED. It is the single source of truth for
|
|
60
|
+
# JS dependency resolution and apps/web/Dockerfile installs with --frozen-lockfile.
|
|
61
|
+
# Ignoring it is what previously left bun.lock as the only committed lockfile,
|
|
62
|
+
# so reproducible installs used bun's resolution while the docs mandated pnpm.
|
|
63
|
+
|
|
64
|
+
# mkcert certificates are safe to commit - they only work when the local CA is trusted
|
|
65
|
+
# infra/caddy/certs/ - intentionally NOT ignored for zero-config local dev
|
|
66
|
+
|
|
67
|
+
# Terraform
|
|
68
|
+
*.tfstate
|
|
69
|
+
*.tfstate.*
|
|
70
|
+
*.tfvars
|
|
71
|
+
!terraform.tfvars.example
|
|
72
|
+
.terraform/
|
|
73
|
+
|
|
74
|
+
# Atlantis secrets (Helm values with GitHub tokens)
|
|
75
|
+
infra/k8s/atlantis/secrets.yaml
|
|
76
|
+
|
|
77
|
+
# Agent harness state.
|
|
78
|
+
# Worktrees are multi-GB full repo clones (workmux scratch); leaving them
|
|
79
|
+
# unignored puts them in every agent's glob/grep scope and times out searches.
|
|
80
|
+
.claude/worktrees/
|
|
81
|
+
.claude/settings.local.json
|
|
82
|
+
|
|
83
|
+
# Local MCP client config for Claude Code in this repo (personal endpoint/keys).
|
|
84
|
+
# Root-anchored so the tracked plugins/ama-agent/.mcp.json is unaffected.
|
|
85
|
+
/.mcp.json
|
|
86
|
+
|
|
87
|
+
# Compiled email templates are committed (read at runtime by ama.modules.email.templates;
|
|
88
|
+
# the prod image copies packages/ without running a pnpm build)
|
|
89
|
+
!packages/email-templates/dist/
|
|
90
|
+
!packages/email-templates/dist/**
|
|
91
|
+
|
|
92
|
+
# TypeScript incremental build artifacts
|
|
93
|
+
*.tsbuildinfo
|
|
94
|
+
.design-sync/sb-reference/
|
|
95
|
+
.design-sync/learnings/
|
|
96
|
+
.design-sync/.cache/
|
|
97
|
+
.design-sync/node_modules
|
|
98
|
+
.ds-sync/
|
|
99
|
+
ds-bundle/
|
ama_cli-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ama-cli
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Connect your coding agent to Ama.
|
|
5
|
+
Classifier: Development Status :: 3 - Alpha
|
|
6
|
+
Classifier: Environment :: Console
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: rich>=13.0
|
|
13
|
+
Requires-Dist: typer>=0.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# ama
|
|
17
|
+
|
|
18
|
+
Connect your coding agent to [Ama](https://ama.dev).
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
curl -fsSL https://ama.dev/install | sh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
That installs the CLI and runs `ama onboard`. Three questions and you are done:
|
|
25
|
+
|
|
26
|
+
1. **Sign in** — in your browser.
|
|
27
|
+
2. **Confirm** — which agents, and where.
|
|
28
|
+
3. **Start now?** — opens your agent on a live notebook.
|
|
29
|
+
|
|
30
|
+
Already have the CLI?
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ama onboard
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Re-running either line upgrades.
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
| Command | Does |
|
|
41
|
+
|---|---|
|
|
42
|
+
| `ama onboard` | Sign in, detect agents, register MCP. Start here. |
|
|
43
|
+
| `ama detect` | Show which coding agents were found, and why. |
|
|
44
|
+
| `ama login` / `logout` / `whoami` | Manage credentials. |
|
|
45
|
+
| `ama mcp add` / `list` / `remove` | Manage MCP registration directly. |
|
|
46
|
+
|
|
47
|
+
## Supported agents
|
|
48
|
+
|
|
49
|
+
Registered automatically:
|
|
50
|
+
|
|
51
|
+
| Agent | |
|
|
52
|
+
|---|---|
|
|
53
|
+
| Claude Code | `claude mcp add -s user` |
|
|
54
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
55
|
+
| Codex CLI | `codex mcp add` — also export `AMA_API_KEY`; Codex stores the variable name, not the token |
|
|
56
|
+
| Gemini CLI | `gemini mcp add` |
|
|
57
|
+
| VS Code (Copilot) | `code --add-mcp` |
|
|
58
|
+
|
|
59
|
+
**Detected but not yet registered automatically:** Windsurf, Zed, Cline, Roo
|
|
60
|
+
Code. `ama onboard` tells you rather than pretending, and prints what to paste.
|
|
61
|
+
|
|
62
|
+
`ama detect` explains what it found and where, so you can check before anything
|
|
63
|
+
is written. `ama onboard --dry-run` prints the exact diff for every file it
|
|
64
|
+
would touch — and signs in to nothing, so no key is created.
|
|
65
|
+
|
|
66
|
+
## Self-hosted
|
|
67
|
+
|
|
68
|
+
Point at your own deployment; the CLI reads `/.well-known/mcp.json` for the
|
|
69
|
+
endpoint and auth method rather than assuming anything.
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
ama onboard --host https://ama.internal.example.com
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Install without the script
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
uv tool install ama-cli # binary is `ama`
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Notes
|
|
82
|
+
|
|
83
|
+
This is the public client. It speaks HTTP to a hosted or self-hosted Ama and
|
|
84
|
+
carries no server dependencies — `typer` and `rich`, nothing else.
|
|
85
|
+
|
|
86
|
+
Your API key is stored in `~/.ama/credentials.json`, mode `0600`. The CLI never
|
|
87
|
+
writes a literal token into a project-scoped config file, since those get
|
|
88
|
+
committed; project scope uses environment-variable indirection instead.
|
|
89
|
+
|
|
90
|
+
Licensed under Apache-2.0.
|
ama_cli-0.1.0/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ama
|
|
2
|
+
|
|
3
|
+
Connect your coding agent to [Ama](https://ama.dev).
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
curl -fsSL https://ama.dev/install | sh
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
That installs the CLI and runs `ama onboard`. Three questions and you are done:
|
|
10
|
+
|
|
11
|
+
1. **Sign in** — in your browser.
|
|
12
|
+
2. **Confirm** — which agents, and where.
|
|
13
|
+
3. **Start now?** — opens your agent on a live notebook.
|
|
14
|
+
|
|
15
|
+
Already have the CLI?
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
ama onboard
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Re-running either line upgrades.
|
|
22
|
+
|
|
23
|
+
## Commands
|
|
24
|
+
|
|
25
|
+
| Command | Does |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `ama onboard` | Sign in, detect agents, register MCP. Start here. |
|
|
28
|
+
| `ama detect` | Show which coding agents were found, and why. |
|
|
29
|
+
| `ama login` / `logout` / `whoami` | Manage credentials. |
|
|
30
|
+
| `ama mcp add` / `list` / `remove` | Manage MCP registration directly. |
|
|
31
|
+
|
|
32
|
+
## Supported agents
|
|
33
|
+
|
|
34
|
+
Registered automatically:
|
|
35
|
+
|
|
36
|
+
| Agent | |
|
|
37
|
+
|---|---|
|
|
38
|
+
| Claude Code | `claude mcp add -s user` |
|
|
39
|
+
| Cursor | `~/.cursor/mcp.json` |
|
|
40
|
+
| Codex CLI | `codex mcp add` — also export `AMA_API_KEY`; Codex stores the variable name, not the token |
|
|
41
|
+
| Gemini CLI | `gemini mcp add` |
|
|
42
|
+
| VS Code (Copilot) | `code --add-mcp` |
|
|
43
|
+
|
|
44
|
+
**Detected but not yet registered automatically:** Windsurf, Zed, Cline, Roo
|
|
45
|
+
Code. `ama onboard` tells you rather than pretending, and prints what to paste.
|
|
46
|
+
|
|
47
|
+
`ama detect` explains what it found and where, so you can check before anything
|
|
48
|
+
is written. `ama onboard --dry-run` prints the exact diff for every file it
|
|
49
|
+
would touch — and signs in to nothing, so no key is created.
|
|
50
|
+
|
|
51
|
+
## Self-hosted
|
|
52
|
+
|
|
53
|
+
Point at your own deployment; the CLI reads `/.well-known/mcp.json` for the
|
|
54
|
+
endpoint and auth method rather than assuming anything.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
ama onboard --host https://ama.internal.example.com
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Install without the script
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
uv tool install ama-cli # binary is `ama`
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Notes
|
|
67
|
+
|
|
68
|
+
This is the public client. It speaks HTTP to a hosted or self-hosted Ama and
|
|
69
|
+
carries no server dependencies — `typer` and `rich`, nothing else.
|
|
70
|
+
|
|
71
|
+
Your API key is stored in `~/.ama/credentials.json`, mode `0600`. The CLI never
|
|
72
|
+
writes a literal token into a project-scoped config file, since those get
|
|
73
|
+
committed; project scope uses environment-variable indirection instead.
|
|
74
|
+
|
|
75
|
+
Licensed under Apache-2.0.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "ama-cli"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Connect your coding agent to Ama."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
# Deliberately thin. This package is on the install path of a one-line
|
|
8
|
+
# onboarding command, so every dependency is weight a new user waits for.
|
|
9
|
+
# It must never depend on ama-core (enforced by .importlinter) -- that is the
|
|
10
|
+
# server, and it is unpublishable anyway (path + git sources).
|
|
11
|
+
dependencies = [
|
|
12
|
+
"typer>=0.9",
|
|
13
|
+
"rich>=13.0",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.scripts]
|
|
25
|
+
ama = "ama_cli.cli:app"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/ama_cli"]
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"pytest>=8.0",
|
|
37
|
+
"ruff>=0.8",
|
|
38
|
+
"ty>=0.0.1a8",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[tool.ruff]
|
|
42
|
+
line-length = 100
|
|
43
|
+
target-version = "py311"
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Coding-agent detection and MCP registration."""
|
|
2
|
+
|
|
3
|
+
from ama_cli.agents.base import (
|
|
4
|
+
AgentSpec,
|
|
5
|
+
Confidence,
|
|
6
|
+
Detection,
|
|
7
|
+
Env,
|
|
8
|
+
Platform,
|
|
9
|
+
Signal,
|
|
10
|
+
SignalKind,
|
|
11
|
+
)
|
|
12
|
+
from ama_cli.agents.registry import REGISTRY, detect_all, get_spec
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"REGISTRY",
|
|
16
|
+
"AgentSpec",
|
|
17
|
+
"Confidence",
|
|
18
|
+
"Detection",
|
|
19
|
+
"Env",
|
|
20
|
+
"Platform",
|
|
21
|
+
"Signal",
|
|
22
|
+
"SignalKind",
|
|
23
|
+
"detect_all",
|
|
24
|
+
"get_spec",
|
|
25
|
+
]
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""The adapter registry.
|
|
2
|
+
|
|
3
|
+
v1 covers Claude Code, Cursor, Codex, Gemini CLI and VS Code. Windsurf, Zed,
|
|
4
|
+
Cline and Roo are detected but not yet registerable — adapters are cheap once
|
|
5
|
+
the seam exists, but each needs testing against a real install, and that is the
|
|
6
|
+
actual cost. `ama mcp add` says so rather than failing obscurely.
|
|
7
|
+
|
|
8
|
+
Zed is the only remaining target needing a comment-preserving (JSONC) writer;
|
|
9
|
+
every v1 agent is either CLI-backed or plain JSON.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from ama_cli.agents.adapters.cli_backed import (
|
|
15
|
+
ClaudeCodeAdapter,
|
|
16
|
+
CodexAdapter,
|
|
17
|
+
GeminiAdapter,
|
|
18
|
+
VSCodeAdapter,
|
|
19
|
+
)
|
|
20
|
+
from ama_cli.agents.adapters.cursor import CursorAdapter
|
|
21
|
+
from ama_cli.agents.mcp import AgentAdapter
|
|
22
|
+
|
|
23
|
+
ADAPTERS: tuple[AgentAdapter, ...] = (
|
|
24
|
+
ClaudeCodeAdapter(),
|
|
25
|
+
CursorAdapter(),
|
|
26
|
+
CodexAdapter(),
|
|
27
|
+
GeminiAdapter(),
|
|
28
|
+
VSCodeAdapter(),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
ADAPTERS_BY_ID: dict[str, AgentAdapter] = {a.spec.id: a for a in ADAPTERS}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_adapter(agent_id: str) -> AgentAdapter | None:
|
|
35
|
+
"""Look up an adapter by agent id, or None when the agent has no adapter yet."""
|
|
36
|
+
return ADAPTERS_BY_ID.get(agent_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"ADAPTERS",
|
|
41
|
+
"ADAPTERS_BY_ID",
|
|
42
|
+
"ClaudeCodeAdapter",
|
|
43
|
+
"CodexAdapter",
|
|
44
|
+
"CursorAdapter",
|
|
45
|
+
"GeminiAdapter",
|
|
46
|
+
"VSCodeAdapter",
|
|
47
|
+
"get_adapter",
|
|
48
|
+
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Adapters for agents that ship their own non-interactive MCP CLI.
|
|
2
|
+
|
|
3
|
+
Preferred over writing config files: the vendor's CLI owns the dialect, so it
|
|
4
|
+
insulates us from schema and path churn. It matters most for VS Code, whose docs
|
|
5
|
+
describe the user `mcp.json` as living "in your user profile" but never state OS
|
|
6
|
+
paths — and those paths break under named profiles, Insiders and portable
|
|
7
|
+
installs anyway.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from ama_cli.agents.base import Env
|
|
13
|
+
from ama_cli.agents.mcp import (
|
|
14
|
+
Plan,
|
|
15
|
+
RemoteServer,
|
|
16
|
+
RunCommand,
|
|
17
|
+
Scope,
|
|
18
|
+
unsupported_plan,
|
|
19
|
+
)
|
|
20
|
+
from ama_cli.agents.registry import CLAUDE_CODE, CODEX, GEMINI_CLI, VSCODE
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _replace_first(argv: tuple[str, ...]) -> RunCommand:
|
|
24
|
+
"""A pre-emptive remove, so a re-run replaces rather than erroring.
|
|
25
|
+
|
|
26
|
+
None of these CLIs have a --force, and `mcp add` errors when the server
|
|
27
|
+
already exists -- so re-running onboard would fail without this. Replace
|
|
28
|
+
rather than skip: someone re-onboarding after rotating their key must end up
|
|
29
|
+
with the new token, not silently keep the old one.
|
|
30
|
+
|
|
31
|
+
Tolerates failure because on a clean machine there is nothing to remove.
|
|
32
|
+
"""
|
|
33
|
+
return RunCommand(argv, tolerate_failure=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ClaudeCodeAdapter:
|
|
37
|
+
"""Claude Code, via `claude mcp add`.
|
|
38
|
+
|
|
39
|
+
Scopes are Claude's own: `local` (default -- this project only), `project`
|
|
40
|
+
(.mcp.json, VCS-shared), `user` (all projects). We map GLOBAL -> user.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
spec = CLAUDE_CODE
|
|
44
|
+
|
|
45
|
+
def plan_add(self, server: RemoteServer, scope: Scope, env: Env) -> Plan:
|
|
46
|
+
if not env.which("claude"):
|
|
47
|
+
return unsupported_plan(self.spec.id, "claude is not on PATH")
|
|
48
|
+
|
|
49
|
+
if scope is Scope.PROJECT:
|
|
50
|
+
# Claude expands ${VAR} and ${VAR:-default} in url and headers, so
|
|
51
|
+
# .mcp.json can be committed without leaking the token.
|
|
52
|
+
auth = server.authorization(scope, env_ref=f"${{{server.token_env_var}}}")
|
|
53
|
+
else:
|
|
54
|
+
auth = server.authorization(scope)
|
|
55
|
+
|
|
56
|
+
# `-s user` is required for global: `claude mcp add` defaults to
|
|
57
|
+
# `-s local`, which would scope the server to this directory only.
|
|
58
|
+
claude_scope = "project" if scope is Scope.PROJECT else "user"
|
|
59
|
+
argv = (
|
|
60
|
+
"claude",
|
|
61
|
+
"mcp",
|
|
62
|
+
"add",
|
|
63
|
+
"--transport",
|
|
64
|
+
"http",
|
|
65
|
+
"-s",
|
|
66
|
+
claude_scope,
|
|
67
|
+
server.name,
|
|
68
|
+
server.url,
|
|
69
|
+
"--header",
|
|
70
|
+
f"Authorization: {auth}",
|
|
71
|
+
)
|
|
72
|
+
return Plan(
|
|
73
|
+
self.spec.id,
|
|
74
|
+
steps=(
|
|
75
|
+
_replace_first(("claude", "mcp", "remove", "-s", claude_scope, server.name)),
|
|
76
|
+
RunCommand(argv, secret=server.token),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def plan_remove(self, name: str, scope: Scope, env: Env) -> Plan:
|
|
81
|
+
if not env.which("claude"):
|
|
82
|
+
return unsupported_plan(self.spec.id, "claude is not on PATH")
|
|
83
|
+
claude_scope = "project" if scope is Scope.PROJECT else "user"
|
|
84
|
+
return Plan(
|
|
85
|
+
self.spec.id,
|
|
86
|
+
steps=(RunCommand(("claude", "mcp", "remove", "-s", claude_scope, name)),),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
def list_servers(self, scope: Scope, env: Env) -> list[str]:
|
|
90
|
+
return _list_via_cli(("claude", "mcp", "list"), env, "claude")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class GeminiAdapter:
|
|
94
|
+
"""Gemini CLI, via `gemini mcp add`.
|
|
95
|
+
|
|
96
|
+
Gemini distinguishes transport by *key*: `httpUrl` is streamable HTTP while
|
|
97
|
+
`url` is SSE. Using `url` for an HTTP endpoint is the classic Gemini
|
|
98
|
+
misconfiguration -- `--transport http` is what keeps us on the right one.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
spec = GEMINI_CLI
|
|
102
|
+
|
|
103
|
+
def plan_add(self, server: RemoteServer, scope: Scope, env: Env) -> Plan:
|
|
104
|
+
if not env.which("gemini"):
|
|
105
|
+
return unsupported_plan(self.spec.id, "gemini is not on PATH")
|
|
106
|
+
|
|
107
|
+
auth = (
|
|
108
|
+
server.authorization(scope, env_ref=f"${{{server.token_env_var}}}")
|
|
109
|
+
if scope is Scope.PROJECT
|
|
110
|
+
else server.authorization(scope)
|
|
111
|
+
)
|
|
112
|
+
argv = (
|
|
113
|
+
"gemini",
|
|
114
|
+
"mcp",
|
|
115
|
+
"add",
|
|
116
|
+
"--transport",
|
|
117
|
+
"http",
|
|
118
|
+
"-s",
|
|
119
|
+
"project" if scope is Scope.PROJECT else "user",
|
|
120
|
+
"--header",
|
|
121
|
+
f"Authorization: {auth}",
|
|
122
|
+
server.name,
|
|
123
|
+
server.url,
|
|
124
|
+
)
|
|
125
|
+
# Deliberately not passing --trust: it bypasses tool-call confirmation,
|
|
126
|
+
# which is not ours to switch off silently.
|
|
127
|
+
return Plan(
|
|
128
|
+
self.spec.id,
|
|
129
|
+
steps=(
|
|
130
|
+
_replace_first(("gemini", "mcp", "remove", server.name)),
|
|
131
|
+
RunCommand(argv, secret=server.token),
|
|
132
|
+
),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def plan_remove(self, name: str, scope: Scope, env: Env) -> Plan:
|
|
136
|
+
if not env.which("gemini"):
|
|
137
|
+
return unsupported_plan(self.spec.id, "gemini is not on PATH")
|
|
138
|
+
return Plan(self.spec.id, steps=(RunCommand(("gemini", "mcp", "remove", name)),))
|
|
139
|
+
|
|
140
|
+
def list_servers(self, scope: Scope, env: Env) -> list[str]:
|
|
141
|
+
return _list_via_cli(("gemini", "mcp", "list"), env, "gemini")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class CodexAdapter:
|
|
145
|
+
"""Codex CLI, via `codex mcp add`.
|
|
146
|
+
|
|
147
|
+
Two things make Codex the odd one out:
|
|
148
|
+
|
|
149
|
+
1. It is global-only -- no project config exists.
|
|
150
|
+
2. Bearer tokens are env-var-indirected *by design*: it stores the variable
|
|
151
|
+
name, never the token. So registration alone is not enough -- AMA_API_KEY
|
|
152
|
+
must also be exported in the user's shell, or the server silently fails
|
|
153
|
+
to authenticate.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
spec = CODEX
|
|
157
|
+
|
|
158
|
+
def plan_add(self, server: RemoteServer, scope: Scope, env: Env) -> Plan:
|
|
159
|
+
if not env.which("codex"):
|
|
160
|
+
return unsupported_plan(self.spec.id, "codex is not on PATH")
|
|
161
|
+
if scope is Scope.PROJECT:
|
|
162
|
+
return unsupported_plan(
|
|
163
|
+
self.spec.id, "Codex has no project-level config; it is global-only"
|
|
164
|
+
)
|
|
165
|
+
argv = (
|
|
166
|
+
"codex",
|
|
167
|
+
"mcp",
|
|
168
|
+
"add",
|
|
169
|
+
server.name,
|
|
170
|
+
"--url",
|
|
171
|
+
server.url,
|
|
172
|
+
"--bearer-token-env-var",
|
|
173
|
+
server.token_env_var,
|
|
174
|
+
)
|
|
175
|
+
# No secret= : the token never reaches this command by design.
|
|
176
|
+
return Plan(
|
|
177
|
+
self.spec.id,
|
|
178
|
+
steps=(
|
|
179
|
+
_replace_first(("codex", "mcp", "remove", server.name)),
|
|
180
|
+
RunCommand(argv),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def plan_remove(self, name: str, scope: Scope, env: Env) -> Plan:
|
|
185
|
+
if not env.which("codex"):
|
|
186
|
+
return unsupported_plan(self.spec.id, "codex is not on PATH")
|
|
187
|
+
return Plan(self.spec.id, steps=(RunCommand(("codex", "mcp", "remove", name)),))
|
|
188
|
+
|
|
189
|
+
def list_servers(self, scope: Scope, env: Env) -> list[str]:
|
|
190
|
+
return _list_via_cli(("codex", "mcp", "list"), env, "codex")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class VSCodeAdapter:
|
|
194
|
+
"""VS Code / Copilot, via `code --add-mcp`.
|
|
195
|
+
|
|
196
|
+
VS Code's root key is `servers`, not `mcpServers` -- the single biggest
|
|
197
|
+
schema divergence in the ecosystem. We never write that file: `--add-mcp`
|
|
198
|
+
does, and it knows where the user's profile actually is.
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
spec = VSCODE
|
|
202
|
+
|
|
203
|
+
def _binary(self, env: Env) -> str | None:
|
|
204
|
+
for candidate in ("code", "code-insiders"):
|
|
205
|
+
if env.which(candidate):
|
|
206
|
+
return candidate
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
def plan_add(self, server: RemoteServer, scope: Scope, env: Env) -> Plan:
|
|
210
|
+
binary = self._binary(env)
|
|
211
|
+
if not binary:
|
|
212
|
+
return unsupported_plan(self.spec.id, "code is not on PATH")
|
|
213
|
+
if scope is Scope.PROJECT:
|
|
214
|
+
return unsupported_plan(
|
|
215
|
+
self.spec.id,
|
|
216
|
+
"`code --add-mcp` writes user scope only; project scope needs .vscode/mcp.json",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
import json as _json
|
|
220
|
+
|
|
221
|
+
payload = _json.dumps(
|
|
222
|
+
{
|
|
223
|
+
"name": server.name,
|
|
224
|
+
"type": "http",
|
|
225
|
+
"url": server.url,
|
|
226
|
+
"headers": {"Authorization": server.authorization(scope)},
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
return Plan(
|
|
230
|
+
self.spec.id,
|
|
231
|
+
steps=(RunCommand((binary, "--add-mcp", payload), secret=server.token),),
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def plan_remove(self, name: str, scope: Scope, env: Env) -> Plan:
|
|
235
|
+
return unsupported_plan(
|
|
236
|
+
self.spec.id,
|
|
237
|
+
"VS Code has no MCP remove command; use the MCP: Open User Configuration command",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
def list_servers(self, scope: Scope, env: Env) -> list[str]:
|
|
241
|
+
# No documented list command; the config lives at a path VS Code's docs
|
|
242
|
+
# never state. Reporting nothing beats guessing wrong.
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _list_via_cli(argv: tuple[str, ...], env: Env, binary: str) -> list[str]:
|
|
247
|
+
"""Parse server names out of an agent's `mcp list` output.
|
|
248
|
+
|
|
249
|
+
Best-effort by construction: these are human-readable listings with no
|
|
250
|
+
documented machine format, so this reports what it can recognise and stays
|
|
251
|
+
quiet otherwise rather than inventing structure.
|
|
252
|
+
"""
|
|
253
|
+
import re
|
|
254
|
+
import subprocess
|
|
255
|
+
|
|
256
|
+
if not env.which(binary):
|
|
257
|
+
return []
|
|
258
|
+
try:
|
|
259
|
+
proc = subprocess.run(argv, capture_output=True, text=True, timeout=20)
|
|
260
|
+
except (OSError, subprocess.SubprocessError):
|
|
261
|
+
return []
|
|
262
|
+
if proc.returncode != 0:
|
|
263
|
+
return []
|
|
264
|
+
|
|
265
|
+
names: list[str] = []
|
|
266
|
+
for line in proc.stdout.splitlines():
|
|
267
|
+
# Matches the common "name: url" / "name url" listing shapes.
|
|
268
|
+
m = re.match(r"^\s*([A-Za-z0-9_-]+)\s*[::]\s*\S", line)
|
|
269
|
+
if m:
|
|
270
|
+
names.append(m.group(1))
|
|
271
|
+
return names
|