jaylog-mcp 0.0.0-stage → 0.1.2

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 CHANGED
@@ -1,3 +1,182 @@
1
- # Temporary Holding Version
1
+ # Jaylog MCP Server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ ## Sumário
10
+
11
+ - [Instalação](#instalação)
12
+ - [Autenticação](#autenticação)
13
+ - [Configuração](#configuração)
14
+ - [Desenvolvimento](#desenvolvimento)
15
+ - [Tools](#tools)
16
+ - [Logs, métricas e recursos](#logs-métricas-e-recursos)
17
+ - [Serviços](#serviços)
18
+ - [Agendamento de tarefas](#agendamento-de-tarefas)
19
+ - [Usuários chave](#usuários-chave)
20
+ - [Erros / Issues](#erros--issues)
21
+ - [Skills](#skills)
22
+ - [Estrutura](#estrutura)
23
+ - [Debugging](#debugging)
24
+
25
+ ## Instalação
26
+
27
+ Requer Node.js 18+. Não precisa instalar nada: o `npx` baixa e executa o pacote.
28
+
29
+ ```bash
30
+ export JAYLOG_URL=https://seu-app-jaylog # URL do app web do Jaylog
31
+ npx -y jaylog-mcp login # abre o navegador para autenticar
32
+ ```
33
+
34
+ > Uma única URL basta: o MCP chama a API pelo proxy do app web (`<JAYLOG_URL>/proxy/*`). Para
35
+ > apontar a API diretamente (ex.: desenvolvimento local), veja [Configuração](#configuração).
36
+
37
+ Depois do login, registre o servidor no seu cliente MCP.
38
+
39
+ **Claude Code:**
40
+
41
+ ```bash
42
+ claude mcp add jaylog --env JAYLOG_URL=https://seu-app-jaylog -- npx -y jaylog-mcp
43
+ ```
44
+
45
+ **Outros clientes (Claude Desktop, Cursor etc.):**
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "jaylog": {
51
+ "command": "npx",
52
+ "args": ["-y", "jaylog-mcp"],
53
+ "env": {
54
+ "JAYLOG_URL": "https://seu-app-jaylog"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ > Rodando via `npx`/Node, o arquivo `.env` **não** é lido. Defina as variáveis no shell ou no
62
+ > bloco `env` do cliente MCP, como acima.
63
+
64
+ ## Autenticação
65
+
66
+ `npx -y jaylog-mcp login` abre o navegador numa tela de consentimento do app web do Jaylog
67
+ (`/mcp/authorize`) — o mesmo padrão do `claude login`/`gh auth login`: o MCP sobe um servidor HTTP
68
+ local efêmero (`127.0.0.1:<porta>`), o navegador confirma sua identidade (sessão já logada ou
69
+ login + 2FA) e, ao clicar em "Permitir", o Jaylog emite um token pessoal que volta pro MCP via esse
70
+ callback local.
71
+
72
+ - O token é salvo em `~/.config/jaylog-mcp/credentials.json` (permissão `600`).
73
+ - Expira em 15 dias, renovado automaticamente a cada uso — não expira por ficar o MCP parado, só por
74
+ ficar **sem uso**.
75
+ - Pode ser revogado a qualquer momento na tela "Tokens MCP" do app, sem precisar reinstalar nada.
76
+ - `logout` apenas apaga a credencial local; para invalidar o token de fato, revogue-o no app.
77
+
78
+ ```bash
79
+ npx -y jaylog-mcp login # autentica (abre o navegador)
80
+ npx -y jaylog-mcp logout # remove a credencial local
81
+ ```
82
+
83
+ ## Configuração
84
+
85
+ | Variável | Descrição |
86
+ |---|---|
87
+ | `JAYLOG_URL` | URL do app web do Jaylog. É a única variável necessária: o `login` abre `<JAYLOG_URL>/mcp/authorize` e as tools chamam a API em `<JAYLOG_URL>/proxy/*`. |
88
+ | `JAYLOG_API_BASE_URL` | Override opcional: URL base da API chamada diretamente, sem passar pelo proxy (ex: `http://localhost:3000`). Quando definida, tem prioridade sobre `JAYLOG_URL` nas chamadas à API. |
89
+ | `JAYLOG_FRONTEND_URL` | Override opcional: URL do app web usada só pelo `login`. Útil em desenvolvimento local, onde a API e o app web rodam em portas diferentes. Se omitida, cai para `JAYLOG_URL`. |
90
+ | `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. |
91
+
92
+ ## Desenvolvimento
93
+
94
+ Para rodar a partir do código-fonte (requer [Bun](https://bun.sh)):
95
+
96
+ ```bash
97
+ bun install
98
+ cp .env.example .env # edite com as URLs do app web/API
99
+ bun run login # autentica (abre o navegador)
100
+ bun run start # roda direto com Bun
101
+ bun run dev # com --watch
102
+ bun run build # compila para dist/ (uso via Node/bin)
103
+ ```
104
+
105
+ ## Tools
106
+
107
+ ### Logs, métricas e recursos
108
+
109
+ - **`list-logs`**: lista logs com filtros (serviço, hostname, username, ipv4, nível, mensagem, intervalo de tempo).
110
+ - **`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).
111
+ - **`get-log`**: um log específico por ID, com a mensagem completa.
112
+ - **`get-error-histogram`**: histograma de contagem de logs/erros por período, a partir dos logs brutos (janela limitada pela retenção).
113
+ - **`get-error-history`**: histórico diário consolidado de erros por serviço (janela maior que o histograma).
114
+ - **`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.
115
+ - **`list-hosts`**: instâncias registradas (ambiente, versão do Python/jaylog, estado do git, venv).
116
+ - **`get-host-stats`**: contagem agregada por modo de execução, versão do Python/jaylog, tipo de venv e branch do git.
117
+
118
+ ### Serviços
119
+
120
+ - **`list-services`**: lista serviços (aplicações/bots que enviam logs), com empresa e setor resolvidos. O `owner` vem como ID de usuário.
121
+ - **`list-favorite-services`**: serviços favoritados pelo usuário autenticado (os favoritos são sempre do usuário logado no MCP).
122
+ - **`search-services-by-owner`**: busca serviços pelo nome e/ou e-mail do dono (substring, sem diferenciar maiúsculas), retornando um item por usuário encontrado com seus serviços. Requer perfil `standard` ou superior.
123
+
124
+ ### Agendamento de tarefas
125
+
126
+ - **`list-service-schedules`**: horários agendados dos serviços — manuais ou sincronizados do Task Scheduler do Windows (somente leitura nesse caso).
127
+
128
+ ### Usuários chave
129
+
130
+ - **`list-key-users`**: usuários chave (contatos de negócio ligados a serviços — não são usuários de login do sistema).
131
+ - **`list-service-key-users`**: vínculos entre serviços e seus usuários chave.
132
+
133
+ ### Erros / Issues
134
+
135
+ - **`list-issues`**: issues (bugs/erros) abertos manualmente contra um serviço, com autor e responsável resolvidos.
136
+ - **`get-issue`**: uma issue específica por ID.
137
+
138
+ ## Skills
139
+
140
+ O repositório inclui skills (pasta `skills/`) com fluxos prontos que usam as tools acima. Elas
141
+ seguem o padrão do [skills.sh](https://skills.sh) e funcionam em Claude Code, Cursor e outros
142
+ agentes compatíveis. Requerem o MCP configurado e autenticado.
143
+
144
+ ```bash
145
+ npx skills add Gpocas/jaylog-mcp # instala todas
146
+ npx skills add Gpocas/jaylog-mcp --skill jaylog-incident-report # instala uma
147
+ npx skills add Gpocas/jaylog-mcp -g # global (~/.claude/skills)
148
+ ```
149
+
150
+ - **`jaylog-incident-report`**: relatório em markdown de incidentes (erros nos logs, serviços
151
+ parados, agendamentos não cumpridos e issues). Começa com uma entrevista de múltipla escolha
152
+ para personalizar escopo (todos, favoritos ou serviços de um owner), categorias, período
153
+ (padrão: dia anterior) e extras (contatos, mensagens completas, só críticos). Salva em
154
+ `./reports/incidentes-AAAA-MM-DD.md`.
155
+ - **`jaylog-fix-service-errors`**: levanta os erros recentes de um serviço (escolhido por nome,
156
+ owner ou entre os favoritos), localiza o código responsável na pasta atual e **propõe**
157
+ correções (não edita arquivos).
158
+
159
+ ## Estrutura
160
+
161
+ ```
162
+ skills/
163
+ ├── jaylog-incident-report/SKILL.md
164
+ └── jaylog-fix-service-errors/SKILL.md
165
+ src/
166
+ ├── index.ts → bootstrap do McpServer (stdio)
167
+ ├── api.ts → fetch autenticado contra a API do Jaylog
168
+ └── tools/
169
+ ├── logs.ts → logs, métricas, recursos, hosts
170
+ ├── services.ts → serviços
171
+ ├── schedules.ts → agendamento de tarefas
172
+ ├── keyUsers.ts → usuários chave
173
+ └── issues.ts → erros/issues
174
+ ```
175
+
176
+ ## Debugging
177
+
178
+ Como o MCP roda via stdio, use o [MCP Inspector](https://github.com/modelcontextprotocol/inspector):
179
+
180
+ ```bash
181
+ npx @modelcontextprotocol/inspector npx -y jaylog-mcp
182
+ ```
package/dist/api.js ADDED
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveApiBaseUrl = resolveApiBaseUrl;
4
+ exports.buildUrl = buildUrl;
5
+ exports.fetchJSON = fetchJSON;
6
+ const credentials_1 = require("./credentials");
7
+ const trimSlashes = (value) => value.replace(/\/+$/, '');
8
+ /**
9
+ * Where API calls go. `JAYLOG_API_BASE_URL` wins when set (local dev, where the
10
+ * backend and the web app run on different ports, or a deployment that exposes
11
+ * the API directly). Otherwise the single `JAYLOG_URL` is the web app's address
12
+ * and the API is reached through its `/proxy/*` route, which forwards to the
13
+ * backend — that is what lets users configure one URL instead of two.
14
+ */
15
+ function resolveApiBaseUrl() {
16
+ const apiBase = process.env.JAYLOG_API_BASE_URL;
17
+ if (apiBase)
18
+ return trimSlashes(apiBase);
19
+ const appUrl = process.env.JAYLOG_URL;
20
+ if (appUrl)
21
+ return `${trimSlashes(appUrl)}/proxy`;
22
+ return undefined;
23
+ }
24
+ /**
25
+ * Resolution order: `JAYLOG_API_TOKEN` env var (explicit override, handy for CI
26
+ * or scripting) first, then the credential saved by `login` — a personal,
27
+ * 15-day sliding-expiration token minted via the browser consent flow at
28
+ * `${JAYLOG_FRONTEND_URL}/mcp/authorize`. Re-read on every call (not cached)
29
+ * so a `logout`/`login` in another terminal takes effect without a restart.
30
+ */
31
+ async function getAuthHeaders() {
32
+ const envToken = process.env.JAYLOG_API_TOKEN;
33
+ if (envToken)
34
+ return { Authorization: `Bearer ${envToken}` };
35
+ const stored = await (0, credentials_1.readCredentials)();
36
+ if (!stored)
37
+ return {};
38
+ return { Authorization: `Bearer ${stored.token}` };
39
+ }
40
+ function buildUrl(path, qs = {}) {
41
+ const baseUrl = resolveApiBaseUrl();
42
+ if (!baseUrl) {
43
+ throw new Error('JAYLOG_URL is not set. Set it to the Jaylog web app URL (or JAYLOG_API_BASE_URL to call the API directly).');
44
+ }
45
+ // `new URL('/x', 'https://host/proxy')` drops `/proxy`, so keep the base's
46
+ // path by resolving a relative path against a base that ends in `/`.
47
+ const url = new URL(path.replace(/^\/+/, ''), `${baseUrl}/`);
48
+ for (const [k, v] of Object.entries(qs)) {
49
+ if (v !== undefined && v !== null)
50
+ url.searchParams.set(k, String(v));
51
+ }
52
+ return url.toString();
53
+ }
54
+ async function fetchJSON(path, qs = {}) {
55
+ const url = buildUrl(path, qs);
56
+ const headers = await getAuthHeaders();
57
+ if (!headers.Authorization) {
58
+ throw new Error('Not authenticated. Run `bun run login` (or set JAYLOG_API_TOKEN) before using jaylog-mcp.');
59
+ }
60
+ const resp = await fetch(url, { headers });
61
+ if (!resp.ok) {
62
+ const body = await resp.text().catch(() => '');
63
+ throw new Error(`Jaylog API error [${resp.status}] ${resp.statusText}${body ? `: ${body}` : ''}`);
64
+ }
65
+ return (await resp.json());
66
+ }
@@ -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_URL || process.env.JAYLOG_API_BASE_URL;
34
+ if (!frontendUrl) {
35
+ throw new Error('Set JAYLOG_URL (the Jaylog web app 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,64 @@
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 company and sector names ' +
19
+ 'already resolved. `owner` is a user ID; use `search-services-by-owner` to search by the ' +
20
+ 'owner\'s name or email instead.',
21
+ inputSchema: {
22
+ name: zod_1.z.string().optional().describe('Substring match against the service name'),
23
+ owner: zod_1.z.string().uuid().optional().describe('User ID of the service owner'),
24
+ company_id: zod_1.z.string().uuid().optional().describe('Restrict to a company'),
25
+ sector_id: zod_1.z.string().uuid().optional().describe('Restrict to a sector')
26
+ }
27
+ }, async (params) => asText(await (0, api_1.fetchJSON)('/service/list', params)));
28
+ server.registerTool('list-favorite-services', {
29
+ description: 'List the services the authenticated user has marked as favorites, with the same fields as ' +
30
+ '`list-services` (company and sector names resolved). Favorites belong to the user the MCP is ' +
31
+ 'logged in as; there is no way to read someone else\'s.'
32
+ }, async () => {
33
+ const favorites = await (0, api_1.fetchJSON)('/favorites/list');
34
+ if (favorites.length === 0)
35
+ return asText([]);
36
+ // The favorites route only returns IDs, so resolve them against the service list.
37
+ const favoriteIds = new Set(favorites.map(favorite => favorite.service_id));
38
+ const services = await (0, api_1.fetchJSON)('/service/list');
39
+ return asText(services.filter(service => favoriteIds.has(service.id)));
40
+ });
41
+ server.registerTool('search-services-by-owner', {
42
+ description: 'Find the services owned by a person, searching by the owner\'s name and/or email (case-insensitive ' +
43
+ 'substring match). Returns one entry per matching user with that user\'s services. A user that ' +
44
+ 'matches but owns nothing is returned with an empty list. Use `list-services` with `owner` ' +
45
+ 'instead when you already have the owner\'s user ID.',
46
+ inputSchema: {
47
+ name: zod_1.z.string().min(1).optional().describe('Substring match against the owner\'s name'),
48
+ email: zod_1.z.string().min(1).optional().describe('Substring match against the owner\'s email')
49
+ }
50
+ }, async ({ name, email }) => {
51
+ if (!name && !email) {
52
+ throw new Error('Provide at least one of `name` or `email`.');
53
+ }
54
+ const users = await (0, api_1.fetchJSON)('/user/list', { name, email });
55
+ if (users.length === 0)
56
+ return asText([]);
57
+ // One service request filtered client-side beats one request per matching user.
58
+ const services = await (0, api_1.fetchJSON)('/service/list');
59
+ return asText(users.map(user => ({
60
+ owner: { id: user.id, name: user.name, email: user.email, is_active: user.is_active },
61
+ services: services.filter(service => service.owner === user.id)
62
+ })));
63
+ });
64
+ }
package/package.json CHANGED
@@ -1,6 +1,36 @@
1
1
  {
2
2
  "name": "jaylog-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.2",
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
+ }