innov-mcp-tasks 1.2.0 → 1.4.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 +4 -2
  2. package/index.mjs +158 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,11 +52,13 @@ Na raiz do repo existe [`.cursor/mcp.json`](../.cursor/mcp.json) com **dois** se
52
52
 
53
53
  ### Tarefas e projetos
54
54
 
55
- - `tasks_list` — opcional `project_id`
55
+ - `tasks_list` — opcional `project_id`, `milestones_only` (filtra marcos no cliente)
56
56
  - `my_tasks` — tarefas do utilizador do token (endpoint “minhas” da API)
57
57
  - `projects_list` — lista projetos visíveis (`GET /api/v1/projects`)
58
58
  - `project_create` — cria projeto (`POST /api/v1/projects`)
59
- - `task_get`, `task_create`, `task_update_status`, `task_assign`, `task_assign_to_me`
59
+ - `task_get`, `task_create`, `task_update`, `task_update_status`, `task_assign`, `task_assign_to_me`
60
+ - `task_create` / `task_update` — marcos: `is_milestone`, `start_date` (YYYY-MM-DD), `duration` (dias); marco exige `project_id` na criação
61
+ - `task_attachment_download` — baixa anexo por `attachment_id` (conteúdo em base64; ids em `task_get` → `attachments`)
60
62
 
61
63
  ### Anotações / documentação
62
64
 
package/index.mjs CHANGED
@@ -50,13 +50,86 @@ 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
+
90
+ /** Campos de marco / cronograma (is_milestone exige project_id na API). */
91
+ const taskMilestoneInputSchema = {
92
+ is_milestone: z.boolean().optional(),
93
+ start_date: z
94
+ .string()
95
+ .optional()
96
+ .describe('Data de início (YYYY-MM-DD); usada em marcos e Gantt'),
97
+ duration: z
98
+ .number()
99
+ .int()
100
+ .min(1)
101
+ .optional()
102
+ .describe('Duração em dias (marco/cronograma)'),
103
+ };
104
+
105
+ function pickDefined(fields) {
106
+ return Object.fromEntries(
107
+ Object.entries(fields).filter(([, v]) => v !== undefined),
108
+ );
109
+ }
110
+
111
+ function filterTasksMilestonesOnly(data) {
112
+ if (!Array.isArray(data)) {
113
+ return data;
114
+ }
115
+ return data.filter((t) => t && t.is_milestone);
116
+ }
117
+
53
118
  const server = new McpServer({ name: 'innov-tasks', version: '1.0.0' });
54
119
 
