suite-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/CONTRACT.md ADDED
@@ -0,0 +1,143 @@
1
+ # Contrato
2
+
3
+ A fonte deste contrato é o `CONTRACT.md` do repo da extensão (`suite-timesheet-importer`). Esta
4
+ cópia existe para quem só clona este pacote. Se divergirem, manda o da extensão.
5
+
6
+ ## Ponte (opcional): o serviço manda comandos à extensão
7
+
8
+ Um serviço que implemente estas três rotas passa a poder pedir leituras e propor lançamentos
9
+ à extensão. Quem só quer entregar um ficheiro de horas não precisa delas.
10
+
11
+ | método | rota | corpo / resposta |
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 |
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
+ | `POST` | `/bridge/result/{id}` | a extensão publica o resultado do comando `id` |
16
+
17
+ Comando: `{"id": "texto", "tipo": "estado" | "projetos" | "ler" | "propor" | "aplicar", "ano": 2026, "mes": 9}`.
18
+ `propor` leva ainda `"linhas": [{option_id | project, date, hours}]`, `"espelho": false` e
19
+ `"nome": "texto"`. O mês tem de ser o que a página mostra; caso contrário a resposta é
20
+ `ERR_MES_DIFERENTE` e nada é lido nem escrito.
21
+
22
+ ### `aplicar`: escreve uma proposta pendente sem o clique no painel
23
+
24
+ `aplicar` leva ainda `"proposta": "id"`, o `id` de um comando `propor` já entregue à extensão e
25
+ ainda sem `final` (ou seja, com o painel aberto à espera de decisão). Em vez de esperar que o
26
+ utilizador clique Aplicar, o cliente MCP (com a aprovação humana já dada no chat, e a aprovação
27
+ da própria chamada da tool) manda escrever essa proposta diretamente.
28
+
29
+ A extensão trata `aplicar` como um comando novo e independente (com o seu próprio `id`), que
30
+ resolve o `propor` referido em `proposta`:
31
+
32
+ - Escreve linha a linha, tal como faria depois do clique em Aplicar — mesmas validações (linhas
33
+ trancadas recusadas, sessão expirada para tudo), mesmo `report`.
34
+ - Responde ao comando `aplicar` com `{"ok": true, "dados": {"estado": "aplicado" | "erro",
35
+ "report"?, "stopped"?, "erro"?}}` (uma só fase, sem `previa`) ou `{"ok": false, "erro":
36
+ {"code", "message"}}`.
37
+ - Publica também o `final` do `propor` original (mesmo `{"fase": "final", "estado": ...}` de
38
+ sempre), para quem ainda estiver a chamar `resultado(<id do propor>)` continuar a funcionar.
39
+
40
+ `ERR_PROPOSTA_DESCONHECIDA`: não há nenhum `propor` pendente com o `id` indicado em `proposta`
41
+ (nunca existiu, já teve `final`, ou o `serve`/worker reiniciou e perdeu-o). `aplicar` nunca é
42
+ bloqueado por `ERR_OCUPADO` — não é um novo `propor` à espera de painel, é a resolução de um que
43
+ já existe.
44
+
45
+ Resultado: `{"ok": true, "dados": ...}` ou `{"ok": false, "erro": {"code", "message"}}`.
46
+ `propor` publica dois resultados com o mesmo `id`: primeiro `{"fase": "previa", "previa": {...}}`
47
+ com o plano calculado (o painel abre nesse momento), e depois, quando o utilizador decide,
48
+ `{"fase": "final", "estado": "aplicado" | "cancelado" | "erro", ...}`.
49
+
50
+ A extensão continua a ser a única coisa que escreve no Suite, sempre depois de o utilizador
51
+ clicar Aplicar no painel ou de aprovar a tool `aplicar` no cliente MCP (ver secção "aplicar"
52
+ abaixo). Nunca submete o mês, nunca muda o mês.
53
+
54
+ ### Prazo de `/bridge/next`
55
+
56
+ O `serve` tem de responder a `GET /bridge/next?wait=25` dentro de, aproximadamente, 28 segundos
57
+ — mesmo sem comando (nesse caso, `204`). O worker pede `wait=25` (e este `serve` recusa um
58
+ `wait` maior que 25), mas o Chrome mata um service worker MV3 cujo `fetch` demora mais de 30 s a
59
+ resolver; um `serve` que ignore o `wait` ou que demore a responder além desse limite faz o
60
+ worker perder o ciclo (e reentrar só ao alarme de 30 s seguinte), mesmo que a resposta acabe por
61
+ chegar.
62
+
63
+ ### Formato de `dados` por tipo de comando
64
+
65
+ Resultado com `"ok": true`, no campo `dados`:
66
+
67
+ | `tipo` | forma de `dados` |
68
+ |---|---|
69
+ | `estado` | `{ano, mes, projetos: N, loteEmCurso: boolean}` — `projetos` é a contagem, não a lista |
70
+ | `projetos` | `[{option_id, nome, trancado}]` |
71
+ | `ler` | `{ano, mes, linhas: [{option_id, nome, status, dias: {D: horas}, total}], totaisPorDia: {D: horas}}` |
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
+ | `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
+ | `aplicar` (uma só fase) | `{estado: 'aplicado' \| 'erro', report?, stopped?, erro?}` — sem `fase: 'previa'`; ver secção "aplicar" acima |
75
+
76
+ ### Códigos de erro
77
+
78
+ `{"ok": false, "erro": {"code", "message"}}`. Códigos que o worker pode devolver:
79
+
80
+ | código | motivo |
81
+ |---|---|
82
+ | `ERR_COMANDO` | corpo do comando malformado (id/tipo/ano/mes/linhas inválidos) |
83
+ | `ERR_MES_DIFERENTE` | o comando pede um mês diferente do que a página do Suite tem aberto |
84
+ | `ERR_SEM_CONTEXTO` | a Folha de Horas do Suite ainda não foi aberta nesta sessão |
85
+ | `ERR_SEM_ABA` | não há nenhuma aba do Suite aberta (só em `propor`; sem aba o painel não abre) |
86
+ | `ERR_OCUPADO` | já há uma proposta do MCP à espera de confirmação no painel, ou (no APPLY) uma proposta a ser calculada |
87
+ | `ERR_LOTE_A_CORRER` | já está um lote a escrever no Suite (Aplicar), ou (no `propor`) já está um propor a ser calculado |
88
+ | `ERR_SESSION` | o Suite devolveu HTML de login: a sessão expirou |
89
+ | `ERR_MONTH_MISMATCH` | o read do Suite não cobre os projetos visíveis na página |
90
+ | `ERR_PROPOSTA_DESCONHECIDA` | `aplicar` referiu, em `proposta`, um `id` sem `propor` pendente (só em `aplicar`) |
91
+ | `ERR_DESCONHECIDO` | qualquer outro erro não classificado |
92
+
93
+ Este `serve` acrescenta o seu próprio `ERR_EXPIRADO` (ver secção seguinte) e usa
94
+ `ERR_SEM_CHROME`/`ERR_SEM_CONTEXTO` nas suas próprias rotas `/mcp/*` (ver "Rotas deste serve").
95
+
96
+ ### Um comando sem resposta nunca é respondido
97
+
98
+ Se o service worker morrer (o Chrome pode matá-lo a qualquer momento fora de um fetch pendente)
99
+ depois de o `serve` entregar um comando em `/bridge/next` mas antes de ele publicar o resultado
100
+ em `/bridge/result/{id}`, esse comando fica sem resposta — o worker que reentra ao alarme
101
+ seguinte não retoma comandos a meio, só pede o próximo.
102
+
103
+ Este `serve` dá um timeout a cada comando pendente: expira com
104
+ `{"ok": false, "erro": {"code": "ERR_EXPIRADO", ...}}` se nunca chega a ser entregue dentro do
105
+ `timeout_s` do pedido que o enfileirou (defeito 30 s), ou se é um `propor` entregue mas que não
106
+ devolveu a previa em 120 s. Um comando expirado torna-se final, o que também liberta o próximo
107
+ `propor` bloqueado.
108
+
109
+ ## CORS
110
+
111
+ A extensão chama o serviço a partir do service worker com `host_permissions` para
112
+ `http://127.0.0.1/*`; **não** é preciso enviar cabeçalhos CORS.
113
+
114
+ ## Rotas deste `serve`
115
+
116
+ ### `/mcp/*` — só o `mcp.js` chama
117
+
118
+ | método | rota | efeito |
119
+ |---|---|---|
120
+ | `GET` | `/mcp/estado` | contexto conhecido + `ponte: 'ligada' \| 'sem-chrome'` (ligada = long-poll ativo há menos de 40 s) + `ultimoMcp` (ISO do último pedido a `/mcp/*`, incluindo este) |
121
+ | `POST` | `/mcp/comando` | enfileira, espera até `timeout_s` s pelo resultado (defeito 30, máximo 290; `aplicar` tem defeito 120 — escreve linha a linha), devolve `{id, estado, resultado?}` |
122
+ | `GET` | `/mcp/comando/{id}?wait=N&fase=previa\|final` | espera pelo resultado de um comando já enfileirado; `fase` (defeito `final`) escolhe entre a pré-visualização ou o estado final de um `propor` |
123
+
124
+ `ERR_SEM_CHROME`: sem long-poll recente (o worker não está a correr, ou a extensão não tem a
125
+ permissão de `http://127.0.0.1/*`). `ERR_SEM_CONTEXTO`: a ponte está viva (o worker continua a
126
+ fazer long-poll) mas o `serve` reiniciou depois de a página já ter carregado — falta um novo
127
+ `POST /bridge/contexto`, que só volta a chegar quando a Folha de Horas recarregar.
128
+
129
+ ### Contrato antigo (ficheiro/serviço simples, sem MCP)
130
+
131
+ Estas duas rotas são o mesmo contrato "serviço local" que o `CONTRACT.md` da extensão descreve
132
+ para quem não precisa da ponte nem do MCP — só entregar horas por HTTP.
133
+
134
+ | método | rota | efeito |
135
+ |---|---|---|
136
+ | `GET` | `/health` | `200 application/json` com `{nome: 'suite-timesheet-serve', versao, ultimoMcp, ponte}`; `ultimoMcp` é o ISO do último pedido a `/mcp/*` (ou `null` se nenhum desde o arranque) e `ponte` é o mesmo valor que `/mcp/estado`. A extensão usa `nome` para confirmar que é este serve (e não outro programa) a responder na porta — um corpo que não seja este JSON conta como "outro programa na porta". |
137
+ | `GET` | `/timesheet?year=YYYY&month=M` | `200 application/json` com `{year, month, generated_at, rows}`; `rows` vem da última proposta aceite (por `propor`) para esse mês, ou `[]` |
138
+
139
+ ## O que este `serve` NÃO faz
140
+
141
+ Não escreve no Suite, não fala com o Suite. Toda a escrita é feita pela extensão, dentro do
142
+ browser, com a sessão do utilizador — sempre depois de o utilizador clicar Aplicar no painel ou
143
+ de aprovar a tool `aplicar` no cliente MCP.
package/LICENSE ADDED
@@ -0,0 +1,99 @@
1
+ suite-timesheet-mcp — Source-Available License
2
+ Copyright (c) 2026 Willian Saez. All rights reserved.
3
+
4
+ The English text below is the governing version. The Portuguese translation that follows is
5
+ provided for convenience; if the two differ, the English text prevails.
6
+
7
+ -------------------------------------------------------------------------------
8
+ ENGLISH
9
+ -------------------------------------------------------------------------------
10
+
11
+ 1. Definitions. "Software" means the source code, documentation and other files in this
12
+ repository and in any package built from it. "You" means the individual or legal entity
13
+ exercising the permissions granted by this license.
14
+
15
+ 2. Permitted use. Subject to the conditions of this license, you may:
16
+ a) view and read the Software;
17
+ b) download, install and run unmodified copies of the Software, including through package
18
+ tools such as npm or npx, solely for your own personal use or for the internal business
19
+ purposes of your organization, by you and your organization's employees and contractors.
20
+
21
+ 3. Restrictions. Except as expressly permitted in section 2, you may not, in whole or in part:
22
+ a) modify, adapt, translate or create derivative works of the Software;
23
+ b) copy the Software, except for the copies strictly necessary for the use permitted in
24
+ section 2 (including those made automatically by package managers and caches);
25
+ c) distribute, publish, sublicense, sell, rent, lend or otherwise make the Software available
26
+ to any third party, including by hosting copies, mirrors or forks of it on any platform or
27
+ offering it as a service;
28
+ d) remove or alter this license or any copyright notice.
29
+
30
+ A fork created through a code-hosting platform's own features (for example, under the terms
31
+ of service of GitHub) grants no rights beyond viewing it on that platform; any other use of
32
+ such a fork is subject to the restrictions above.
33
+
34
+ 4. Reservation of rights. All rights not expressly granted are reserved by the copyright holder.
35
+ No right is granted to any trademark, name or logo.
36
+
37
+ 5. Termination. This license terminates automatically if you breach any of its terms. Upon
38
+ termination you must stop using the Software and delete all copies in your possession.
39
+
40
+ 6. No warranty. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
42
+ PARTICULAR PURPOSE AND NON-INFRINGEMENT.
43
+
44
+ 7. Limitation of liability. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM,
45
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
46
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OF OR OTHER DEALINGS IN THE
47
+ SOFTWARE, INCLUDING ANY DATA ENTERED INTO THIRD-PARTY SYSTEMS THROUGH IT.
48
+
49
+ 8. Other permissions. Any use not permitted by this license, including contributions,
50
+ modifications or redistribution, requires the prior written permission of the copyright
51
+ holder.
52
+
53
+ -------------------------------------------------------------------------------
54
+ PORTUGUÊS (tradução)
55
+ -------------------------------------------------------------------------------
56
+
57
+ 1. Definições. "Software" designa o código-fonte, a documentação e os demais ficheiros deste
58
+ repositório e de qualquer pacote gerado a partir dele. "Tu" designa a pessoa singular ou
59
+ coletiva que exerce as permissões concedidas por esta licença.
60
+
61
+ 2. Uso permitido. Nas condições desta licença, podes:
62
+ a) ver e ler o Software;
63
+ b) descarregar, instalar e executar cópias não modificadas do Software, incluindo através de
64
+ ferramentas de pacotes como npm ou npx, exclusivamente para uso pessoal ou para fins
65
+ internos da tua organização, por ti e pelos colaboradores e prestadores de serviços dessa
66
+ organização.
67
+
68
+ 3. Restrições. Salvo o expressamente permitido na secção 2, não podes, no todo ou em parte:
69
+ a) modificar, adaptar, traduzir ou criar obras derivadas do Software;
70
+ b) copiar o Software, exceto as cópias estritamente necessárias ao uso permitido na secção 2
71
+ (incluindo as feitas automaticamente por gestores de pacotes e caches);
72
+ c) distribuir, publicar, sublicenciar, vender, alugar, emprestar ou de outra forma
73
+ disponibilizar o Software a terceiros, incluindo alojar cópias, espelhos ou forks dele em
74
+ qualquer plataforma ou oferecê-lo como serviço;
75
+ d) remover ou alterar esta licença ou qualquer aviso de direitos de autor.
76
+
77
+ Um fork criado através das funcionalidades de uma plataforma de alojamento de código (por
78
+ exemplo, ao abrigo dos termos de serviço do GitHub) não confere nenhum direito além de o
79
+ ver nessa plataforma; qualquer outro uso desse fork está sujeito às restrições acima.
80
+
81
+ 4. Reserva de direitos. Todos os direitos não expressamente concedidos ficam reservados ao
82
+ titular dos direitos de autor. Não é concedido nenhum direito sobre marcas, nomes ou logótipos.
83
+
84
+ 5. Cessação. Esta licença cessa automaticamente se violares qualquer dos seus termos. Com a
85
+ cessação, deves deixar de usar o Software e apagar todas as cópias em teu poder.
86
+
87
+ 6. Sem garantia. O SOFTWARE É FORNECIDO "TAL COMO ESTÁ", SEM GARANTIA DE QUALQUER TIPO,
88
+ EXPRESSA OU IMPLÍCITA, INCLUINDO, SEM LIMITAÇÃO, AS GARANTIAS DE COMERCIABILIDADE, ADEQUAÇÃO
89
+ A UM FIM ESPECÍFICO E NÃO VIOLAÇÃO.
90
+
91
+ 7. Limitação de responsabilidade. EM CASO ALGUM O TITULAR DOS DIREITOS DE AUTOR SERÁ
92
+ RESPONSÁVEL POR QUALQUER RECLAMAÇÃO, DANO OU OUTRA RESPONSABILIDADE, SEJA EM AÇÃO
93
+ CONTRATUAL, EXTRACONTRATUAL OU OUTRA, DECORRENTE DO SOFTWARE, DO SEU USO OU DE OUTRAS
94
+ OPERAÇÕES COM ELE, INCLUINDO QUAISQUER DADOS INTRODUZIDOS EM SISTEMAS DE TERCEIROS ATRAVÉS
95
+ DELE.
96
+
97
+ 8. Outras permissões. Qualquer uso não permitido por esta licença, incluindo contribuições,
98
+ modificações ou redistribuição, requer autorização prévia e por escrito do titular dos
99
+ direitos de autor.
package/README.md ADDED
@@ -0,0 +1,202 @@
1
+ # suite-timesheet-mcp
2
+
3
+ Servidor MCP que deixa um assistente (Claude, GitHub Copilot, ou outro cliente MCP) **ler e
4
+ propor horas** na Folha de Horas do Suite, através da extensão **Suite Timesheet Importer**.
5
+
6
+ - **Nunca fala com o Suite.** Quem lê e escreve é a extensão, no teu browser, com a tua sessão.
7
+ - **Nunca escreve sem ti.** Cada lançamento é uma proposta; só é escrito quando clicas Aplicar
8
+ no painel ou aprovas a tool `aplicar` no teu cliente MCP.
9
+ - **Nunca submete meses** nem muda o mês aberto na página.
10
+ - **Só local.** Escuta apenas em `127.0.0.1`, guarda tudo em memória, não tem telemetria.
11
+
12
+ Detalhes e forma de verificar cada afirmação: [SECURITY.md](SECURITY.md).
13
+
14
+ ## Requisitos
15
+
16
+ - Node.js ≥ 20 (`node -v`).
17
+ - Chrome ou Edge com a extensão Suite Timesheet Importer instalada.
18
+ - A Folha de Horas do Suite aberta, com sessão válida.
19
+
20
+ ## Instalação
21
+
22
+ ### 1. Ligar a ponte na extensão
23
+
24
+ Nas **Opções** da extensão, liga **Ligação ao servidor local (ponte MCP)**, clica em
25
+ **Guardar** e aceita a permissão para `http://127.0.0.1/*`. Depois recarrega a Folha de Horas.
26
+
27
+ Este passo é manual e feito uma vez: sem ele a extensão nunca toca na rede local.
28
+
29
+ ### 2. Registar o servidor no teu cliente MCP
30
+
31
+ Não é preciso clonar nada: o `npx` descarrega o pacote do npm e corre-o. `@1` fixa a versão
32
+ principal: recebes as correções 1.x, nunca uma versão incompatível. A primeira execução demora
33
+ alguns segundos; as seguintes usam a cache.
34
+
35
+ **Claude Code**
36
+
37
+ ```bash
38
+ claude mcp add --scope user suite-timesheet -- npx -y suite-timesheet-mcp@1
39
+ ```
40
+
41
+ **GitHub Copilot (VS Code)**
42
+
43
+ ```bash
44
+ code --add-mcp '{"name":"suite-timesheet","command":"npx","args":["-y","suite-timesheet-mcp@1"]}'
45
+ ```
46
+
47
+ Depois recarrega a janela; as tools aparecem no modo **Agent** do Copilot.
48
+
49
+ **Claude Desktop (inclui Cowork)**
50
+
51
+ Em Definições → Programador → Editar configuração
52
+ (`~/Library/Application Support/Claude/claude_desktop_config.json` no macOS,
53
+ `%APPDATA%\Claude\claude_desktop_config.json` no Windows):
54
+
55
+ ```json
56
+ {
57
+ "mcpServers": {
58
+ "suite-timesheet": {
59
+ "command": "npx",
60
+ "args": ["-y", "suite-timesheet-mcp@1"]
61
+ }
62
+ }
63
+ }
64
+ ```
65
+
66
+ Reinicia o Claude Desktop.
67
+
68
+ **Outros clientes**
69
+
70
+ O mesmo comando: `npx -y suite-timesheet-mcp@1`, por stdio. Atenção à
71
+ chave do ficheiro de configuração: o VS Code usa `servers`, a maioria dos outros usa
72
+ `mcpServers`.
73
+
74
+ > Se o cliente não encontrar o `npx` (comum em apps abertas pelo Dock, que não herdam o PATH
75
+ > do terminal), usa o caminho absoluto dado por `which npx`. No Windows, usa
76
+ > `"command": "cmd"` com `"args": ["/c", "npx", "-y", "…"]`.
77
+
78
+ ### 3. Verificar
79
+
80
+ Pede ao assistente: *"consulta o estado da ponte do Suite Timesheet"*. A tool `estado` deve
81
+ responder com a ponte ligada e o mês visível na página. As Opções da extensão passam a mostrar
82
+ "Claude ligado há N s" (o texto é o mesmo para qualquer cliente).
83
+
84
+ ## Instalação feita por um agente
85
+
86
+ Se estás a pedir a um agente para instalar isto, ele deve seguir esta ordem e parar no primeiro
87
+ passo que falhe:
88
+
89
+ 1. Confirmar `node -v` ≥ 20.
90
+ 2. Correr o comando do passo 2 correspondente ao cliente em uso.
91
+ 3. Pedir ao utilizador para fazer o passo 1 (Opções da extensão). O agente não o consegue fazer.
92
+ 4. Pedir ao utilizador para recarregar o cliente MCP, se o cliente o exigir.
93
+ 5. Chamar a tool `estado`. Sucesso = ponte ligada e mês visível. Qualquer `ERR_*`: ver
94
+ [Erros](#erros).
95
+
96
+ O agente **não** deve pôr a tool `aplicar` em aprovação automática.
97
+
98
+ ## Usar
99
+
100
+ Exemplos de pedidos:
101
+
102
+ - *"Quantas horas tenho lançadas este mês no Suite?"* → `ler_mes`
103
+ - *"Que projetos tenho disponíveis?"* → `projetos`
104
+ - *"Lança 8 h no projeto X de segunda a sexta desta semana."* → `propor`, depois `aplicar`
105
+
106
+ Lançar horas tem sempre duas fases:
107
+
108
+ 1. **`propor`** calcula o plano, abre o painel da extensão com a pré-visualização e devolve-a ao
109
+ assistente. Nada é escrito.
110
+ 2. **Confirmação**, de uma de duas formas:
111
+ - clicas **Aplicar** no painel, ou
112
+ - dizes OK no chat e aprovas a chamada da tool **`aplicar`** no cliente MCP.
113
+
114
+ Por isso: **mantém a aprovação manual da tool `aplicar`**. Com ela em aprovação automática,
115
+ qualquer proposta é escrita sem mais nenhuma confirmação. Linhas trancadas e propostas
116
+ bloqueadas são sempre recusadas pela extensão.
117
+
118
+ ## Tools
119
+
120
+ | tool | faz |
121
+ |---|---|
122
+ | `estado` | ponte ligada?, mês visível, nº de projetos, lote a correr |
123
+ | `projetos` | projetos do dropdown da página: `option_id`, nome, trancado |
124
+ | `ler_mes` | horas já lançadas no mês visível |
125
+ | `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 |
127
+ | `resultado` | espera pela decisão (painel ou `aplicar`): aplicado, cancelado, erro ou pendente |
128
+
129
+ ## Erros
130
+
131
+ | erro | o que fazer |
132
+ |---|---|
133
+ | `ERR_SERVE_EM_BAIXO` | o serviço local não responde; reinicia o cliente MCP (ele arranca-o sozinho) |
134
+ | `ERR_SEM_CHROME` | abre a Folha de Horas e confirma que a ponte está ligada nas Opções |
135
+ | `ERR_SEM_CONTEXTO` | recarrega a Folha de Horas (o serviço reiniciou depois de a página carregar) |
136
+ | `ERR_MES_DIFERENTE` | muda o mês na página; o MCP nunca o muda por ti |
137
+ | `ERR_OCUPADO` | há uma proposta à espera no painel; aplica-a ou cancela-a primeiro |
138
+ | `ERR_PROPOSTA_DESCONHECIDA` | o `id` passado a `aplicar` não corresponde a nenhuma proposta pendente |
139
+
140
+ ## Como funciona
141
+
142
+ ```
143
+ cliente MCP ──stdio──▶ mcp.js ──HTTP──▶ serve.js ◀──long-poll── extensão ──▶ Suite
144
+ 127.0.0.1 │
145
+ painel: Aplicar
146
+ ```
147
+
148
+ 1. O cliente MCP arranca o `mcp.js`. Se o serviço local (`serve.js`) não estiver a correr, o
149
+ `mcp.js` arranca-o em segundo plano, em `127.0.0.1:18765`.
150
+ 2. A extensão, com a Folha de Horas aberta, faz long-poll ao serviço e publica o mês visível.
151
+ 3. Cada tool enfileira um comando; a extensão executa-o no browser e devolve o resultado.
152
+
153
+ O protocolo entre o serviço e a extensão está em [CONTRACT.md](CONTRACT.md).
154
+
155
+ ## Configuração avançada
156
+
157
+ Variáveis de ambiente, no bloco `env` da configuração do cliente MCP:
158
+
159
+ | variável | efeito |
160
+ |---|---|
161
+ | `SUITE_TIMESHEET_BASE` | outro endereço local, ex. `http://127.0.0.1:18770`; põe o mesmo nas Opções da extensão |
162
+ | `SUITE_TIMESHEET_SEM_AUTOARRANQUE=1` | o `mcp.js` não arranca o serviço; corres tu `node serve.js [porta]` |
163
+
164
+ Com o serviço gerido à mão, arranca-o **antes** de abrir a Folha de Horas, ou recarrega a página
165
+ depois: a extensão só publica o contexto quando a página carrega ou muda de mês.
166
+
167
+ 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 (no Claude Code, 60 s por defeito).
169
+
170
+ ## Atualizar
171
+
172
+ As correções 1.x chegam ao reiniciar o cliente MCP (o `npx` volta a consultar o npm). Para uma
173
+ versão principal nova, muda `@1` no comando. O serviço local fica a correr entre sessões; para
174
+ ele pegar na versão nova, termina-o uma vez
175
+ (`lsof -ti tcp:18765 | xargs kill`) e o `mcp.js` arranca o novo.
176
+
177
+ ## Limitações
178
+
179
+ - Sem autenticação local: qualquer processo do teu utilizador consegue falar com o serviço.
180
+ Páginas web não conseguem. Ver [SECURITY.md](SECURITY.md).
181
+ - Tudo em memória: reiniciar o serviço esquece propostas e contexto (recarrega a Folha de Horas).
182
+ - Um mês de cada vez: o que está aberto na página.
183
+
184
+ ## Correr a partir de um clone
185
+
186
+ ```bash
187
+ git clone https://github.com/williansaez/suite-timesheet-mcp.git
188
+ cd suite-timesheet-mcp
189
+ npm ci
190
+ node --test
191
+ ```
192
+
193
+ Para usar a cópia local num cliente, troca o comando `npx …` por
194
+ `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.0.0` (precisa de `git`).
196
+
197
+ ## Licença
198
+
199
+ Código disponível para consulta, **não open source**. Podes instalar e usar o pacote sem
200
+ alterações, para uso pessoal ou interno da tua organização. Não é permitido alterá-lo,
201
+ redistribuí-lo nem publicar cópias ou forks. Os termos completos (em inglês, com tradução para
202
+ português) estão em [LICENSE](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,80 @@
1
+ # Segurança
2
+
3
+ Para quem precisa de decidir se pode correr este pacote numa máquina da empresa. Descreve o que
4
+ ele faz, o que nunca faz, e como verificar cada afirmação.
5
+
6
+ ## Em uma frase
7
+
8
+ Um processo local (`serve.js`) que guarda em memória uma fila de comandos entre o Claude e a
9
+ extensão Suite Timesheet Importer, e um servidor MCP por stdio (`mcp.js`) que expõe seis tools ao
10
+ Claude. Este pacote **nunca fala com o Suite**: quem lê e escreve no Suite é a extensão, dentro do
11
+ browser, com a sessão do utilizador, e só depois de o utilizador clicar Aplicar no painel ou de
12
+ aprovar a tool `aplicar` no cliente MCP (ver a tabela abaixo).
13
+
14
+ ## O que nunca faz (verificado pelo CI)
15
+
16
+ | regra | como é garantida |
17
+ |---|---|
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
+ | 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 (ex.: Claude Code a pedir confirmação antes de correr a tool) — uma das duas é sempre exigida |
21
+ | Nunca guarda dados em disco | fila, contexto e propostas vivem em memória; reiniciar o `serve` esquece tudo |
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
+ | Nunca contém dados reais | testes e exemplos só com projetos, ids e horas fictícios (guard de nomes de cliente no CI) |
24
+
25
+ ## Superfície de rede
26
+
27
+ Só um socket: `127.0.0.1:18765` (porta configurável). Rotas:
28
+
29
+ - `/bridge/*`: usadas pela extensão (long-poll, contexto da página, resultados).
30
+ - `/mcp/*`: usadas pelo `mcp.js` na mesma máquina.
31
+ - `/health`, `/timesheet`: contrato antigo do serviço local, só leitura. `/health` devolve a
32
+ identidade do serve (`nome`, `versao`) mais `ultimoMcp` (a hora do último pedido a `/mcp/*`,
33
+ ou `null`) e `ponte`; não expõe nada além disso — nem contexto, nem propostas.
34
+
35
+ Corpo dos pedidos limitado a 1 MB. Ligações abortadas, URLs malformados e erros internos
36
+ respondem com erro e nunca derrubam o processo.
37
+
38
+ O `mcp.js` pode arrancar o `serve.js` (mesmo pacote, mesmo utilizador, só loopback) quando liga e
39
+ não encontra nada a responder em `/health`; nunca arranca nem chama nenhum outro programa
40
+ (`SUITE_TIMESHEET_SEM_AUTOARRANQUE=1` desliga isto).
41
+
42
+ ## Dados
43
+
44
+ Em memória: o mês visível e a lista de projetos que a extensão publica, as propostas enviadas
45
+ pelo Claude, e os resultados que a extensão devolve (prévia e resultado final). Resultados expiram
46
+ ao fim de 1 h; comandos sem resposta expiram com `ERR_EXPIRADO`. Nada é registado além de uma
47
+ linha por comando em `stderr` (tipo e mês, sem horas).
48
+
49
+ ## Dependências
50
+
51
+ Uma, declarada e fixada em `package-lock.json`: `@modelcontextprotocol/sdk` (só importada por
52
+ `mcp.js`). O `serve.js` usa apenas `node:http`. `npm audit` faz parte da verificação abaixo.
53
+
54
+ ## Limitações conhecidas
55
+
56
+ - **Sem autenticação local.** Qualquer processo a correr com o teu utilizador consegue falar com
57
+ o `serve` (enfileirar um `propor`, ler o contexto). Páginas web não conseguem (ver acima). Um
58
+ `propor` malicioso ainda assim só chega ao Suite se clicares Aplicar no painel ou aprovares a
59
+ chamada da tool `aplicar` no cliente MCP.
60
+ - **Sem TLS.** Loopback apenas; nada sai da máquina.
61
+ - **Sem persistência.** Um reinício perde a fila; o MCP recebe `ERR_COMANDO_DESCONHECIDO` e repete.
62
+
63
+ ## Como a tua equipa pode verificar
64
+
65
+ ```bash
66
+ npm ci
67
+ node --test # 73 testes, sem rede além de loopback
68
+ npm audit --omit=dev # dependências conhecidas
69
+ grep -rnoE "https?://[a-zA-Z0-9.-]+" --include='*.js' --exclude-dir=node_modules --exclude-dir=test . # esperado: só 127.0.0.1
70
+ grep -rniE "submitaction" --include='*.js' . --exclude-dir=node_modules | grep -v '/test/' # esperado: vazio
71
+ grep -rn "listen(" serve.js # esperado: só 127.0.0.1
72
+ ```
73
+
74
+ ## Reportar um problema
75
+
76
+ Encontraste uma forma de este pacote contactar algo fora da máquina, de aceitar pedidos de uma
77
+ página web, ou de provocar uma escrita no Suite sem o clique em Aplicar nem a aprovação da tool
78
+ `aplicar`? Abre um issue privado
79
+ neste repositório com o rótulo `segurança` ou contacta o dono do repositório. Sem cookies, tokens
80
+ nem dados reais no relatório.
@@ -0,0 +1,90 @@
1
+ // Auto-arranque do serve: o mcp.js chama isto antes de se ligar por stdio, para o utilizador
2
+ // não ter de arrancar o `serve` à mão. Puro-ish: fetch, spawn e pausa são injetáveis (só
3
+ // `spawnServeReal`, usado como valor por defeito, toca em node:child_process a sério).
4
+
5
+ import { spawn as spawnReal } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ const PORTA_POR_DEFEITO = 18765;
9
+
10
+ function portaDe(base) {
11
+ try {
12
+ const porta = new URL(base).port;
13
+ return porta ? Number(porta) : PORTA_POR_DEFEITO;
14
+ } catch {
15
+ return PORTA_POR_DEFEITO;
16
+ }
17
+ }
18
+
19
+ // Só isto identifica um "serve" desta package: nome fixo devolvido por GET /health
20
+ // (ver lib/rotas.js). Qualquer outra coisa na porta (outro programa, um proxy, etc.) não passa.
21
+ function ehIdentidade(json) {
22
+ return Boolean(json) && typeof json === 'object' && json.nome === 'suite-timesheet-serve';
23
+ }
24
+
25
+ function pausaReal(ms) {
26
+ return new Promise((resolve) => { setTimeout(resolve, ms); });
27
+ }
28
+
29
+ // Arranca o serve.js real, desligado deste processo: detached (não morre com o mcp.js),
30
+ // stdio 'ignore' (não herda nem escreve no stdio que o protocolo MCP usa) e unref() (não
31
+ // impede o mcp.js de terminar enquanto o serve continua a correr).
32
+ function spawnServeReal(porta) {
33
+ const caminhoServe = fileURLToPath(new URL('../serve.js', import.meta.url));
34
+ const filho = spawnReal(process.execPath, [caminhoServe, String(porta)], {
35
+ detached: true,
36
+ stdio: 'ignore',
37
+ });
38
+ filho.unref();
39
+ return filho;
40
+ }
41
+
42
+ // GET base/health, sem lançar: { ligou: false } se o fetch falhar (nada à escuta),
43
+ // { ligou: true, status, json } caso contrário (json pode ser null se o corpo não for JSON).
44
+ async function verHealth(base, fetchImpl) {
45
+ let resposta;
46
+ try {
47
+ resposta = await fetchImpl(`${base}/health`);
48
+ } catch {
49
+ return { ligou: false };
50
+ }
51
+ const json = await resposta.json().catch(() => null);
52
+ return { ligou: true, status: resposta.status, json };
53
+ }
54
+
55
+ /**
56
+ * Garante que há um `serve` desta package a responder em `base`. Nunca lança: devolve sempre
57
+ * { estado, detalhe }, com estado um de 'ja-corria' | 'arrancado' | 'porta-ocupada' | 'falhou'.
58
+ */
59
+ export async function garantirServe({
60
+ base,
61
+ fetch: fetchImpl = globalThis.fetch,
62
+ spawn: spawnImpl,
63
+ pausa = pausaReal,
64
+ tentativas = 15,
65
+ } = {}) {
66
+ const porta = portaDe(base);
67
+ const arrancar = spawnImpl ?? (() => spawnServeReal(porta));
68
+
69
+ const primeira = await verHealth(base, fetchImpl);
70
+
71
+ if (primeira.ligou) {
72
+ if (primeira.status === 200 && ehIdentidade(primeira.json)) {
73
+ return { estado: 'ja-corria', detalhe: `Já havia um serve a responder em ${base}.` };
74
+ }
75
+ // Responde, mas não é este serve: outro programa qualquer está nesta porta. Nunca arrancar
76
+ // o serve por cima disso.
77
+ return { estado: 'porta-ocupada', detalhe: `Na porta ${porta} responde outro programa.` };
78
+ }
79
+
80
+ // Nada à escuta: arrancar o serve e esperar que fique saudável.
81
+ arrancar();
82
+ for (let i = 0; i < tentativas; i += 1) {
83
+ await pausa(200);
84
+ const tentativa = await verHealth(base, fetchImpl);
85
+ if (tentativa.ligou && tentativa.status === 200 && ehIdentidade(tentativa.json)) {
86
+ return { estado: 'arrancado', detalhe: `Arranquei o serve em ${base}.` };
87
+ }
88
+ }
89
+ return { estado: 'falhou', detalhe: `Não consegui arrancar o serve em ${base} (tenta "node serve.js").` };
90
+ }
package/lib/cliente.js ADDED
@@ -0,0 +1,30 @@
1
+ // Cliente HTTP do serve, usado pelo mcp.js. fetch injetável para os testes.
2
+
3
+ export function criarCliente({ base, fetch: fetchImpl = globalThis.fetch, comandoArranque = 'node serve.js' }) {
4
+ async function pedir(metodo, caminho, corpo) {
5
+ let resposta;
6
+ try {
7
+ resposta = await fetchImpl(`${base}${caminho}`, {
8
+ method: metodo,
9
+ headers: corpo ? { 'content-type': 'application/json' } : {},
10
+ body: corpo ? JSON.stringify(corpo) : undefined,
11
+ });
12
+ } catch (e) {
13
+ throw Object.assign(
14
+ new Error(`O serviço local não responde em ${base} (${e.message}). Arranca-o com: ${comandoArranque}`),
15
+ { code: 'ERR_SERVE_EM_BAIXO' },
16
+ );
17
+ }
18
+ const json = await resposta.json().catch(() => null);
19
+ if (!resposta.ok) {
20
+ const erro = json?.erro ?? { code: 'ERR_SERVE', message: `O serviço local devolveu HTTP ${resposta.status}.` };
21
+ throw Object.assign(new Error(erro.message), { code: erro.code });
22
+ }
23
+ return json;
24
+ }
25
+ return {
26
+ estado: () => pedir('GET', '/mcp/estado'),
27
+ comando: (corpo) => pedir('POST', '/mcp/comando', corpo),
28
+ resultado: (id, waitS, fase = 'final') => pedir('GET', `/mcp/comando/${encodeURIComponent(id)}?wait=${waitS}&fase=${fase}`),
29
+ };
30
+ }
@@ -0,0 +1,14 @@
1
+ // Puro. O último contexto que a extensão publicou e a última proposta por mês.
2
+
3
+ export function criarContexto({ agora = () => Date.now() } = {}) {
4
+ let atual = null;
5
+ const propostas = new Map();
6
+ const chave = (ano, mes) => `${ano}-${mes}`;
7
+ return {
8
+ guardar(ctx) { atual = { ...ctx, recebidoEm: agora() }; },
9
+ atual: () => atual,
10
+ mesVisivel: () => (atual ? { ano: atual.ano, mes: atual.mes } : null),
11
+ guardarProposta(ano, mes, linhas) { propostas.set(chave(ano, mes), linhas); },
12
+ proposta: (ano, mes) => propostas.get(chave(ano, mes)) ?? [],
13
+ };
14
+ }