r2mcp 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.
- r2mcp-0.1.0/PKG-INFO +41 -0
- r2mcp-0.1.0/README.md +30 -0
- r2mcp-0.1.0/pyproject.toml +29 -0
- r2mcp-0.1.0/r2mcp.egg-info/PKG-INFO +41 -0
- r2mcp-0.1.0/r2mcp.egg-info/SOURCES.txt +18 -0
- r2mcp-0.1.0/r2mcp.egg-info/dependency_links.txt +1 -0
- r2mcp-0.1.0/r2mcp.egg-info/entry_points.txt +2 -0
- r2mcp-0.1.0/r2mcp.egg-info/requires.txt +3 -0
- r2mcp-0.1.0/r2mcp.egg-info/top_level.txt +1 -0
- r2mcp-0.1.0/r2mcp_cli/__init__.py +0 -0
- r2mcp-0.1.0/r2mcp_cli/api_client.py +108 -0
- r2mcp-0.1.0/r2mcp_cli/commands/__init__.py +0 -0
- r2mcp-0.1.0/r2mcp_cli/commands/auth.py +149 -0
- r2mcp-0.1.0/r2mcp_cli/commands/link.py +186 -0
- r2mcp-0.1.0/r2mcp_cli/commands/logs.py +66 -0
- r2mcp-0.1.0/r2mcp_cli/commands/servers.py +261 -0
- r2mcp-0.1.0/r2mcp_cli/commands/tools.py +112 -0
- r2mcp-0.1.0/r2mcp_cli/config.py +67 -0
- r2mcp-0.1.0/r2mcp_cli/main.py +34 -0
- r2mcp-0.1.0/setup.cfg +4 -0
r2mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: r2mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI para gestao de servidores MCP via rest2mcp gateway
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: typer>=0.12.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: httpx
|
|
11
|
+
|
|
12
|
+
# r2mcp
|
|
13
|
+
|
|
14
|
+
CLI para gestão de servidores MCP via rest2mcp gateway.
|
|
15
|
+
|
|
16
|
+
## Instalação
|
|
17
|
+
|
|
18
|
+
Requer Python 3.10+ e pipx.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pipx install r2mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Uso
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
r2mcp login
|
|
28
|
+
r2mcp config set-env prod
|
|
29
|
+
r2mcp servers create --name my-api --spec-url https://example.com/openapi.json
|
|
30
|
+
r2mcp tools list <server_id>
|
|
31
|
+
r2mcp tools call <server_id> <tool_name> --args '{"key": "value"}'
|
|
32
|
+
r2mcp link cursor <server_id>
|
|
33
|
+
r2mcp --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Desenvolvimento
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -e .
|
|
40
|
+
python -m build
|
|
41
|
+
```
|
r2mcp-0.1.0/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# r2mcp
|
|
2
|
+
|
|
3
|
+
CLI para gestão de servidores MCP via rest2mcp gateway.
|
|
4
|
+
|
|
5
|
+
## Instalação
|
|
6
|
+
|
|
7
|
+
Requer Python 3.10+ e pipx.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install r2mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Uso
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
r2mcp login
|
|
17
|
+
r2mcp config set-env prod
|
|
18
|
+
r2mcp servers create --name my-api --spec-url https://example.com/openapi.json
|
|
19
|
+
r2mcp tools list <server_id>
|
|
20
|
+
r2mcp tools call <server_id> <tool_name> --args '{"key": "value"}'
|
|
21
|
+
r2mcp link cursor <server_id>
|
|
22
|
+
r2mcp --help
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Desenvolvimento
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install -e .
|
|
29
|
+
python -m build
|
|
30
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "r2mcp"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CLI para gestao de servidores MCP via rest2mcp gateway"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"typer>=0.12.0",
|
|
14
|
+
"rich>=13.0.0",
|
|
15
|
+
"httpx",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
r2mcp = "r2mcp_cli.main:app"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
include = ["r2mcp_cli", "r2mcp_cli.*"]
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
target-version = "py310"
|
|
26
|
+
line-length = 120
|
|
27
|
+
|
|
28
|
+
[tool.ruff.lint]
|
|
29
|
+
select = ["E", "F", "W", "I"]
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: r2mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CLI para gestao de servidores MCP via rest2mcp gateway
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: typer>=0.12.0
|
|
9
|
+
Requires-Dist: rich>=13.0.0
|
|
10
|
+
Requires-Dist: httpx
|
|
11
|
+
|
|
12
|
+
# r2mcp
|
|
13
|
+
|
|
14
|
+
CLI para gestão de servidores MCP via rest2mcp gateway.
|
|
15
|
+
|
|
16
|
+
## Instalação
|
|
17
|
+
|
|
18
|
+
Requer Python 3.10+ e pipx.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pipx install r2mcp
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Uso
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
r2mcp login
|
|
28
|
+
r2mcp config set-env prod
|
|
29
|
+
r2mcp servers create --name my-api --spec-url https://example.com/openapi.json
|
|
30
|
+
r2mcp tools list <server_id>
|
|
31
|
+
r2mcp tools call <server_id> <tool_name> --args '{"key": "value"}'
|
|
32
|
+
r2mcp link cursor <server_id>
|
|
33
|
+
r2mcp --help
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Desenvolvimento
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install -e .
|
|
40
|
+
python -m build
|
|
41
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
r2mcp.egg-info/PKG-INFO
|
|
4
|
+
r2mcp.egg-info/SOURCES.txt
|
|
5
|
+
r2mcp.egg-info/dependency_links.txt
|
|
6
|
+
r2mcp.egg-info/entry_points.txt
|
|
7
|
+
r2mcp.egg-info/requires.txt
|
|
8
|
+
r2mcp.egg-info/top_level.txt
|
|
9
|
+
r2mcp_cli/__init__.py
|
|
10
|
+
r2mcp_cli/api_client.py
|
|
11
|
+
r2mcp_cli/config.py
|
|
12
|
+
r2mcp_cli/main.py
|
|
13
|
+
r2mcp_cli/commands/__init__.py
|
|
14
|
+
r2mcp_cli/commands/auth.py
|
|
15
|
+
r2mcp_cli/commands/link.py
|
|
16
|
+
r2mcp_cli/commands/logs.py
|
|
17
|
+
r2mcp_cli/commands/servers.py
|
|
18
|
+
r2mcp_cli/commands/tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
r2mcp_cli
|
|
File without changes
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from r2mcp_cli.config import get_base_url, get_token
|
|
5
|
+
|
|
6
|
+
console = Console()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class APIError(Exception):
|
|
10
|
+
def __init__(self, status_code: int, detail: str):
|
|
11
|
+
self.status_code = status_code
|
|
12
|
+
self.detail = detail
|
|
13
|
+
super().__init__(f"HTTP {status_code}: {detail}")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class APIClient:
|
|
17
|
+
def __init__(self):
|
|
18
|
+
self.base_url = get_base_url()
|
|
19
|
+
self._client = httpx.Client(timeout=30.0)
|
|
20
|
+
|
|
21
|
+
def _headers(self) -> dict:
|
|
22
|
+
headers = {"Content-Type": "application/json"}
|
|
23
|
+
token = get_token()
|
|
24
|
+
if token:
|
|
25
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
26
|
+
return headers
|
|
27
|
+
|
|
28
|
+
def _request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
29
|
+
url = f"{self.base_url}{path}"
|
|
30
|
+
try:
|
|
31
|
+
resp = self._client.request(method, url, headers=self._headers(), **kwargs)
|
|
32
|
+
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException) as e:
|
|
33
|
+
raise APIError(0, f"Erro de conexao: {e}")
|
|
34
|
+
if resp.status_code >= 400:
|
|
35
|
+
detail = self._extract_detail(resp)
|
|
36
|
+
raise APIError(resp.status_code, detail)
|
|
37
|
+
return resp
|
|
38
|
+
|
|
39
|
+
def _extract_detail(self, resp: httpx.Response) -> str:
|
|
40
|
+
try:
|
|
41
|
+
body = resp.json()
|
|
42
|
+
if isinstance(body, dict):
|
|
43
|
+
return body.get("detail", resp.text)
|
|
44
|
+
return resp.text
|
|
45
|
+
except Exception:
|
|
46
|
+
return resp.text
|
|
47
|
+
|
|
48
|
+
def get_me(self) -> dict:
|
|
49
|
+
resp = self._request("GET", "/v1/me")
|
|
50
|
+
return resp.json()
|
|
51
|
+
|
|
52
|
+
def list_servers(self) -> list[dict]:
|
|
53
|
+
resp = self._request("GET", "/v1/servers")
|
|
54
|
+
return resp.json()
|
|
55
|
+
|
|
56
|
+
def create_server(self, name: str, spec_url: str, transport: str = "sse") -> dict:
|
|
57
|
+
resp = self._request("POST", "/v1/servers", json={"name": name, "spec_url": spec_url, "transport": transport})
|
|
58
|
+
return resp.json()
|
|
59
|
+
|
|
60
|
+
def update_server(self, server_id: str, data: dict) -> dict:
|
|
61
|
+
resp = self._request("PATCH", f"/v1/servers/{server_id}", json=data)
|
|
62
|
+
return resp.json()
|
|
63
|
+
|
|
64
|
+
def delete_server(self, server_id: str):
|
|
65
|
+
self._request("DELETE", f"/v1/servers/{server_id}")
|
|
66
|
+
|
|
67
|
+
def list_tools(self, server_id: str) -> list[dict]:
|
|
68
|
+
resp = self._request("GET", f"/v1/servers/{server_id}/tools")
|
|
69
|
+
data = resp.json()
|
|
70
|
+
return data.get("tools", [])
|
|
71
|
+
|
|
72
|
+
def call_tool(self, server_id: str, tool_name: str, arguments: dict) -> dict:
|
|
73
|
+
resp = self._request(
|
|
74
|
+
"POST",
|
|
75
|
+
f"/v1/servers/{server_id}/tools/call",
|
|
76
|
+
json={"name": tool_name, "arguments": arguments},
|
|
77
|
+
)
|
|
78
|
+
return resp.json()
|
|
79
|
+
|
|
80
|
+
def get_logs(self, server_id: str, limit: int = 20) -> list[dict]:
|
|
81
|
+
resp = self._request("GET", f"/v1/servers/{server_id}/logs", params={"limit": limit})
|
|
82
|
+
return resp.json()
|
|
83
|
+
|
|
84
|
+
def get_auth_status(self, server_id: str) -> dict:
|
|
85
|
+
resp = self._request("GET", f"/v1/servers/{server_id}/auth")
|
|
86
|
+
return resp.json()
|
|
87
|
+
|
|
88
|
+
def get_server_health(self, server_id: str) -> dict:
|
|
89
|
+
resp = self._request("GET", f"/v1/servers/{server_id}/health")
|
|
90
|
+
return resp.json()
|
|
91
|
+
|
|
92
|
+
def login_server(self, server_id: str, credentials: dict) -> dict:
|
|
93
|
+
resp = self._request("POST", f"/v1/servers/{server_id}/auth/login", json=credentials)
|
|
94
|
+
return resp.json()
|
|
95
|
+
|
|
96
|
+
def set_credentials(self, server_id: str, credentials: dict) -> dict:
|
|
97
|
+
resp = self._request("PUT", f"/v1/servers/{server_id}/auth/credentials", json=credentials)
|
|
98
|
+
return resp.json()
|
|
99
|
+
|
|
100
|
+
def check_credentials(self, server_id: str) -> dict:
|
|
101
|
+
resp = self._request("GET", f"/v1/servers/{server_id}/auth/credentials")
|
|
102
|
+
return resp.json()
|
|
103
|
+
|
|
104
|
+
def delete_credentials(self, server_id: str):
|
|
105
|
+
self._request("DELETE", f"/v1/servers/{server_id}/auth/credentials")
|
|
106
|
+
|
|
107
|
+
def close(self):
|
|
108
|
+
self._client.close()
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich import box
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from r2mcp_cli.api_client import APIClient, APIError
|
|
7
|
+
from r2mcp_cli.config import get_base_url, get_token, set_base_url, set_token, clear_token, validate_token
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
config_app = typer.Typer(help="Gerir configuracao local")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def login():
|
|
14
|
+
token = get_token()
|
|
15
|
+
if token:
|
|
16
|
+
ok, _ = validate_token(token)
|
|
17
|
+
if not ok:
|
|
18
|
+
console.print(
|
|
19
|
+
"[yellow]Token expirado. Faz logout primeiro, obtém um novo em https://rest2mcp.pages.dev/ e faz login novamente.[/yellow]"
|
|
20
|
+
)
|
|
21
|
+
return
|
|
22
|
+
console.print("[yellow]Ja estas autenticado. Faz logout primeiro se quiseres mudar de conta.[/yellow]")
|
|
23
|
+
return
|
|
24
|
+
|
|
25
|
+
token = typer.prompt("Introduz o teu JWT do Supabase (obtem em https://rest2mcp.pages.dev/)", hide_input=True)
|
|
26
|
+
if not token:
|
|
27
|
+
console.print("[red]Token invalido.[/red]")
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
ok, msg = validate_token(token)
|
|
31
|
+
if not ok:
|
|
32
|
+
console.print(f"[red]Token invalido: {msg}[/red]")
|
|
33
|
+
raise typer.Exit(1)
|
|
34
|
+
|
|
35
|
+
set_token(token)
|
|
36
|
+
|
|
37
|
+
with console.status("[bold green]A testar conexao..."):
|
|
38
|
+
try:
|
|
39
|
+
client = APIClient()
|
|
40
|
+
me = client.get_me()
|
|
41
|
+
client.close()
|
|
42
|
+
except APIError as e:
|
|
43
|
+
clear_token()
|
|
44
|
+
console.print(f"[red]Erro na autenticacao: {e.detail}. Obtém um token em https://rest2mcp.pages.dev/[/red]")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
clear_token()
|
|
48
|
+
console.print(f"[red]Erro de conexao: {e}. Obtém um token em https://rest2mcp.pages.dev/[/red]")
|
|
49
|
+
raise typer.Exit(1)
|
|
50
|
+
|
|
51
|
+
table = Table(box=box.SIMPLE)
|
|
52
|
+
table.add_column("Campo", style="bold cyan")
|
|
53
|
+
table.add_column("Valor")
|
|
54
|
+
table.add_row("Email", me.get("email", "-"))
|
|
55
|
+
table.add_row("Plano", me.get("plan_tier", "free"))
|
|
56
|
+
table.add_row("Servidores", f"{me.get('servers_count', 0)} / {me.get('servers_limit', 1)}")
|
|
57
|
+
console.print(table)
|
|
58
|
+
console.print("[green]Autenticado com sucesso![/green]")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def logout():
|
|
62
|
+
if not get_token():
|
|
63
|
+
console.print("[yellow]Nao estás autenticado.[/yellow]")
|
|
64
|
+
return
|
|
65
|
+
clear_token()
|
|
66
|
+
console.print("[green]Token removido.[/green]")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def me():
|
|
70
|
+
token = get_token()
|
|
71
|
+
if not token:
|
|
72
|
+
console.print(
|
|
73
|
+
"[red]ERRO: Nao autenticado. Obtém um token em https://rest2mcp.pages.dev/ e corre r2mcp login.[/red]"
|
|
74
|
+
)
|
|
75
|
+
raise typer.Exit(1)
|
|
76
|
+
ok, msg = validate_token(token)
|
|
77
|
+
if not ok:
|
|
78
|
+
console.print(f"[red]ERRO: {msg}. Obtém um token novo em https://rest2mcp.pages.dev/[/red]")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
with console.status("[bold green]A obter informacoes do utilizador..."):
|
|
82
|
+
try:
|
|
83
|
+
client = APIClient()
|
|
84
|
+
data = client.get_me()
|
|
85
|
+
client.close()
|
|
86
|
+
except APIError as e:
|
|
87
|
+
if e.status_code in (401, 403):
|
|
88
|
+
console.print("[red]ERRO: Nao autenticado. Por favor, corre r2mcp login primeiro.[/red]")
|
|
89
|
+
else:
|
|
90
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
91
|
+
raise typer.Exit(1)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
console.print(f"[red]Erro de conexao: {e}[/red]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
|
|
96
|
+
table = Table(box=box.SIMPLE)
|
|
97
|
+
table.add_column("Campo", style="bold cyan")
|
|
98
|
+
table.add_column("Valor")
|
|
99
|
+
table.add_row("ID", data.get("id", "-"))
|
|
100
|
+
table.add_row("Email", data.get("email", "-"))
|
|
101
|
+
table.add_row("Nome", data.get("name", "-"))
|
|
102
|
+
table.add_row("Status", data.get("status", "-"))
|
|
103
|
+
table.add_row("Plano", data.get("plan_tier", "free"))
|
|
104
|
+
table.add_row("Servidores", f"{data.get('servers_count', 0)} / {data.get('servers_limit', 1)}")
|
|
105
|
+
console.print(table)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
PROD_URL = "https://rest2mcp.fly.dev"
|
|
109
|
+
LOCAL_URL = "http://localhost:8080"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@config_app.command(name="show")
|
|
113
|
+
def config_show():
|
|
114
|
+
cfg_base = get_base_url()
|
|
115
|
+
token = get_token()
|
|
116
|
+
token_status = "[green]configurado[/green]" if token else "[dim]nenhum[/dim]"
|
|
117
|
+
if cfg_base == PROD_URL:
|
|
118
|
+
env = "[blue]producao[/blue]"
|
|
119
|
+
elif cfg_base == LOCAL_URL:
|
|
120
|
+
env = "[yellow]local[/yellow]"
|
|
121
|
+
else:
|
|
122
|
+
env = f"[magenta]custom[/magenta] ({cfg_base})"
|
|
123
|
+
table = Table(box=box.SIMPLE)
|
|
124
|
+
table.add_column("Configuracao", style="bold cyan")
|
|
125
|
+
table.add_column("Valor")
|
|
126
|
+
table.add_row("Ambiente", env)
|
|
127
|
+
table.add_row("API URL", cfg_base)
|
|
128
|
+
table.add_row("Token", token_status)
|
|
129
|
+
console.print(table)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@config_app.command(name="set-env")
|
|
133
|
+
def config_set_env(
|
|
134
|
+
env: str = typer.Argument(..., help="Ambiente: local ou prod"),
|
|
135
|
+
):
|
|
136
|
+
if env == "local":
|
|
137
|
+
set_base_url(LOCAL_URL)
|
|
138
|
+
elif env == "prod":
|
|
139
|
+
set_base_url(PROD_URL)
|
|
140
|
+
else:
|
|
141
|
+
console.print(f"[red]Ambiente '{env}' invalido. Usa 'local' ou 'prod'.[/red]")
|
|
142
|
+
raise typer.Exit(1)
|
|
143
|
+
console.print(f"[green]Ambiente alterado para {env}: {get_base_url()}[/green]")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@config_app.command(name="set-base")
|
|
147
|
+
def set_base(base_url: str = typer.Argument(..., help="URL base da API")):
|
|
148
|
+
set_base_url(base_url)
|
|
149
|
+
console.print(f"[green]URL base alterada para: {get_base_url()}[/green]")
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from r2mcp_cli.api_client import APIClient, APIError
|
|
9
|
+
from r2mcp_cli.config import get_token, validate_token
|
|
10
|
+
|
|
11
|
+
console = Console()
|
|
12
|
+
link_app = typer.Typer(help="Ligar servidor a editor IA")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
EDITOR_CONFIGS = {
|
|
16
|
+
"claude-code": {
|
|
17
|
+
"paths": lambda: [
|
|
18
|
+
Path.cwd() / ".mcp.json",
|
|
19
|
+
Path.home() / ".claude.json",
|
|
20
|
+
],
|
|
21
|
+
"key": "mcpServers",
|
|
22
|
+
},
|
|
23
|
+
"cursor": {
|
|
24
|
+
"paths": lambda: [Path.cwd() / ".cursor" / "mcp.json", Path.home() / ".cursor" / "mcp.json"],
|
|
25
|
+
"key": "mcpServers",
|
|
26
|
+
},
|
|
27
|
+
"vscode": {
|
|
28
|
+
"paths": lambda: [Path.cwd() / ".vscode" / "mcp.json"],
|
|
29
|
+
"key": "servers",
|
|
30
|
+
},
|
|
31
|
+
"opencode": {
|
|
32
|
+
"paths": lambda: [Path.cwd() / "opencode.json"],
|
|
33
|
+
"key": "mcp",
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _require_auth():
|
|
39
|
+
token = get_token()
|
|
40
|
+
if not token:
|
|
41
|
+
console.print(
|
|
42
|
+
"[red]ERRO: Nao autenticado. Obtém um token em https://rest2mcp.pages.dev/ e corre r2mcp login.[/red]"
|
|
43
|
+
)
|
|
44
|
+
raise typer.Exit(1)
|
|
45
|
+
ok, msg = validate_token(token)
|
|
46
|
+
if not ok:
|
|
47
|
+
console.print(f"[red]ERRO: {msg}. Obtém um token novo em https://rest2mcp.pages.dev/[/red]")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _find_server(server_id: str) -> Optional[dict]:
|
|
52
|
+
client = APIClient()
|
|
53
|
+
try:
|
|
54
|
+
servers = client.list_servers()
|
|
55
|
+
client.close()
|
|
56
|
+
except APIError as e:
|
|
57
|
+
client.close()
|
|
58
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
59
|
+
raise typer.Exit(1)
|
|
60
|
+
except Exception as e:
|
|
61
|
+
client.close()
|
|
62
|
+
console.print(f"[red]Erro de conexao: {e}[/red]")
|
|
63
|
+
raise typer.Exit(1)
|
|
64
|
+
|
|
65
|
+
for s in servers:
|
|
66
|
+
if s["server_id"] == server_id:
|
|
67
|
+
return s
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _resolve_config_path(editor: str) -> Optional[Path]:
|
|
72
|
+
info = EDITOR_CONFIGS.get(editor)
|
|
73
|
+
if not info:
|
|
74
|
+
return None
|
|
75
|
+
for p in info["paths"]():
|
|
76
|
+
if str(p).strip():
|
|
77
|
+
return p
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _read_json(path: Path) -> dict:
|
|
82
|
+
if path.exists():
|
|
83
|
+
try:
|
|
84
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
85
|
+
except (json.JSONDecodeError, OSError):
|
|
86
|
+
return {}
|
|
87
|
+
return {}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@link_app.callback(invoke_without_command=True)
|
|
91
|
+
def link(
|
|
92
|
+
editor: str = typer.Argument(..., help="Editor: claude-code, cursor, vscode, opencode"),
|
|
93
|
+
server_id: str = typer.Argument(..., help="ID do servidor"),
|
|
94
|
+
):
|
|
95
|
+
_require_auth()
|
|
96
|
+
|
|
97
|
+
if editor not in EDITOR_CONFIGS:
|
|
98
|
+
console.print(f"[red]Editor '{editor}' nao suportado. Opcoes: {', '.join(EDITOR_CONFIGS.keys())}[/red]")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
|
|
101
|
+
config_path = _resolve_config_path(editor)
|
|
102
|
+
if not config_path:
|
|
103
|
+
console.print(f"[red]Nao foi possivel determinar o caminho de configuracao para '{editor}'.[/red]")
|
|
104
|
+
raise typer.Exit(1)
|
|
105
|
+
|
|
106
|
+
server = _find_server(server_id)
|
|
107
|
+
if not server:
|
|
108
|
+
console.print(f"[red]Servidor '{server_id}' nao encontrado.[/red]")
|
|
109
|
+
raise typer.Exit(1)
|
|
110
|
+
if server["status"] == "active":
|
|
111
|
+
try:
|
|
112
|
+
client = APIClient()
|
|
113
|
+
auth_info = client.get_auth_status(server_id)
|
|
114
|
+
client.close()
|
|
115
|
+
if auth_info.get("required_fields") and not auth_info.get("authenticated"):
|
|
116
|
+
console.print(
|
|
117
|
+
"[red]ERRO: Servidor precisa de autenticacao. Faz o login primeiro com 'r2mcp servers credentials'.[/red]"
|
|
118
|
+
)
|
|
119
|
+
raise typer.Exit(1)
|
|
120
|
+
except APIError:
|
|
121
|
+
pass
|
|
122
|
+
apikey = server.get("apikey", "")
|
|
123
|
+
url = f"http://localhost:8080/v1/{server_id}/{apikey}/mcp"
|
|
124
|
+
name = server.get("name", server_id)
|
|
125
|
+
transport = "streamable-http"
|
|
126
|
+
|
|
127
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
128
|
+
config = _read_json(config_path)
|
|
129
|
+
|
|
130
|
+
if editor == "claude-code":
|
|
131
|
+
mcp_url = url.replace("/sse", "/mcp") if url.endswith("/sse") else url
|
|
132
|
+
key = "mcpServers"
|
|
133
|
+
if key not in config:
|
|
134
|
+
config[key] = {}
|
|
135
|
+
config[key][name] = {
|
|
136
|
+
"command": "npx",
|
|
137
|
+
"args": ["-y", "mcp-remote", mcp_url],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
elif editor == "cursor":
|
|
141
|
+
key = "mcpServers"
|
|
142
|
+
if key not in config:
|
|
143
|
+
config[key] = {}
|
|
144
|
+
config[key][name] = {
|
|
145
|
+
"url": url,
|
|
146
|
+
"transport": transport,
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
elif editor == "vscode":
|
|
150
|
+
key = "servers"
|
|
151
|
+
if key not in config:
|
|
152
|
+
config[key] = {}
|
|
153
|
+
config[key][name] = {
|
|
154
|
+
"type": transport,
|
|
155
|
+
"url": url,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
elif editor == "opencode":
|
|
159
|
+
key = "mcp"
|
|
160
|
+
if key not in config:
|
|
161
|
+
config[key] = {}
|
|
162
|
+
config[key][name] = {
|
|
163
|
+
"type": "remote",
|
|
164
|
+
"url": url,
|
|
165
|
+
"enabled": True,
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
config_path.write_text(json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8")
|
|
169
|
+
|
|
170
|
+
editor_name = {
|
|
171
|
+
"claude-code": "Claude Code",
|
|
172
|
+
"cursor": "Cursor",
|
|
173
|
+
"vscode": "VS Code",
|
|
174
|
+
"opencode": "OpenCode",
|
|
175
|
+
}.get(editor, editor)
|
|
176
|
+
|
|
177
|
+
console.print(
|
|
178
|
+
f"[green]Servidor ligado ao {editor_name}! Por favor, reinicia o editor para aplicar as alteracoes.[/green]"
|
|
179
|
+
)
|
|
180
|
+
console.print(f" Ficheiro: [bold]{config_path}[/bold]")
|
|
181
|
+
|
|
182
|
+
if editor == "claude-code":
|
|
183
|
+
console.print(
|
|
184
|
+
"[yellow] Nota: O Claude Code usa 'mcp-remote' como bridge. "
|
|
185
|
+
"Certifica-te de que o Node.js esta instalado e disponivel no PATH para correr npx.[/yellow]"
|
|
186
|
+
)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich import box
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.table import Table
|
|
5
|
+
|
|
6
|
+
from r2mcp_cli.api_client import APIClient, APIError
|
|
7
|
+
from r2mcp_cli.config import get_token, validate_token
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
logs_app = typer.Typer(help="Comandos de logs")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _require_auth():
|
|
14
|
+
token = get_token()
|
|
15
|
+
if not token:
|
|
16
|
+
console.print(
|
|
17
|
+
"[red]ERRO: Nao autenticado. Obtém um token em https://rest2mcp.pages.dev/ e corre r2mcp login.[/red]"
|
|
18
|
+
)
|
|
19
|
+
raise typer.Exit(1)
|
|
20
|
+
ok, msg = validate_token(token)
|
|
21
|
+
if not ok:
|
|
22
|
+
console.print(f"[red]ERRO: {msg}. Obtém um token novo em https://rest2mcp.pages.dev/[/red]")
|
|
23
|
+
raise typer.Exit(1)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@logs_app.callback(invoke_without_command=True)
|
|
27
|
+
def logs_default(
|
|
28
|
+
ctx: typer.Context,
|
|
29
|
+
server_id: str = typer.Argument(..., help="ID do servidor"),
|
|
30
|
+
limit: int = typer.Option(20, "--limit", "-n", help="Número de logs a mostrar"),
|
|
31
|
+
):
|
|
32
|
+
_require_auth()
|
|
33
|
+
with console.status(f"[bold green]A obter logs do servidor '{server_id}'..."):
|
|
34
|
+
try:
|
|
35
|
+
client = APIClient()
|
|
36
|
+
entries = client.get_logs(server_id, limit)
|
|
37
|
+
client.close()
|
|
38
|
+
except APIError as e:
|
|
39
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
40
|
+
raise typer.Exit(1)
|
|
41
|
+
except Exception as e:
|
|
42
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
43
|
+
raise typer.Exit(1)
|
|
44
|
+
|
|
45
|
+
if not entries:
|
|
46
|
+
console.print("[yellow]Nenhum log encontrado.[/yellow]")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
table = Table(box=box.SIMPLE)
|
|
50
|
+
table.add_column("ID", style="dim")
|
|
51
|
+
table.add_column("Timestamp")
|
|
52
|
+
table.add_column("Tool")
|
|
53
|
+
table.add_column("Método")
|
|
54
|
+
table.add_column("Status")
|
|
55
|
+
table.add_column("Duração (ms)")
|
|
56
|
+
for entry in entries:
|
|
57
|
+
status_style = "green" if entry.get("status_code", 0) < 400 else "red"
|
|
58
|
+
table.add_row(
|
|
59
|
+
str(entry.get("id", "")),
|
|
60
|
+
entry.get("timestamp", ""),
|
|
61
|
+
entry.get("tool_called", ""),
|
|
62
|
+
entry.get("method", "") or "-",
|
|
63
|
+
f"[{status_style}]{entry.get('status_code', '?')}[/{status_style}]",
|
|
64
|
+
f"{entry.get('duration_ms', 0):.1f}",
|
|
65
|
+
)
|
|
66
|
+
console.print(table)
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich import box
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
from rich.prompt import Confirm, Prompt
|
|
10
|
+
|
|
11
|
+
from r2mcp_cli.api_client import APIClient, APIError
|
|
12
|
+
from r2mcp_cli.config import get_token, validate_token
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
servers_app = typer.Typer(help="Comandos de gestão de servidores")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _require_auth():
|
|
19
|
+
token = get_token()
|
|
20
|
+
if not token:
|
|
21
|
+
console.print(
|
|
22
|
+
"[red]ERRO: Nao autenticado. Obtém um token em https://rest2mcp.pages.dev/ e corre r2mcp login.[/red]"
|
|
23
|
+
)
|
|
24
|
+
raise typer.Exit(1)
|
|
25
|
+
ok, msg = validate_token(token)
|
|
26
|
+
if not ok:
|
|
27
|
+
console.print(f"[red]ERRO: {msg}. Obtém um token novo em https://rest2mcp.pages.dev/[/red]")
|
|
28
|
+
raise typer.Exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@servers_app.command()
|
|
32
|
+
def create(
|
|
33
|
+
name: str = typer.Option(..., "--name", "-n", help="Nome do servidor"),
|
|
34
|
+
spec_url: str = typer.Option(..., "--spec-url", "-u", help="URL do spec OpenAPI"),
|
|
35
|
+
transport: str = typer.Option("sse", "--transport", "-t", help="Transporte (sse ou http)"),
|
|
36
|
+
):
|
|
37
|
+
_require_auth()
|
|
38
|
+
with console.status(f"[bold green]A criar servidor '{name}'..."):
|
|
39
|
+
try:
|
|
40
|
+
client = APIClient()
|
|
41
|
+
result = client.create_server(name, spec_url, transport)
|
|
42
|
+
client.close()
|
|
43
|
+
except APIError as e:
|
|
44
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
48
|
+
raise typer.Exit(1)
|
|
49
|
+
|
|
50
|
+
table = Table(box=box.SIMPLE)
|
|
51
|
+
table.add_column("Campo", style="bold cyan")
|
|
52
|
+
table.add_column("Valor")
|
|
53
|
+
table.add_row("ID", result["server_id"])
|
|
54
|
+
table.add_row("Nome", result["name"])
|
|
55
|
+
table.add_row("Status", result["status"])
|
|
56
|
+
table.add_row("Transporte", result["transport"])
|
|
57
|
+
table.add_row("URL MCP", result["url_sse"])
|
|
58
|
+
table.add_row("API Key", result["apikey"])
|
|
59
|
+
console.print(table)
|
|
60
|
+
console.print("[green]Servidor criado![/green]")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@servers_app.command()
|
|
64
|
+
def list():
|
|
65
|
+
_require_auth()
|
|
66
|
+
console.print("[bold green]A listar servidores...[/bold green]")
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
client = APIClient()
|
|
70
|
+
servers = client.list_servers()
|
|
71
|
+
client.close()
|
|
72
|
+
except APIError as e:
|
|
73
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
74
|
+
raise typer.Exit(1)
|
|
75
|
+
except Exception as e:
|
|
76
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
77
|
+
raise typer.Exit(1)
|
|
78
|
+
|
|
79
|
+
if not servers:
|
|
80
|
+
console.print("[yellow]Nenhum servidor encontrado.[/yellow]")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
cols = shutil.get_terminal_size().columns
|
|
84
|
+
sys.stdout.write(f"\r{' ' * cols}\r")
|
|
85
|
+
sys.stdout.flush()
|
|
86
|
+
|
|
87
|
+
table = Table(box=box.SIMPLE)
|
|
88
|
+
table.add_column("ID", style="cyan", no_wrap=True)
|
|
89
|
+
table.add_column("Nome", no_wrap=True)
|
|
90
|
+
table.add_column("Status", no_wrap=True)
|
|
91
|
+
table.add_column("Transporte", no_wrap=True)
|
|
92
|
+
table.add_column("URL MCP", no_wrap=True)
|
|
93
|
+
for s in servers:
|
|
94
|
+
status_style = "green" if s["status"] == "active" else "red"
|
|
95
|
+
sid = s["server_id"]
|
|
96
|
+
if len(sid) > 14:
|
|
97
|
+
sid = sid[:11] + "..."
|
|
98
|
+
url = s["url_sse"]
|
|
99
|
+
url_display = url if len(url) <= 50 else url[:47] + "..."
|
|
100
|
+
table.add_row(
|
|
101
|
+
sid,
|
|
102
|
+
s["name"],
|
|
103
|
+
f"[{status_style}]{s['status']}[/{status_style}]",
|
|
104
|
+
s.get("transport", "http"),
|
|
105
|
+
url_display,
|
|
106
|
+
)
|
|
107
|
+
console.print(table)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@servers_app.command()
|
|
111
|
+
def delete(server_id: str = typer.Argument(..., help="ID do servidor")):
|
|
112
|
+
_require_auth()
|
|
113
|
+
if not Confirm.ask(f"[yellow]Tens a certeza que queres apagar o servidor '{server_id}'?[/yellow]"):
|
|
114
|
+
console.print("[yellow]Operação cancelada.[/yellow]")
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
with console.status(f"[bold red]A apagar servidor '{server_id}'..."):
|
|
118
|
+
try:
|
|
119
|
+
client = APIClient()
|
|
120
|
+
client.delete_server(server_id)
|
|
121
|
+
client.close()
|
|
122
|
+
except APIError as e:
|
|
123
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
124
|
+
raise typer.Exit(1)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
127
|
+
raise typer.Exit(1)
|
|
128
|
+
|
|
129
|
+
console.print(f"[green]Servidor '{server_id}' apagado.[/green]")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@servers_app.command()
|
|
133
|
+
def pause(server_id: str = typer.Argument(..., help="ID do servidor")):
|
|
134
|
+
_require_auth()
|
|
135
|
+
with console.status(f"[bold yellow]A pausar servidor '{server_id}'..."):
|
|
136
|
+
try:
|
|
137
|
+
client = APIClient()
|
|
138
|
+
client.update_server(server_id, {"status": "inactive"})
|
|
139
|
+
client.close()
|
|
140
|
+
except APIError as e:
|
|
141
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
142
|
+
raise typer.Exit(1)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
145
|
+
raise typer.Exit(1)
|
|
146
|
+
|
|
147
|
+
console.print(f"[green]Servidor '{server_id}' pausado.[/green]")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@servers_app.command()
|
|
151
|
+
def resume(server_id: str = typer.Argument(..., help="ID do servidor")):
|
|
152
|
+
_require_auth()
|
|
153
|
+
with console.status(f"[bold green]A retomar servidor '{server_id}'..."):
|
|
154
|
+
try:
|
|
155
|
+
client = APIClient()
|
|
156
|
+
client.update_server(server_id, {"status": "active"})
|
|
157
|
+
client.close()
|
|
158
|
+
except APIError as e:
|
|
159
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
160
|
+
raise typer.Exit(1)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
163
|
+
raise typer.Exit(1)
|
|
164
|
+
|
|
165
|
+
console.print(f"[green]Servidor '{server_id}' retomado.[/green]")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@servers_app.command()
|
|
169
|
+
def credentials(
|
|
170
|
+
server_id: str = typer.Argument(..., help="ID do servidor"),
|
|
171
|
+
key_value: List[str] = typer.Option(
|
|
172
|
+
[], "--set", "-s", help="Par chave=valor (ex: -s username=admin -s password=123456)"
|
|
173
|
+
),
|
|
174
|
+
):
|
|
175
|
+
_require_auth()
|
|
176
|
+
client = APIClient()
|
|
177
|
+
|
|
178
|
+
if key_value:
|
|
179
|
+
creds = {}
|
|
180
|
+
for kv in key_value:
|
|
181
|
+
if "=" not in kv:
|
|
182
|
+
console.print(f"[red]Formato invalido: '{kv}'. Use chave=valor.[/red]")
|
|
183
|
+
raise typer.Exit(1)
|
|
184
|
+
k, v = kv.split("=", 1)
|
|
185
|
+
creds[k] = v
|
|
186
|
+
try:
|
|
187
|
+
result = client.set_credentials(server_id, creds)
|
|
188
|
+
client.close()
|
|
189
|
+
console.print(f"[green]Credenciais salvas para '{server_id}'.[/green]")
|
|
190
|
+
except APIError as e:
|
|
191
|
+
client.close()
|
|
192
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
193
|
+
raise typer.Exit(1)
|
|
194
|
+
else:
|
|
195
|
+
try:
|
|
196
|
+
info = client.check_credentials(server_id)
|
|
197
|
+
client.close()
|
|
198
|
+
except APIError as e:
|
|
199
|
+
client.close()
|
|
200
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
201
|
+
raise typer.Exit(1)
|
|
202
|
+
if info.get("has_credentials"):
|
|
203
|
+
console.print(f"[green]Servidor '{server_id}' tem credenciais configuradas.[/green]")
|
|
204
|
+
else:
|
|
205
|
+
console.print(f"[yellow]Servidor '{server_id}' nao tem credenciais configuradas.[/yellow]")
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
PASSWORD_KEYS = {"password", "senha", "pwd", "pass", "secret", "palavra-passe", "passwd"}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@servers_app.command()
|
|
212
|
+
def login(
|
|
213
|
+
server_id: str = typer.Argument(..., help="ID do servidor"),
|
|
214
|
+
):
|
|
215
|
+
_require_auth()
|
|
216
|
+
client = APIClient()
|
|
217
|
+
try:
|
|
218
|
+
auth_info = client.get_auth_status(server_id)
|
|
219
|
+
except APIError as e:
|
|
220
|
+
client.close()
|
|
221
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
222
|
+
raise typer.Exit(1)
|
|
223
|
+
|
|
224
|
+
fields = auth_info.get("required_fields", [])
|
|
225
|
+
if not fields:
|
|
226
|
+
console.print("[yellow]Este servidor nao requer autenticacao.[/yellow]")
|
|
227
|
+
client.close()
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
if auth_info.get("authenticated"):
|
|
231
|
+
console.print("[green]Servidor ja esta autenticado.[/green]")
|
|
232
|
+
client.close()
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
console.print(f"[bold]Login no servidor {server_id}[/bold]")
|
|
236
|
+
console.print(f"[dim]Campos necessarios: {', '.join(fields)}[/dim]")
|
|
237
|
+
|
|
238
|
+
values = {}
|
|
239
|
+
for field in fields:
|
|
240
|
+
is_password = field.lower() in PASSWORD_KEYS
|
|
241
|
+
values[field] = Prompt.ask(
|
|
242
|
+
field,
|
|
243
|
+
password=is_password,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
result = client.login_server(server_id, values)
|
|
248
|
+
client.close()
|
|
249
|
+
if result.get("token"):
|
|
250
|
+
console.print("[green]Autenticado com sucesso![/green]")
|
|
251
|
+
else:
|
|
252
|
+
console.print("[red]Resposta inesperada do servidor.[/red]")
|
|
253
|
+
except APIError as e:
|
|
254
|
+
client.close()
|
|
255
|
+
if "Internal Server Error" in e.detail or e.status_code == 500:
|
|
256
|
+
console.print(
|
|
257
|
+
"[red]API do servidor nao esta acessivel ou recusou o login. Verifica se a API esta online.[/red]"
|
|
258
|
+
)
|
|
259
|
+
else:
|
|
260
|
+
console.print(f"[red]Erro no login: {e.detail}[/red]")
|
|
261
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich import box
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.syntax import Syntax
|
|
9
|
+
|
|
10
|
+
from r2mcp_cli.api_client import APIClient, APIError
|
|
11
|
+
from r2mcp_cli.config import get_token, validate_token
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
tools_app = typer.Typer(help="Comandos de interação com ferramentas")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _require_auth():
|
|
18
|
+
token = get_token()
|
|
19
|
+
if not token:
|
|
20
|
+
console.print(
|
|
21
|
+
"[red]ERRO: Nao autenticado. Obtém um token em https://rest2mcp.pages.dev/ e corre r2mcp login.[/red]"
|
|
22
|
+
)
|
|
23
|
+
raise typer.Exit(1)
|
|
24
|
+
ok, msg = validate_token(token)
|
|
25
|
+
if not ok:
|
|
26
|
+
console.print(f"[red]ERRO: {msg}. Obtém um token novo em https://rest2mcp.pages.dev/[/red]")
|
|
27
|
+
raise typer.Exit(1)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@tools_app.command()
|
|
31
|
+
def list(
|
|
32
|
+
server_id: str = typer.Argument(..., help="ID do servidor"),
|
|
33
|
+
):
|
|
34
|
+
_require_auth()
|
|
35
|
+
with console.status(f"[bold green]A listar tools do servidor '{server_id}'..."):
|
|
36
|
+
try:
|
|
37
|
+
client = APIClient()
|
|
38
|
+
tools = client.list_tools(server_id)
|
|
39
|
+
client.close()
|
|
40
|
+
except APIError as e:
|
|
41
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
42
|
+
raise typer.Exit(1)
|
|
43
|
+
except Exception as e:
|
|
44
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
45
|
+
raise typer.Exit(1)
|
|
46
|
+
|
|
47
|
+
if not tools:
|
|
48
|
+
console.print("[yellow]Nenhuma tool encontrada.[/yellow]")
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
table = Table(box=box.SIMPLE)
|
|
52
|
+
table.add_column("#", style="dim")
|
|
53
|
+
table.add_column("Nome", style="cyan")
|
|
54
|
+
table.add_column("Descrição")
|
|
55
|
+
for i, t in enumerate(tools, 1):
|
|
56
|
+
desc = t.get("description", "") or ""
|
|
57
|
+
if len(desc) > 80:
|
|
58
|
+
desc = desc[:77] + "..."
|
|
59
|
+
table.add_row(str(i), t.get("name", "?"), desc)
|
|
60
|
+
console.print(table)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@tools_app.command()
|
|
64
|
+
def call(
|
|
65
|
+
server_id: str = typer.Argument(..., help="ID do servidor"),
|
|
66
|
+
tool_name: str = typer.Argument(..., help="Nome da tool"),
|
|
67
|
+
args: Optional[str] = typer.Option(None, "--args", help="JSON string de argumentos"),
|
|
68
|
+
arg: List[str] = typer.Option([], "-A", help="Argumentos key=value (pode usar múltiplas vezes)"),
|
|
69
|
+
):
|
|
70
|
+
_require_auth()
|
|
71
|
+
|
|
72
|
+
arguments = {}
|
|
73
|
+
if args:
|
|
74
|
+
try:
|
|
75
|
+
arguments = json.loads(args)
|
|
76
|
+
except json.JSONDecodeError as e:
|
|
77
|
+
console.print(f"[red]JSON inválido em --args: {e}[/red]")
|
|
78
|
+
raise typer.Exit(1)
|
|
79
|
+
elif arg:
|
|
80
|
+
for kv in arg:
|
|
81
|
+
if "=" not in kv:
|
|
82
|
+
console.print(f"[red]Formato inválido: '{kv}'. Esperado key=value.[/red]")
|
|
83
|
+
raise typer.Exit(1)
|
|
84
|
+
key, val = kv.split("=", 1)
|
|
85
|
+
arguments[key] = val
|
|
86
|
+
|
|
87
|
+
with console.status(f"[bold green]A chamar tool '{tool_name}'..."):
|
|
88
|
+
try:
|
|
89
|
+
client = APIClient()
|
|
90
|
+
result = client.call_tool(server_id, tool_name, arguments)
|
|
91
|
+
client.close()
|
|
92
|
+
except APIError as e:
|
|
93
|
+
console.print(f"[red]Erro: {e.detail}[/red]")
|
|
94
|
+
raise typer.Exit(1)
|
|
95
|
+
except Exception as e:
|
|
96
|
+
console.print(f"[red]Erro de conexão: {e}[/red]")
|
|
97
|
+
raise typer.Exit(1)
|
|
98
|
+
|
|
99
|
+
if result.get("isError"):
|
|
100
|
+
console.print("[red]A tool retornou um erro:[/red]")
|
|
101
|
+
|
|
102
|
+
for content in result.get("content", []):
|
|
103
|
+
ctype = content.get("type", "")
|
|
104
|
+
text = content.get("text", "")
|
|
105
|
+
if ctype == "text":
|
|
106
|
+
console.print(text)
|
|
107
|
+
elif ctype in ("json", "json_object"):
|
|
108
|
+
parsed = json.loads(text) if isinstance(text, str) else text
|
|
109
|
+
syntax = Syntax(json.dumps(parsed, indent=2), "json", theme="monokai")
|
|
110
|
+
console.print(syntax)
|
|
111
|
+
else:
|
|
112
|
+
console.print(f"[{ctype}] {text}")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
CONFIG_DIR = Path.home() / ".r2mcp"
|
|
8
|
+
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
9
|
+
DEFAULT_BASE_URL = "http://localhost:8080"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _ensure_config_dir():
|
|
13
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def load_config() -> dict:
|
|
17
|
+
_ensure_config_dir()
|
|
18
|
+
if CONFIG_FILE.exists():
|
|
19
|
+
try:
|
|
20
|
+
return json.loads(CONFIG_FILE.read_text())
|
|
21
|
+
except (json.JSONDecodeError, OSError):
|
|
22
|
+
return {}
|
|
23
|
+
return {}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def save_config(config: dict):
|
|
27
|
+
_ensure_config_dir()
|
|
28
|
+
CONFIG_FILE.write_text(json.dumps(config, indent=2))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_token() -> Optional[str]:
|
|
32
|
+
return load_config().get("token")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def set_token(token: str):
|
|
36
|
+
cfg = load_config()
|
|
37
|
+
cfg["token"] = token
|
|
38
|
+
save_config(cfg)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clear_token():
|
|
42
|
+
cfg = load_config()
|
|
43
|
+
cfg.pop("token", None)
|
|
44
|
+
save_config(cfg)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_base_url() -> str:
|
|
48
|
+
return load_config().get("base_url", DEFAULT_BASE_URL)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def set_base_url(url: str):
|
|
52
|
+
cfg = load_config()
|
|
53
|
+
cfg["base_url"] = url.rstrip("/")
|
|
54
|
+
save_config(cfg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def validate_token(token: str) -> tuple[bool, str]:
|
|
58
|
+
try:
|
|
59
|
+
payload_b64 = token.split(".")[1]
|
|
60
|
+
payload_b64 += "=" * (4 - len(payload_b64) % 4)
|
|
61
|
+
payload = json.loads(base64.urlsafe_b64decode(payload_b64))
|
|
62
|
+
exp = payload.get("exp")
|
|
63
|
+
if exp is not None and time.time() > exp:
|
|
64
|
+
return False, "Token expirado. Faz r2mcp login primeiro."
|
|
65
|
+
return True, ""
|
|
66
|
+
except (IndexError, ValueError, json.JSONDecodeError):
|
|
67
|
+
return False, "Token invalido. Faz r2mcp login primeiro."
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from r2mcp_cli.commands.auth import login, logout, me, config_app
|
|
5
|
+
from r2mcp_cli.commands.servers import servers_app
|
|
6
|
+
from r2mcp_cli.commands.tools import tools_app
|
|
7
|
+
from r2mcp_cli.commands.link import link_app
|
|
8
|
+
from r2mcp_cli.commands.logs import logs_app
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="r2mcp",
|
|
14
|
+
help="CLI para gestão de servidores MCP via rest2mcp gateway",
|
|
15
|
+
no_args_is_help=True,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app.command()(login)
|
|
19
|
+
app.command()(logout)
|
|
20
|
+
app.command()(me)
|
|
21
|
+
app.add_typer(config_app, name="config", help="Gerir configuração local")
|
|
22
|
+
app.add_typer(servers_app, name="servers", help="Gerir servidores MCP")
|
|
23
|
+
app.add_typer(tools_app, name="tools", help="Interagir com ferramentas MCP")
|
|
24
|
+
app.add_typer(link_app, name="link", help="Ligar servidor a editor IA")
|
|
25
|
+
app.add_typer(logs_app, name="logs", help="Ver logs de servidores")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def callback():
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
app()
|
r2mcp-0.1.0/setup.cfg
ADDED