senq-mcp 1.3.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.
package/dist/client.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare const BASE_URL: string;
1
2
  export declare function apiGet<T>(path: string, params?: Record<string, string>): Promise<T>;
2
3
  export declare function apiPost<T>(path: string, body: unknown): Promise<T>;
3
4
  export declare function apiPatch<T>(path: string, body: unknown): Promise<T>;
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- const BASE_URL = (process.env.SENQ_BASE_URL ?? "https://senq.serenity.agency").replace(/\/$/, "");
1
+ export const BASE_URL = (process.env.SENQ_BASE_URL ?? "https://senq.serenity.agency").replace(/\/$/, "");
2
2
  function getApiKey() {
3
3
  const key = process.env.SENQ_API_KEY ?? "";
4
4
  if (!key) {
package/dist/index.js CHANGED
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
- import { apiGet, apiPost, apiPatch } from "./client.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
+ import { apiGet, apiPost, apiPatch, BASE_URL } from "./client.js";
6
6
  import { storePending, consumePending } from "./pending-changes.js";
7
7
  if (process.argv[2] === "setup") {
8
8
  const { runSetup } = await import("./setup.js");
9
9
  await runSetup();
10
10
  process.exit(0);
11
11
  }
12
- const server = new Server({ name: "senq-mcp", version: "1.3.0" }, { capabilities: { tools: {} } });
12
+ const server = new Server({ name: "senq-mcp", version: "1.4.0" }, { capabilities: { tools: {}, prompts: {} } });
13
13
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
14
14
  tools: [
15
15
  {
@@ -106,6 +106,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
106
106
  },
107
107
  },
108
108
  },
109
+ {
110
+ name: "find_employee",
111
+ description: "Найти сотрудника по имени или email. Возвращает ID, который нужен для list_team_tasks и get_team_report.",
112
+ inputSchema: {
113
+ type: "object",
114
+ required: ["name"],
115
+ properties: {
116
+ name: { type: "string", description: "Имя, фамилия или email сотрудника" },
117
+ },
118
+ },
119
+ },
109
120
  {
110
121
  name: "list_team_tasks",
111
122
  description: "Задачи сотрудника — только для администраторов. Возвращает все задачи, где сотрудник является исполнителем. Можно фильтровать по проекту, области, статусу и диапазону дат.",
@@ -141,6 +152,15 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
141
152
  },
142
153
  ],
143
154
  }));
155
+ function withUrl(task) {
156
+ if (typeof task.id === "string") {
157
+ return { ...task, url: `${BASE_URL}/#/tasks/${task.id}` };
158
+ }
159
+ return task;
160
+ }
161
+ function withUrls(tasks) {
162
+ return tasks.map((t) => withUrl(t));
163
+ }
144
164
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
145
165
  const { name, arguments: args } = req.params;
146
166
  try {
@@ -157,19 +177,36 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
157
177
  if (args?.limit)
158
178
  params.limit = String(args.limit);
159
179
  const data = await apiGet("/api/tasks", params);
160
- const tasks = data.tasks ?? (Array.isArray(data) ? data : []);
180
+ const tasks = withUrls(data.tasks ?? (Array.isArray(data) ? data : []));
161
181
  return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
162
182
  }
163
183
  if (name === "get_task") {
164
184
  const id = String(args?.id ?? "");
165
185
  const data = await apiGet(`/api/tasks/${id}`);
166
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
186
+ return { content: [{ type: "text", text: JSON.stringify(withUrl(data), null, 2) }] };
167
187
  }
168
188
  if (name === "search_tasks") {
169
189
  const query = String(args?.query ?? "");
170
190
  const limit = args?.limit ? String(args.limit) : "10";
171
191
  const data = await apiGet("/api/tasks/search", { q: query, limit });
172
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
192
+ const tasks = withUrls(data.tasks ?? (Array.isArray(data) ? data : []));
193
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
194
+ }
195
+ if (name === "find_employee") {
196
+ const query = String(args?.name ?? "").toLowerCase().trim();
197
+ if (!query)
198
+ return { content: [{ type: "text", text: "Ошибка: name обязателен" }] };
199
+ const data = await apiGet("/api/registry");
200
+ const employees = data.employees ?? [];
201
+ const found = employees.filter((e) => {
202
+ const name = (e.displayName ?? "").toLowerCase();
203
+ const email = (e.email ?? "").toLowerCase();
204
+ return name.includes(query) || email.includes(query);
205
+ });
206
+ if (!found.length)
207
+ return { content: [{ type: "text", text: `Сотрудник «${args?.name}» не найден` }] };
208
+ const result = found.map((e) => ({ id: e.id, displayName: e.displayName, email: e.email }));
209
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
173
210
  }
