suite-timesheet-mcp 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRACT.md +5 -4
- package/README.md +132 -27
- package/SECURITY.md +5 -5
- package/lib/ferramentas.js +52 -13
- package/lib/rotas.js +3 -3
- package/lib/validar.js +30 -6
- package/mcp.js +1 -1
- package/package.json +3 -3
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,48 @@
|
|
|
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="https://williansaez.github.io/suite-timesheet-mcp/"><strong>Site do projeto</strong></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="docs/media/lancamento-1.1.0.mp4">
|
|
27
|
+
<img src="docs/media/agente.gif" alt="Um agente recebe um pedido, propõe o lançamento e pede aprovação" width="820">
|
|
28
|
+
</a>
|
|
29
|
+
<br>
|
|
30
|
+
<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>
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
## Porquê
|
|
34
|
+
|
|
35
|
+
O Suite não tem API para agentes. Este pacote dá-lhe uma, sem tocar no Suite:
|
|
36
|
+
|
|
37
|
+
- **Pedes em linguagem natural.** "Lança 4 h de hoje no Alfa: migração de dados."
|
|
38
|
+
- **O agente lê e propõe.** Vê o mês aberto, os projetos do teu dropdown e o que já está
|
|
39
|
+
lançado, e monta a proposta.
|
|
40
|
+
- **Tu decides.** A proposta abre no painel da extensão com a pré-visualização. Só é escrita
|
|
41
|
+
depois de aprovares.
|
|
42
|
+
|
|
43
|
+
Funciona com qualquer agente que fale MCP. O servidor não sabe nem precisa de saber qual é.
|
|
44
|
+
|
|
45
|
+
## Garantias
|
|
5
46
|
|
|
6
47
|
- **Nunca fala com o Suite.** Quem lê e escreve é a extensão, no teu browser, com a tua sessão.
|
|
7
48
|
- **Nunca escreve sem ti.** Cada lançamento é uma proposta; só é escrito quando clicas Aplicar
|
|
@@ -14,30 +55,25 @@ Detalhes e forma de verificar cada afirmação: [SECURITY.md](SECURITY.md).
|
|
|
14
55
|
## Requisitos
|
|
15
56
|
|
|
16
57
|
- Node.js ≥ 20 (`node -v`).
|
|
17
|
-
-
|
|
58
|
+
- Microsoft Edge com a extensão [Suite Timesheet Importer](https://microsoftedge.microsoft.com/addons/detail/suite-timesheet-importer/ccmnogdllbomjogpddfedgolgeobhdef) instalada.
|
|
18
59
|
- A Folha de Horas do Suite aberta, com sessão válida.
|
|
19
60
|
|
|
20
61
|
## Instalação
|
|
21
62
|
|
|
22
63
|
### 1. Ligar a ponte na extensão
|
|
23
64
|
|
|
24
|
-
|
|
65
|
+
Abre `edge://extensions`, clica em **Detalhes** na Suite Timesheet Importer e depois em **Opções da
|
|
66
|
+
extensão**. Aí, liga **Ligação ao servidor local (ponte MCP)**, clica em
|
|
25
67
|
**Guardar** e aceita a permissão para `http://127.0.0.1/*`. Depois recarrega a Folha de Horas.
|
|
26
68
|
|
|
27
69
|
Este passo é manual e feito uma vez: sem ele a extensão nunca toca na rede local.
|
|
28
70
|
|
|
29
|
-
### 2. Registar o servidor no teu
|
|
71
|
+
### 2. Registar o servidor no teu agente
|
|
30
72
|
|
|
31
73
|
Não é preciso clonar nada: o `npx` descarrega o pacote do npm e corre-o. `@1` fixa a versão
|
|
32
74
|
principal: recebes as correções 1.x, nunca uma versão incompatível. A primeira execução demora
|
|
33
75
|
alguns segundos; as seguintes usam a cache.
|
|
34
76
|
|
|
35
|
-
**Claude Code**
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
|
|
39
|
-
```
|
|
40
|
-
|
|
41
77
|
**GitHub Copilot (VS Code)**
|
|
42
78
|
|
|
43
79
|
```bash
|
|
@@ -46,6 +82,12 @@ code --add-mcp '{"name":"suite-timesheet","command":"npx","args":["-y","suite-ti
|
|
|
46
82
|
|
|
47
83
|
Depois recarrega a janela; as tools aparecem no modo **Agent** do Copilot.
|
|
48
84
|
|
|
85
|
+
**Claude Code**
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
|
|
89
|
+
```
|
|
90
|
+
|
|
49
91
|
**Claude Desktop (inclui Cowork)**
|
|
50
92
|
|
|
51
93
|
Em Definições → Programador → Editar configuração
|
|
@@ -65,7 +107,7 @@ Em Definições → Programador → Editar configuração
|
|
|
65
107
|
|
|
66
108
|
Reinicia o Claude Desktop.
|
|
67
109
|
|
|
68
|
-
**Outros
|
|
110
|
+
**Outros agentes**
|
|
69
111
|
|
|
70
112
|
O mesmo comando: `npx -y suite-timesheet-mcp@1`, por stdio. Atenção à
|
|
71
113
|
chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outros usa
|
|
@@ -77,9 +119,9 @@ chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outr
|
|
|
77
119
|
|
|
78
120
|
### 3. Verificar
|
|
79
121
|
|
|
80
|
-
Pede ao
|
|
122
|
+
Pede ao agente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
|
|
81
123
|
responder com a ponte ligada e o mês visível na página. As Opções da extensão passam a mostrar
|
|
82
|
-
"
|
|
124
|
+
"Agente ligado há N s" (o texto é o mesmo para qualquer agente).
|
|
83
125
|
|
|
84
126
|
## Instalação feita por um agente
|
|
85
127
|
|
|
@@ -102,18 +144,22 @@ Exemplos de pedidos:
|
|
|
102
144
|
- *"Quantas horas tenho lançadas este mês no Suite?"* → `ler_mes`
|
|
103
145
|
- *"Que projetos tenho disponíveis?"* → `projetos`
|
|
104
146
|
- *"Lança 8 h no projeto X de segunda a sexta desta semana."* → `propor`, depois `aplicar`
|
|
147
|
+
- *"Soma 2 h ao dia de hoje no projeto X, com a nota 'reunião de arranque'."* → `propor` em
|
|
148
|
+
modo `somar`, com `comment`
|
|
105
149
|
|
|
106
150
|
Lançar horas tem sempre duas fases:
|
|
107
151
|
|
|
108
152
|
1. **`propor`** calcula o plano, abre o painel da extensão com a pré-visualização e devolve-a ao
|
|
109
|
-
|
|
153
|
+
agente. Nada é escrito.
|
|
110
154
|
2. **Confirmação**, de uma de duas formas:
|
|
111
155
|
- clicas **Aplicar** no painel, ou
|
|
112
156
|
- dizes OK no chat e aprovas a chamada da tool **`aplicar`** no cliente MCP.
|
|
113
157
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
158
|
+
> [!IMPORTANT]
|
|
159
|
+
> Mantém a aprovação manual da tool `aplicar`. Com ela em aprovação automática, qualquer
|
|
160
|
+
> proposta é escrita sem mais nenhuma confirmação.
|
|
161
|
+
|
|
162
|
+
Linhas trancadas e propostas bloqueadas são sempre recusadas pela extensão.
|
|
117
163
|
|
|
118
164
|
## Tools
|
|
119
165
|
|
|
@@ -121,11 +167,56 @@ bloqueadas são sempre recusadas pela extensão.
|
|
|
121
167
|
|---|---|
|
|
122
168
|
| `estado` | ponte ligada?, mês visível, nº de projetos, lote a correr |
|
|
123
169
|
| `projetos` | projetos do dropdown da página: `option_id`, nome, trancado |
|
|
124
|
-
| `ler_mes` | horas já lançadas no mês visível |
|
|
170
|
+
| `ler_mes` | horas e Observações já lançadas no mês visível |
|
|
125
171
|
| `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 |
|
|
172
|
+
| `aplicar` | escreve a proposta `id` no Suite através da extensão, sem esperar pelo clique no painel |
|
|
127
173
|
| `resultado` | espera pela decisão (painel ou `aplicar`): aplicado, cancelado, erro ou pendente |
|
|
128
174
|
|
|
175
|
+
### `propor`
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{
|
|
179
|
+
"modo": "somar",
|
|
180
|
+
"linhas": [
|
|
181
|
+
{
|
|
182
|
+
"project": "Alfa - Projeto SAP",
|
|
183
|
+
"date": "2026-10-14",
|
|
184
|
+
"hours": 4,
|
|
185
|
+
"comment": "Migração de dados"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
| campo | notas |
|
|
192
|
+
|---|---|
|
|
193
|
+
| `linhas[].option_id` ou `project` | um dos dois: id do dropdown ou nome tal como lá aparece |
|
|
194
|
+
| `linhas[].date` | `YYYY-MM-DD`, dentro do mês visível |
|
|
195
|
+
| `linhas[].hours` | 0,5 a 23,5 em passos de 0,5; em `somar` pode ser negativo |
|
|
196
|
+
| `linhas[].comment` | opcional; vai para as Observações do projeto no mês |
|
|
197
|
+
| `modo` | `normal` (defeito), `somar` ou `sobregravar` |
|
|
198
|
+
| `nome` | opcional; rótulo que aparece no painel |
|
|
199
|
+
|
|
200
|
+
| modo | o que vem nas linhas | o que não vem |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `normal` | fica com o valor pedido | fica como está |
|
|
203
|
+
| `somar` | Suite + pedido (negativos subtraem) | fica como está |
|
|
204
|
+
| `sobregravar` | fica com o valor pedido | é apagado |
|
|
205
|
+
|
|
206
|
+
Propor duas vezes em `somar` duplica. Mais de 8 h num dia é só aviso.
|
|
207
|
+
|
|
208
|
+
## Novidades da 1.1.0
|
|
209
|
+
|
|
210
|
+
- **`propor` aceita `modo`**: `normal`, `somar` ou `sobregravar`, no lugar do antigo `espelho`
|
|
211
|
+
(que continua aceite).
|
|
212
|
+
- **`propor` aceita `comment` por linha**, que vai para as Observações do projeto.
|
|
213
|
+
- **`ler_mes` devolve as Observações.**
|
|
214
|
+
- **Comentários que o Suite ignoraria em silêncio são recusados** antes de propor.
|
|
215
|
+
- **`ERR_EXTENSAO_ANTIGA`**: com uma extensão anterior à 1.1.0, os modos `normal` e `somar` são
|
|
216
|
+
recusados, porque a extensão antiga faria outra coisa.
|
|
217
|
+
|
|
218
|
+
Os modos `normal` e `somar` e os comentários precisam da extensão ≥ 1.1.0.
|
|
219
|
+
|
|
129
220
|
## Erros
|
|
130
221
|
|
|
131
222
|
| erro | o que fazer |
|
|
@@ -133,16 +224,25 @@ bloqueadas são sempre recusadas pela extensão.
|
|
|
133
224
|
| `ERR_SERVE_EM_BAIXO` | o serviço local não responde; reinicia o cliente MCP (ele arranca-o sozinho) |
|
|
134
225
|
| `ERR_SEM_CHROME` | abre a Folha de Horas e confirma que a ponte está ligada nas Opções |
|
|
135
226
|
| `ERR_SEM_CONTEXTO` | recarrega a Folha de Horas (o serviço reiniciou depois de a página carregar) |
|
|
227
|
+
| `ERR_SEM_ABA` | não há nenhum separador do Suite aberto; abre-o ou recarrega-o |
|
|
136
228
|
| `ERR_MES_DIFERENTE` | muda o mês na página; o MCP nunca o muda por ti |
|
|
137
229
|
| `ERR_OCUPADO` | há uma proposta à espera no painel; aplica-a ou cancela-a primeiro |
|
|
230
|
+
| `ERR_LOTE_A_CORRER` | já está um lote a escrever no Suite; espera que termine |
|
|
231
|
+
| `ERR_SESSION` | a sessão do Suite expirou; entra outra vez |
|
|
138
232
|
| `ERR_PROPOSTA_DESCONHECIDA` | o `id` passado a `aplicar` não corresponde a nenhuma proposta pendente |
|
|
233
|
+
| `ERR_EXTENSAO_ANTIGA` | atualiza a extensão para a 1.1.0, ou usa `modo: "sobregravar"` |
|
|
234
|
+
|
|
235
|
+
Lista completa em [CONTRACT.md](CONTRACT.md).
|
|
139
236
|
|
|
140
237
|
## Como funciona
|
|
141
238
|
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
239
|
+
```mermaid
|
|
240
|
+
flowchart LR
|
|
241
|
+
C["Agente<br>cliente MCP"] -->|stdio| M["mcp.js"]
|
|
242
|
+
M -->|HTTP local| S["serve.js<br>127.0.0.1:18765"]
|
|
243
|
+
E["Extensão<br>no browser"] -->|long-poll| S
|
|
244
|
+
E -->|"lê e escreve<br>com a tua sessão"| T["Folha de Horas<br>do Suite"]
|
|
245
|
+
E --- P["Painel: pré-visualização<br>e Aplicar"]
|
|
146
246
|
```
|
|
147
247
|
|
|
148
248
|
1. O cliente MCP arranca o `mcp.js`. Se o serviço local (`serve.js`) não estiver a correr, o
|
|
@@ -165,7 +265,8 @@ Com o serviço gerido à mão, arranca-o **antes** de abrir a Folha de Horas, ou
|
|
|
165
265
|
depois: a extensão só publica o contexto quando a página carrega ou muda de mês.
|
|
166
266
|
|
|
167
267
|
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 (
|
|
268
|
+
cortar a chamada antes disso, aumenta o timeout do cliente MCP (muitos cortam aos 60 s por
|
|
269
|
+
defeito).
|
|
169
270
|
|
|
170
271
|
## Atualizar
|
|
171
272
|
|
|
@@ -192,7 +293,7 @@ node --test
|
|
|
192
293
|
|
|
193
294
|
Para usar a cópia local num cliente, troca o comando `npx …` por
|
|
194
295
|
`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.
|
|
296
|
+
GitHub, sem npm: `npx -y github:williansaez/suite-timesheet-mcp#v1.1.1` (precisa de `git`).
|
|
196
297
|
|
|
197
298
|
## Licença
|
|
198
299
|
|
|
@@ -200,3 +301,7 @@ Código disponível para consulta, **não open source**. Podes instalar e usar o
|
|
|
200
301
|
alterações, para uso pessoal ou interno da tua organização. Não é permitido alterá-lo,
|
|
201
302
|
redistribuí-lo nem publicar cópias ou forks. Os termos completos (em inglês, com tradução para
|
|
202
303
|
português) estão em [LICENSE](LICENSE).
|
|
304
|
+
|
|
305
|
+
<p align="center">
|
|
306
|
+
<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">
|
|
307
|
+
</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 Edge. 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;
|
|
@@ -14,7 +14,7 @@ const TIMEOUT_APLICAR_S = 120;
|
|
|
14
14
|
export const FERRAMENTAS = [
|
|
15
15
|
{
|
|
16
16
|
name: 'estado',
|
|
17
|
-
description: `Estado da ponte com o
|
|
17
|
+
description: `Estado da ponte com o Edge: se a Folha de Horas está aberta, que mês mostra, quantos projetos tem e se há um lote a correr. ${AVISO}`,
|
|
18
18
|
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
19
19
|
},
|
|
20
20
|
{
|
|
@@ -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
|
},
|
|
@@ -98,7 +107,7 @@ const erro = (code, message) => Object.assign(new Error(message), { code });
|
|
|
98
107
|
// Um comando síncrono do ponto de vista da tool: ou há dados, ou é erro.
|
|
99
108
|
async function dadosDe(cliente, corpo) {
|
|
100
109
|
const r = await cliente.comando(corpo);
|
|
101
|
-
if (!r.resultado) throw erro('ERR_PENDENTE', `O
|
|
110
|
+
if (!r.resultado) throw erro('ERR_PENDENTE', `O Edge não respondeu a tempo ao comando ${r.id}. Confirma que a Folha de Horas está aberta e tenta de novo.`);
|
|
102
111
|
if (!r.resultado.ok) throw erro(r.resultado.erro.code, r.resultado.erro.message);
|
|
103
112
|
return { id: r.id, dados: r.resultado.dados };
|
|
104
113
|
}
|
|
@@ -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,20 +20,20 @@ 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
|
|
|
27
27
|
async function comando(corpo) {
|
|
28
28
|
if (!corpo || !TIPOS.includes(corpo.tipo)) return erro(400, 'ERR_COMANDO', `tipo tem de ser um de ${TIPOS.join(', ')}.`);
|
|
29
29
|
if (!fila.ponteViva()) {
|
|
30
|
-
return erro(409, 'ERR_SEM_CHROME', 'Abre a Folha de Horas do Suite no
|
|
30
|
+
return erro(409, 'ERR_SEM_CHROME', 'Abre a Folha de Horas do Suite no Edge e espera uns segundos.');
|
|
31
31
|
}
|
|
32
32
|
const visivel = contexto.mesVisivel();
|
|
33
33
|
if (!visivel) {
|
|
34
34
|
// A ponte está viva (o worker está a fazer long-poll), mas o serve reiniciou depois de a
|
|
35
35
|
// página ter carregado: perdeu o contexto e ainda não houve recarregamento para o repor.
|
|
36
|
-
return erro(409, 'ERR_SEM_CONTEXTO', 'A ponte está ligada mas ainda não recebi o mês da página. Recarrega a Folha de Horas no
|
|
36
|
+
return erro(409, 'ERR_SEM_CONTEXTO', 'A ponte está ligada mas ainda não recebi o mês da página. Recarrega a Folha de Horas no Edge.');
|
|
37
37
|
}
|
|
38
38
|
const ano = inteiro(corpo.ano, visivel.ano);
|
|
39
39
|
const mes = inteiro(corpo.mes, visivel.mes);
|
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,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "suite-timesheet-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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
|
-
"homepage": "https://github.
|
|
8
|
+
"homepage": "https://williansaez.github.io/suite-timesheet-mcp/",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/williansaez/suite-timesheet-mcp.git"
|