innov-mcp-tasks 1.1.0 → 1.3.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.
Files changed (3) hide show
  1. package/README.md +17 -25
  2. package/index.mjs +198 -0
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,18 +6,16 @@ Servidor [MCP](https://modelcontextprotocol.io) em **stdio** para ferramentas de
6
6
 
7
7
  ## Variáveis obrigatórias
8
8
 
9
-
10
- | Variável | Descrição |
11
- | -------------------- | -------------------------------------------------------------------------------- |
9
+ | Variável | Descrição |
10
+ |----------|-----------|
12
11
  | `INNOV_API_BASE_URL` | URL base **sem** `/api/v1` (definida por ti: dev local, staging, produção, etc.) |
13
- | `INNOV_API_TOKEN` | Token pessoal Sanctum (ex.: **Perfil** → Tokens de API na app Innov) |
14
-
12
+ | `INNOV_API_TOKEN` | Token pessoal Sanctum (ex.: **Perfil** → Tokens de API na app Innov) |
15
13
 
16
- Copia `[.env.example](./.env.example)` para um ficheiro `.env` à tua escolha e preenche (esse ficheiro **não** vem do npm com valores; no repo monorepo, mantém `.env` fora do Git).
14
+ Copia [`.env.example`](./.env.example) para um ficheiro `.env` à tua escolha e preenche (esse ficheiro **não** vem do npm com valores; no repo monorepo, mantém `.env` fora do Git).
17
15
 
18
16
  ## Cursor com pacote npm (`npx`)
19
17
 
20
- Nome do pacote no npm: `**innov-mcp-tasks`** (se o nome estiver ocupado, publica como scoped, ex. `@tua-org/innov-mcp-tasks`, e ajusta os exemplos).
18
+ Nome do pacote no npm: **`innov-mcp-tasks`** (se o nome estiver ocupado, publica como scoped, ex. `@tua-org/innov-mcp-tasks`, e ajusta os exemplos).
21
19
 
22
20
  ```json
23
21
  {
@@ -35,20 +33,18 @@ Nome do pacote no npm: `**innov-mcp-tasks`** (se o nome estiver ocupado, publica
35
33
  }
36
34
  ```
37
35
 
38
- Segredos: prefere `[${env:INNOV_API_TOKEN}](https://cursor.com/docs/mcp)` apontando para variáveis já definidas no SO, ou `envFile` para um `.env` **fora** do repositório (ex. `C:\\Users\\…\\.config\\innov\\mcp.env`).
36
+ Segredos: prefere [`${env:INNOV_API_TOKEN}`](https://cursor.com/docs/mcp) apontando para variáveis já definidas no SO, ou `envFile` para um `.env` **fora** do repositório (ex. `C:\\Users\\…\\.config\\innov\\mcp.env`).
39
37
 
40
38
  Também podes apontar o binário instalado globalmente: `"command": "innov-mcp-tasks"` (após `npm install -g innov-mcp-tasks`).
41
39
 
42
40
  ## Monorepo (desenvolvimento)
43
41
 
44
- Na raiz do repo existe `[.cursor/mcp.json](../.cursor/mcp.json)` com **dois** servidores que partilham o mesmo `mcp-tasks/.env`:
42
+ Na raiz do repo existe [`.cursor/mcp.json`](../.cursor/mcp.json) com **dois** servidores que partilham o mesmo `mcp-tasks/.env`:
45
43
 
46
-
47
- | Servidor | Uso |
48
- | ------------------- | -------------------------------------------------------- |
44
+ | Servidor | Uso |
45
+ |----------|-----|
49
46
  | `innov-tasks-local` | Corre o `index.mjs` do clon (óptimo para alterar o MCP). |
50
- | `innov-tasks-npm` | Usa `npx -y innov-mcp-tasks` (testa o pacote publicado). |
51
-
47
+ | `innov-tasks-npm` | Usa `npx -y innov-mcp-tasks` (testa o pacote publicado). |
52
48
 
53
49
  **Ativa só um** em *Settings → Features → Model Context Protocol* (dois ao mesmo tempo duplicam ferramentas com o mesmo nome). Se publicares com scope (`@org/innov-mcp-tasks`), edita os `args` em `innov-tasks-npm` para esse nome.
54
50
 
@@ -61,6 +57,7 @@ Na raiz do repo existe `[.cursor/mcp.json](../.cursor/mcp.json)` com **dois** se
61
57
  - `projects_list` — lista projetos visíveis (`GET /api/v1/projects`)
62
58
  - `project_create` — cria projeto (`POST /api/v1/projects`)
63
59
  - `task_get`, `task_create`, `task_update_status`, `task_assign`, `task_assign_to_me`
60
+ - `task_attachment_download` — baixa anexo por `attachment_id` (conteúdo em base64; ids em `task_get` → `attachments`)
64
61
 
65
62
  ### Anotações / documentação
66
63
 
@@ -72,6 +69,12 @@ Na raiz do repo existe `[.cursor/mcp.json](../.cursor/mcp.json)` com **dois** se
72
69
  - `notes_trashed`, `note_restore`
73
70
  - `annotations_search` — busca em cadernos, notas e fontes (`q` ≥ 2 caracteres)
74
71
 
72
+ ### Cadernos (notebooks)
73
+
74
+ - `notebooks_list` — lista com filtro opcional `project_id`
75
+ - `notebook_get`, `notebook_create`, `notebook_update`, `notebook_delete`
76
+ - `notebook_documentation` — notas + fontes do caderno
77
+
75
78
  ## Publicar no npm (mantenedor)
76
79
 
77
80
  1. Define o **nome** em `package.json` (`innov-mcp-tasks` ou `@scope/innov-mcp-tasks` se o nome simples estiver tomado).
@@ -84,16 +87,6 @@ npm test
84
87
  npm publish --access public
85
88
  ```
86
89
 
87
-
88
-
89
- Se estiver deslogado para logar faça o comando
90
-
91
- ```bash
92
- npm login
93
- ```
94
-
95
-
96
-
97
90
  Para **scoped** (`@org/pkg`): a primeira vez costuma precisar de `--access public` se for pacote OSS.
98
91
 
99
92
  ## Testes
@@ -101,4 +94,3 @@ Para **scoped** (`@org/pkg`): a primeira vez costuma precisar de `--access publi
101
94
  ```bash
102
95
  npm test
103
96
  ```
104
-
package/index.mjs CHANGED
@@ -50,6 +50,43 @@ async function apiFetch(path, init = {}) {
50
50
  return data;
51
51
  }
52
52
 
53
+ async function apiFetchBinary(path) {
54
+ if (!base || !token) {
55
+ throw new Error(
56
+ 'Defina INNOV_API_BASE_URL e INNOV_API_TOKEN no ambiente ou num .env.',
57
+ );
58
+ }
59
+ const url = `${base}${path.startsWith('/') ? path : `/${path}`}`;
60
+ const res = await fetch(url, {
61
+ headers: {
62
+ Authorization: `Bearer ${token}`,
63
+ },
64
+ });
65
+ if (!res.ok) {
66
+ const text = await res.text();
67
+ let msg = text.slice(0, 800);
68
+ try {
69
+ const data = text ? JSON.parse(text) : null;
70
+ if (data && (data.message || data.error)) {
71
+ msg = String(data.message || data.error);
72
+ }
73
+ } catch {
74
+ /* texto bruto */
75
+ }
76
+ throw new Error(`HTTP ${res.status}: ${msg}`);
77
+ }
78
+ const buffer = Buffer.from(await res.arrayBuffer());
79
+ const disposition = res.headers.get('content-disposition') || '';
80
+ const filenameMatch = disposition.match(/filename="?([^";\n]+)"?/i);
81
+ const filename = filenameMatch?.[1]?.trim() || 'attachment';
82
+ return {
83
+ buffer,
84
+ contentType: res.headers.get('content-type') || 'application/octet-stream',
85
+ filename,
86
+ size: buffer.length,
87
+ };
88
+ }
89
+
53
90
  const server = new McpServer({ name: 'innov-tasks', version: '1.0.0' });
54
91
 
55
92
  server.registerTool(
@@ -105,6 +142,34 @@ server.registerTool(
105
142
  },
106
143
  );
107
144
 
145
+ server.registerTool(
146
+ 'task_attachment_download',
147
+ {
148
+ description:
149
+ 'Baixa anexo de tarefa (GET /task-attachments/{id}/download). Requer Bearer Sanctum. ' +
150
+ 'Retorna metadados e conteúdo em base64 (útil para zip, pdf, docx, txt, srt, etc.). ' +
151
+ 'Use task_get para listar attachments[].id da tarefa.',
152
+ inputSchema: { attachment_id: z.number().int().positive() },
153
+ },
154
+ async (args) => {
155
+ try {
156
+ const file = await apiFetchBinary(
157
+ `/api/v1/task-attachments/${args.attachment_id}/download`,
158
+ );
159
+ return jsonText({
160
+ attachment_id: args.attachment_id,
161
+ filename: file.filename,
162
+ content_type: file.contentType,
163
+ size_bytes: file.size,
164
+ content_base64: file.buffer.toString('base64'),
165
+ hint: 'Decodifique content_base64 para gravar o ficheiro localmente.',
166
+ });
167
+ } catch (e) {
168
+ return jsonError(e instanceof Error ? e.message : String(e));
169
+ }
170
+ },
171
+ );
172
+
108
173
  server.registerTool(
109
174
  'projects_list',
110
175
  {
@@ -496,5 +561,138 @@ server.registerTool(
496
561
  },
497
562
  );
498
563
 
564
+ server.registerTool(
565
+ 'notebooks_list',
566
+ {
567
+ description:
568
+ 'Lista cadernos visíveis ao utilizador do token (GET /notebooks). Filtro opcional project_id.',
569
+ inputSchema: {
570
+ project_id: z.number().int().positive().optional(),
571
+ },
572
+ },
573
+ async (args) => {
574
+ try {
575
+ const qs =
576
+ args.project_id != null
577
+ ? `?project_id=${encodeURIComponent(String(args.project_id))}`
578
+ : '';
579
+ const data = await apiFetch(`/api/v1/notebooks${qs}`);
580
+ return jsonText(data);
581
+ } catch (e) {
582
+ return jsonError(e instanceof Error ? e.message : String(e));
583
+ }
584
+ },
585
+ );
586
+
587
+ server.registerTool(
588
+ 'notebook_get',
589
+ {
590
+ description: 'Obtém um caderno por id (GET /notebooks/{id}).',
591
+ inputSchema: { notebook_id: z.number().int().positive() },
592
+ },
593
+ async (args) => {
594
+ try {
595
+ const data = await apiFetch(`/api/v1/notebooks/${args.notebook_id}`);
596
+ return jsonText(data);
597
+ } catch (e) {
598
+ return jsonError(e instanceof Error ? e.message : String(e));
599
+ }
600
+ },
601
+ );
602
+
603
+ server.registerTool(
604
+ 'notebook_create',
605
+ {
606
+ description:
607
+ 'Cria caderno (POST /notebooks). Pessoal sem project_id; de projeto com project_id. name obrigatório.',
608
+ inputSchema: {
609
+ name: z.string().min(1).max(255),
610
+ description: z.string().optional(),
611
+ project_id: z.number().int().positive().optional(),
612
+ },
613
+ },
614
+ async (args) => {
615
+ try {
616
+ const body = {
617
+ name: args.name,
618
+ description: args.description ?? null,
619
+ project_id: args.project_id ?? null,
620
+ };
621
+ const data = await apiFetch('/api/v1/notebooks', {
622
+ method: 'POST',
623
+ body: JSON.stringify(body),
624
+ });
625
+ return jsonText(data);
626
+ } catch (e) {
627
+ return jsonError(e instanceof Error ? e.message : String(e));
628
+ }
629
+ },
630
+ );
631
+
632
+ server.registerTool(
633
+ 'notebook_update',
634
+ {
635
+ description: 'Atualiza caderno (PATCH /notebooks/{id}). Só cadernos do utilizador.',
636
+ inputSchema: {
637
+ notebook_id: z.number().int().positive(),
638
+ name: z.string().min(1).max(255).optional(),
639
+ description: z.string().optional(),
640
+ project_id: z.number().int().positive().optional(),
641
+ },
642
+ },
643
+ async (args) => {
644
+ try {
645
+ const { notebook_id, ...fields } = args;
646
+ const body = Object.fromEntries(
647
+ Object.entries(fields).filter(([, v]) => v !== undefined),
648
+ );
649
+ const data = await apiFetch(`/api/v1/notebooks/${notebook_id}`, {
650
+ method: 'PATCH',
651
+ body: JSON.stringify(body),
652
+ });
653
+ return jsonText(data);
654
+ } catch (e) {
655
+ return jsonError(e instanceof Error ? e.message : String(e));
656
+ }
657
+ },
658
+ );
659
+
660
+ server.registerTool(
661
+ 'notebook_delete',
662
+ {
663
+ description: 'Apaga caderno (DELETE /notebooks/{id}). Só cadernos do utilizador.',
664
+ inputSchema: { notebook_id: z.number().int().positive() },
665
+ },
666
+ async (args) => {
667
+ try {
668
+ const data = await apiFetch(`/api/v1/notebooks/${args.notebook_id}`, {
669
+ method: 'DELETE',
670
+ });
671
+ return jsonText(data);
672
+ } catch (e) {
673
+ return jsonError(e instanceof Error ? e.message : String(e));
674
+ }
675
+ },
676
+ );
677
+
678
+ server.registerTool(
679
+ 'notebook_documentation',
680
+ {
681
+ description:
682
+ 'Documentação agregada do caderno: notas do utilizador + fontes (GET /notebooks/{id}/documentation).',
683
+ inputSchema: { notebook_id: z.number().int().positive() },
684
+ },
685
+ async (args) => {
686
+ try {
687
+ const data = await apiFetch(
688
+ `/api/v1/notebooks/${args.notebook_id}/documentation`,
689
+ );
690
+ return jsonText(data);
691
+ } catch (e) {
692
+ return jsonError(e instanceof Error ? e.message : String(e));
693
+ }
694
+ },
695
+ );
696
+
499
697
  const transport = new StdioServerTransport();
500
698
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innov-mcp-tasks",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP stdio — tarefas e anotações Innov (INNOV_API_BASE_URL + token Sanctum)",
5
5
  "type": "module",
6
6
  "main": "index.mjs",