@proteo-mcp/cli 0.1.0
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.
- package/dist/bridge.js +81 -0
- package/dist/commands/login.js +46 -0
- package/dist/commands/logout.js +18 -0
- package/dist/commands/whoami.js +24 -0
- package/dist/index.js +45 -0
- package/dist/mcpUrl.js +29 -0
- package/dist/prompt.js +48 -0
- package/dist/store.js +40 -0
- package/package.json +35 -0
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge.ts — stdio ↔ HTTP bridge para o mcp-proteo
|
|
3
|
+
*
|
|
4
|
+
* Quando o CLI é invocado sem subcomando (ex: `npx @proteo/mcp-proteo`),
|
|
5
|
+
* actua como servidor MCP via stdio, encaminhando cada mensagem JSON-RPC
|
|
6
|
+
* para POST /mcp no servidor remoto com a API key armazenada.
|
|
7
|
+
*
|
|
8
|
+
* O servidor remoto opera em modo stateless (uma instância McpServer por
|
|
9
|
+
* request), portanto cada mensagem pode ser enviada independentemente.
|
|
10
|
+
*/
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import { loadCredentials } from "./store.js";
|
|
13
|
+
import { normalizeMcpUrl } from "./mcpUrl.js";
|
|
14
|
+
const BRIDGE_USER_AGENT = "mcp-proteo-cli/0.1.0";
|
|
15
|
+
function writeJsonRpcError(id, code, message) {
|
|
16
|
+
const payload = JSON.stringify({ jsonrpc: "2.0", id: id ?? null, error: { code, message } });
|
|
17
|
+
process.stdout.write(payload + "\n");
|
|
18
|
+
}
|
|
19
|
+
export async function runBridge() {
|
|
20
|
+
const creds = loadCredentials();
|
|
21
|
+
if (!creds) {
|
|
22
|
+
writeJsonRpcError(null, -32001, "Nao autenticado. Execute: npx @proteo/mcp-proteo login");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const mcpUrl = `${normalizeMcpUrl(creds.url)}/mcp`;
|
|
26
|
+
const authHeader = `Bearer ${creds.apiKey}`;
|
|
27
|
+
const rl = createInterface({ input: process.stdin, terminal: false });
|
|
28
|
+
for await (const line of rl) {
|
|
29
|
+
const trimmed = line.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
continue;
|
|
32
|
+
// Extrai id da mensagem para erros formatados
|
|
33
|
+
let msgId = null;
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(trimmed);
|
|
36
|
+
msgId = parsed.id ?? null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// ignora parse error — encaminha mesmo assim
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch(mcpUrl, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: {
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
Accept: "application/json, text/event-stream",
|
|
47
|
+
Authorization: authHeader,
|
|
48
|
+
"User-Agent": BRIDGE_USER_AGENT,
|
|
49
|
+
},
|
|
50
|
+
body: trimmed,
|
|
51
|
+
});
|
|
52
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
53
|
+
if (contentType.includes("text/event-stream")) {
|
|
54
|
+
// SSE: extrai campos `data:` e escreve cada um como linha JSON
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
for (const sseLineRaw of text.split("\n")) {
|
|
57
|
+
const sseLine = sseLineRaw.trim();
|
|
58
|
+
if (sseLine.startsWith("data: ") && sseLine.length > 6) {
|
|
59
|
+
process.stdout.write(sseLine.slice(6) + "\n");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
const body = await res.text();
|
|
65
|
+
const trimmedBody = body.trim();
|
|
66
|
+
if (!res.ok && !trimmedBody.startsWith("{")) {
|
|
67
|
+
// Resposta de erro não-JSON (ex: 401 sem body JSON-RPC)
|
|
68
|
+
writeJsonRpcError(msgId, -32001, `Servidor respondeu ${res.status}: ${trimmedBody.slice(0, 200)}`);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (trimmedBody) {
|
|
72
|
+
process.stdout.write(trimmedBody + "\n");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
writeJsonRpcError(msgId, -32603, `Bridge error: ${err instanceof Error ? err.message : String(err)}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=bridge.js.map
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { saveCredentials } from "../store.js";
|
|
3
|
+
import { promptHidden, promptLine } from "../prompt.js";
|
|
4
|
+
import { normalizeMcpUrl } from "../mcpUrl.js";
|
|
5
|
+
const DEFAULT_MCP_URL = "https://mcp.proteo.tech";
|
|
6
|
+
export async function loginCommand(options) {
|
|
7
|
+
const mcpUrl = normalizeMcpUrl(options.url ?? process.env.MCP_PROTEO_URL ?? DEFAULT_MCP_URL);
|
|
8
|
+
console.log("");
|
|
9
|
+
console.log(kleur.bold().blue(" Proteo MCP — Login"));
|
|
10
|
+
console.log("");
|
|
11
|
+
console.log(` ${kleur.dim("Servidor:")} ${mcpUrl}`);
|
|
12
|
+
console.log("");
|
|
13
|
+
const username = (options.username?.trim() || (await promptLine(" Usuário: "))).trim();
|
|
14
|
+
if (!username) {
|
|
15
|
+
throw new Error("Informe o usuário Proteo.");
|
|
16
|
+
}
|
|
17
|
+
const password = (await promptHidden(" Senha: ")).replace(/\r/g, "");
|
|
18
|
+
if (!password) {
|
|
19
|
+
throw new Error("Informe a senha Proteo.");
|
|
20
|
+
}
|
|
21
|
+
console.log("");
|
|
22
|
+
console.log(" " + kleur.dim("Autenticando..."));
|
|
23
|
+
const res = await fetch(`${mcpUrl}/v1/clients/register`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
26
|
+
body: JSON.stringify({ username, password }),
|
|
27
|
+
});
|
|
28
|
+
const body = (await res.json().catch(() => ({})));
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
throw new Error(body.error_description ?? body.error ?? `Falha ao autenticar (${res.status}).`);
|
|
31
|
+
}
|
|
32
|
+
const apiKey = body.apiKey;
|
|
33
|
+
if (!apiKey?.startsWith("mcp_live_")) {
|
|
34
|
+
throw new Error("Resposta inválida do servidor: apiKey ausente.");
|
|
35
|
+
}
|
|
36
|
+
const tenantId = body.tenantId?.trim() || username;
|
|
37
|
+
saveCredentials({ url: mcpUrl, apiKey, tenantId, username });
|
|
38
|
+
console.log("");
|
|
39
|
+
console.log(kleur.green(" ✔ Autenticado com sucesso!"));
|
|
40
|
+
console.log(` ${kleur.dim("Usuário:")} ${kleur.bold(username)}`);
|
|
41
|
+
console.log(` ${kleur.dim("Servidor:")} ${kleur.bold(mcpUrl)}`);
|
|
42
|
+
console.log("");
|
|
43
|
+
console.log(` ${kleur.dim("Sessão salva em")} ${kleur.bold("~/.proteo/mcp-proteo.json")}`);
|
|
44
|
+
console.log("");
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=login.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { clearCredentials, configFilePath } from "../store.js";
|
|
3
|
+
export function logoutCommand() {
|
|
4
|
+
const removed = clearCredentials();
|
|
5
|
+
if (removed) {
|
|
6
|
+
console.log("");
|
|
7
|
+
console.log(kleur.green(" ✔ Credenciais removidas."));
|
|
8
|
+
console.log(` ${kleur.dim("Arquivo excluído:")} ${kleur.bold(configFilePath())}`);
|
|
9
|
+
console.log("");
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
console.log("");
|
|
13
|
+
console.log(kleur.yellow(" Nenhuma credencial encontrada."));
|
|
14
|
+
console.log(` ${kleur.dim("Execute")} ${kleur.bold("npx @proteo/mcp-proteo login")} ${kleur.dim("para autenticar.")}`);
|
|
15
|
+
console.log("");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=logout.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
import { loadCredentials, configFilePath } from "../store.js";
|
|
3
|
+
export function whoamiCommand() {
|
|
4
|
+
const creds = loadCredentials();
|
|
5
|
+
if (!creds) {
|
|
6
|
+
console.log("");
|
|
7
|
+
console.log(kleur.yellow(" Não autenticado."));
|
|
8
|
+
console.log(` ${kleur.dim("Execute")} ${kleur.bold("npx @proteo/mcp-proteo login")} ${kleur.dim("para autenticar.")}`);
|
|
9
|
+
console.log("");
|
|
10
|
+
process.exitCode = 1;
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const savedAt = new Date(creds.savedAt).toLocaleString("pt-BR");
|
|
14
|
+
console.log("");
|
|
15
|
+
console.log(kleur.bold().blue(" Proteo MCP — sessão ativa"));
|
|
16
|
+
console.log("");
|
|
17
|
+
console.log(` ${kleur.dim("Tenant:")} ${kleur.bold(creds.tenantId)}`);
|
|
18
|
+
console.log(` ${kleur.dim("Usuário:")} ${kleur.bold(creds.username)}`);
|
|
19
|
+
console.log(` ${kleur.dim("Servidor:")} ${kleur.bold(creds.url)}`);
|
|
20
|
+
console.log(` ${kleur.dim("Autenticado em:")} ${kleur.bold(savedAt)}`);
|
|
21
|
+
console.log(` ${kleur.dim("Config:")} ${kleur.bold(configFilePath())}`);
|
|
22
|
+
console.log("");
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=whoami.js.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { loginCommand } from "./commands/login.js";
|
|
4
|
+
import { logoutCommand } from "./commands/logout.js";
|
|
5
|
+
import { whoamiCommand } from "./commands/whoami.js";
|
|
6
|
+
import { runBridge } from "./bridge.js";
|
|
7
|
+
program
|
|
8
|
+
.name("mcp-proteo")
|
|
9
|
+
.description("CLI do Proteo MCP — autenticação e bridge stdio")
|
|
10
|
+
.version("0.1.0");
|
|
11
|
+
program
|
|
12
|
+
.command("login")
|
|
13
|
+
.description("Autenticar com usuário e senha Proteo")
|
|
14
|
+
.option("-u, --url <url>", "URL do servidor mcp-proteo", process.env.MCP_PROTEO_URL)
|
|
15
|
+
.option("--username <username>", "Usuário Proteo (senão, pergunta no terminal)")
|
|
16
|
+
.action((options) => {
|
|
17
|
+
loginCommand({ url: options.url, username: options.username }).catch((err) => {
|
|
18
|
+
process.stderr.write(`\n Erro: ${err instanceof Error ? err.message : String(err)}\n\n`);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
program
|
|
23
|
+
.command("logout")
|
|
24
|
+
.description("Remover credenciais salvas")
|
|
25
|
+
.action(() => {
|
|
26
|
+
logoutCommand();
|
|
27
|
+
});
|
|
28
|
+
program
|
|
29
|
+
.command("whoami")
|
|
30
|
+
.description("Exibir a sessão ativa")
|
|
31
|
+
.action(() => {
|
|
32
|
+
whoamiCommand();
|
|
33
|
+
});
|
|
34
|
+
// Comando padrão (sem subcomando): bridge stdio → HTTP
|
|
35
|
+
// Usado pelo host MCP (Cursor, Claude, etc.) ao iniciar via `command: npx @proteo/mcp-proteo`
|
|
36
|
+
if (process.argv.length === 2 || (process.argv.length === 3 && process.argv[2] === "start")) {
|
|
37
|
+
runBridge().catch((err) => {
|
|
38
|
+
process.stderr.write(`Bridge fatal: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
program.parse();
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=index.js.map
|
package/dist/mcpUrl.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Garante que o CLI só envie usuário/senha/API key para um servidor MCP
|
|
3
|
+
* em HTTPS. HTTP fica restrito a localhost (dev).
|
|
4
|
+
*/
|
|
5
|
+
export function normalizeMcpUrl(raw) {
|
|
6
|
+
const trimmed = raw.trim().replace(/\/$/, "");
|
|
7
|
+
let url;
|
|
8
|
+
try {
|
|
9
|
+
url = new URL(trimmed);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
throw new Error("URL do servidor MCP inválida.");
|
|
13
|
+
}
|
|
14
|
+
if (url.username || url.password) {
|
|
15
|
+
throw new Error("A URL do servidor não pode conter credenciais.");
|
|
16
|
+
}
|
|
17
|
+
const isLocal = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1";
|
|
18
|
+
if (url.protocol === "http:") {
|
|
19
|
+
if (!isLocal) {
|
|
20
|
+
throw new Error("O servidor MCP precisa usar HTTPS (HTTP só é aceito em localhost).");
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
if (url.protocol !== "https:") {
|
|
25
|
+
throw new Error("O servidor MCP precisa usar HTTPS.");
|
|
26
|
+
}
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=mcpUrl.js.map
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
export async function promptLine(question) {
|
|
3
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4
|
+
try {
|
|
5
|
+
return await new Promise((resolve) => {
|
|
6
|
+
rl.question(question, (answer) => resolve(answer.trim()));
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
finally {
|
|
10
|
+
rl.close();
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Lê senha no terminal mostrando asteriscos. Evita `setRawMode`, que no
|
|
15
|
+
* Windows (especialmente no terminal integrado do Cursor) engole as teclas
|
|
16
|
+
* e faz parecer que a senha não pode ser digitada.
|
|
17
|
+
*/
|
|
18
|
+
export async function promptHidden(question) {
|
|
19
|
+
const rl = createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
terminal: Boolean(process.stdin.isTTY),
|
|
23
|
+
});
|
|
24
|
+
const mutable = rl;
|
|
25
|
+
mutable._writeToOutput = (s) => {
|
|
26
|
+
if (s === question) {
|
|
27
|
+
process.stdout.write(s);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (s === "\n" || s === "\r" || s === "\r\n") {
|
|
31
|
+
process.stdout.write("\n");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const printable = s.replace(/[\r\n]/g, "");
|
|
35
|
+
if (printable.length > 0) {
|
|
36
|
+
process.stdout.write("*".repeat(printable.length));
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
try {
|
|
40
|
+
return await new Promise((resolve) => {
|
|
41
|
+
rl.question(question, (answer) => resolve(answer));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
rl.close();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=prompt.js.map
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync, chmodSync } from "node:fs";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".proteo");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "mcp-proteo.json");
|
|
6
|
+
export function loadCredentials() {
|
|
7
|
+
if (!existsSync(CONFIG_FILE))
|
|
8
|
+
return undefined;
|
|
9
|
+
try {
|
|
10
|
+
const raw = readFileSync(CONFIG_FILE, "utf8");
|
|
11
|
+
return JSON.parse(raw);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function saveCredentials(creds) {
|
|
18
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
19
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
|
|
20
|
+
}
|
|
21
|
+
const full = { ...creds, savedAt: new Date().toISOString() };
|
|
22
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(full, null, 2), { encoding: "utf8", mode: 0o600 });
|
|
23
|
+
try {
|
|
24
|
+
chmodSync(CONFIG_DIR, 0o700);
|
|
25
|
+
chmodSync(CONFIG_FILE, 0o600);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
// Windows ignora POSIX mode; a sessão ainda não contém a senha Proteo.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function clearCredentials() {
|
|
32
|
+
if (!existsSync(CONFIG_FILE))
|
|
33
|
+
return false;
|
|
34
|
+
unlinkSync(CONFIG_FILE);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
export function configFilePath() {
|
|
38
|
+
return CONFIG_FILE;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=store.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@proteo-mcp/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI e bridge stdio do mcp-proteo. Use `npx @proteo/mcp-proteo login` para autenticar e configure no seu cliente MCP.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mcp-proteo": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**/*.js"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"pack:check": "node scripts/verify-pack.mjs",
|
|
16
|
+
"prepublishOnly": "npm run build && npm run pack:check"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"registry": "https://registry.npmjs.org/"
|
|
21
|
+
},
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.17"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.19.1",
|
|
27
|
+
"commander": "^12.1.0",
|
|
28
|
+
"kleur": "^4.1.5"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "^22.10.0",
|
|
32
|
+
"tsx": "^4.19.2",
|
|
33
|
+
"typescript": "^5.7.2"
|
|
34
|
+
}
|
|
35
|
+
}
|