174
211
  if (name === "propose_task_change") {
175
212
  const action = String(args?.action ?? "create");
@@ -290,7 +327,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
290
327
  if (args?.offset)
291
328
  params.offset = String(args.offset);
292
329
  const data = await apiGet("/api/tasks/team", params);
293
- const tasks = data.tasks ?? [];
330
+ const tasks = withUrls(data.tasks ?? []);
294
331
  const total = data.total ?? tasks.length;
295
332
  return {
296
333
  content: [{
@@ -353,5 +390,69 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
353
390
  return { content: [{ type: "text", text: `Ошибка: ${msg}` }], isError: true };
354
391
  }
355
392
  });
393
+ const PROMPTS = [
394
+ {
395
+ name: "задачи-сегодня",
396
+ description: "Что у меня запланировано на сегодня",
397
+ arguments: [],
398
+ },
399
+ {
400
+ name: "что-просрочено",
401
+ description: "Мои просроченные задачи и дедлайны",
402
+ arguments: [],
403
+ },
404
+ {
405
+ name: "создать-задачу",
406
+ description: "Создать новую задачу",
407
+ arguments: [{ name: "описание", description: "Что нужно сделать", required: true }],
408
+ },
409
+ {
410
+ name: "отчёт-сотрудника",
411
+ description: "Отчёт по задачам сотрудника за период (для руководителей)",
412
+ arguments: [
413
+ { name: "сотрудник", description: "Имя или email сотрудника", required: true },
414
+ { name: "период", description: "Например: июнь 2026 или 2026-06-01 / 2026-06-30", required: false },
415
+ ],
416
+ },
417
+ {
418
+ name: "найти-задачу",
419
+ description: "Найти задачу по теме или ключевым словам",
420
+ arguments: [{ name: "тема", description: "Что искать", required: true }],
421
+ },
422
+ ];
423
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
424
+ prompts: PROMPTS.map(({ name, description, arguments: args }) => ({
425
+ name,
426
+ description,
427
+ arguments: args,
428
+ })),
429
+ }));
430
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
431
+ const { name, arguments: promptArgs } = req.params;
432
+ const messages = [];
433
+ if (name === "задачи-сегодня") {
434
+ messages.push({ role: "user", content: { type: "text", text: "Покажи мои задачи на сегодня" } });
435
+ }
436
+ else if (name === "что-просрочено") {
437
+ messages.push({ role: "user", content: { type: "text", text: "Что у меня просрочено? Покажи задачи с прошедшим дедлайном" } });
438
+ }
439
+ else if (name === "создать-задачу") {
440
+ const desc = promptArgs?.["описание"] ?? "...";
441
+ messages.push({ role: "user", content: { type: "text", text: `Создай задачу: ${desc}` } });
442
+ }
443
+ else if (name === "отчёт-сотрудника") {
444
+ const emp = promptArgs?.["сотрудник"] ?? "...";
445
+ const period = promptArgs?.["период"] ? ` за ${promptArgs["период"]}` : "";
446
+ messages.push({ role: "user", content: { type: "text", text: `Сначала найди сотрудника «${emp}» через find_employee, затем сделай отчёт по его задачам${period}: что просрочено, что выполнено вовремя, что в работе` } });
447
+ }
448
+ else if (name === "найти-задачу") {
449
+ const topic = promptArgs?.["тема"] ?? "...";
450
+ messages.push({ role: "user", content: { type: "text", text: `Найди задачи по теме: ${topic}` } });
451
+ }
452
+ else {
453
+ messages.push({ role: "user", content: { type: "text", text: name } });
454
+ }
455
+ return { messages };
456
+ });
356
457
  const transport = new StdioServerTransport();
357
458
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "senq-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server for Senq — connect your AI to tasks and knowledge",
5
5
  "type": "module",
