innov-mcp-tasks 1.3.0 → 1.5.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 CHANGED
@@ -52,11 +52,12 @@ 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
60
61
  - `task_attachment_download` — baixa anexo por `attachment_id` (conteúdo em base64; ids em `task_get` → `attachments`)
61
62
 
62
63
  ### Anotações / documentação
@@ -74,6 +75,12 @@ Na raiz do repo existe [`.cursor/mcp.json`](../.cursor/mcp.json) com **dois** se
74
75
  - `notebooks_list` — lista com filtro opcional `project_id`
75
76
  - `notebook_get`, `notebook_create`, `notebook_update`, `notebook_delete`
76
77
  - `notebook_documentation` — notas + fontes do caderno
78
+ - `annotations_semantic_search` — busca semântica em notas e metadados (`q` ≥ 2 caracteres)
79
+
80
+ ### Agentes de IA (SPEC-015)
81
+
82
+ - `agents_list` — agentes utilizáveis pelo token (`GET /api/v1/ai-agents?scope=usable`); opcional `scope=manageable` para gestão
83
+ - `agent_chat` — envia `message` a um agente (`POST /api/v1/ai-agents/{ulid}/chat`); devolve `response` e `citations`; opcional `conversation_ulid`, `project_id`, `notebook_id`
77
84
 
78
85
  ## Publicar no npm (mantenedor)
79
86
 
package/index.mjs CHANGED
@@ -6,6 +6,7 @@ import 'dotenv/config';
6
6
  import { z } from 'zod';
7
7
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
8
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
9
+ import { agentChat, agentsList } from './lib/agents.mjs';
9
10
 
10
11
  const base = (process.env.INNOV_API_BASE_URL || '').replace(/\/$/, '');
11
12
  const token = (process.env.INNOV_API_TOKEN || '').trim();
@@ -87,13 +88,49 @@ async function apiFetchBinary(path) {
87
88
  };
88
89
  }
89
90
 
91
+ /** Campos de marco / cronograma (is_milestone exige project_id na API). */
92
+ const taskMilestoneInputSchema = {
93
+ is_milestone: z.boolean().optional(),
94
+ start_date: z
95
+ .string()
96
+ .optional()
97
+ .describe('Data de início (YYYY-MM-DD); usada em marcos e Gantt'),
98
+ duration: z
99
+ .number()
100
+ .int()
101
+ .min(1)
102
+ .optional()
103
+ .describe('Duração em dias (marco/cronograma)'),
104
+ };
105
+
106
+ function pickDefined(fields) {
107
+ return Object.fromEntries(
108
+ Object.entries(fields).filter(([, v]) => v !== undefined),
109
+ );
110
+ }
111
+
112
+ function filterTasksMilestonesOnly(data) {
113
+ if (!Array.isArray(data)) {
114
+ return data;
115
+ }
116
+ return data.filter((t) => t && t.is_milestone);
117
+ }
118
+
90
119
  const server = new McpServer({ name: 'innov-tasks', version: '1.0.0' });
91
120
 
