@re9ti/timesheet-mcp 1.0.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/README.md +90 -0
- package/bin/cli.js +25 -0
- package/dist/api.d.ts +36 -0
- package/dist/api.js +144 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +391 -0
- package/dist/setup.d.ts +12 -0
- package/dist/setup.js +165 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @re9ti/timesheet-mcp
|
|
2
|
+
|
|
3
|
+
MCP Server para integrar o **Claude** (Code, Desktop, Web) com o **Timesheet Inteligente RE9TI**.
|
|
4
|
+
|
|
5
|
+
Lance timesheets, consulte horas, gerencie timers — tudo via linguagem natural no Claude.
|
|
6
|
+
|
|
7
|
+
## Setup rápido
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @re9ti/timesheet-mcp setup
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
O wizard vai:
|
|
14
|
+
1. Pedir seu email e senha do Timesheet
|
|
15
|
+
2. Validar suas credenciais
|
|
16
|
+
3. Configurar o Claude Code e/ou Desktop automaticamente
|
|
17
|
+
|
|
18
|
+
Reinicie o Claude e pronto!
|
|
19
|
+
|
|
20
|
+
## Uso
|
|
21
|
+
|
|
22
|
+
Depois de configurado, fale naturalmente com o Claude:
|
|
23
|
+
|
|
24
|
+
| Exemplo | O que faz |
|
|
25
|
+
|---------|-----------|
|
|
26
|
+
| "Lança 2h para cliente Silva, reunião de alinhamento" | Cria timesheet |
|
|
27
|
+
| "Quantas horas trabalhei hoje?" | Resumo do dia |
|
|
28
|
+
| "Lista meus lançamentos da semana" | Lista timesheets |
|
|
29
|
+
| "Inicia timer para projeto X" | Inicia cronômetro |
|
|
30
|
+
| "Para o timer" | Para e registra |
|
|
31
|
+
| "Quais clientes tenho?" | Lista clientes |
|
|
32
|
+
|
|
33
|
+
## Ferramentas disponíveis
|
|
34
|
+
|
|
35
|
+
| Ferramenta | Descrição |
|
|
36
|
+
|------------|-----------|
|
|
37
|
+
| `login` | Autenticar no sistema |
|
|
38
|
+
| `criar_timesheet` | Criar lançamento |
|
|
39
|
+
| `listar_timesheets` | Listar lançamentos |
|
|
40
|
+
| `consultar_horas` | Resumo de horas + top apps |
|
|
41
|
+
| `listar_clientes` | Clientes disponíveis |
|
|
42
|
+
| `listar_contratos` | Contratos por cliente |
|
|
43
|
+
| `chat_timesheet` | Linguagem natural |
|
|
44
|
+
| `timer_iniciar` | Iniciar cronômetro |
|
|
45
|
+
| `timer_parar` | Parar cronômetro |
|
|
46
|
+
| `timer_pausar` | Pausar cronômetro |
|
|
47
|
+
| `timer_retomar` | Retomar cronômetro |
|
|
48
|
+
| `timer_status` | Status dos timers |
|
|
49
|
+
|
|
50
|
+
## Segurança
|
|
51
|
+
|
|
52
|
+
- Sua senha é usada **apenas uma vez** durante o setup e **nunca é armazenada**
|
|
53
|
+
- O setup gera um **device token** (90 dias, revogável) que é salvo no config
|
|
54
|
+
- O admin pode revogar o token a qualquer momento no painel Dispositivos
|
|
55
|
+
- Quando o token expirar, re-execute `npx @re9ti/timesheet-mcp setup`
|
|
56
|
+
|
|
57
|
+
## Configuração manual
|
|
58
|
+
|
|
59
|
+
Se preferir configurar manualmente, adicione ao `settings.json` do Claude Code (`~/.claude/settings.json`):
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"mcpServers": {
|
|
64
|
+
"timesheet-re9ti": {
|
|
65
|
+
"command": "npx",
|
|
66
|
+
"args": ["-y", "@re9ti/timesheet-mcp"],
|
|
67
|
+
"env": {
|
|
68
|
+
"TIMESHEET_API_URL": "https://timesheet.re9ti.com.br",
|
|
69
|
+
"TIMESHEET_DEVICE_TOKEN": "seu-token-aqui"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Para obter um token manualmente, use `POST /auth/login-device` na API.
|
|
77
|
+
|
|
78
|
+
## Variáveis de ambiente
|
|
79
|
+
|
|
80
|
+
| Variável | Default | Descrição |
|
|
81
|
+
|----------|---------|-----------|
|
|
82
|
+
| `TIMESHEET_API_URL` | `https://timesheet.re9ti.com.br` | URL da API |
|
|
83
|
+
| `TIMESHEET_DEVICE_TOKEN` | — | Token de dispositivo (preferido) |
|
|
84
|
+
| `TIMESHEET_EMAIL` | — | Email (fallback legacy) |
|
|
85
|
+
| `TIMESHEET_PASSWORD` | — | Senha (fallback legacy) |
|
|
86
|
+
|
|
87
|
+
## Requisitos
|
|
88
|
+
|
|
89
|
+
- Node.js >= 18
|
|
90
|
+
- Conta no Timesheet Inteligente RE9TI
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI entry point for @re9ti/timesheet-mcp
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* npx @re9ti/timesheet-mcp → run MCP server (stdio)
|
|
8
|
+
* npx @re9ti/timesheet-mcp setup → interactive setup wizard
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createRequire } from "module";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { dirname, join } from "path";
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
|
|
17
|
+
const command = process.argv[2];
|
|
18
|
+
|
|
19
|
+
if (command === "setup") {
|
|
20
|
+
const setup = await import(join(__dirname, "..", "dist", "setup.js"));
|
|
21
|
+
setup.default();
|
|
22
|
+
} else {
|
|
23
|
+
// Default: run MCP server via stdio
|
|
24
|
+
await import(join(__dirname, "..", "dist", "index.js"));
|
|
25
|
+
}
|
package/dist/api.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Timesheet Inteligente API.
|
|
3
|
+
*
|
|
4
|
+
* Auth priority:
|
|
5
|
+
* 1. TIMESHEET_DEVICE_TOKEN (device token from setup wizard — preferred, no password stored)
|
|
6
|
+
* 2. TIMESHEET_EMAIL + TIMESHEET_PASSWORD (legacy fallback)
|
|
7
|
+
*/
|
|
8
|
+
export declare function setCredentials(e: string, p: string): void;
|
|
9
|
+
export declare function setDeviceToken(token: string): void;
|
|
10
|
+
/**
|
|
11
|
+
* Obtain a device token via login-device endpoint.
|
|
12
|
+
* Used by the setup wizard — exchanges email+password for a long-lived revocable token.
|
|
13
|
+
*/
|
|
14
|
+
export declare function obtainDeviceToken(loginEmail: string, loginPassword: string): Promise<{
|
|
15
|
+
device_token: string;
|
|
16
|
+
user_id: string;
|
|
17
|
+
org_id: string;
|
|
18
|
+
user_name: string;
|
|
19
|
+
user_email: string;
|
|
20
|
+
} | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Login with email/password (legacy fallback).
|
|
23
|
+
*/
|
|
24
|
+
export declare function login(overrideEmail?: string, overridePassword?: string): Promise<boolean>;
|
|
25
|
+
/**
|
|
26
|
+
* Make an authenticated API call.
|
|
27
|
+
* Uses device token (Bearer) if available, falls back to session cookie/token.
|
|
28
|
+
*/
|
|
29
|
+
export declare function api(method: string, path: string, options?: {
|
|
30
|
+
params?: Record<string, string | number>;
|
|
31
|
+
body?: unknown;
|
|
32
|
+
}): Promise<Record<string, unknown>>;
|
|
33
|
+
export declare function getApiUrl(): string;
|
|
34
|
+
export declare function getEmail(): string;
|
|
35
|
+
export declare function isAuthenticated(): boolean;
|
|
36
|
+
export declare function hasDeviceToken(): boolean;
|
package/dist/api.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP client for the Timesheet Inteligente API.
|
|
3
|
+
*
|
|
4
|
+
* Auth priority:
|
|
5
|
+
* 1. TIMESHEET_DEVICE_TOKEN (device token from setup wizard — preferred, no password stored)
|
|
6
|
+
* 2. TIMESHEET_EMAIL + TIMESHEET_PASSWORD (legacy fallback)
|
|
7
|
+
*/
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
9
|
+
import { hostname } from "os";
|
|
10
|
+
const API_URL = process.env.TIMESHEET_API_URL || "https://timesheet.re9ti.com.br";
|
|
11
|
+
// Device token auth (preferred)
|
|
12
|
+
let deviceToken = process.env.TIMESHEET_DEVICE_TOKEN || null;
|
|
13
|
+
// Legacy email/password fallback
|
|
14
|
+
let email = process.env.TIMESHEET_EMAIL || "";
|
|
15
|
+
let password = process.env.TIMESHEET_PASSWORD || "";
|
|
16
|
+
// Session state
|
|
17
|
+
let authToken = null;
|
|
18
|
+
let authCookie = null;
|
|
19
|
+
export function setCredentials(e, p) {
|
|
20
|
+
email = e;
|
|
21
|
+
password = p;
|
|
22
|
+
}
|
|
23
|
+
export function setDeviceToken(token) {
|
|
24
|
+
deviceToken = token;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Obtain a device token via login-device endpoint.
|
|
28
|
+
* Used by the setup wizard — exchanges email+password for a long-lived revocable token.
|
|
29
|
+
*/
|
|
30
|
+
export async function obtainDeviceToken(loginEmail, loginPassword) {
|
|
31
|
+
const deviceId = `mcp-${randomUUID()}`;
|
|
32
|
+
const resp = await fetch(`${API_URL}/auth/login-device`, {
|
|
33
|
+
method: "POST",
|
|
34
|
+
headers: { "Content-Type": "application/json" },
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
email: loginEmail,
|
|
37
|
+
password: loginPassword,
|
|
38
|
+
device_id: deviceId,
|
|
39
|
+
hostname: `mcp-${hostname()}`,
|
|
40
|
+
}),
|
|
41
|
+
});
|
|
42
|
+
if (!resp.ok)
|
|
43
|
+
return null;
|
|
44
|
+
const data = (await resp.json());
|
|
45
|
+
// Store for immediate use
|
|
46
|
+
deviceToken = data.device_token;
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Login with email/password (legacy fallback).
|
|
51
|
+
*/
|
|
52
|
+
export async function login(overrideEmail, overridePassword) {
|
|
53
|
+
const loginEmail = overrideEmail || email;
|
|
54
|
+
const loginPassword = overridePassword || password;
|
|
55
|
+
if (!loginEmail || !loginPassword) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const resp = await fetch(`${API_URL}/auth/login`, {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "Content-Type": "application/json" },
|
|
61
|
+
body: JSON.stringify({ email: loginEmail, password: loginPassword }),
|
|
62
|
+
});
|
|
63
|
+
if (!resp.ok)
|
|
64
|
+
return false;
|
|
65
|
+
const data = (await resp.json());
|
|
66
|
+
authToken = data.token ?? null;
|
|
67
|
+
const setCookie = resp.headers.get("set-cookie");
|
|
68
|
+
if (setCookie) {
|
|
69
|
+
const match = setCookie.match(/att_session=([^;]+)/);
|
|
70
|
+
if (match)
|
|
71
|
+
authCookie = match[1];
|
|
72
|
+
}
|
|
73
|
+
if (overrideEmail)
|
|
74
|
+
email = overrideEmail;
|
|
75
|
+
if (overridePassword)
|
|
76
|
+
password = overridePassword;
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Make an authenticated API call.
|
|
81
|
+
* Uses device token (Bearer) if available, falls back to session cookie/token.
|
|
82
|
+
*/
|
|
83
|
+
export async function api(method, path, options = {}) {
|
|
84
|
+
// Auto-authenticate if needed
|
|
85
|
+
if (!deviceToken && !authToken && !authCookie) {
|
|
86
|
+
await login();
|
|
87
|
+
}
|
|
88
|
+
const url = new URL(path, API_URL);
|
|
89
|
+
if (options.params) {
|
|
90
|
+
for (const [k, v] of Object.entries(options.params)) {
|
|
91
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
92
|
+
url.searchParams.set(k, String(v));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const headers = { "Content-Type": "application/json" };
|
|
97
|
+
// Auth priority: device token > cookie > JWT
|
|
98
|
+
if (deviceToken) {
|
|
99
|
+
headers["Authorization"] = `Bearer ${deviceToken}`;
|
|
100
|
+
}
|
|
101
|
+
else if (authCookie) {
|
|
102
|
+
headers["Cookie"] = `att_session=${authCookie}`;
|
|
103
|
+
}
|
|
104
|
+
else if (authToken) {
|
|
105
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
106
|
+
}
|
|
107
|
+
let resp = await fetch(url.toString(), {
|
|
108
|
+
method,
|
|
109
|
+
headers,
|
|
110
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
111
|
+
});
|
|
112
|
+
// On 401, try re-login (only for legacy email/password mode)
|
|
113
|
+
if (resp.status === 401 && !deviceToken) {
|
|
114
|
+
const ok = await login();
|
|
115
|
+
if (ok) {
|
|
116
|
+
if (authCookie)
|
|
117
|
+
headers["Cookie"] = `att_session=${authCookie}`;
|
|
118
|
+
else if (authToken)
|
|
119
|
+
headers["Authorization"] = `Bearer ${authToken}`;
|
|
120
|
+
resp = await fetch(url.toString(), {
|
|
121
|
+
method,
|
|
122
|
+
headers,
|
|
123
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!resp.ok) {
|
|
128
|
+
const text = await resp.text();
|
|
129
|
+
return { error: true, status: resp.status, detail: text };
|
|
130
|
+
}
|
|
131
|
+
return (await resp.json());
|
|
132
|
+
}
|
|
133
|
+
export function getApiUrl() {
|
|
134
|
+
return API_URL;
|
|
135
|
+
}
|
|
136
|
+
export function getEmail() {
|
|
137
|
+
return email;
|
|
138
|
+
}
|
|
139
|
+
export function isAuthenticated() {
|
|
140
|
+
return !!(deviceToken || authToken || authCookie);
|
|
141
|
+
}
|
|
142
|
+
export function hasDeviceToken() {
|
|
143
|
+
return !!deviceToken;
|
|
144
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server for Timesheet Inteligente — RE9TI
|
|
3
|
+
*
|
|
4
|
+
* Exposes timesheet management tools to Claude via Model Context Protocol.
|
|
5
|
+
* Runs over stdio transport (standard for Claude Code / Desktop).
|
|
6
|
+
*/
|
|
7
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
import { api, login, setCredentials, getEmail, hasDeviceToken } from "./api.js";
|
|
11
|
+
const server = new McpServer({
|
|
12
|
+
name: "timesheet-re9ti",
|
|
13
|
+
version: "1.0.0",
|
|
14
|
+
});
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
function fmt(seconds) {
|
|
19
|
+
const h = Math.floor(seconds / 3600);
|
|
20
|
+
const m = Math.floor((seconds % 3600) / 60);
|
|
21
|
+
return `${h}h${m.toString().padStart(2, "0")}min`;
|
|
22
|
+
}
|
|
23
|
+
function today() {
|
|
24
|
+
return new Date().toISOString().slice(0, 10);
|
|
25
|
+
}
|
|
26
|
+
function nowHHMM() {
|
|
27
|
+
return new Date().toTimeString().slice(0, 5);
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Tools
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
server.tool("login", "Autenticar no sistema de timesheet. Normalmente desnecessário — o token de dispositivo do setup já autentica automaticamente. Use apenas se precisar trocar de conta.", { email: z.string().optional(), password: z.string().optional() }, async ({ email: e, password: p }) => {
|
|
33
|
+
if (hasDeviceToken()) {
|
|
34
|
+
return {
|
|
35
|
+
content: [
|
|
36
|
+
{
|
|
37
|
+
type: "text",
|
|
38
|
+
text: "Já autenticado via token de dispositivo (configurado no setup). Não é necessário login manual.",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (e)
|
|
44
|
+
setCredentials(e, p || "");
|
|
45
|
+
const ok = await login(e, p);
|
|
46
|
+
return {
|
|
47
|
+
content: [
|
|
48
|
+
{
|
|
49
|
+
type: "text",
|
|
50
|
+
text: ok
|
|
51
|
+
? `Login realizado com sucesso como ${getEmail()}`
|
|
52
|
+
: "Falha no login. Verifique email e senha.",
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
server.tool("criar_timesheet", `Criar um novo lançamento de timesheet.
|
|
58
|
+
Informe descricao + cliente + (hora_inicio/hora_fim OU duracao_minutos).
|
|
59
|
+
Data default = hoje. Horário default = agora.`, {
|
|
60
|
+
descricao: z.string().describe("Descrição do trabalho realizado"),
|
|
61
|
+
cliente: z.string().describe("Nome do cliente"),
|
|
62
|
+
data: z.string().optional().describe("Data YYYY-MM-DD (default: hoje)"),
|
|
63
|
+
hora_inicio: z.string().optional().describe("HH:MM início"),
|
|
64
|
+
hora_fim: z.string().optional().describe("HH:MM fim"),
|
|
65
|
+
duracao_minutos: z.number().optional().describe("Duração em minutos"),
|
|
66
|
+
projeto: z.string().optional().describe("Nome do projeto/contrato"),
|
|
67
|
+
cobravel: z.boolean().optional().describe("Hora cobrável (default: true)"),
|
|
68
|
+
profissional: z.string().optional().describe("Nome do profissional"),
|
|
69
|
+
referencia: z.string().optional().describe("Referência externa"),
|
|
70
|
+
}, async (args) => {
|
|
71
|
+
const targetDate = args.data || today();
|
|
72
|
+
let startedAt;
|
|
73
|
+
let endedAt;
|
|
74
|
+
let durationS;
|
|
75
|
+
if (args.hora_inicio && args.hora_fim) {
|
|
76
|
+
startedAt = `${targetDate}T${args.hora_inicio}:00`;
|
|
77
|
+
endedAt = `${targetDate}T${args.hora_fim}:00`;
|
|
78
|
+
durationS = (new Date(endedAt).getTime() - new Date(startedAt).getTime()) / 1000;
|
|
79
|
+
}
|
|
80
|
+
else if (args.duracao_minutos && args.duracao_minutos > 0) {
|
|
81
|
+
durationS = args.duracao_minutos * 60;
|
|
82
|
+
if (args.hora_inicio) {
|
|
83
|
+
startedAt = `${targetDate}T${args.hora_inicio}:00`;
|
|
84
|
+
const end = new Date(new Date(startedAt).getTime() + durationS * 1000);
|
|
85
|
+
endedAt = `${targetDate}T${end.toTimeString().slice(0, 5)}:00`;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const now = new Date();
|
|
89
|
+
endedAt = `${targetDate}T${nowHHMM()}:00`;
|
|
90
|
+
const start = new Date(now.getTime() - durationS * 1000);
|
|
91
|
+
startedAt = `${targetDate}T${start.toTimeString().slice(0, 5)}:00`;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
return {
|
|
96
|
+
content: [
|
|
97
|
+
{
|
|
98
|
+
type: "text",
|
|
99
|
+
text: "Erro: informe hora_inicio + hora_fim OU duracao_minutos.",
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if (durationS <= 0) {
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: "Erro: duração deve ser positiva." }],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const body = {
|
|
110
|
+
client_name: args.cliente,
|
|
111
|
+
description: args.descricao,
|
|
112
|
+
started_at: startedAt,
|
|
113
|
+
ended_at: endedAt,
|
|
114
|
+
duration_s: durationS,
|
|
115
|
+
billable: args.cobravel !== false,
|
|
116
|
+
source_type: "mcp",
|
|
117
|
+
status: "draft",
|
|
118
|
+
};
|
|
119
|
+
if (args.projeto)
|
|
120
|
+
body.project_name = args.projeto;
|
|
121
|
+
if (args.profissional)
|
|
122
|
+
body.professional_name = args.profissional;
|
|
123
|
+
if (args.referencia)
|
|
124
|
+
body.entry_reference = args.referencia;
|
|
125
|
+
const result = await api("POST", "/api/v1/timesheets", { body });
|
|
126
|
+
if (result.error) {
|
|
127
|
+
return {
|
|
128
|
+
content: [
|
|
129
|
+
{ type: "text", text: `Erro ao criar timesheet: ${result.detail}` },
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
content: [
|
|
135
|
+
{
|
|
136
|
+
type: "text",
|
|
137
|
+
text: [
|
|
138
|
+
"Timesheet criado com sucesso!",
|
|
139
|
+
`- ID: ${result.timesheet_id}`,
|
|
140
|
+
`- Cliente: ${args.cliente}`,
|
|
141
|
+
`- Data: ${targetDate}`,
|
|
142
|
+
`- Duração: ${fmt(durationS)}`,
|
|
143
|
+
`- Cobrável: ${args.cobravel !== false ? "Sim" : "Não"}`,
|
|
144
|
+
`- Status: rascunho`,
|
|
145
|
+
].join("\n"),
|
|
146
|
+
},
|
|
147
|
+
],
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
server.tool("listar_timesheets", "Listar lançamentos de timesheet com filtros por data, cliente, profissional.", {
|
|
151
|
+
data_inicio: z.string().optional().describe("Data inicial YYYY-MM-DD (default: hoje)"),
|
|
152
|
+
data_fim: z.string().optional().describe("Data final YYYY-MM-DD (default: hoje)"),
|
|
153
|
+
cliente: z.string().optional().describe("Filtrar por cliente"),
|
|
154
|
+
profissional: z.string().optional().describe("Filtrar por profissional"),
|
|
155
|
+
fonte: z.string().optional().describe("local | glpi | both (default: local)"),
|
|
156
|
+
limite: z.number().optional().describe("Máximo de resultados (default: 20)"),
|
|
157
|
+
}, async (args) => {
|
|
158
|
+
const params = {
|
|
159
|
+
date_from: args.data_inicio || today(),
|
|
160
|
+
date_to: args.data_fim || today(),
|
|
161
|
+
limit: args.limite || 20,
|
|
162
|
+
};
|
|
163
|
+
if (args.cliente)
|
|
164
|
+
params.client_name = args.cliente;
|
|
165
|
+
if (args.profissional)
|
|
166
|
+
params.professional_name = args.profissional;
|
|
167
|
+
if (args.fonte && args.fonte !== "local")
|
|
168
|
+
params.source = args.fonte;
|
|
169
|
+
const result = await api("GET", "/api/v1/timesheets", { params });
|
|
170
|
+
if (result.error) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: `Erro: ${result.detail}` }],
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const entries = (result.timesheets || result.entries || []);
|
|
176
|
+
if (!entries.length) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: "Nenhum lançamento encontrado." }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
let totalS = 0;
|
|
182
|
+
const lines = [`**${entries.length} lançamento(s):**\n`];
|
|
183
|
+
for (const ts of entries) {
|
|
184
|
+
const dur = ts.duration_s || 0;
|
|
185
|
+
totalS += dur;
|
|
186
|
+
const billable = ts.billable ? "$$" : "--";
|
|
187
|
+
lines.push(`- [${billable}] ${ts.date || "?"} ${ts.time_start || "?"}-${ts.time_end || "?"} (${fmt(dur)}) | ${ts.client_name || "?"} | ${(ts.description || "").slice(0, 60)} [${ts.status}]`);
|
|
188
|
+
}
|
|
189
|
+
lines.push(`\n**Total: ${fmt(totalS)}**`);
|
|
190
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
191
|
+
});
|
|
192
|
+
server.tool("consultar_horas", "Consultar resumo de horas trabalhadas no período, com top apps.", {
|
|
193
|
+
data_inicio: z.string().optional().describe("YYYY-MM-DD (default: hoje)"),
|
|
194
|
+
data_fim: z.string().optional().describe("YYYY-MM-DD (default: hoje)"),
|
|
195
|
+
}, async (args) => {
|
|
196
|
+
const params = {
|
|
197
|
+
date_from: args.data_inicio || today(),
|
|
198
|
+
date_to: args.data_fim || today(),
|
|
199
|
+
};
|
|
200
|
+
const result = await api("GET", "/api/v1/timesheet", { params });
|
|
201
|
+
if (result.error) {
|
|
202
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
203
|
+
}
|
|
204
|
+
const total = result.total_seconds || 0;
|
|
205
|
+
const active = result.active_seconds || 0;
|
|
206
|
+
const idle = result.idle_seconds || 0;
|
|
207
|
+
const entries = (result.entries || []);
|
|
208
|
+
const appTotals = {};
|
|
209
|
+
for (const e of entries) {
|
|
210
|
+
const app = e.app_name || "?";
|
|
211
|
+
appTotals[app] = (appTotals[app] || 0) + (e.total_seconds || 0);
|
|
212
|
+
}
|
|
213
|
+
const lines = [
|
|
214
|
+
`**Resumo (${params.date_from} a ${params.date_to}):**\n`,
|
|
215
|
+
`- Total capturado: ${fmt(total)}`,
|
|
216
|
+
`- Ativo: ${fmt(active)}`,
|
|
217
|
+
`- Ocioso: ${fmt(idle)}`,
|
|
218
|
+
`- Apps distintos: ${new Set(entries.map((e) => e.app_name)).size}`,
|
|
219
|
+
];
|
|
220
|
+
const sorted = Object.entries(appTotals).sort((a, b) => b[1] - a[1]);
|
|
221
|
+
if (sorted.length) {
|
|
222
|
+
lines.push("\n**Top apps:**");
|
|
223
|
+
for (const [app, secs] of sorted.slice(0, 8)) {
|
|
224
|
+
lines.push(`- ${app}: ${fmt(secs)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
228
|
+
});
|
|
229
|
+
server.tool("listar_clientes", "Listar clientes disponíveis (GLPI + manuais).", {}, async () => {
|
|
230
|
+
const result = await api("GET", "/api/v1/glpi/clients");
|
|
231
|
+
if (result.error) {
|
|
232
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
233
|
+
}
|
|
234
|
+
const clients = (Array.isArray(result) ? result : result.clients || []);
|
|
235
|
+
if (!clients.length) {
|
|
236
|
+
return { content: [{ type: "text", text: "Nenhum cliente encontrado." }] };
|
|
237
|
+
}
|
|
238
|
+
const lines = [`**${clients.length} cliente(s):**\n`];
|
|
239
|
+
for (const c of clients.slice(0, 50)) {
|
|
240
|
+
lines.push(`- [${c.id}] ${c.name || c.completename}`);
|
|
241
|
+
}
|
|
242
|
+
if (clients.length > 50)
|
|
243
|
+
lines.push(`\n... e mais ${clients.length - 50}`);
|
|
244
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
245
|
+
});
|
|
246
|
+
server.tool("listar_contratos", "Listar contratos disponíveis, opcionalmente filtrados por cliente.", { cliente_id: z.number().optional().describe("ID do cliente (0 = todos)") }, async (args) => {
|
|
247
|
+
const params = {};
|
|
248
|
+
if (args.cliente_id)
|
|
249
|
+
params.entity_id = args.cliente_id;
|
|
250
|
+
const result = await api("GET", "/api/v1/glpi/contracts", { params });
|
|
251
|
+
if (result.error) {
|
|
252
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
253
|
+
}
|
|
254
|
+
const contracts = (Array.isArray(result) ? result : result.contracts || []);
|
|
255
|
+
if (!contracts.length) {
|
|
256
|
+
return { content: [{ type: "text", text: "Nenhum contrato encontrado." }] };
|
|
257
|
+
}
|
|
258
|
+
const lines = [`**${contracts.length} contrato(s):**\n`];
|
|
259
|
+
for (const c of contracts.slice(0, 30)) {
|
|
260
|
+
lines.push(`- [${c.id}] ${c.name}`);
|
|
261
|
+
}
|
|
262
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
263
|
+
});
|
|
264
|
+
server.tool("chat_timesheet", `Enviar mensagem em linguagem natural para criar/consultar timesheets.
|
|
265
|
+
Exemplos: "Lançar 2h para Silva, reunião" / "Quantas horas hoje?" / "Listar semana"`, { mensagem: z.string().describe("Mensagem em linguagem natural") }, async (args) => {
|
|
266
|
+
const result = await api("POST", "/api/v1/chat/message", {
|
|
267
|
+
body: { message: args.mensagem, mode: "direct" },
|
|
268
|
+
});
|
|
269
|
+
if (result.error) {
|
|
270
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
271
|
+
}
|
|
272
|
+
const reply = result.reply || "Sem resposta";
|
|
273
|
+
const action = result.action;
|
|
274
|
+
let text = reply;
|
|
275
|
+
if (action?.type === "created") {
|
|
276
|
+
text += `\n\nTimesheet criado com ID: ${action.timesheet_id}`;
|
|
277
|
+
}
|
|
278
|
+
else if (action?.type === "prefill") {
|
|
279
|
+
const data = action.data || {};
|
|
280
|
+
text += `\n\nDados extraídos:\n- Cliente: ${data.client_name}\n- Duração: ${Math.floor((data.duration_s || 0) / 60)} min\n- Descrição: ${data.description}\n\nUse criar_timesheet para confirmar.`;
|
|
281
|
+
}
|
|
282
|
+
return { content: [{ type: "text", text }] };
|
|
283
|
+
});
|
|
284
|
+
// --- Timer tools ---
|
|
285
|
+
server.tool("timer_iniciar", "Iniciar um timer/cronômetro de trabalho.", {
|
|
286
|
+
cliente: z.string().optional().describe("Nome do cliente"),
|
|
287
|
+
projeto: z.string().optional().describe("Nome do projeto"),
|
|
288
|
+
descricao: z.string().optional().describe("Descrição do trabalho"),
|
|
289
|
+
}, async (args) => {
|
|
290
|
+
const body = {};
|
|
291
|
+
if (args.cliente)
|
|
292
|
+
body.client_name = args.cliente;
|
|
293
|
+
if (args.projeto)
|
|
294
|
+
body.project_name = args.projeto;
|
|
295
|
+
if (args.descricao)
|
|
296
|
+
body.description = args.descricao;
|
|
297
|
+
const result = await api("POST", "/api/v1/timesheets/timer/start", { body });
|
|
298
|
+
if (result.error) {
|
|
299
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
content: [
|
|
303
|
+
{
|
|
304
|
+
type: "text",
|
|
305
|
+
text: `Timer iniciado!\n- ID: ${result.entry_id}\n- Início: ${result.started_at}\nUse timer_parar quando terminar.`,
|
|
306
|
+
},
|
|
307
|
+
],
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
server.tool("timer_parar", "Parar um timer e converter em lançamento de timesheet.", { timer_id: z.string().optional().describe("ID do timer (vazio = timer ativo)") }, async (args) => {
|
|
311
|
+
let timerId = args.timer_id;
|
|
312
|
+
if (!timerId) {
|
|
313
|
+
const status = await api("GET", "/api/v1/timesheets/timer/status");
|
|
314
|
+
if (status.error) {
|
|
315
|
+
return { content: [{ type: "text", text: `Erro: ${status.detail}` }] };
|
|
316
|
+
}
|
|
317
|
+
const timers = status.timers || [];
|
|
318
|
+
const running = timers.find((t) => t.status === "running");
|
|
319
|
+
timerId = (running || timers[0])?.entry_id || "";
|
|
320
|
+
if (!timerId) {
|
|
321
|
+
return { content: [{ type: "text", text: "Nenhum timer ativo." }] };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const result = await api("POST", `/api/v1/timesheets/timer/stop/${timerId}`);
|
|
325
|
+
if (result.error) {
|
|
326
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
327
|
+
}
|
|
328
|
+
const dur = result.duration_s || 0;
|
|
329
|
+
return {
|
|
330
|
+
content: [
|
|
331
|
+
{
|
|
332
|
+
type: "text",
|
|
333
|
+
text: [
|
|
334
|
+
"Timer parado!",
|
|
335
|
+
`- Duração: ${fmt(dur)}`,
|
|
336
|
+
`- Horário: ${result.time_start} - ${result.time_end}`,
|
|
337
|
+
`- Cliente: ${result.client_name || "-"}`,
|
|
338
|
+
`- Descrição: ${result.description || "-"}`,
|
|
339
|
+
].join("\n"),
|
|
340
|
+
},
|
|
341
|
+
],
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
server.tool("timer_status", "Verificar status dos timers ativos.", {}, async () => {
|
|
345
|
+
const result = await api("GET", "/api/v1/timesheets/timer/status");
|
|
346
|
+
if (result.error) {
|
|
347
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
348
|
+
}
|
|
349
|
+
const timers = result.timers || [];
|
|
350
|
+
if (!timers.length) {
|
|
351
|
+
return { content: [{ type: "text", text: "Nenhum timer ativo." }] };
|
|
352
|
+
}
|
|
353
|
+
const lines = [`**${timers.length} timer(s):**\n`];
|
|
354
|
+
for (const t of timers) {
|
|
355
|
+
const elapsed = t.elapsed_s || 0;
|
|
356
|
+
const icon = t.status === "running" ? "▶" : "⏸";
|
|
357
|
+
lines.push(`- ${icon} [${(t.entry_id || "").slice(0, 8)}] ${fmt(elapsed)} | ${t.client_name || "-"} | ${t.description || "-"} (${t.status})`);
|
|
358
|
+
}
|
|
359
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
360
|
+
});
|
|
361
|
+
server.tool("timer_pausar", "Pausar um timer ativo.", { timer_id: z.string().describe("ID do timer") }, async (args) => {
|
|
362
|
+
const result = await api("POST", `/api/v1/timesheets/timer/pause/${args.timer_id}`);
|
|
363
|
+
if (result.error) {
|
|
364
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
content: [
|
|
368
|
+
{ type: "text", text: `Timer pausado. Acumulado: ${fmt(result.elapsed_s || 0)}` },
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
server.tool("timer_retomar", "Retomar um timer pausado.", { timer_id: z.string().describe("ID do timer") }, async (args) => {
|
|
373
|
+
const result = await api("POST", `/api/v1/timesheets/timer/resume/${args.timer_id}`);
|
|
374
|
+
if (result.error) {
|
|
375
|
+
return { content: [{ type: "text", text: `Erro: ${result.detail}` }] };
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: `Timer retomado! Status: ${result.status}` }],
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
// ---------------------------------------------------------------------------
|
|
382
|
+
// Start server
|
|
383
|
+
// ---------------------------------------------------------------------------
|
|
384
|
+
async function main() {
|
|
385
|
+
const transport = new StdioServerTransport();
|
|
386
|
+
await server.connect(transport);
|
|
387
|
+
}
|
|
388
|
+
main().catch((err) => {
|
|
389
|
+
console.error("Fatal:", err);
|
|
390
|
+
process.exit(1);
|
|
391
|
+
});
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive setup wizard for @re9ti/timesheet-mcp.
|
|
3
|
+
*
|
|
4
|
+
* Security model:
|
|
5
|
+
* 1. User provides email + password ONCE during setup
|
|
6
|
+
* 2. We exchange them for a device token (90-day, revocable)
|
|
7
|
+
* 3. Only the device token is saved to config — password is NEVER stored
|
|
8
|
+
* 4. Token can be revoked by admin in the Timesheet dashboard
|
|
9
|
+
*
|
|
10
|
+
* Run: npx @re9ti/timesheet-mcp setup
|
|
11
|
+
*/
|
|
12
|
+
export default function setup(): Promise<void>;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive setup wizard for @re9ti/timesheet-mcp.
|
|
3
|
+
*
|
|
4
|
+
* Security model:
|
|
5
|
+
* 1. User provides email + password ONCE during setup
|
|
6
|
+
* 2. We exchange them for a device token (90-day, revocable)
|
|
7
|
+
* 3. Only the device token is saved to config — password is NEVER stored
|
|
8
|
+
* 4. Token can be revoked by admin in the Timesheet dashboard
|
|
9
|
+
*
|
|
10
|
+
* Run: npx @re9ti/timesheet-mcp setup
|
|
11
|
+
*/
|
|
12
|
+
import { createInterface } from "readline";
|
|
13
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
14
|
+
import { homedir, hostname } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { randomUUID } from "crypto";
|
|
17
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
18
|
+
function ask(question, defaultVal = "") {
|
|
19
|
+
const suffix = defaultVal ? ` [${defaultVal}]` : "";
|
|
20
|
+
return new Promise((resolve) => {
|
|
21
|
+
rl.question(`${question}${suffix}: `, (answer) => {
|
|
22
|
+
resolve(answer.trim() || defaultVal);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function banner() {
|
|
27
|
+
console.log(`
|
|
28
|
+
╔══════════════════════════════════════════════════╗
|
|
29
|
+
║ Timesheet Inteligente — MCP Setup ║
|
|
30
|
+
║ Integração com Claude Code / Desktop ║
|
|
31
|
+
╚══════════════════════════════════════════════════╝
|
|
32
|
+
`);
|
|
33
|
+
}
|
|
34
|
+
function getClaudeCodeSettingsPath() {
|
|
35
|
+
return join(homedir(), ".claude", "settings.json");
|
|
36
|
+
}
|
|
37
|
+
function getClaudeDesktopConfigPath() {
|
|
38
|
+
const platform = process.platform;
|
|
39
|
+
if (platform === "win32") {
|
|
40
|
+
return join(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
|
|
41
|
+
}
|
|
42
|
+
else if (platform === "darwin") {
|
|
43
|
+
return join(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
44
|
+
}
|
|
45
|
+
return join(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
46
|
+
}
|
|
47
|
+
function readJsonFile(path) {
|
|
48
|
+
if (!existsSync(path))
|
|
49
|
+
return {};
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function writeJsonFile(path, data) {
|
|
58
|
+
const dir = join(path, "..");
|
|
59
|
+
if (!existsSync(dir))
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
62
|
+
}
|
|
63
|
+
async function obtainDeviceToken(apiUrl, email, password) {
|
|
64
|
+
const deviceId = `mcp-${randomUUID()}`;
|
|
65
|
+
const resp = await fetch(`${apiUrl}/auth/login-device`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
headers: { "Content-Type": "application/json" },
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
email,
|
|
70
|
+
password,
|
|
71
|
+
device_id: deviceId,
|
|
72
|
+
hostname: `mcp-${hostname()}`,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
if (!resp.ok)
|
|
76
|
+
return null;
|
|
77
|
+
return (await resp.json());
|
|
78
|
+
}
|
|
79
|
+
export default async function setup() {
|
|
80
|
+
banner();
|
|
81
|
+
// 1. Collect credentials (used only once, never stored)
|
|
82
|
+
const apiUrl = await ask("URL da API do Timesheet", "https://timesheet.re9ti.com.br");
|
|
83
|
+
const email = await ask("Seu email de login");
|
|
84
|
+
const password = await ask("Sua senha");
|
|
85
|
+
if (!email || !password) {
|
|
86
|
+
console.log("\n Email e senha sao obrigatorios.");
|
|
87
|
+
rl.close();
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
// 2. Exchange credentials for device token
|
|
91
|
+
console.log("\nAutenticando e obtendo token de dispositivo...");
|
|
92
|
+
let deviceTokenData = null;
|
|
93
|
+
try {
|
|
94
|
+
deviceTokenData = await obtainDeviceToken(apiUrl, email, password);
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
console.log(` Erro de conexao: ${err}`);
|
|
98
|
+
rl.close();
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
if (!deviceTokenData) {
|
|
102
|
+
console.log(" Login falhou. Verifique email e senha.");
|
|
103
|
+
rl.close();
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
console.log(` Autenticado! Bem-vindo, ${deviceTokenData.user_name || email}`);
|
|
107
|
+
console.log(" Token de dispositivo obtido (90 dias, revogavel pelo admin).");
|
|
108
|
+
console.log(" Sua senha NAO sera armazenada.\n");
|
|
109
|
+
// 3. Choose target
|
|
110
|
+
console.log("Onde deseja configurar?\n");
|
|
111
|
+
console.log(" 1) Claude Code (global — recomendado)");
|
|
112
|
+
console.log(" 2) Claude Desktop");
|
|
113
|
+
console.log(" 3) Ambos");
|
|
114
|
+
const choice = await ask("Escolha", "1");
|
|
115
|
+
// Only device token + API URL are stored — no email, no password
|
|
116
|
+
const mcpConfig = {
|
|
117
|
+
command: "npx",
|
|
118
|
+
args: ["-y", "@re9ti/timesheet-mcp"],
|
|
119
|
+
env: {
|
|
120
|
+
TIMESHEET_API_URL: apiUrl,
|
|
121
|
+
TIMESHEET_DEVICE_TOKEN: deviceTokenData.device_token,
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const targets = [];
|
|
125
|
+
if (choice === "1" || choice === "3") {
|
|
126
|
+
const ccPath = getClaudeCodeSettingsPath();
|
|
127
|
+
const settings = readJsonFile(ccPath);
|
|
128
|
+
if (!settings.mcpServers)
|
|
129
|
+
settings.mcpServers = {};
|
|
130
|
+
settings.mcpServers["timesheet-re9ti"] = mcpConfig;
|
|
131
|
+
writeJsonFile(ccPath, settings);
|
|
132
|
+
targets.push(`Claude Code: ${ccPath}`);
|
|
133
|
+
}
|
|
134
|
+
if (choice === "2" || choice === "3") {
|
|
135
|
+
const cdPath = getClaudeDesktopConfigPath();
|
|
136
|
+
const config = readJsonFile(cdPath);
|
|
137
|
+
if (!config.mcpServers)
|
|
138
|
+
config.mcpServers = {};
|
|
139
|
+
config.mcpServers["timesheet-re9ti"] = mcpConfig;
|
|
140
|
+
writeJsonFile(cdPath, config);
|
|
141
|
+
targets.push(`Claude Desktop: ${cdPath}`);
|
|
142
|
+
}
|
|
143
|
+
// 4. Done
|
|
144
|
+
console.log("\n Configuracao salva em:");
|
|
145
|
+
for (const t of targets) {
|
|
146
|
+
console.log(` ${t}`);
|
|
147
|
+
}
|
|
148
|
+
console.log(`
|
|
149
|
+
+--------------------------------------------------+
|
|
150
|
+
| Pronto! Reinicie o Claude Code / Desktop. |
|
|
151
|
+
| |
|
|
152
|
+
| Depois, basta dizer: |
|
|
153
|
+
| "Lanca 2h para cliente Silva, reuniao" |
|
|
154
|
+
| "Quantas horas trabalhei hoje?" |
|
|
155
|
+
| "Inicia timer para projeto X" |
|
|
156
|
+
| |
|
|
157
|
+
| Seguranca: |
|
|
158
|
+
| - Apenas o token de dispositivo foi salvo |
|
|
159
|
+
| - Sua senha NAO esta armazenada em nenhum lugar |
|
|
160
|
+
| - Token expira em 90 dias (re-execute o setup) |
|
|
161
|
+
| - Admin pode revogar o token a qualquer momento |
|
|
162
|
+
+--------------------------------------------------+
|
|
163
|
+
`);
|
|
164
|
+
rl.close();
|
|
165
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@re9ti/timesheet-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server para integrar Claude com o Timesheet Inteligente RE9TI",
|
|
5
|
+
"keywords": ["mcp", "timesheet", "claude", "re9ti"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "RE9TI <contato@re9ti.com.br>",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"bin": {
|
|
10
|
+
"re9ti-timesheet-mcp": "./bin/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"bin",
|
|
16
|
+
"README.md"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"start": "node dist/index.js",
|
|
22
|
+
"setup": "node dist/setup.js",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
27
|
+
"zod": "^3.23.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"typescript": "^5.5.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|