6
6
  "bin": {
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- const BASE_URL = (process.env.SENQ_BASE_URL ?? "https://senq.serenity.agency").replace(/\/$/, "");
1
+ export const BASE_URL = (process.env.SENQ_BASE_URL ?? "https://senq.serenity.agency").replace(/\/$/, "");
2
2
 
3
3
  function getApiKey(): string {
4
4
  const key = process.env.SENQ_API_KEY ?? "";
package/src/index.ts CHANGED
@@ -4,8 +4,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import {
5
5
  CallToolRequestSchema,
6
6
  ListToolsRequestSchema,
7
+ ListPromptsRequestSchema,
8
+ GetPromptRequestSchema,
7
9
  } from "@modelcontextprotocol/sdk/types.js";
8
- import { apiGet, apiPost, apiPatch } from "./client.js";
10
+ import { apiGet, apiPost, apiPatch, BASE_URL } from "./client.js";
9
11
  import { storePending, consumePending } from "./pending-changes.js";
10
12
 
11
13
  if (process.argv[2] === "setup") {
@@ -15,8 +17,8 @@ if (process.argv[2] === "setup") {
15
17
  }
16
18
 
17
19
  const server = new Server(
18
- { name: "senq-mcp", version: "1.3.0" },
19
- { capabilities: { tools: {} } },
20
+ { name: "senq-mcp", version: "1.4.0" },
21
+ { capabilities: { tools: {}, prompts: {} } },
20
22
  );
21
23
 
22
24
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
@@ -115,6 +117,17 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
115
117
  },
116
118
  },
117
119
  },
120
+ {
121
+ name: "find_employee",
122
+ description: "Найти сотрудника по имени или email. Возвращает ID, который нужен для list_team_tasks и get_team_report.",
123
+ inputSchema: {
124
+ type: "object",
125
+ required: ["name"],
126
+ properties: {
127
+ name: { type: "string", description: "Имя, фамилия или email сотрудника" },
128
+ },
129
+ },
130
+ },
118
131
  {
119
132
  name: "list_team_tasks",
120
133
  description: "Задачи сотрудника — только для администраторов. Возвращает все задачи, где сотрудник является исполнителем. Можно фильтровать по проекту, области, статусу и диапазону дат.",
@@ -151,6 +164,19 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
151
164
  ],
152
165
  }));
153
166
 