92
121
  server.registerTool(
93
122
  'tasks_list',
94
123
  {
95
- description: 'Lista tarefas. Com project_id usa GET /tasks?project_id=…; sem project_id usa GET /tasks/kanban.',
96
- inputSchema: { project_id: z.number().int().positive().optional() },
124
+ description:
125
+ 'Lista tarefas. Com project_id usa GET /tasks?project_id=…; sem project_id usa GET /tasks/kanban. ' +
126
+ 'Cada item inclui is_milestone, start_date e duration. Com milestones_only=true filtra só marcos (no cliente).',
127
+ inputSchema: {
128
+ project_id: z.number().int().positive().optional(),
129
+ milestones_only: z
130
+ .boolean()
131
+ .optional()
132
+ .describe('Se true, devolve apenas tarefas com is_milestone=true'),
133
+ },
97
134
  },
98
135
  async (args) => {
99
136
  try {
@@ -101,7 +138,10 @@ server.registerTool(
101
138
  args.project_id != null
102
139
  ? `/api/v1/tasks?project_id=${encodeURIComponent(String(args.project_id))}`
103
140
  : '/api/v1/tasks/kanban';
104
- const data = await apiFetch(path);
141
+ let data = await apiFetch(path);
142
+ if (args.milestones_only) {
143
+ data = filterTasksMilestonesOnly(data);
144
+ }
105
145
  return jsonText(data);
106
146
  } catch (e) {
107
147
  return jsonError(e instanceof Error ? e.message : String(e));
@@ -129,7 +169,8 @@ server.registerTool(
129
169
  server.registerTool(
130
170
  'task_get',
131
171
  {
132
- description: 'Obtém uma tarefa por id (GET /tasks/{id}).',
172
+ description:
173
+ 'Obtém uma tarefa por id (GET /tasks/{id}). Inclui is_milestone, start_date e duration.',
133
174
  inputSchema: { task_id: z.number().int().positive() },
134
175
  },
135
176
  async (args) => {
@@ -233,7 +274,8 @@ server.registerTool(
233
274
  server.registerTool(
234
275
  'task_create',
235
276
  {
236
- description: 'Cria tarefa (POST /tasks).',
277
+ description:
278
+ 'Cria tarefa (POST /tasks). Marcos: is_milestone=true exige project_id; opcional start_date e duration (dias).',
237
279
  inputSchema: {
238
280
  title: z.string().min(1).max(255),
239
281
  project_id: z.number().int().positive().optional(),
@@ -241,18 +283,24 @@ server.registerTool(
241
283
  description: z.string().optional(),
242
284
  assigned_to: z.number().int().positive().optional(),
243
285
  priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
286
+ due_date: z.string().optional().describe('Prazo (YYYY-MM-DD)'),
287
+ ...taskMilestoneInputSchema,
244
288
  },
245
289
  },
246
290
  async (args) => {
247
291
  try {
248
- const body = {
292
+ const body = pickDefined({
249
293
  title: args.title,
250
- project_id: args.project_id ?? null,
251
- team_id: args.team_id ?? null,
252
- description: args.description ?? null,
253
- assigned_to: args.assigned_to ?? null,
294
+ project_id: args.project_id,
295
+ team_id: args.team_id,
296
+ description: args.description,
297
+ assigned_to: args.assigned_to,
254
298
  priority: args.priority ?? 'medium',
255
- };
299
+ due_date: args.due_date,
300
+ is_milestone: args.is_milestone,
301
+ start_date: args.start_date,
302
+ duration: args.duration,
303
+ });
256
304
  const data = await apiFetch('/api/v1/tasks', {
257
305
  method: 'POST',
258
306
  body: JSON.stringify(body),
@@ -264,6 +312,41 @@ server.registerTool(
264
312
  },
265
313
  );
266
314
 
315
+ server.registerTool(
316
+ 'task_update',
317
+ {
318
+ description:
319
+ 'Atualiza tarefa (PATCH /tasks/{id}). Campos opcionais; marcos: is_milestone, start_date, duration (dias). ' +
320
+ 'Marco exige projeto e não pode ser subtarefa (regras da API).',
321
+ inputSchema: {
322
+ task_id: z.number().int().positive(),
323
+ title: z.string().min(1).max(255).optional(),
324
+ description: z.string().optional(),
325
+ status: z
326
+ .enum(['pending', 'in_progress', 'in_review', 'completed', 'cancelled'])
327
+ .optional(),
328
+ priority: z.enum(['low', 'medium', 'high', 'urgent']).optional(),
329
+ due_date: z.string().optional(),
330
+ assigned_to: z.number().int().positive().optional(),
331
+ progress: z.number().int().min(0).max(100).optional(),
332
+ ...taskMilestoneInputSchema,
333
+ },
334
+ },
335
+ async (args) => {
336
+ try {
337
+ const { task_id, ...fields } = args;
338
+ const body = pickDefined(fields);
339
+ const data = await apiFetch(`/api/v1/tasks/${task_id}`, {
340
+ method: 'PATCH',
341
+ body: JSON.stringify(body),
342
+ });
343
+ return jsonText(data);
344
+ } catch (e) {
345
+ return jsonError(e instanceof Error ? e.message : String(e));
346
+ }
347
+ },
348
+ );
349
+
267
350
  server.registerTool(
268
351
  'task_update_status',
269
352
  {
@@ -675,6 +758,40 @@ server.registerTool(
675
758
  },
676
759
  );
677
760
 
761
+ server.registerTool(
762
+ 'annotations_semantic_search',
763
+ {
764
+ description:
765
+ 'Busca semântica em notas e metadados de cadernos (GET /annotations/semantic-search).',
766
+ inputSchema: {
767
+ q: z.string().min(2),
768
+ notebook_id: z.number().int().positive().optional(),
769
+ limit: z.number().int().min(1).max(30).optional(),
770
+ min_score: z.number().min(0).max(1).optional(),
771
+ },
772
+ },
773
+ async (args) => {
774
+ try {
775
+ const params = new URLSearchParams({ q: args.q });
776
+ if (args.notebook_id != null) {
777
+ params.set('notebook_id', String(args.notebook_id));
778
+ }
779
+ if (args.limit != null) {
780
+ params.set('limit', String(args.limit));
781
+ }
782
+ if (args.min_score != null) {
783
+ params.set('min_score', String(args.min_score));
784
+ }
785
+ const data = await apiFetch(
786
+ `/api/v1/annotations/semantic-search?${params.toString()}`,
787
+ );
788
+ return jsonText(data);
789
+ } catch (e) {
790
+ return jsonError(e instanceof Error ? e.message : String(e));
791
+ }
792
+ },
793
+ );
794
+
678
795
  server.registerTool(
679
796
  'notebook_documentation',
680
797
  {
@@ -694,5 +811,56 @@ server.registerTool(
694
811
  },
695
812
  );
696
813
 
814
+ server.registerTool(
815
+ 'agents_list',
816
+ {
817
+ description:
818
+ 'Lista agentes de IA (GET /ai-agents). Padrão scope=usable — agentes que o token pode usar no chat (ACL). ' +
819
+ 'Use scope=manageable para gestão (/agentes; requer can_manage_agents).',
820
+ inputSchema: {
821
+ scope: z
822
+ .enum(['usable', 'manageable'])
823
+ .optional()
824
+ .describe('Padrão: usable (selector de chat conforme ACL)'),
825
+ },
826
+ },
827
+ async (args) => {
828
+ try {
829
+ const data = await agentsList(apiFetch, args.scope ?? 'usable');
830
+ return jsonText(data);
831
+ } catch (e) {
832
+ return jsonError(e instanceof Error ? e.message : String(e));
833
+ }
834
+ },
835
+ );
836
+
837
+ server.registerTool(
838
+ 'agent_chat',
839
+ {
840
+ description:
841
+ 'Envia mensagem a um agente de IA (POST /ai-agents/{ulid}/chat). ' +
842
+ 'Devolve resposta do agente e citações quando ferramentas de KB forem usadas. ' +
843
+ 'Opcional conversation_ulid para continuar conversa; project_id/notebook_id para contexto.',
844
+ inputSchema: {
845
+ agent_ulid: z.string().min(1).describe('ULID do agente (ver agents_list)'),
846
+ message: z.string().min(1),
847
+ conversation_ulid: z
848
+ .string()
849
+ .optional()
850
+ .describe('ULID da conversa para continuar o turno'),
851
+ project_id: z.number().int().positive().optional(),
852
+ notebook_id: z.number().int().positive().optional(),
853
+ },
854
+ },
855
+ async (args) => {
856
+ try {
857
+ const data = await agentChat(apiFetch, args);
858
+ return jsonText(data);
859
+ } catch (e) {
860
+ return jsonError(e instanceof Error ? e.message : String(e));
861
+ }
862
+ },
863
+ );
864
+
697
865
  const transport = new StdioServerTransport();
698
866
  await server.connect(transport);
package/lib/agents.mjs ADDED
@@ -0,0 +1,97 @@
1
+ /** Helpers e handlers testáveis para tools de agentes de IA (SPEC-015 Fase 8). */
2
+
3
+ export const AGENT_SCOPES = ['usable', 'manageable'];
4
+
5
+ /**
6
+ * @param {'usable' | 'manageable'} [scope]
7
+ */
8
+ export function agentsListPath(scope = 'usable') {
9
+ if (!AGENT_SCOPES.includes(scope)) {
10
+ throw new Error(`scope inválido: ${scope}. Use manageable ou usable.`);
11
+ }
12
+ return `/api/v1/ai-agents?scope=${encodeURIComponent(scope)}`;
13
+ }
14
+
15
+ /**
16
+ * @param {string} ulid
17
+ */
18
+ export function agentChatPath(ulid) {
19
+ const trimmed = String(ulid ?? '').trim();
20
+ if (!trimmed) {
21
+ throw new Error('agent_ulid é obrigatório');
22
+ }
23
+ return `/api/v1/ai-agents/${encodeURIComponent(trimmed)}/chat`;
24
+ }
25
+
26
+ /**
27
+ * @param {{
28
+ * message: string;
29
+ * conversation_ulid?: string;
30
+ * project_id?: number;
31
+ * notebook_id?: number;
32
+ * }} args
33
+ */
34
+ export function buildAgentChatBody(args) {
35
+ const body = { message: args.message };
36
+ if (args.conversation_ulid != null) {
37
+ body.conversation_ulid = args.conversation_ulid;
38
+ }
39
+ if (args.project_id != null) {
40
+ body.project_id = args.project_id;
41
+ }
42
+ if (args.notebook_id != null) {
43
+ body.notebook_id = args.notebook_id;
44
+ }
45
+ return body;
46
+ }
47
+
48
+ /**
49
+ * Normaliza resposta do chat para o consumidor MCP (resposta + citações).
50
+ * @param {unknown} data
51
+ */
52
+ export function formatAgentChatResponse(data) {
53
+ if (!data || typeof data !== 'object') {
54
+ return { response: null, citations: [] };
55
+ }
56
+ const payload = /** @type {Record<string, unknown>} */ (data);
57
+ return {
58
+ success: payload.success,
59
+ response:
60
+ typeof payload.response === 'string'
61
+ ? payload.response
62
+ : typeof payload.message === 'string'
63
+ ? payload.message
64
+ : null,
65
+ citations: Array.isArray(payload.citations) ? payload.citations : [],
66
+ conversation_ulid: payload.conversation_ulid ?? null,
67
+ actions: payload.actions,
68
+ token_usage: payload.token_usage,
69
+ pending_approval: payload.pending_approval ?? null,
70
+ };
71
+ }
72
+
73
+ /**
74
+ * @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
75
+ * @param {'usable' | 'manageable'} [scope]
76
+ */
77
+ export async function agentsList(apiFetch, scope = 'usable') {
78
+ return apiFetch(agentsListPath(scope));
79
+ }
80
+
81
+ /**
82
+ * @param {(path: string, init?: RequestInit) => Promise<unknown>} apiFetch
83
+ * @param {{
84
+ * agent_ulid: string;
85
+ * message: string;
86
+ * conversation_ulid?: string;
87
+ * project_id?: number;
88
+ * notebook_id?: number;
89
+ * }} args
90
+ */
91
+ export async function agentChat(apiFetch, args) {
92
+ const data = await apiFetch(agentChatPath(args.agent_ulid), {
93
+ method: 'POST',
94
+ body: JSON.stringify(buildAgentChatBody(args)),
95
+ });
96
+ return formatAgentChatResponse(data);
97
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "innov-mcp-tasks",
3
- "version": "1.3.0",
4
- "description": "MCP stdio — tarefas e anotações Innov (INNOV_API_BASE_URL + token Sanctum)",
3
+ "version": "1.5.0",
4
+ "description": "MCP stdio — tarefas, anotações e agentes de IA Innov (INNOV_API_BASE_URL + token Sanctum)",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
7
7
  "bin": {
@@ -9,13 +9,14 @@
9
9
  },
10
10
  "files": [
11
11
  "index.mjs",
12
+ "lib/**",
12
13
  "README.md",
13
14
  ".env.example",
14
15
  "scripts/postinstall-notice.mjs"
15
16
  ],
16
17
  "scripts": {
17
18
  "start": "node index.mjs",
18
- "test": "node --check index.mjs",
19
+ "test": "node --check index.mjs && node --test test/*.test.mjs",
19
20
  "prepack": "node --check index.mjs",
20
21
  "postinstall": "node ./scripts/postinstall-notice.mjs"
21
22
  },
@@ -28,6 +29,8 @@
28
29
  "tasks",
29
30
  "notes",
30
31
  "annotations",
32
+ "ai-agents",
33
+ "agents",
31
34
  "innov"
32
35
  ],
33
36
  "author": "",