jaylog-mcp 0.0.0-stage → 0.1.1
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/README.md +135 -2
- package/dist/api.js +48 -0
- package/dist/auth/login.js +96 -0
- package/dist/auth/logout.js +13 -0
- package/dist/auth/open.js +27 -0
- package/dist/cli/colors.js +30 -0
- package/dist/credentials.js +37 -0
- package/dist/index.js +39 -0
- package/dist/tools/issues.js +35 -0
- package/dist/tools/keyUsers.js +40 -0
- package/dist/tools/logs.js +116 -0
- package/dist/tools/schedules.js +43 -0
- package/dist/tools/services.js +27 -0
- package/package.json +34 -4
package/README.md
CHANGED
|
@@ -1,3 +1,136 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Jaylog MCP Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Servidor [Model Context Protocol](https://github.com/modelcontextprotocol) para observabilidade do
|
|
4
|
+
ecossistema Jaylog: consulta de logs, métricas de recursos, erros, usuários chave, serviços e
|
|
5
|
+
agendamento de tarefas, contra a API do Jaylog.
|
|
6
|
+
|
|
7
|
+
> **Escopo inicial:** apenas ferramentas de leitura (GET). Nenhuma tool cria, altera ou apaga dados.
|
|
8
|
+
|
|
9
|
+
## Instalação
|
|
10
|
+
|
|
11
|
+
Requer Node.js 18+. Não precisa instalar nada: o `npx` baixa e executa o pacote.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
export JAYLOG_API_BASE_URL=https://sua-api-jaylog # URL da API
|
|
15
|
+
npx -y jaylog-mcp-server login # abre o navegador para autenticar
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Depois do login, registre o servidor no seu cliente MCP.
|
|
19
|
+
|
|
20
|
+
**Claude Code:**
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
claude mcp add jaylog --env JAYLOG_API_BASE_URL=https://sua-api-jaylog -- npx -y jaylog-mcp-server
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**Outros clientes (Claude Desktop, Cursor etc.):**
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"mcpServers": {
|
|
31
|
+
"jaylog": {
|
|
32
|
+
"command": "npx",
|
|
33
|
+
"args": ["-y", "jaylog-mcp-server"],
|
|
34
|
+
"env": {
|
|
35
|
+
"JAYLOG_API_BASE_URL": "https://sua-api-jaylog"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> Rodando via `npx`/Node, o arquivo `.env` **não** é lido. Defina as variáveis no shell ou no
|
|
43
|
+
> bloco `env` do cliente MCP, como acima.
|
|
44
|
+
|
|
45
|
+
## Autenticação
|
|
46
|
+
|
|
47
|
+
`npx -y jaylog-mcp-server login` abre o navegador numa tela de consentimento do app web do Jaylog
|
|
48
|
+
(`/mcp/authorize`) — o mesmo padrão do `claude login`/`gh auth login`: o MCP sobe um servidor HTTP
|
|
49
|
+
local efêmero (`127.0.0.1:<porta>`), o navegador confirma sua identidade (sessão já logada ou
|
|
50
|
+
login + 2FA) e, ao clicar em "Permitir", o Jaylog emite um token pessoal que volta pro MCP via esse
|
|
51
|
+
callback local.
|
|
52
|
+
|
|
53
|
+
- O token é salvo em `~/.config/jaylog-mcp/credentials.json` (permissão `600`).
|
|
54
|
+
- Expira em 15 dias, renovado automaticamente a cada uso — não expira por ficar o MCP parado, só por
|
|
55
|
+
ficar **sem uso**.
|
|
56
|
+
- Pode ser revogado a qualquer momento na tela "Tokens MCP" do app, sem precisar reinstalar nada.
|
|
57
|
+
- `logout` apenas apaga a credencial local; para invalidar o token de fato, revogue-o no app.
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx -y jaylog-mcp-server login # autentica (abre o navegador)
|
|
61
|
+
npx -y jaylog-mcp-server logout # remove a credencial local
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuração
|
|
65
|
+
|
|
66
|
+
| Variável | Descrição |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `JAYLOG_API_BASE_URL` | URL base da API do Jaylog (ex: `http://localhost:3000`) |
|
|
69
|
+
| `JAYLOG_FRONTEND_URL` | URL do app web do Jaylog, usada só pelo `login` para abrir `/mcp/authorize`. Se omitida, cai para `JAYLOG_API_BASE_URL` (ok quando os dois estão atrás do mesmo domínio). |
|
|
70
|
+
| `JAYLOG_API_TOKEN` | Override explícito: se definida, tem prioridade sobre a credencial salva pelo `login`. Útil para CI/scripting. Qualquer perfil (guest, standard, staff, admin) funciona — cada tool exige o perfil mínimo que a rota correspondente já exige na API. |
|
|
71
|
+
|
|
72
|
+
## Desenvolvimento
|
|
73
|
+
|
|
74
|
+
Para rodar a partir do código-fonte (requer [Bun](https://bun.sh)):
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
bun install
|
|
78
|
+
cp .env.example .env # edite com a URL da API
|
|
79
|
+
bun run login # autentica (abre o navegador)
|
|
80
|
+
bun run start # roda direto com Bun
|
|
81
|
+
bun run dev # com --watch
|
|
82
|
+
bun run build # compila para dist/ (uso via Node/bin)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Tools
|
|
86
|
+
|
|
87
|
+
### Logs, métricas e recursos
|
|
88
|
+
|
|
89
|
+
- **`list-logs`**: lista logs com filtros (serviço, hostname, username, ipv4, nível, mensagem, intervalo de tempo).
|
|
90
|
+
- **`get-latest-logs`**: último log de cada instância em execução, com dados de liveness (execução ativa, última atividade, múltiplas execuções vivas).
|
|
91
|
+
- **`get-log`**: um log específico por ID, com a mensagem completa.
|
|
92
|
+
- **`get-error-histogram`**: histograma de contagem de logs/erros por período, a partir dos logs brutos (janela limitada pela retenção).
|
|
93
|
+
- **`get-error-history`**: histórico diário consolidado de erros por serviço (janela maior que o histograma).
|
|
94
|
+
- **`get-host-metrics`**: uso de CPU/memória/disco/I/O de uma instância ao longo do tempo, com os limites de hardware da máquina.
|
|
95
|
+
- **`list-hosts`**: instâncias registradas (ambiente, versão do Python/jaylog, estado do git, venv).
|
|
96
|
+
- **`get-host-stats`**: contagem agregada por modo de execução, versão do Python/jaylog, tipo de venv e branch do git.
|
|
97
|
+
|
|
98
|
+
### Serviços
|
|
99
|
+
|
|
100
|
+
- **`list-services`**: lista serviços (aplicações/bots que enviam logs), com dono, empresa e setor resolvidos.
|
|
101
|
+
|
|
102
|
+
### Agendamento de tarefas
|
|
103
|
+
|
|
104
|
+
- **`list-service-schedules`**: horários agendados dos serviços — manuais ou sincronizados do Task Scheduler do Windows (somente leitura nesse caso).
|
|
105
|
+
|
|
106
|
+
### Usuários chave
|
|
107
|
+
|
|
108
|
+
- **`list-key-users`**: usuários chave (contatos de negócio ligados a serviços — não são usuários de login do sistema).
|
|
109
|
+
- **`list-service-key-users`**: vínculos entre serviços e seus usuários chave.
|
|
110
|
+
|
|
111
|
+
### Erros / Issues
|
|
112
|
+
|
|
113
|
+
- **`list-issues`**: issues (bugs/erros) abertos manualmente contra um serviço, com autor e responsável resolvidos.
|
|
114
|
+
- **`get-issue`**: uma issue específica por ID.
|
|
115
|
+
|
|
116
|
+
## Estrutura
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
src/
|
|
120
|
+
├── index.ts → bootstrap do McpServer (stdio)
|
|
121
|
+
├── api.ts → fetch autenticado contra a API do Jaylog
|
|
122
|
+
└── tools/
|
|
123
|
+
├── logs.ts → logs, métricas, recursos, hosts
|
|
124
|
+
├── services.ts → serviços
|
|
125
|
+
├── schedules.ts → agendamento de tarefas
|
|
126
|
+
├── keyUsers.ts → usuários chave
|
|
127
|
+
└── issues.ts → erros/issues
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Debugging
|
|
131
|
+
|
|
132
|
+
Como o MCP roda via stdio, use o [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npx @modelcontextprotocol/inspector npx -y jaylog-mcp-server
|
|
136
|
+
```
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildUrl = buildUrl;
|
|
4
|
+
exports.fetchJSON = fetchJSON;
|
|
5
|
+
const credentials_1 = require("./credentials");
|
|
6
|
+
const config = {
|
|
7
|
+
baseUrl: process.env.JAYLOG_API_BASE_URL
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Resolution order: `JAYLOG_API_TOKEN` env var (explicit override, handy for CI
|
|
11
|
+
* or scripting) first, then the credential saved by `login` — a personal,
|
|
12
|
+
* 15-day sliding-expiration token minted via the browser consent flow at
|
|
13
|
+
* `${JAYLOG_FRONTEND_URL}/mcp/authorize`. Re-read on every call (not cached)
|
|
14
|
+
* so a `logout`/`login` in another terminal takes effect without a restart.
|
|
15
|
+
*/
|
|
16
|
+
async function getAuthHeaders() {
|
|
17
|
+
const envToken = process.env.JAYLOG_API_TOKEN;
|
|
18
|
+
if (envToken)
|
|
19
|
+
return { Authorization: `Bearer ${envToken}` };
|
|
20
|
+
const stored = await (0, credentials_1.readCredentials)();
|
|
21
|
+
if (!stored)
|
|
22
|
+
return {};
|
|
23
|
+
return { Authorization: `Bearer ${stored.token}` };
|
|
24
|
+
}
|
|
25
|
+
function buildUrl(path, qs = {}) {
|
|
26
|
+
if (!config.baseUrl) {
|
|
27
|
+
throw new Error('JAYLOG_API_BASE_URL is not set. Configure it (and JAYLOG_API_TOKEN) in the environment.');
|
|
28
|
+
}
|
|
29
|
+
const url = new URL(path, config.baseUrl);
|
|
30
|
+
for (const [k, v] of Object.entries(qs)) {
|
|
31
|
+
if (v !== undefined && v !== null)
|
|
32
|
+
url.searchParams.set(k, String(v));
|
|
33
|
+
}
|
|
34
|
+
return url.toString();
|
|
35
|
+
}
|
|
36
|
+
async function fetchJSON(path, qs = {}) {
|
|
37
|
+
const url = buildUrl(path, qs);
|
|
38
|
+
const headers = await getAuthHeaders();
|
|
39
|
+
if (!headers.Authorization) {
|
|
40
|
+
throw new Error('Not authenticated. Run `bun run login` (or set JAYLOG_API_TOKEN) before using jaylog-mcp.');
|
|
41
|
+
}
|
|
42
|
+
const resp = await fetch(url, { headers });
|
|
43
|
+
if (!resp.ok) {
|
|
44
|
+
const body = await resp.text().catch(() => '');
|
|
45
|
+
throw new Error(`Jaylog API error [${resp.status}] ${resp.statusText}${body ? `: ${body}` : ''}`);
|
|
46
|
+
}
|
|
47
|
+
return (await resp.json());
|
|
48
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.login = login;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const node_http_1 = require("node:http");
|
|
6
|
+
const node_os_1 = require("node:os");
|
|
7
|
+
const colors_1 = require("../cli/colors");
|
|
8
|
+
const credentials_1 = require("../credentials");
|
|
9
|
+
const open_1 = require("./open");
|
|
10
|
+
const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
11
|
+
function page(message) {
|
|
12
|
+
return `<!doctype html>
|
|
13
|
+
<html>
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8">
|
|
16
|
+
<title>Jaylog MCP</title>
|
|
17
|
+
<style>
|
|
18
|
+
body { font-family: system-ui, sans-serif; background: #18181b; color: #e4e4e7; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
|
19
|
+
main { text-align: center; max-width: 28rem; padding: 0 1.5rem; }
|
|
20
|
+
h1 { font-size: 1.1rem; font-weight: 600; }
|
|
21
|
+
p { color: #a1a1aa; font-size: 0.9rem; }
|
|
22
|
+
</style>
|
|
23
|
+
</head>
|
|
24
|
+
<body><main><h1>${message}</h1><p>Você pode fechar esta aba e voltar pro terminal.</p></main></body>
|
|
25
|
+
</html>`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Loopback browser login (RFC 8252), the same shape `gh auth login` and
|
|
29
|
+
* `claude login` use: open the frontend's consent page, wait for it to hand a
|
|
30
|
+
* freshly minted personal token back to a one-shot local server, store it.
|
|
31
|
+
*/
|
|
32
|
+
async function login() {
|
|
33
|
+
const frontendUrl = process.env.JAYLOG_FRONTEND_URL || process.env.JAYLOG_API_BASE_URL;
|
|
34
|
+
if (!frontendUrl) {
|
|
35
|
+
throw new Error('Set JAYLOG_FRONTEND_URL (or JAYLOG_API_BASE_URL) before running login.');
|
|
36
|
+
}
|
|
37
|
+
const state = (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
38
|
+
const result = await new Promise((resolve, reject) => {
|
|
39
|
+
let settled = false;
|
|
40
|
+
const done = (fn) => {
|
|
41
|
+
if (settled)
|
|
42
|
+
return;
|
|
43
|
+
settled = true;
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
fn();
|
|
46
|
+
};
|
|
47
|
+
const server = (0, node_http_1.createServer)((req, res) => {
|
|
48
|
+
const url = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
49
|
+
if (url.pathname !== '/callback') {
|
|
50
|
+
res.writeHead(404).end();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const receivedState = url.searchParams.get('state');
|
|
54
|
+
const token = url.searchParams.get('token');
|
|
55
|
+
const expiresAt = url.searchParams.get('expires_at') ?? undefined;
|
|
56
|
+
if (receivedState !== state || !token) {
|
|
57
|
+
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
58
|
+
res.end(page('Falha na autorização: parâmetros inválidos.'));
|
|
59
|
+
done(() => reject(new Error('Invalid callback: state mismatch or missing token.')));
|
|
60
|
+
server.close();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
64
|
+
res.end(page('Autenticado com sucesso!'));
|
|
65
|
+
done(() => resolve({ token, expiresAt }));
|
|
66
|
+
server.close();
|
|
67
|
+
});
|
|
68
|
+
const timeout = setTimeout(() => {
|
|
69
|
+
done(() => reject(new Error('Login timed out after 5 minutes. Run `login` again.')));
|
|
70
|
+
server.close();
|
|
71
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
72
|
+
timeout.unref();
|
|
73
|
+
server.on('error', error => done(() => reject(error)));
|
|
74
|
+
server.listen(0, '127.0.0.1', () => {
|
|
75
|
+
const address = server.address();
|
|
76
|
+
if (!address || typeof address === 'string') {
|
|
77
|
+
done(() => reject(new Error('Failed to start local callback server.')));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const authorizeUrl = new URL('/mcp/authorize', frontendUrl);
|
|
81
|
+
authorizeUrl.searchParams.set('redirect_uri', `http://127.0.0.1:${address.port}/callback`);
|
|
82
|
+
authorizeUrl.searchParams.set('state', state);
|
|
83
|
+
authorizeUrl.searchParams.set('client_name', `Jaylog MCP (${(0, node_os_1.hostname)()})`);
|
|
84
|
+
console.error(colors_1.colors.white('Opening browser to authorize:'));
|
|
85
|
+
console.error(` ${colors_1.colors.cyan(authorizeUrl.toString())}\n`);
|
|
86
|
+
console.error(colors_1.colors.dim('If it does not open automatically, copy the link above into your browser.'));
|
|
87
|
+
(0, open_1.openBrowser)(authorizeUrl.toString());
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
await (0, credentials_1.writeCredentials)({
|
|
91
|
+
token: result.token,
|
|
92
|
+
expiresAt: result.expiresAt,
|
|
93
|
+
obtainedAt: new Date().toISOString()
|
|
94
|
+
});
|
|
95
|
+
console.error(colors_1.colors.boldGreen(`Logged in. Credential saved${result.expiresAt ? ` (valid until ${result.expiresAt})` : ''}.`));
|
|
96
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.logout = logout;
|
|
4
|
+
const colors_1 = require("../cli/colors");
|
|
5
|
+
const credentials_1 = require("../credentials");
|
|
6
|
+
async function logout() {
|
|
7
|
+
const cleared = await (0, credentials_1.clearCredentials)();
|
|
8
|
+
console.error(cleared
|
|
9
|
+
? colors_1.colors.boldGreen('Logged out. Local credential removed.') +
|
|
10
|
+
'\n' +
|
|
11
|
+
colors_1.colors.dim('The token itself still works until it expires or is revoked from "Tokens MCP" in the app.')
|
|
12
|
+
: colors_1.colors.yellow('No stored credential found.'));
|
|
13
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.openBrowser = openBrowser;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
/**
|
|
6
|
+
* Best-effort browser launch. The caller always prints the URL too, since this
|
|
7
|
+
* silently does nothing useful over a headless SSH session or inside WSL
|
|
8
|
+
* without `wslview`/`xdg-open` wired to the Windows browser.
|
|
9
|
+
*/
|
|
10
|
+
function openBrowser(url) {
|
|
11
|
+
try {
|
|
12
|
+
const platform = process.platform;
|
|
13
|
+
const child = platform === 'darwin'
|
|
14
|
+
? (0, node_child_process_1.spawn)('open', [url], { stdio: 'ignore', detached: true })
|
|
15
|
+
: platform === 'win32'
|
|
16
|
+
? (0, node_child_process_1.spawn)('cmd', ['/c', 'start', '""', url], { stdio: 'ignore', detached: true })
|
|
17
|
+
: (0, node_child_process_1.spawn)('xdg-open', [url], { stdio: 'ignore', detached: true });
|
|
18
|
+
// Without this, a missing binary (e.g. no `xdg-open` in a bare WSL image)
|
|
19
|
+
// emits an unhandled 'error' event on the child process and crashes the
|
|
20
|
+
// CLI — the printed URL is the real fallback, so just swallow it.
|
|
21
|
+
child.on('error', () => { });
|
|
22
|
+
child.unref();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// Ignored: the printed URL is the real fallback.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.colors = void 0;
|
|
4
|
+
// Bun's `console.error` wraps everything written to it in ANSI red by
|
|
5
|
+
// default (confirmed: `bun -e "console.error('x')"` emits `\x1b[31m`) — and
|
|
6
|
+
// stdout is reserved for the MCP JSON-RPC protocol, so stderr is the only
|
|
7
|
+
// place these messages can go. Each call below resets *before* applying its
|
|
8
|
+
// own code, which is required, not just tidy: a color code (e.g. white/cyan)
|
|
9
|
+
// replaces Bun's red, but `dim` alone only adds faintness to whatever
|
|
10
|
+
// foreground is already active, so without the leading reset it would render
|
|
11
|
+
// as dim *red* instead of a neutral dim. Disabled outside a TTY
|
|
12
|
+
// (piped/redirected output) and when NO_COLOR is set, per https://no-color.org.
|
|
13
|
+
const supportsColor = !!process.stderr.isTTY && !process.env.NO_COLOR;
|
|
14
|
+
function wrap(code) {
|
|
15
|
+
return text => (supportsColor ? `\x1b[0m\x1b[${code}m${text}\x1b[0m` : text);
|
|
16
|
+
}
|
|
17
|
+
// `boldGreen`/`boldRed` are single combined codes, not `bold(green(x))`: each
|
|
18
|
+
// `wrap()` call resets first (see above), so nesting two of them would have
|
|
19
|
+
// the inner reset wipe out the outer's bold — two SGR codes in one escape
|
|
20
|
+
// sequence is the only way to actually get both at once.
|
|
21
|
+
exports.colors = {
|
|
22
|
+
white: wrap('37'),
|
|
23
|
+
dim: wrap('2'),
|
|
24
|
+
cyan: wrap('36'),
|
|
25
|
+
green: wrap('32'),
|
|
26
|
+
yellow: wrap('33'),
|
|
27
|
+
red: wrap('31'),
|
|
28
|
+
boldGreen: wrap('1;32'),
|
|
29
|
+
boldRed: wrap('1;31')
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readCredentials = readCredentials;
|
|
4
|
+
exports.writeCredentials = writeCredentials;
|
|
5
|
+
exports.clearCredentials = clearCredentials;
|
|
6
|
+
const promises_1 = require("node:fs/promises");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
function credentialsDir() {
|
|
10
|
+
const base = process.env.XDG_CONFIG_HOME || (0, node_path_1.join)((0, node_os_1.homedir)(), '.config');
|
|
11
|
+
return (0, node_path_1.join)(base, 'jaylog-mcp');
|
|
12
|
+
}
|
|
13
|
+
function credentialsPath() {
|
|
14
|
+
return (0, node_path_1.join)(credentialsDir(), 'credentials.json');
|
|
15
|
+
}
|
|
16
|
+
async function readCredentials() {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await (0, promises_1.readFile)(credentialsPath(), 'utf8');
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function writeCredentials(creds) {
|
|
26
|
+
await (0, promises_1.mkdir)(credentialsDir(), { recursive: true, mode: 0o700 });
|
|
27
|
+
await (0, promises_1.writeFile)(credentialsPath(), JSON.stringify(creds, null, 2), { mode: 0o600 });
|
|
28
|
+
}
|
|
29
|
+
async function clearCredentials() {
|
|
30
|
+
try {
|
|
31
|
+
await (0, promises_1.rm)(credentialsPath());
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
5
|
+
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
+
const login_1 = require("./auth/login");
|
|
7
|
+
const logout_1 = require("./auth/logout");
|
|
8
|
+
const colors_1 = require("./cli/colors");
|
|
9
|
+
const issues_1 = require("./tools/issues");
|
|
10
|
+
const keyUsers_1 = require("./tools/keyUsers");
|
|
11
|
+
const logs_1 = require("./tools/logs");
|
|
12
|
+
const schedules_1 = require("./tools/schedules");
|
|
13
|
+
const services_1 = require("./tools/services");
|
|
14
|
+
async function runServer() {
|
|
15
|
+
const server = new mcp_js_1.McpServer({
|
|
16
|
+
name: 'Jaylog',
|
|
17
|
+
version: '0.1.0'
|
|
18
|
+
});
|
|
19
|
+
(0, logs_1.registerLogTools)(server);
|
|
20
|
+
(0, services_1.registerServiceTools)(server);
|
|
21
|
+
(0, schedules_1.registerScheduleTools)(server);
|
|
22
|
+
(0, keyUsers_1.registerKeyUserTools)(server);
|
|
23
|
+
(0, issues_1.registerIssueTools)(server);
|
|
24
|
+
const transport = new stdio_js_1.StdioServerTransport();
|
|
25
|
+
await server.connect(transport);
|
|
26
|
+
console.error(colors_1.colors.dim('Jaylog MCP Server running on stdio'));
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const command = process.argv[2];
|
|
30
|
+
if (command === 'login')
|
|
31
|
+
return (0, login_1.login)();
|
|
32
|
+
if (command === 'logout')
|
|
33
|
+
return (0, logout_1.logout)();
|
|
34
|
+
return runServer();
|
|
35
|
+
}
|
|
36
|
+
main().catch(error => {
|
|
37
|
+
console.error(colors_1.colors.boldRed(`Fatal error: ${error instanceof Error ? error.message : error}`));
|
|
38
|
+
process.exit(1);
|
|
39
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerIssueTools = registerIssueTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const issueStates = ['open', 'working', 'merged', 'closed'];
|
|
7
|
+
function asText(data) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: JSON.stringify(data, null, 2)
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function registerIssueTools(server) {
|
|
18
|
+
server.registerTool('list-issues', {
|
|
19
|
+
description: 'List issues—manually tracked bugs/errors opened against a service, with author/assignee names ' +
|
|
20
|
+
'resolved.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
title: zod_1.z.string().optional().describe('Substring match against the title'),
|
|
23
|
+
state: zod_1.z.enum(issueStates).optional().describe('Filter by issue state'),
|
|
24
|
+
service: zod_1.z.string().uuid().optional().describe('Restrict to a service'),
|
|
25
|
+
author: zod_1.z.string().uuid().optional().describe('Restrict to the user who opened the issue'),
|
|
26
|
+
assignee: zod_1.z.string().uuid().optional().describe('Restrict to the assigned user')
|
|
27
|
+
}
|
|
28
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/issues/list', params)));
|
|
29
|
+
server.registerTool('get-issue', {
|
|
30
|
+
description: 'Retrieve a single issue by ID, with author/assignee/updated-by names resolved.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
id: zod_1.z.string().uuid().describe('The issue ID')
|
|
33
|
+
}
|
|
34
|
+
}, async ({ id }) => asText(await (0, api_1.fetchJSON)(`/issues/${id}`)));
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerKeyUserTools = registerKeyUserTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
function asText(data) {
|
|
7
|
+
return {
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: 'text',
|
|
11
|
+
text: JSON.stringify(data, null, 2)
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function registerKeyUserTools(server) {
|
|
17
|
+
server.registerTool('list-key-users', {
|
|
18
|
+
description: 'List key users—business stakeholders/contacts tracked for services (not system login users). ' +
|
|
19
|
+
'Each has a name, email and optional branch/cellphone.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
name: zod_1.z.string().optional().describe('Substring match against the name'),
|
|
22
|
+
email: zod_1.z.string().optional().describe('Substring match against the email'),
|
|
23
|
+
status: zod_1.z
|
|
24
|
+
.union([zod_1.z.boolean(), zod_1.z.literal('all')])
|
|
25
|
+
.optional()
|
|
26
|
+
.describe('Filter by active status (true, default) or "all" to include inactive ones')
|
|
27
|
+
}
|
|
28
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/key-user/list', params)));
|
|
29
|
+
server.registerTool('list-service-key-users', {
|
|
30
|
+
description: 'List the links between services and their key users.',
|
|
31
|
+
inputSchema: {
|
|
32
|
+
service_id: zod_1.z.string().uuid().optional().describe('Restrict to a service'),
|
|
33
|
+
key_user_id: zod_1.z.string().uuid().optional().describe('Restrict to a key user'),
|
|
34
|
+
status: zod_1.z
|
|
35
|
+
.union([zod_1.z.boolean(), zod_1.z.literal('all')])
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('Filter by the key user\'s active status (true, default) or "all"')
|
|
38
|
+
}
|
|
39
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/service-key-user/list', params)));
|
|
40
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerLogTools = registerLogTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const logLevels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', 'EXCEPTION'];
|
|
7
|
+
function asText(data) {
|
|
8
|
+
return {
|
|
9
|
+
content: [
|
|
10
|
+
{
|
|
11
|
+
type: 'text',
|
|
12
|
+
text: JSON.stringify(data, null, 2)
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function registerLogTools(server) {
|
|
18
|
+
server.registerTool('list-logs', {
|
|
19
|
+
description: 'List logs with optional filters. Returns the raw log entries ordered by most recent first. ' +
|
|
20
|
+
'Use `get-error-histogram` instead when you only need error counts over time.',
|
|
21
|
+
inputSchema: {
|
|
22
|
+
service: zod_1.z.string().uuid().optional().describe('Service ID to filter by'),
|
|
23
|
+
hostname: zod_1.z.string().optional().describe('Exact hostname to filter by'),
|
|
24
|
+
username: zod_1.z.string().optional().describe('Exact username to filter by'),
|
|
25
|
+
ipv4: zod_1.z.string().optional().describe('Exact IPv4 address to filter by'),
|
|
26
|
+
service_path: zod_1.z.string().optional().describe('Substring match against the service path'),
|
|
27
|
+
is_exception: zod_1.z.boolean().optional().describe('Filter logs raised from an exception handler'),
|
|
28
|
+
log_level: zod_1.z.enum(logLevels).optional().describe('Exact log level to filter by'),
|
|
29
|
+
log_message: zod_1.z.string().optional().describe('Substring match against the log message'),
|
|
30
|
+
log_timestamp_from: zod_1.z
|
|
31
|
+
.string()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe('ISO 8601 timestamp; only logs at or after this instant'),
|
|
34
|
+
log_timestamp_to: zod_1.z
|
|
35
|
+
.string()
|
|
36
|
+
.optional()
|
|
37
|
+
.describe('ISO 8601 timestamp; only logs at or before this instant')
|
|
38
|
+
}
|
|
39
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/logs/list', params)));
|
|
40
|
+
server.registerTool('get-latest-logs', {
|
|
41
|
+
description: 'Get the most recent log per running instance (service + hostname + username), enriched ' +
|
|
42
|
+
'with service/company/sector info and liveness data (active run, last activity, whether more ' +
|
|
43
|
+
'than one live run was detected). This is the data behind the dashboard\'s service list.'
|
|
44
|
+
}, async () => asText(await (0, api_1.fetchJSON)('/logs/last')));
|
|
45
|
+
server.registerTool('get-log', {
|
|
46
|
+
description: 'Retrieve a single log entry by ID, including its full message and service context.',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
id: zod_1.z.string().uuid().describe('The log ID')
|
|
49
|
+
}
|
|
50
|
+
}, async ({ id }) => asText(await (0, api_1.fetchJSON)(`/logs/${id}`)));
|
|
51
|
+
server.registerTool('get-error-histogram', {
|
|
52
|
+
description: 'Get a time-bucketed histogram of log counts and error/critical/exception counts for a service, ' +
|
|
53
|
+
'built from raw logs. Limited to however far back the raw logs retention goes. Use ' +
|
|
54
|
+
'`get-error-history` instead for a longer-range daily summary.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
service: zod_1.z.string().uuid().describe('Service ID (required)'),
|
|
57
|
+
hostname: zod_1.z.string().optional().describe('Restrict to a single hostname'),
|
|
58
|
+
username: zod_1.z.string().optional().describe('Restrict to a single username'),
|
|
59
|
+
bucket_minutes: zod_1.z
|
|
60
|
+
.number()
|
|
61
|
+
.int()
|
|
62
|
+
.min(1)
|
|
63
|
+
.max(1440)
|
|
64
|
+
.optional()
|
|
65
|
+
.describe('Bucket width in minutes (default: 60)')
|
|
66
|
+
}
|
|
67
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/logs/error-histogram', params)));
|
|
68
|
+
server.registerTool('get-error-history', {
|
|
69
|
+
description: 'Get the daily consolidated error history for a service (total/error/critical/exception counts ' +
|
|
70
|
+
'per day), built by the nightly snapshot job. Covers a longer range than ' +
|
|
71
|
+
'`get-error-histogram`, which depends on raw log retention.',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
service: zod_1.z.string().uuid().describe('Service ID (required)'),
|
|
74
|
+
from: zod_1.z.string().optional().describe('Start date (YYYY-MM-DD), inclusive'),
|
|
75
|
+
to: zod_1.z.string().optional().describe('End date (YYYY-MM-DD), inclusive')
|
|
76
|
+
}
|
|
77
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/logs/error-history', params)));
|
|
78
|
+
server.registerTool('get-host-metrics', {
|
|
79
|
+
description: 'Get time-bucketed resource usage (CPU, memory, disk, process I/O) for a single running instance ' +
|
|
80
|
+
'(service + hostname + username), along with the machine\'s hardware limits. Samples are sent by ' +
|
|
81
|
+
'the jaylog client roughly every minute and retained for 48 hours.',
|
|
82
|
+
inputSchema: {
|
|
83
|
+
service: zod_1.z.string().uuid().describe('Service ID (required)'),
|
|
84
|
+
hostname: zod_1.z.string().min(1).describe('Hostname of the instance (required)'),
|
|
85
|
+
username: zod_1.z.string().min(1).describe('Username running the instance (required)'),
|
|
86
|
+
run_id: zod_1.z.string().uuid().optional().describe('Restrict to a single run; omit for the active run'),
|
|
87
|
+
bucket_minutes: zod_1.z
|
|
88
|
+
.number()
|
|
89
|
+
.int()
|
|
90
|
+
.min(1)
|
|
91
|
+
.max(1440)
|
|
92
|
+
.optional()
|
|
93
|
+
.describe('Bucket width in minutes (default: 1)')
|
|
94
|
+
}
|
|
95
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/logs/host-metrics', params)));
|
|
96
|
+
server.registerTool('list-hosts', {
|
|
97
|
+
description: 'List registered host/instance records (one per run_id by default, or every heartbeat with ' +
|
|
98
|
+
'`all_runs`), including environment details (OS, Python version, git state, venv) reported once ' +
|
|
99
|
+
'per run by the jaylog client.',
|
|
100
|
+
inputSchema: {
|
|
101
|
+
service_id: zod_1.z.string().uuid().optional().describe('Restrict to a service'),
|
|
102
|
+
run_id: zod_1.z.string().uuid().optional().describe('Restrict to a single run'),
|
|
103
|
+
hostname: zod_1.z.string().optional().describe('Restrict to a hostname'),
|
|
104
|
+
username: zod_1.z.string().optional().describe('Restrict to a username'),
|
|
105
|
+
live: zod_1.z.boolean().optional().describe('Only include runs considered currently alive'),
|
|
106
|
+
all_runs: zod_1.z
|
|
107
|
+
.boolean()
|
|
108
|
+
.optional()
|
|
109
|
+
.describe('Return every run instead of only the most relevant one per instance')
|
|
110
|
+
}
|
|
111
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/logs/hosts', params)));
|
|
112
|
+
server.registerTool('get-host-stats', {
|
|
113
|
+
description: 'Get an aggregate breakdown (value -> count) of execution mode, Python version, jaylog version, ' +
|
|
114
|
+
'venv kind and git branch across every active instance. Useful for a quick fleet overview.'
|
|
115
|
+
}, async () => asText(await (0, api_1.fetchJSON)('/logs/host-stats')));
|
|
116
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerScheduleTools = registerScheduleTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
const repetitions = ['CONTINUO', 'DIARIO', 'SEMANA', 'DIA'];
|
|
7
|
+
const weekDays = [
|
|
8
|
+
'SUNDAY',
|
|
9
|
+
'MONDAY',
|
|
10
|
+
'TUESDAY',
|
|
11
|
+
'WEDNESDAY',
|
|
12
|
+
'THURSDAY',
|
|
13
|
+
'FRIDAY',
|
|
14
|
+
'SATURDAY'
|
|
15
|
+
];
|
|
16
|
+
const sources = ['MANUAL', 'TASK_SCHEDULER'];
|
|
17
|
+
function asText(data) {
|
|
18
|
+
return {
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: 'text',
|
|
22
|
+
text: JSON.stringify(data, null, 2)
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function registerScheduleTools(server) {
|
|
28
|
+
server.registerTool('list-service-schedules', {
|
|
29
|
+
description: 'List the scheduled run times for services—either manually registered or synced from the ' +
|
|
30
|
+
'host\'s Windows Task Scheduler (`source: TASK_SCHEDULER`, read-only). `repetition` drives which ' +
|
|
31
|
+
'other fields are set: CONTINUO (none), DIARIO (time only), SEMANA (day_of_week + time), DIA ' +
|
|
32
|
+
'(day_of_month + time).',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
service_id: zod_1.z.string().uuid().optional().describe('Restrict to a service'),
|
|
35
|
+
repetition: zod_1.z.enum(repetitions).optional().describe('Filter by repetition kind'),
|
|
36
|
+
day_of_month: zod_1.z.number().int().min(1).max(31).optional().describe('Filter by day of month'),
|
|
37
|
+
day_of_week: zod_1.z.enum(weekDays).optional().describe('Filter by day of week'),
|
|
38
|
+
time: zod_1.z.string().optional().describe('Filter by exact time (HH:MM, 24h)'),
|
|
39
|
+
source: zod_1.z.enum(sources).optional().describe('Filter by origin of the schedule entry'),
|
|
40
|
+
status: zod_1.z.boolean().optional().describe('Include only active (true, default) or inactive schedules')
|
|
41
|
+
}
|
|
42
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/service-schedule/list', params)));
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerServiceTools = registerServiceTools;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const api_1 = require("../api");
|
|
6
|
+
function asText(data) {
|
|
7
|
+
return {
|
|
8
|
+
content: [
|
|
9
|
+
{
|
|
10
|
+
type: 'text',
|
|
11
|
+
text: JSON.stringify(data, null, 2)
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function registerServiceTools(server) {
|
|
17
|
+
server.registerTool('list-services', {
|
|
18
|
+
description: 'List registered services (the applications/bots that send logs), with their owner, company and ' +
|
|
19
|
+
'sector already resolved.',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
name: zod_1.z.string().optional().describe('Substring match against the service name'),
|
|
22
|
+
owner: zod_1.z.string().uuid().optional().describe('User ID of the service owner'),
|
|
23
|
+
company_id: zod_1.z.string().uuid().optional().describe('Restrict to a company'),
|
|
24
|
+
sector_id: zod_1.z.string().uuid().optional().describe('Restrict to a sector')
|
|
25
|
+
}
|
|
26
|
+
}, async (params) => asText(await (0, api_1.fetchJSON)('/service/list', params)));
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jaylog-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Model Context Protocol server for Jaylog observability (logs, metrics, services, schedules)",
|
|
5
|
+
"bin": {
|
|
6
|
+
"jaylog-mcp-server": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "bun src/index.ts",
|
|
13
|
+
"dev": "bun --watch src/index.ts",
|
|
14
|
+
"login": "bun src/index.ts login",
|
|
15
|
+
"logout": "bun src/index.ts logout",
|
|
16
|
+
"build": "tsc && chmod +x dist/*.js",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"jaylog",
|
|
22
|
+
"mcp",
|
|
23
|
+
"observability",
|
|
24
|
+
"claude"
|
|
25
|
+
],
|
|
26
|
+
"author": "gpocas",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
30
|
+
"zod": "^4.5.4"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22.10.0",
|
|
34
|
+
"typescript": "^5.7.0"
|
|
35
|
+
}
|
|
36
|
+
}
|