167
+ type TaskLike = Record<string, unknown>;
168
+
169
+ function withUrl(task: TaskLike): TaskLike {
170
+ if (typeof task.id === "string") {
171
+ return { ...task, url: `${BASE_URL}/#/tasks/${task.id}` };
172
+ }
173
+ return task;
174
+ }
175
+
176
+ function withUrls(tasks: unknown[]): TaskLike[] {
177
+ return tasks.map((t) => withUrl(t as TaskLike));
178
+ }
179
+
154
180
  server.setRequestHandler(CallToolRequestSchema, async (req) => {
155
181
  const { name, arguments: args } = req.params;
156
182
 
@@ -163,21 +189,37 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
163
189
  if (args?.status) params.status = String(args.status);
164
190
  if (args?.limit) params.limit = String(args.limit);
165
191
  const data = await apiGet<{ tasks?: unknown[] }>("/api/tasks", params);
166
- const tasks = data.tasks ?? (Array.isArray(data) ? data : []);
192
+ const tasks = withUrls(data.tasks ?? (Array.isArray(data) ? data : []));
167
193
  return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
168
194
  }
169
195
 
170
196
  if (name === "get_task") {
171
197
  const id = String(args?.id ?? "");
172
- const data = await apiGet<unknown>(`/api/tasks/${id}`);
173
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
198
+ const data = await apiGet<TaskLike>(`/api/tasks/${id}`);
199
+ return { content: [{ type: "text", text: JSON.stringify(withUrl(data), null, 2) }] };
174
200
  }
175
201
 
176
202
  if (name === "search_tasks") {
177
203
  const query = String(args?.query ?? "");
178
204
  const limit = args?.limit ? String(args.limit) : "10";
179
- const data = await apiGet<unknown>("/api/tasks/search", { q: query, limit });
180
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
205
+ const data = await apiGet<{ tasks?: unknown[] }>("/api/tasks/search", { q: query, limit });
206
+ const tasks = withUrls(data.tasks ?? (Array.isArray(data) ? data : []));
207
+ return { content: [{ type: "text", text: JSON.stringify(tasks, null, 2) }] };
208
+ }
209
+
210
+ if (name === "find_employee") {
211
+ const query = String(args?.name ?? "").toLowerCase().trim();
212
+ if (!query) return { content: [{ type: "text", text: "Ошибка: name обязателен" }] };
213
+ const data = await apiGet<{ employees?: Array<{ id: string; displayName?: string; email?: string; slackUserId?: string }> }>("/api/registry");
214
+ const employees = data.employees ?? [];
215
+ const found = employees.filter((e) => {
216
+ const name = (e.displayName ?? "").toLowerCase();
217
+ const email = (e.email ?? "").toLowerCase();
218
+ return name.includes(query) || email.includes(query);
219
+ });
220
+ if (!found.length) return { content: [{ type: "text", text: `Сотрудник «${args?.name}» не найден` }] };
221
+ const result = found.map((e) => ({ id: e.id, displayName: e.displayName, email: e.email }));
222
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
181
223
  }
182
224
 
183
225
  if (name === "propose_task_change") {
@@ -267,7 +309,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
267
309
  if (args?.limit) params.limit = String(args.limit);
268
310
  if (args?.offset) params.offset = String(args.offset);
269
311
  const data = await apiGet<{ tasks?: unknown[]; total?: number }>("/api/tasks/team", params);
270
- const tasks = data.tasks ?? [];
312
+ const tasks = withUrls(data.tasks ?? []);
271
313
  const total = data.total ?? tasks.length;
272
314
  return {
273
315
  content: [{
@@ -340,5 +382,69 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
340
382
  }
341
383
  });
342
384
 
385
+ const PROMPTS = [
386
+ {
387
+ name: "задачи-сегодня",
388
+ description: "Что у меня запланировано на сегодня",
389
+ arguments: [],
390
+ },
391
+ {
392
+ name: "что-просрочено",
393
+ description: "Мои просроченные задачи и дедлайны",
394
+ arguments: [],
395
+ },
396
+ {
397
+ name: "создать-задачу",
398
+ description: "Создать новую задачу",
399
+ arguments: [{ name: "описание", description: "Что нужно сделать", required: true }],
400
+ },
401
+ {
402
+ name: "отчёт-сотрудника",
403
+ description: "Отчёт по задачам сотрудника за период (для руководителей)",
404
+ arguments: [
405
+ { name: "сотрудник", description: "Имя или email сотрудника", required: true },
406
+ { name: "период", description: "Например: июнь 2026 или 2026-06-01 / 2026-06-30", required: false },
407
+ ],
408
+ },
409
+ {
410
+ name: "найти-задачу",
411
+ description: "Найти задачу по теме или ключевым словам",
412
+ arguments: [{ name: "тема", description: "Что искать", required: true }],
413
+ },
414
+ ] as const;
415
+
416
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
417
+ prompts: PROMPTS.map(({ name, description, arguments: args }) => ({
418
+ name,
419
+ description,
420
+ arguments: (args as unknown) as { name: string; description: string; required: boolean }[],
421
+ })),
422
+ }));
423
+
424
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
425
+ const { name, arguments: promptArgs } = req.params;
426
+ const messages: { role: "user"; content: { type: "text"; text: string } }[] = [];
427
+
428
+ if (name === "задачи-сегодня") {
429
+ messages.push({ role: "user", content: { type: "text", text: "Покажи мои задачи на сегодня" } });
430
+ } else if (name === "что-просрочено") {
431
+ messages.push({ role: "user", content: { type: "text", text: "Что у меня просрочено? Покажи задачи с прошедшим дедлайном" } });
432
+ } else if (name === "создать-задачу") {
433
+ const desc = promptArgs?.["описание"] ?? "...";
434
+ messages.push({ role: "user", content: { type: "text", text: `Создай задачу: ${desc}` } });
435
+ } else if (name === "отчёт-сотрудника") {
436
+ const emp = promptArgs?.["сотрудник"] ?? "...";
437
+ const period = promptArgs?.["период"] ? ` за ${promptArgs["период"]}` : "";
438
+ messages.push({ role: "user", content: { type: "text", text: `Сначала найди сотрудника «${emp}» через find_employee, затем сделай отчёт по его задачам${period}: что просрочено, что выполнено вовремя, что в работе` } });
439
+ } else if (name === "найти-задачу") {
440
+ const topic = promptArgs?.["тема"] ?? "...";
441
+ messages.push({ role: "user", content: { type: "text", text: `Найди задачи по теме: ${topic}` } });
442
+ } else {
443
+ messages.push({ role: "user", content: { type: "text", text: name } });
444
+ }
445
+
446
+ return { messages };
447
+ });
448
+
343
449
  const transport = new StdioServerTransport();
344
450
  await server.connect(transport);