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.
- package/README.md +17 -25
- package/index.mjs +198 -0
- 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
|
-
|
|
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`
|
|
14
|
-
|
|
12
|
+
| `INNOV_API_TOKEN` | Token pessoal Sanctum (ex.: **Perfil** → Tokens de API na app Innov) |
|
|
15
13
|
|
|
16
|
-
Copia
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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`
|
|
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);
|