55
120
  server.registerTool(
56
121
  'tasks_list',
57
122
  {
58
- description: 'Lista tarefas. Com project_id usa GET /tasks?project_id=…; sem project_id usa GET /tasks/kanban.',
59
- inputSchema: { project_id: z.number().int().positive().optional() },
123
+ description:
124
+ 'Lista tarefas. Com project_id usa GET /tasks?project_id=…; sem project_id usa GET /tasks/kanban. ' +
125
+ 'Cada item inclui is_milestone, start_date e duration. Com milestones_only=true filtra só marcos (no cliente).',
126
+ inputSchema: {
127
+ project_id: z.number().int().positive().optional(),
128
+ milestones_only: z
129
+ .boolean()
130
+ .optional()
131
+ .describe('Se true, devolve apenas tarefas com is_milestone=true'),
132
+ },
60
133
  },
61
134
  async (args) => {
62
135
  try {
@@ -64,7 +137,10 @@ server.registerTool(
64
137
  args.project_id != null
65
138
  ? `/api/v1/tasks?project_id=${encodeURIComponent(String(args.project_id))}`
66
139
  : '/api/v1/tasks/kanban';
67
- const data = await apiFetch(path);
140
+ let data = await apiFetch(path);
141
+ if (args.milestones_only) {
142
+ data = filterTasksMilestonesOnly(data);
143
+ }
68
144
  return jsonText(data);
69
145
  } catch (e) {
70
146
  return jsonError(e instanceof Error ? e.message : String(e));
@@ -92,7 +168,8 @@ server.registerTool(
92
168
  server.registerTool(
93
169
  'task_get',
94
170
  {
95
- description: 'Obtém uma tarefa por id (GET /tasks/{id}).',
171
+ description:
172
+ 'Obtém uma tarefa por id (GET /tasks/{id}). Inclui is_milestone, start_date e duration.',
96
173
  inputSchema: { task_id: z.number().int().positive() },
97
174
  },
98
175
  async (args) => {
@@ -105,6 +182,34 @@ server.registerTool(
105
182
  },
106
183
  );
107
184
 
185
+ server.registerTool(
186
+ 'task_attachment_download',
187
+ {
188
+ description:
189
+ 'Baixa anexo de tarefa (GET /task-attachments/{id}/download). Requer Bearer Sanctum. ' +
190
+ 'Retorna metadados e conteúdo em base64 (útil para zip, pdf, docx, txt, srt, etc.). ' +
191
+ 'Use task_get para listar attachments[].id da tarefa.',
192
+ inputSchema: { attachment_id: z.number().int().positive() },
193
+ },
194
+ async (args) => {
195
+ try {
196
+ const file = await apiFetchBinary(
197
+ `/api/v1/task-attachments/${args.attachment_id}/download`,
198
+ );
199
+ return jsonText({
200
+ attachment_id: args.attachment_id,
201
+ filename: file.filename,
202
+ content_type: file.contentType,
203
+ size_bytes: file.size,
204
+ content_base64: file.buffer.toString('base64'),
205
+ hint: 'Decodifique content_base64 para gravar o ficheiro localmente.',
206
+ });
207
+ } catch (e) {
208
+ return jsonError(e instanceof Error ? e.message : String(e));
209
+ }
210
+ },
211
+ );
212
+
108
213
  server.registerTool(
109
214
  'projects_list',
110
215
  {
@@ -168,7 +273,8 @@ server.registerTool(
168
273
  server.registerTool(
169
274
  'task_create',
170
275
  {
171
- description: 'Cria tarefa (POST /tasks).',
276
+ description:
277
+ 'Cria tarefa (POST /tasks). Marcos: is_milestone=true exige project_id; opcional start_date e duration (dias).',
172
278
  inputSchema: {
173
279
  title: z.string().min(1).max(255),
174
280
  project_id: z.number().int().positive().optional(),
@@ -176,18 +282,24 @@ server.registerTool(
176
282
  description: z.string().optional(),
177
283
  assigned_to: z.number().int().positive().optional(),
178
284
  priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
285
+ due_date: z.string().optional().describe('Prazo (YYYY-MM-DD)'),
286
+ ...taskMilestoneInputSchema,
179
287
  },
180
288
  },
181
289
  async (args) => {
182
290
  try {
183
- const body = {
291
+ const body = pickDefined({
184
292
  title: args.title,
185
- project_id: args.project_id ?? null,
186
- team_id: args.team_id ?? null,
187
- description: args.description ?? null,
188
- assigned_to: args.assigned_to ?? null,
293
+ project_id: args.project_id,
294
+ team_id: args.team_id,
295
+ description: args.description,
296
+ assigned_to: args.assigned_to,
189
297
  priority: args.priority ?? 'medium',
190
- };
298
+ due_date: args.due_date,
299
+ is_milestone: args.is_milestone,
300
+ start_date: args.start_date,
301
+ duration: args.duration,
302
+ });
191
303
  const data = await apiFetch('/api/v1/tasks', {
192
304
  method: 'POST',
193
305
  body: JSON.stringify(body),
@@ -199,6 +311,41 @@ server.registerTool(
199
311
  },
200
312
  );
201
313
 
314
+ server.registerTool(
315
+ 'task_update',
316
+ {
317
+ description:
318
+ 'Atualiza tarefa (PATCH /tasks/{id}). Campos opcionais; marcos: is_milestone, start_date, duration (dias). ' +
319
+ 'Marco exige projeto e não pode ser subtarefa (regras da API).',
320
+ inputSchema: {
321
+ task_id: z.number().int().positive(),
322
+ title: z.string().min(1).max(255).optional(),
323
+ description: z.string().optional(),
324
+ status: z
325
+ .enum(['pending', 'in_progress', 'in_review', 'completed', 'cancelled'])
326
+ .optional(),
327
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
328
+ due_date: z.string().optional(),
329
+ assigned_to: z.number().int().positive().optional(),
330
+ progress: z.number().int().min(0).max(100).optional(),
331
+ ...taskMilestoneInputSchema,
332
+ },
333
+ },
334
+ async (args) => {
335
+ try {
336
+ const { task_id, ...fields } = args;
337
+ const body = pickDefined(fields);
338
+ const data = await apiFetch(`/api/v1/tasks/${task_id}`, {
339
+ method: 'PATCH',
340
+ body: JSON.stringify(body),
341
+ });
342
+ return jsonText(data);
343
+ } catch (e) {
344
+ return jsonError(e instanceof Error ? e.message : String(e));
345
+ }
346
+ },
347
+ );
348
+
202
349
  server.registerTool(
203
350
  'task_update_status',
204
351
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innov-mcp-tasks",
3
- "version": "1.2.0",
3
+ "version": "1.4.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",