suite-timesheet-mcp 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +5 -4
- package/README.md +125 -25
- package/SECURITY.md +5 -5
- package/lib/ferramentas.js +50 -11
- package/lib/rotas.js +1 -1
- package/lib/validar.js +30 -6
- package/mcp.js +1 -1
- package/package.json +2 -2
package/CONTRACT.md
CHANGED
|
@@ -10,12 +10,12 @@ Um serviço que implemente estas três rotas passa a poder pedir leituras e prop
|
|
|
10
10
|
|
|
11
11
|
| método | rota | corpo / resposta |
|
|
12
12
|
|---|---|---|
|
|
13
|
-
| `POST` | `/bridge/contexto` | a extensão envia `{ano, mes, diasNoMes, projetos: [{option_id, nome, trancado}], lidoEm}` sempre que a Folha de Horas carrega ou muda de mês |
|
|
13
|
+
| `POST` | `/bridge/contexto` | a extensão envia `{ano, mes, diasNoMes, projetos: [{option_id, nome, trancado}], lidoEm, versaoExtensao}` (`versaoExtensao` a partir da extensão 1.1.0) sempre que a Folha de Horas carrega ou muda de mês |
|
|
14
14
|
| `GET` | `/bridge/next?wait=25` | responde `204` sem corpo se não houver comando ao fim de `wait` segundos, ou `200` com um comando |
|
|
15
15
|
| `POST` | `/bridge/result/{id}` | a extensão publica o resultado do comando `id` |
|
|
16
16
|
|
|
17
17
|
Comando: `{"id": "texto", "tipo": "estado" | "projetos" | "ler" | "propor" | "aplicar", "ano": 2026, "mes": 9}`.
|
|
18
|
-
`propor` leva ainda `"linhas": [{option_id | project, date, hours}]
|
|
18
|
+
`propor` leva ainda `"linhas": [{option_id | project, date, hours, comment?}]` (`comment` vai para as Observações do projeto), `"modo": "normal" | "somar" | "sobregravar"` (defeito `normal`; ver a tabela de modos no `CONTRACT.md` da extensão; `"espelho": true`, da 1.0, vale como `sobregravar` e é sempre enviado junto para uma extensão 1.0 perceber o sobregravar) e
|
|
19
19
|
`"nome": "texto"`. O mês tem de ser o que a página mostra; caso contrário a resposta é
|
|
20
20
|
`ERR_MES_DIFERENTE` e nada é lido nem escrito.
|
|
21
21
|
|
|
@@ -49,7 +49,7 @@ com o plano calculado (o painel abre nesse momento), e depois, quando o utilizad
|
|
|
49
49
|
|
|
50
50
|
A extensão continua a ser a única coisa que escreve no Suite, sempre depois de o utilizador
|
|
51
51
|
clicar Aplicar no painel ou de aprovar a tool `aplicar` no cliente MCP (ver secção "aplicar"
|
|
52
|
-
abaixo). Nunca submete
|
|
52
|
+
abaixo), e só no mês aberto na página. Nunca submete as horas, nunca troca o mês selecionado.
|
|
53
53
|
|
|
54
54
|
### Prazo de `/bridge/next`
|
|
55
55
|
|
|
@@ -68,7 +68,7 @@ Resultado com `"ok": true`, no campo `dados`:
|
|
|
68
68
|
|---|---|
|
|
69
69
|
| `estado` | `{ano, mes, projetos: N, loteEmCurso: boolean}` — `projetos` é a contagem, não a lista |
|
|
70
70
|
| `projetos` | `[{option_id, nome, trancado}]` |
|
|
71
|
-
| `ler` | `{ano, mes, linhas: [{option_id, nome, status, dias: {D: horas}, total}], totaisPorDia: {D: horas}}` |
|
|
71
|
+
| `ler` | `{ano, mes, linhas: [{option_id, nome, status, dias: {D: horas}, total, comentario}], totaisPorDia: {D: horas}}` — `comentario` a partir da extensão 1.1.0 |
|
|
72
72
|
| `propor`, fase `previa` | `{fase: 'previa', previa: {matriz, criar, atualizar, apagar, ignoradas, avisos, erros, bloqueio}}` — `criar`/`atualizar`/`apagar` são listas de `{option_id, projeto, dias}`; `bloqueio` é a mensagem em português ou `null` |
|
|
73
73
|
| `propor`, fase `final` | `{fase: 'final', estado: 'aplicado' \| 'cancelado' \| 'erro', report?, motivo?, erro?}` — `report` só em `aplicado` (`{results, stopped, reconciliacao, skipped}`), `motivo` só em `cancelado`, `erro` (`{code, message}`) só em `erro` |
|
|
74
74
|
| `aplicar` (uma só fase) | `{estado: 'aplicado' \| 'erro', report?, stopped?, erro?}` — sem `fase: 'previa'`; ver secção "aplicar" acima |
|
|
@@ -79,6 +79,7 @@ Resultado com `"ok": true`, no campo `dados`:
|
|
|
79
79
|
|
|
80
80
|
| código | motivo |
|
|
81
81
|
|---|---|
|
|
82
|
+
| `ERR_EXTENSAO_ANTIGA` | (só na tool `propor`) a extensão é anterior à 1.1.0 e o modo pedido não é `sobregravar`: a 1.0 faria outra coisa |
|
|
82
83
|
| `ERR_COMANDO` | corpo do comando malformado (id/tipo/ano/mes/linhas inválidos) |
|
|
83
84
|
| `ERR_MES_DIFERENTE` | o comando pede um mês diferente do que a página do Suite tem aberto |
|
|
84
85
|
| `ERR_SEM_CONTEXTO` | a Folha de Horas do Suite ainda não foi aberta nesta sessão |
|
package/README.md
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/media/icone.png" alt="Ícone do Suite Timesheet Importer" width="96">
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
<h1 align="center">suite-timesheet-mcp</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>O agente propõe. Tu aprovas.</strong><br>
|
|
9
|
+
Servidor MCP que deixa um agente de IA <strong>ler e propor horas</strong> na Folha de Horas do
|
|
10
|
+
Suite, através da extensão <strong>Suite Timesheet Importer</strong>.
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<p align="center">
|
|
14
|
+
<a href="https://www.npmjs.com/package/suite-timesheet-mcp"><img alt="versão no npm" src="https://img.shields.io/npm/v/suite-timesheet-mcp?color=1f6699&label=npm"></a>
|
|
15
|
+
<img alt="Model Context Protocol" src="https://img.shields.io/badge/MCP-stdio-2a7fba">
|
|
16
|
+
<img alt="Node 20 ou superior" src="https://img.shields.io/badge/node-%E2%89%A5%2020-1e7a34">
|
|
17
|
+
<img alt="só loopback" src="https://img.shields.io/badge/rede-s%C3%B3%20127.0.0.1-1e7a34">
|
|
18
|
+
<img alt="sem telemetria" src="https://img.shields.io/badge/telemetria-nenhuma-1e7a34">
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="docs/media/lancamento-1.1.0.mp4">
|
|
23
|
+
<img src="docs/media/agente.gif" alt="Um agente recebe um pedido, propõe o lançamento e pede aprovação" width="820">
|
|
24
|
+
</a>
|
|
25
|
+
<br>
|
|
26
|
+
<sub>Excerto do vídeo de lançamento da 1.1.0, com o Copilot como exemplo de agente. Clica para ver o vídeo completo. Dados fictícios.</sub>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
## Porquê
|
|
30
|
+
|
|
31
|
+
O Suite não tem API para agentes. Este pacote dá-lhe uma, sem tocar no Suite:
|
|
32
|
+
|
|
33
|
+
- **Pedes em linguagem natural.** "Lança 4 h de hoje no Alfa: migração de dados."
|
|
34
|
+
- **O agente lê e propõe.** Vê o mês aberto, os projetos do teu dropdown e o que já está
|
|
35
|
+
lançado, e monta a proposta.
|
|
36
|
+
- **Tu decides.** A proposta abre no painel da extensão com a pré-visualização. Só é escrita
|
|
37
|
+
depois de aprovares.
|
|
38
|
+
|
|
39
|
+
Funciona com qualquer agente que fale MCP. O servidor não sabe nem precisa de saber qual é.
|
|
40
|
+
|
|
41
|
+
## Garantias
|
|
5
42
|
|
|
6
43
|
- **Nunca fala com o Suite.** Quem lê e escreve é a extensão, no teu browser, com a tua sessão.
|
|
7
44
|
- **Nunca escreve sem ti.** Cada lançamento é uma proposta; só é escrito quando clicas Aplicar
|
|
@@ -26,18 +63,12 @@ Nas **Opções** da extensão, liga **Ligação ao servidor local (ponte MCP)**,
|
|
|
26
63
|
|
|
27
64
|
Este passo é manual e feito uma vez: sem ele a extensão nunca toca na rede local.
|
|
28
65
|
|
|
29
|
-
### 2. Registar o servidor no teu
|
|
66
|
+
### 2. Registar o servidor no teu agente
|
|
30
67
|
|
|
31
68
|
Não é preciso clonar nada: o `npx` descarrega o pacote do npm e corre-o. `@1` fixa a versão
|
|
32
69
|
principal: recebes as correções 1.x, nunca uma versão incompatível. A primeira execução demora
|
|
33
70
|
alguns segundos; as seguintes usam a cache.
|
|
34
71
|
|
|
35
|
-
**Claude Code**
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
|
|
39
|
-
```
|
|
40
|
-
|
|
41
72
|
**GitHub Copilot (VS Code)**
|
|
42
73
|
|
|
43
74
|
```bash
|
|
@@ -46,6 +77,12 @@ code --add-mcp '{"name":"suite-timesheet","command":"npx","args":["-y","suite-ti
|
|
|
46
77
|
|
|
47
78
|
Depois recarrega a janela; as tools aparecem no modo **Agent** do Copilot.
|
|
48
79
|
|
|
80
|
+
**Claude Code**
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
|
|
84
|
+
```
|
|
85
|
+
|
|
49
86
|
**Claude Desktop (inclui Cowork)**
|
|
50
87
|
|
|
51
88
|
Em Definições → Programador → Editar configuração
|
|
@@ -65,7 +102,7 @@ Em Definições → Programador → Editar configuração
|
|
|
65
102
|
|
|
66
103
|
Reinicia o Claude Desktop.
|
|
67
104
|
|
|
68
|
-
**Outros
|
|
105
|
+
**Outros agentes**
|
|
69
106
|
|
|
70
107
|
O mesmo comando: `npx -y suite-timesheet-mcp@1`, por stdio. Atenção à
|
|
71
108
|
chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outros usa
|
|
@@ -77,9 +114,9 @@ chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outr
|
|
|
77
114
|
|
|
78
115
|
### 3. Verificar
|
|
79
116
|
|
|
80
|
-
Pede ao
|
|
117
|
+
Pede ao agente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
|
|
81
118
|
responder com a ponte ligada e o mês visível na página. As Opções da extensão passam a mostrar
|
|
82
|
-
"
|
|
119
|
+
"Agente ligado há N s" (o texto é o mesmo para qualquer agente).
|
|
83
120
|
|
|
84
121
|
## Instalação feita por um agente
|
|
85
122
|
|
|
@@ -102,18 +139,22 @@ Exemplos de pedidos:
|
|
|
102
139
|
- *"Quantas horas tenho lançadas este mês no Suite?"* → `ler_mes`
|
|
103
140
|
- *"Que projetos tenho disponíveis?"* → `projetos`
|
|
104
141
|
- *"Lança 8 h no projeto X de segunda a sexta desta semana."* → `propor`, depois `aplicar`
|
|
142
|
+
- *"Soma 2 h ao dia de hoje no projeto X, com a nota 'reunião de arranque'."* → `propor` em
|
|
143
|
+
modo `somar`, com `comment`
|
|
105
144
|
|
|
106
145
|
Lançar horas tem sempre duas fases:
|
|
107
146
|
|
|
108
147
|
1. **`propor`** calcula o plano, abre o painel da extensão com a pré-visualização e devolve-a ao
|
|
109
|
-
|
|
148
|
+
agente. Nada é escrito.
|
|
110
149
|
2. **Confirmação**, de uma de duas formas:
|
|
111
150
|
- clicas **Aplicar** no painel, ou
|
|
112
151
|
- dizes OK no chat e aprovas a chamada da tool **`aplicar`** no cliente MCP.
|
|
113
152
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
153
|
+
> [!IMPORTANT]
|
|
154
|
+
> Mantém a aprovação manual da tool `aplicar`. Com ela em aprovação automática, qualquer
|
|
155
|
+
> proposta é escrita sem mais nenhuma confirmação.
|
|
156
|
+
|
|
157
|
+
Linhas trancadas e propostas bloqueadas são sempre recusadas pela extensão.
|
|
117
158
|
|
|
118
159
|
## Tools
|
|
119
160
|
|
|
@@ -121,11 +162,56 @@ bloqueadas são sempre recusadas pela extensão.
|
|
|
121
162
|
|---|---|
|
|
122
163
|
| `estado` | ponte ligada?, mês visível, nº de projetos, lote a correr |
|
|
123
164
|
| `projetos` | projetos do dropdown da página: `option_id`, nome, trancado |
|
|
124
|
-
| `ler_mes` | horas já lançadas no mês visível |
|
|
165
|
+
| `ler_mes` | horas e Observações já lançadas no mês visível |
|
|
125
166
|
| `propor` | calcula o plano, abre o painel, devolve a pré-visualização e um `id` |
|
|
126
|
-
| `aplicar` | escreve a proposta `id` no Suite através da extensão |
|
|
167
|
+
| `aplicar` | escreve a proposta `id` no Suite através da extensão, sem esperar pelo clique no painel |
|
|
127
168
|
| `resultado` | espera pela decisão (painel ou `aplicar`): aplicado, cancelado, erro ou pendente |
|
|
128
169
|
|
|
170
|
+
### `propor`
|
|
171
|
+
|
|
172
|
+
```json
|
|
173
|
+
{
|
|
174
|
+
"modo": "somar",
|
|
175
|
+
"linhas": [
|
|
176
|
+
{
|
|
177
|
+
"project": "Alfa - Projeto SAP",
|
|
178
|
+
"date": "2026-10-14",
|
|
179
|
+
"hours": 4,
|
|
180
|
+
"comment": "Migração de dados"
|
|
181
|
+
}
|
|
182
|
+
]
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
| campo | notas |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `linhas[].option_id` ou `project` | um dos dois: id do dropdown ou nome tal como lá aparece |
|
|
189
|
+
| `linhas[].date` | `YYYY-MM-DD`, dentro do mês visível |
|
|
190
|
+
| `linhas[].hours` | 0,5 a 23,5 em passos de 0,5; em `somar` pode ser negativo |
|
|
191
|
+
| `linhas[].comment` | opcional; vai para as Observações do projeto no mês |
|
|
192
|
+
| `modo` | `normal` (defeito), `somar` ou `sobregravar` |
|
|
193
|
+
| `nome` | opcional; rótulo que aparece no painel |
|
|
194
|
+
|
|
195
|
+
| modo | o que vem nas linhas | o que não vem |
|
|
196
|
+
|---|---|---|
|
|
197
|
+
| `normal` | fica com o valor pedido | fica como está |
|
|
198
|
+
| `somar` | Suite + pedido (negativos subtraem) | fica como está |
|
|
199
|
+
| `sobregravar` | fica com o valor pedido | é apagado |
|
|
200
|
+
|
|
201
|
+
Propor duas vezes em `somar` duplica. Mais de 8 h num dia é só aviso.
|
|
202
|
+
|
|
203
|
+
## Novidades da 1.1.0
|
|
204
|
+
|
|
205
|
+
- **`propor` aceita `modo`**: `normal`, `somar` ou `sobregravar`, no lugar do antigo `espelho`
|
|
206
|
+
(que continua aceite).
|
|
207
|
+
- **`propor` aceita `comment` por linha**, que vai para as Observações do projeto.
|
|
208
|
+
- **`ler_mes` devolve as Observações.**
|
|
209
|
+
- **Comentários que o Suite ignoraria em silêncio são recusados** antes de propor.
|
|
210
|
+
- **`ERR_EXTENSAO_ANTIGA`**: com uma extensão anterior à 1.1.0, os modos `normal` e `somar` são
|
|
211
|
+
recusados, porque a extensão antiga faria outra coisa.
|
|
212
|
+
|
|
213
|
+
Os modos `normal` e `somar` e os comentários precisam da extensão ≥ 1.1.0.
|
|
214
|
+
|
|
129
215
|
## Erros
|
|
130
216
|
|
|
131
217
|
| erro | o que fazer |
|
|
@@ -133,16 +219,25 @@ bloqueadas são sempre recusadas pela extensão.
|
|
|
133
219
|
| `ERR_SERVE_EM_BAIXO` | o serviço local não responde; reinicia o cliente MCP (ele arranca-o sozinho) |
|
|
134
220
|
| `ERR_SEM_CHROME` | abre a Folha de Horas e confirma que a ponte está ligada nas Opções |
|
|
135
221
|
| `ERR_SEM_CONTEXTO` | recarrega a Folha de Horas (o serviço reiniciou depois de a página carregar) |
|
|
222
|
+
| `ERR_SEM_ABA` | não há nenhuma aba do Suite aberta; abre-a ou recarrega-a |
|
|
136
223
|
| `ERR_MES_DIFERENTE` | muda o mês na página; o MCP nunca o muda por ti |
|
|
137
224
|
| `ERR_OCUPADO` | há uma proposta à espera no painel; aplica-a ou cancela-a primeiro |
|
|
225
|
+
| `ERR_LOTE_A_CORRER` | já está um lote a escrever no Suite; espera que termine |
|
|
226
|
+
| `ERR_SESSION` | a sessão do Suite expirou; entra outra vez |
|
|
138
227
|
| `ERR_PROPOSTA_DESCONHECIDA` | o `id` passado a `aplicar` não corresponde a nenhuma proposta pendente |
|
|
228
|
+
| `ERR_EXTENSAO_ANTIGA` | atualiza a extensão para a 1.1.0, ou usa `modo: "sobregravar"` |
|
|
229
|
+
|
|
230
|
+
Lista completa em [CONTRACT.md](CONTRACT.md).
|
|
139
231
|
|
|
140
232
|
## Como funciona
|
|
141
233
|
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
234
|
+
```mermaid
|
|
235
|
+
flowchart LR
|
|
236
|
+
C["Agente<br>cliente MCP"] -->|stdio| M["mcp.js"]
|
|
237
|
+
M -->|HTTP local| S["serve.js<br>127.0.0.1:18765"]
|
|
238
|
+
E["Extensão<br>no browser"] -->|long-poll| S
|
|
239
|
+
E -->|"lê e escreve<br>com a tua sessão"| T["Folha de Horas<br>do Suite"]
|
|
240
|
+
E --- P["Painel: pré-visualização<br>e Aplicar"]
|
|
146
241
|
```
|
|
147
242
|
|
|
148
243
|
1. O cliente MCP arranca o `mcp.js`. Se o serviço local (`serve.js`) não estiver a correr, o
|
|
@@ -165,7 +260,8 @@ Com o serviço gerido à mão, arranca-o **antes** de abrir a Folha de Horas, ou
|
|
|
165
260
|
depois: a extensão só publica o contexto quando a página carrega ou muda de mês.
|
|
166
261
|
|
|
167
262
|
As tools aceitam `timeout_s` até 290 s (útil em `resultado` com lotes grandes). Se o cliente
|
|
168
|
-
cortar a chamada antes disso, aumenta o timeout do cliente MCP (
|
|
263
|
+
cortar a chamada antes disso, aumenta o timeout do cliente MCP (muitos cortam aos 60 s por
|
|
264
|
+
defeito).
|
|
169
265
|
|
|
170
266
|
## Atualizar
|
|
171
267
|
|
|
@@ -192,7 +288,7 @@ node --test
|
|
|
192
288
|
|
|
193
289
|
Para usar a cópia local num cliente, troca o comando `npx …` por
|
|
194
290
|
`node /caminho/para/suite-timesheet-mcp/mcp.js`. Também dá para instalar diretamente de uma tag do
|
|
195
|
-
GitHub, sem npm: `npx -y github:williansaez/suite-timesheet-mcp#v1.
|
|
291
|
+
GitHub, sem npm: `npx -y github:williansaez/suite-timesheet-mcp#v1.1.0` (precisa de `git`).
|
|
196
292
|
|
|
197
293
|
## Licença
|
|
198
294
|
|
|
@@ -200,3 +296,7 @@ Código disponível para consulta, **não open source**. Podes instalar e usar o
|
|
|
200
296
|
alterações, para uso pessoal ou interno da tua organização. Não é permitido alterá-lo,
|
|
201
297
|
redistribuí-lo nem publicar cópias ou forks. Os termos completos (em inglês, com tradução para
|
|
202
298
|
português) estão em [LICENSE](LICENSE).
|
|
299
|
+
|
|
300
|
+
<p align="center">
|
|
301
|
+
<img src="docs/media/fecho.png" alt="Suite Timesheet Importer 1.1.0 e suite-timesheet-mcp 1.1.0. O Suite de sempre, a tecnologia de hoje." width="820">
|
|
302
|
+
</p>
|
package/SECURITY.md
CHANGED
|
@@ -5,9 +5,9 @@ ele faz, o que nunca faz, e como verificar cada afirmação.
|
|
|
5
5
|
|
|
6
6
|
## Em uma frase
|
|
7
7
|
|
|
8
|
-
Um processo local (`serve.js`) que guarda em memória uma fila de comandos entre o
|
|
8
|
+
Um processo local (`serve.js`) que guarda em memória uma fila de comandos entre o agente e a
|
|
9
9
|
extensão Suite Timesheet Importer, e um servidor MCP por stdio (`mcp.js`) que expõe seis tools ao
|
|
10
|
-
|
|
10
|
+
agente. Este pacote **nunca fala com o Suite**: quem lê e escreve no Suite é a extensão, dentro do
|
|
11
11
|
browser, com a sessão do utilizador, e só depois de o utilizador clicar Aplicar no painel ou de
|
|
12
12
|
aprovar a tool `aplicar` no cliente MCP (ver a tabela abaixo).
|
|
13
13
|
|
|
@@ -17,7 +17,7 @@ aprovar a tool `aplicar` no cliente MCP (ver a tabela abaixo).
|
|
|
17
17
|
|---|---|
|
|
18
18
|
| Nunca contacta o Suite nem qualquer host externo | o código não contém nenhum URL além de `127.0.0.1` (guard no CI); o único `listen` é em `127.0.0.1`; o `mcp.js` só chama o `serve` local |
|
|
19
19
|
| Nunca escreve no Suite, nem submete meses | não tem sessão, cookies nem endpoints do Suite; o endpoint de submissão não existe no código (guard no CI) |
|
|
20
|
-
| Nunca provoca uma escrita sem confirmação humana | a tool `propor` só enfileira; a escrita só acontece com o clique em Aplicar no painel **ou** com a aprovação da tool `aplicar` no cliente MCP (
|
|
20
|
+
| Nunca provoca uma escrita sem confirmação humana | a tool `propor` só enfileira; a escrita só acontece com o clique em Aplicar no painel **ou** com a aprovação da tool `aplicar` no cliente MCP (o cliente a pedir confirmação antes de correr a tool) — uma das duas é sempre exigida |
|
|
21
21
|
| Nunca guarda dados em disco | fila, contexto e propostas vivem em memória; reiniciar o `serve` esquece tudo |
|
|
22
22
|
| Nunca aceita pedidos de páginas web | recusa `Origin` que não seja `chrome-extension://`, `Host` que não seja `127.0.0.1`/`localhost`, e POST sem `application/json` |
|
|
23
23
|
| Nunca contém dados reais | testes e exemplos só com projetos, ids e horas fictícios (guard de nomes de cliente no CI) |
|
|
@@ -42,7 +42,7 @@ não encontra nada a responder em `/health`; nunca arranca nem chama nenhum outr
|
|
|
42
42
|
## Dados
|
|
43
43
|
|
|
44
44
|
Em memória: o mês visível e a lista de projetos que a extensão publica, as propostas enviadas
|
|
45
|
-
pelo
|
|
45
|
+
pelo agente, e os resultados que a extensão devolve (prévia e resultado final). Resultados expiram
|
|
46
46
|
ao fim de 1 h; comandos sem resposta expiram com `ERR_EXPIRADO`. Nada é registado além de uma
|
|
47
47
|
linha por comando em `stderr` (tipo e mês, sem horas).
|
|
48
48
|
|
|
@@ -64,7 +64,7 @@ Uma, declarada e fixada em `package-lock.json`: `@modelcontextprotocol/sdk` (só
|
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
66
|
npm ci
|
|
67
|
-
node --test #
|
|
67
|
+
node --test # 85 testes, sem rede além de loopback
|
|
68
68
|
npm audit --omit=dev # dependências conhecidas
|
|
69
69
|
grep -rnoE "https?://[a-zA-Z0-9.-]+" --include='*.js' --exclude-dir=node_modules --exclude-dir=test . # esperado: só 127.0.0.1
|
|
70
70
|
grep -rniE "submitaction" --include='*.js' . --exclude-dir=node_modules | grep -v '/test/' # esperado: vazio
|
package/lib/ferramentas.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { validarProposta } from './validar.js';
|
|
4
4
|
|
|
5
|
-
const AVISO = '
|
|
5
|
+
const AVISO = 'Escreve horas só no mês que a Folha de Horas mostra no Chrome. Nunca submete as horas, nunca troca o mês selecionado.';
|
|
6
6
|
const WAIT_MAX_S = 290;
|
|
7
7
|
// O cliente por defeito do SDK do MCP corta a chamada aos 60 s; 50 dá margem para a
|
|
8
|
-
// tool responder antes disso. timeout_s continua a poder ser pedido até WAIT_MAX_S (
|
|
9
|
-
//
|
|
8
|
+
// tool responder antes disso. timeout_s continua a poder ser pedido até WAIT_MAX_S (alguns
|
|
9
|
+
// clientes MCP deixam subir esse timeout quando isso for preciso).
|
|
10
10
|
const TIMEOUT_TOOL_S = 50;
|
|
11
11
|
// aplicar escreve linha a linha no Suite: pode demorar bem mais do que as outras tools.
|
|
12
12
|
const TIMEOUT_APLICAR_S = 120;
|
|
@@ -24,12 +24,12 @@ export const FERRAMENTAS = [
|
|
|
24
24
|
},
|
|
25
25
|
{
|
|
26
26
|
name: 'ler_mes',
|
|
27
|
-
description: `Lê as horas já lançadas no mês visível: uma linha por projeto com os dias preenchidos, o status e os totais por dia. ${AVISO}`,
|
|
27
|
+
description: `Lê as horas já lançadas no mês visível: uma linha por projeto com os dias preenchidos, o status, as Observações (comentario) e os totais por dia. ${AVISO}`,
|
|
28
28
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
name: 'propor',
|
|
32
|
-
description: `Propõe lançamentos para o mês visível. A extensão calcula o plano (criar/atualizar/apagar), abre o painel com a pré-visualização e devolve-a aqui; nada é escrito até o utilizador clicar Aplicar no painel. Depois chama "resultado" com o id.
|
|
32
|
+
description: `Propõe lançamentos para o mês visível. A extensão calcula o plano (criar/atualizar/apagar), abre o painel com a pré-visualização e devolve-a aqui; nada é escrito até o utilizador clicar Aplicar no painel. Depois chama "resultado" com o id. Modo (defeito "normal"): normal escreve os valores das linhas (preenche ou substitui) e nunca apaga o que não vier; somar soma às horas do Suite (negativos subtraem; propor duas vezes duplica); sobregravar deixa o mês igual às linhas (apaga dias e projetos que não vierem). Mais de 8 h num dia é só aviso. ${AVISO}`,
|
|
33
33
|
inputSchema: {
|
|
34
34
|
type: 'object',
|
|
35
35
|
required: ['linhas'],
|
|
@@ -46,11 +46,20 @@ export const FERRAMENTAS = [
|
|
|
46
46
|
option_id: { type: 'integer', description: 'id do projeto no dropdown (usa "projetos"). Exclusivo com project.' },
|
|
47
47
|
project: { type: 'string', description: 'nome do projeto como aparece no dropdown. Exclusivo com option_id.' },
|
|
48
48
|
date: { type: 'string', description: 'YYYY-MM-DD, dentro do mês visível' },
|
|
49
|
-
hours: { type: 'number', description: 'de 0,5 a 23,5 em passo de 0,5' },
|
|
49
|
+
hours: { type: 'number', description: 'de 0,5 a 23,5 em passo de 0,5 (em modo somar pode ser negativo, para subtrair)' },
|
|
50
|
+
comment: {
|
|
51
|
+
type: 'string',
|
|
52
|
+
description: 'opcional: vai para as Observações do projeto no mês (uma por projeto). Comentários diferentes do mesmo projeto são juntos com "; ". Sem comment, as Observações que o Suite já tem ficam.',
|
|
53
|
+
},
|
|
50
54
|
},
|
|
51
55
|
},
|
|
52
56
|
},
|
|
53
|
-
|
|
57
|
+
modo: {
|
|
58
|
+
type: 'string',
|
|
59
|
+
enum: ['normal', 'somar', 'sobregravar'],
|
|
60
|
+
description: 'normal (defeito): escreve as células das linhas, nunca apaga; somar: soma ao Suite, negativos subtraem; sobregravar: o mês fica igual às linhas',
|
|
61
|
+
},
|
|
62
|
+
espelho: { type: 'boolean', description: 'obsoleto: espelho: true é o mesmo que modo: "sobregravar"' },
|
|
54
63
|
nome: { type: 'string', description: 'rótulo que aparece no painel (ex.: "sprint 12")' },
|
|
55
64
|
},
|
|
56
65
|
},
|
|
@@ -111,6 +120,27 @@ const finalDe = (id, dados) => {
|
|
|
111
120
|
return { id, estado, report, motivo };
|
|
112
121
|
};
|
|
113
122
|
|
|
123
|
+
// A extensão 1.0 descarta o comment sem avisar: antes de propor comentários, confirma a versão
|
|
124
|
+
// que ela publicou no contexto. Nunca falha por isto — no pior caso avisa sem precisar.
|
|
125
|
+
const menorQue = (a, b) => {
|
|
126
|
+
const [x, y] = [a, b].map((v) => String(v).split('.').map(Number));
|
|
127
|
+
for (let i = 0; i < 3; i += 1) if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) < (y[i] ?? 0);
|
|
128
|
+
return false;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// A versão vem do contexto que a extensão publicou. Sem contexto (serve reiniciado, estado em
|
|
132
|
+
// baixo) não se sabe: deixa seguir, o comando falha sozinho se não houver ponte. Com contexto
|
|
133
|
+
// sem versaoExtensao é uma extensão 1.0.
|
|
134
|
+
async function extensaoAntiga(cliente) {
|
|
135
|
+
let contexto = null;
|
|
136
|
+
try { contexto = (await cliente.estado())?.contexto ?? null; } catch { contexto = null; }
|
|
137
|
+
if (!contexto) return { antiga: false, versao: null };
|
|
138
|
+
const versao = contexto.versaoExtensao ?? null;
|
|
139
|
+
return { antiga: !versao || menorQue(versao, '1.1.0'), versao };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const textoVersao = (versao) => (versao ? `está na ${versao}` : 'é anterior à 1.1.0');
|
|
143
|
+
|
|
114
144
|
export async function executar(nome, args = {}, cliente, { estadoServe } = {}) {
|
|
115
145
|
switch (nome) {
|
|
116
146
|
case 'estado': {
|
|
@@ -121,7 +151,7 @@ export async function executar(nome, args = {}, cliente, { estadoServe } = {}) {
|
|
|
121
151
|
// O auto-arranque do mcp.js (ver lib/autoarranque.js) já tentou pôr o serve a
|
|
122
152
|
// responder antes disto. Se não conseguiu, por a porta estar ocupada por outro
|
|
123
153
|
// programa ou por o arranque ter falhado, esta tool não deve lançar: devolve o
|
|
124
|
-
// porquê para o
|
|
154
|
+
// porquê para o agente poder explicar ao utilizador em vez de um erro seco.
|
|
125
155
|
if (err.code === 'ERR_SERVE_EM_BAIXO' && (estadoServe?.estado === 'porta-ocupada' || estadoServe?.estado === 'falhou')) {
|
|
126
156
|
return { ponte: 'sem-serve', serve: estadoServe.estado, detalhe: estadoServe.detalhe };
|
|
127
157
|
}
|
|
@@ -148,15 +178,24 @@ export async function executar(nome, args = {}, cliente, { estadoServe } = {}) {
|
|
|
148
178
|
case 'ler_mes': return (await dadosDe(cliente, { tipo: 'ler', timeout_s: TIMEOUT_TOOL_S })).dados;
|
|
149
179
|
case 'propor': {
|
|
150
180
|
const proposta = validarProposta(args); // erro imediato e legível; o serve valida com o mês
|
|
181
|
+
const { antiga, versao } = await extensaoAntiga(cliente);
|
|
182
|
+
// A 1.0 não conhece normal nem somar: trataria as linhas como "substituir o projeto".
|
|
183
|
+
if (antiga && proposta.modo !== 'sobregravar') {
|
|
184
|
+
throw erro('ERR_EXTENSAO_ANTIGA', `A extensão ${textoVersao(versao)} e só conhece o modo sobregravar. Atualiza a extensão para a 1.1.0 (ou usa modo: "sobregravar").`);
|
|
185
|
+
}
|
|
186
|
+
const aviso = antiga && proposta.linhas.some((l) => l.comment)
|
|
187
|
+
? `A extensão ${textoVersao(versao)}: os comentários (Observações) só são escritos a partir da 1.1.0 e vão ser ignorados.`
|
|
188
|
+
: null;
|
|
189
|
+
const comAviso = (resposta) => (aviso ? { ...resposta, aviso } : resposta);
|
|
151
190
|
const r = await cliente.comando({ tipo: 'propor', ...proposta, timeout_s: TIMEOUT_TOOL_S });
|
|
152
|
-
if (!r.resultado) return { id: r.id, estado: 'pendente' };
|
|
191
|
+
if (!r.resultado) return comAviso({ id: r.id, estado: 'pendente' });
|
|
153
192
|
if (!r.resultado.ok) throw erro(r.resultado.erro.code, r.resultado.erro.message);
|
|
154
193
|
const { dados } = r.resultado;
|
|
155
194
|
// Normalmente chega logo a previa; se a previa se perdeu (ex.: o worker publicou logo o
|
|
156
195
|
// final) dados.fase já vem 'final' — devolve o estado final em vez de fingir "previa".
|
|
157
|
-
if (dados.fase !== 'previa') return finalDe(r.id, dados);
|
|
196
|
+
if (dados.fase !== 'previa') return comAviso(finalDe(r.id, dados));
|
|
158
197
|
const { previa } = dados;
|
|
159
|
-
return { id: r.id, estado: 'previa', previa, bloqueio: previa?.bloqueio ?? null };
|
|
198
|
+
return comAviso({ id: r.id, estado: 'previa', previa, bloqueio: previa?.bloqueio ?? null });
|
|
160
199
|
}
|
|
161
200
|
case 'aplicar': {
|
|
162
201
|
const id = String(args.id);
|
package/lib/rotas.js
CHANGED
|
@@ -20,7 +20,7 @@ const limitar = (v, max) => Math.max(0, Math.min(inteiro(v, max), max));
|
|
|
20
20
|
export function criarRotas({ fila, contexto, log = () => {}, versao, agora = Date.now }) {
|
|
21
21
|
// Timestamp ISO do último pedido a qualquer rota /mcp/*, ou null se ainda não houve nenhum
|
|
22
22
|
// desde que este `serve` arrancou. É o que /health e /mcp/estado mostram como "ultimoMcp",
|
|
23
|
-
// para a extensão (painel) saber se o
|
|
23
|
+
// para a extensão (painel) saber se o agente ainda está a falar com o serviço.
|
|
24
24
|
let ultimoMcp = null;
|
|
25
25
|
const marcarMcp = () => { ultimoMcp = new Date(agora()).toISOString(); };
|
|
26
26
|
|
package/lib/validar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Puro. Primeira validação de uma proposta, para o
|
|
1
|
+
// Puro. Primeira validação de uma proposta, para o agente ter um erro imediato e legível.
|
|
2
2
|
// A extensão volta a validar (normalize/match): esta é a rede, aquela é a verdade.
|
|
3
3
|
|
|
4
4
|
const DATA_ISO = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
@@ -17,11 +17,16 @@ function dataValida(texto) {
|
|
|
17
17
|
return { ano, mes, dia };
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export
|
|
20
|
+
export const MODOS = ['normal', 'somar', 'sobregravar'];
|
|
21
|
+
|
|
22
|
+
// modo (extensão 1.1): normal | somar | sobregravar. espelho: true é o nome da 1.0 para sobregravar.
|
|
23
|
+
export function validarProposta({ linhas, espelho = false, modo: pedido, nome } = {}, mesVisivel = null) {
|
|
24
|
+
const modo = pedido ?? (espelho === true ? 'sobregravar' : 'normal');
|
|
21
25
|
const problemas = [];
|
|
26
|
+
if (!MODOS.includes(modo)) problemas.push(`modo tem de ser ${MODOS.join(', ')}.`);
|
|
22
27
|
const lista = Array.isArray(linhas) ? linhas : [];
|
|
23
28
|
if (!Array.isArray(linhas)) problemas.push('"linhas" tem de ser um array.');
|
|
24
|
-
else if (linhas.length === 0 &&
|
|
29
|
+
else if (linhas.length === 0 && modo !== 'sobregravar') problemas.push('Sem linhas. Para limpar o mês usa modo: "sobregravar".');
|
|
25
30
|
|
|
26
31
|
const normalizadas = lista.map((linha, i) => {
|
|
27
32
|
const n = i + 1;
|
|
@@ -37,21 +42,40 @@ export function validarProposta({ linhas, espelho = false, nome } = {}, mesVisiv
|
|
|
37
42
|
problemas.push(`linha ${n}: ${linha.date} está fora do mês visível ${mesVisivel.ano}-${String(mesVisivel.mes).padStart(2, '0')}.`);
|
|
38
43
|
}
|
|
39
44
|
const horas = Number(linha?.hours);
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
// Em somar um negativo subtrai: a extensão valida o resultado (0 a 23,5) contra o Suite.
|
|
46
|
+
const noIntervalo = modo === 'somar'
|
|
47
|
+
? horas !== 0 && Math.abs(horas) <= 23.5
|
|
48
|
+
: horas > 0 && horas <= 23.5;
|
|
49
|
+
if (!Number.isFinite(horas) || !noIntervalo || Math.round(horas * 2) !== horas * 2) {
|
|
50
|
+
problemas.push(modo === 'somar'
|
|
51
|
+
? `linha ${n}: hours em somar tem de ser diferente de 0, entre -23,5 e 23,5, em passo de 0,5.`
|
|
52
|
+
: `linha ${n}: hours tem de estar entre 0,5 e 23,5 em passo de 0,5.`);
|
|
53
|
+
}
|
|
54
|
+
// comment (Observações do Suite) é opcional; vazio fica de fora e o Suite mantém o que tem.
|
|
55
|
+
const comentario = linha?.comment;
|
|
56
|
+
if (comentario !== undefined && comentario !== null && typeof comentario !== 'string') {
|
|
57
|
+
problemas.push(`linha ${n}: comment tem de ser texto.`);
|
|
58
|
+
}
|
|
59
|
+
const textoComentario = typeof comentario === 'string' ? comentario.trim() : '';
|
|
60
|
+
// O Suite (validação de pedidos do ASP.NET) responde 200 sem gravar estes padrões.
|
|
61
|
+
if (/<[A-Za-z!/?]|&#/.test(textoComentario)) {
|
|
62
|
+
problemas.push(`linha ${n}: comment não pode ter "<" seguido de letra nem "&#" (o Suite ignora-o sem avisar).`);
|
|
42
63
|
}
|
|
43
64
|
return {
|
|
44
65
|
option_id: temId ? Number(linha.option_id) : '',
|
|
45
66
|
project: temNome ? linha.project.trim() : '',
|
|
46
67
|
date: String(linha?.date ?? ''),
|
|
47
68
|
hours: horas,
|
|
69
|
+
...(textoComentario ? { comment: textoComentario } : {}),
|
|
48
70
|
};
|
|
49
71
|
});
|
|
50
72
|
|
|
51
73
|
if (problemas.length > 0) throw erroProposta(problemas);
|
|
52
74
|
return {
|
|
53
75
|
linhas: normalizadas,
|
|
54
|
-
|
|
76
|
+
modo,
|
|
77
|
+
// Enviado também para uma extensão 1.0, que só conhece espelho.
|
|
78
|
+
espelho: modo === 'sobregravar',
|
|
55
79
|
nome: typeof nome === 'string' && nome.trim() ? nome.trim() : 'proposta do Claude',
|
|
56
80
|
};
|
|
57
81
|
}
|
package/mcp.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suite-timesheet-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Servidor MCP para o Suite Timesheet Importer: um
|
|
5
|
+
"description": "Servidor MCP para o Suite Timesheet Importer: um agente de IA lê e propõe horas; a extensão escreve depois de confirmação humana.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
7
7
|
"author": "Willian Saez",
|
|
8
8
|
"homepage": "https://github.com/williansaez/suite-timesheet-mcp#readme",
|