senq-mcp 1.3.0 → 1.4.1

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.1" }, { 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, profileUrl: `${BASE_URL}/#/team/${e.id}` }));
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,12 +327,17 @@ 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 ?? []).map((t) => {
331
+ if (typeof t.assigneeId === "string") {
332
+ return { ...t, assigneeUrl: `${BASE_URL}/#/team/${t.assigneeId}` };
333
+ }
334
+ return t;
335
+ });
294
336
  const total = data.total ?? tasks.length;
295
337
  return {
296
338
  content: [{
297
339
  type: "text",
298
- text: `Найдено задач: ${total}\n\n${JSON.stringify(tasks, null, 2)}`,
340
+ text: `Найдено задач: ${total}\n\nПри отображении форматируй названия задач как markdown-ссылки [название](url), имена исполнителей — как [имя](assigneeUrl).\n\n${JSON.stringify(tasks, null, 2)}`,
299
341
  }],
300
342
  };
301
343
  }
@@ -326,7 +368,18 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
326
368
  const completedLate = filtered.filter((t) => t.status === "completed" && t.dueDate && t.completedAt && t.completedAt.slice(0, 10) > t.dueDate);
327
369
  const overdue = filtered.filter((t) => t.status === "open" && t.dueDate && t.dueDate < today);
328
370
  const inProgress = filtered.filter((t) => t.status === "open" && t.dueDate && t.dueDate >= today);
329
- const fmt = (list) => list.map((t) => ` • ${t.title} (срок: ${t.dueDate ?? "—"}${t.completedAt ? `, выполнена: ${t.completedAt.slice(0, 10)}` : ""}${t.projectName ? `, проект: ${t.projectName}` : ""})`).join("\n");
371
+ const fmt = (list) => list.map((t) => {
372
+ const taskUrl = `${BASE_URL}/#/tasks/${t.id}`;
373
+ const assigneePart = t.assigneeName
374
+ ? `, [${t.assigneeName}](${BASE_URL}/#/team/${assigneeId})`
375
+ : "";
376
+ const parts = [`срок: ${t.dueDate ?? "—"}`];
377
+ if (t.completedAt)
378
+ parts.push(`выполнена: ${t.completedAt.slice(0, 10)}`);
379
+ if (t.projectName)
380
+ parts.push(`проект: ${t.projectName}`);
381
+ return ` • [${t.title}](${taskUrl}) (${parts.join(", ")}${assigneePart})`;
382
+ }).join("\n");
330
383
  const period = dateFrom || dateTo ? ` (период: ${dateFrom ?? "..."} — ${dateTo ?? "..."})` : "";
331
384
  const lines = [
332
385
  `Отчёт по сотруднику${period}`,
@@ -353,5 +406,69 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
353
406
  return { content: [{ type: "text", text: `Ошибка: ${msg}` }], isError: true };
354
407
  }
355
408
  });
409
+ const PROMPTS = [
410
+ {
411
+ name: "задачи-сегодня",
412
+ description: "Что у меня запланировано на сегодня",
413
+ arguments: [],
414
+ },
415
+ {
416
+ name: "что-просрочено",
417
+ description: "Мои просроченные задачи и дедлайны",
418
+ arguments: [],
419
+ },
420
+ {
421
+ name: "создать-задачу",
422
+ description: "Создать новую задачу",
423
+ arguments: [{ name: "описание", description: "Что нужно сделать", required: true }],
424
+ },
425
+ {
426
+ name: "отчёт-сотрудника",
427
+ description: "Отчёт по задачам сотрудника за период (для руководителей)",
428
+ arguments: [
429
+ { name: "сотрудник", description: "Имя или email сотрудника", required: true },
430
+ { name: "период", description: "Например: июнь 2026 или 2026-06-01 / 2026-06-30", required: false },
431
+ ],
432
+ },
433
+ {
434
+ name: "найти-задачу",
435
+ description: "Найти задачу по теме или ключевым словам",
436
+ arguments: [{ name: "тема", description: "Что искать", required: true }],
437
+ },
438
+ ];
439
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
440
+ prompts: PROMPTS.map(({ name, description, arguments: args }) => ({
441
+ name,
442
+ description,
443
+ arguments: args,
444
+ })),
445
+ }));
446
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
447
+ const { name, arguments: promptArgs } = req.params;
448
+ const messages = [];
449
+ if (name === "задачи-сегодня") {
450
+ messages.push({ role: "user", content: { type: "text", text: "Покажи мои задачи на сегодня" } });
451
+ }
452
+ else if (name === "что-просрочено") {
453
+ messages.push({ role: "user", content: { type: "text", text: "Что у меня просрочено? Покажи задачи с прошедшим дедлайном" } });
454
+ }
455
+ else if (name === "создать-задачу") {
456
+ const desc = promptArgs?.["описание"] ?? "...";
457
+ messages.push({ role: "user", content: { type: "text", text: `Создай задачу: ${desc}` } });
458
+ }
459
+ else if (name === "отчёт-сотрудника") {
460
+ const emp = promptArgs?.["сотрудник"] ?? "...";
461
+ const period = promptArgs?.["период"] ? ` за ${promptArgs["период"]}` : "";
462
+ messages.push({ role: "user", content: { type: "text", text: `Сначала найди сотрудника «${emp}» через find_employee, затем сделай отчёт по его задачам${period}: что просрочено, что выполнено вовремя, что в работе` } });
463
+ }
464
+ else if (name === "найти-задачу") {
465
+ const topic = promptArgs?.["тема"] ?? "...";
466
+ messages.push({ role: "user", content: { type: "text", text: `Найди задачи по теме: ${topic}` } });
467
+ }
468
+ else {
469
+ messages.push({ role: "user", content: { type: "text", text: name } });
470
+ }
471
+ return { messages };
472
+ });
356
473
  const transport = new StdioServerTransport();
357
474
  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.1",
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.1" },
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, profileUrl: `${BASE_URL}/#/team/${e.id}` }));
222
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
181
223
  }
182
224
 
183
225
  if (name === "propose_task_change") {
@@ -267,12 +309,17 @@ 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 ?? []).map((t) => {
313
+ if (typeof t.assigneeId === "string") {
314
+ return { ...t, assigneeUrl: `${BASE_URL}/#/team/${t.assigneeId}` };
315
+ }
316
+ return t;
317
+ });
271
318
  const total = data.total ?? tasks.length;
272
319
  return {
273
320
  content: [{
274
321
  type: "text",
275
- text: `Найдено задач: ${total}\n\n${JSON.stringify(tasks, null, 2)}`,
322
+ text: `Найдено задач: ${total}\n\nПри отображении форматируй названия задач как markdown-ссылки [название](url), имена исполнителей — как [имя](assigneeUrl).\n\n${JSON.stringify(tasks, null, 2)}`,
276
323
  }],
277
324
  };
278
325
  }
@@ -310,7 +357,16 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
310
357
  const inProgress = filtered.filter((t) => t.status === "open" && t.dueDate && t.dueDate >= today);
311
358
 
312
359
  const fmt = (list: typeof filtered) =>
313
- list.map((t) => ` • ${t.title} (срок: ${t.dueDate ?? "—"}${t.completedAt ? `, выполнена: ${t.completedAt.slice(0, 10)}` : ""}${t.projectName ? `, проект: ${t.projectName}` : ""})`).join("\n");
360
+ list.map((t) => {
361
+ const taskUrl = `${BASE_URL}/#/tasks/${t.id}`;
362
+ const assigneePart = t.assigneeName
363
+ ? `, [${t.assigneeName}](${BASE_URL}/#/team/${assigneeId})`
364
+ : "";
365
+ const parts = [`срок: ${t.dueDate ?? "—"}`];
366
+ if (t.completedAt) parts.push(`выполнена: ${t.completedAt.slice(0, 10)}`);
367
+ if (t.projectName) parts.push(`проект: ${t.projectName}`);
368
+ return ` • [${t.title}](${taskUrl}) (${parts.join(", ")}${assigneePart})`;
369
+ }).join("\n");
314
370
 
315
371
  const period = dateFrom || dateTo ? ` (период: ${dateFrom ?? "..."} — ${dateTo ?? "..."})` : "";
316
372
  const lines = [
@@ -340,5 +396,69 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
340
396
  }
341
397
  });
342
398
 
399
+ const PROMPTS = [
400
+ {
401
+ name: "задачи-сегодня",
402
+ description: "Что у меня запланировано на сегодня",
403
+ arguments: [],
404
+ },
405
+ {
406
+ name: "что-просрочено",
407
+ description: "Мои просроченные задачи и дедлайны",
408
+ arguments: [],
409
+ },
410
+ {
411
+ name: "создать-задачу",
412
+ description: "Создать новую задачу",
413
+ arguments: [{ name: "описание", description: "Что нужно сделать", required: true }],
414
+ },
415
+ {
416
+ name: "отчёт-сотрудника",
417
+ description: "Отчёт по задачам сотрудника за период (для руководителей)",
418
+ arguments: [
419
+ { name: "сотрудник", description: "Имя или email сотрудника", required: true },
420
+ { name: "период", description: "Например: июнь 2026 или 2026-06-01 / 2026-06-30", required: false },
421
+ ],
422
+ },
423
+ {
424
+ name: "найти-задачу",
425
+ description: "Найти задачу по теме или ключевым словам",
426
+ arguments: [{ name: "тема", description: "Что искать", required: true }],
427
+ },
428
+ ] as const;
429
+
430
+ server.setRequestHandler(ListPromptsRequestSchema, async () => ({
431
+ prompts: PROMPTS.map(({ name, description, arguments: args }) => ({
432
+ name,
433
+ description,
434
+ arguments: (args as unknown) as { name: string; description: string; required: boolean }[],
435
+ })),
436
+ }));
437
+
438
+ server.setRequestHandler(GetPromptRequestSchema, async (req) => {
439
+ const { name, arguments: promptArgs } = req.params;
440
+ const messages: { role: "user"; content: { type: "text"; text: string } }[] = [];
441
+
442
+ if (name === "задачи-сегодня") {
443
+ messages.push({ role: "user", content: { type: "text", text: "Покажи мои задачи на сегодня" } });
444
+ } else if (name === "что-просрочено") {
445
+ messages.push({ role: "user", content: { type: "text", text: "Что у меня просрочено? Покажи задачи с прошедшим дедлайном" } });
446
+ } else if (name === "создать-задачу") {
447
+ const desc = promptArgs?.["описание"] ?? "...";
448
+ messages.push({ role: "user", content: { type: "text", text: `Создай задачу: ${desc}` } });
449
+ } else if (name === "отчёт-сотрудника") {
450
+ const emp = promptArgs?.["сотрудник"] ?? "...";
451
+ const period = promptArgs?.["период"] ? ` за ${promptArgs["период"]}` : "";
452
+ messages.push({ role: "user", content: { type: "text", text: `Сначала найди сотрудника «${emp}» через find_employee, затем сделай отчёт по его задачам${period}: что просрочено, что выполнено вовремя, что в работе` } });
453
+ } else if (name === "найти-задачу") {
454
+ const topic = promptArgs?.["тема"] ?? "...";
455
+ messages.push({ role: "user", content: { type: "text", text: `Найди задачи по теме: ${topic}` } });
456
+ } else {
457
+ messages.push({ role: "user", content: { type: "text", text: name } });
458
+ }
459
+
460
+ return { messages };
461
+ });
462
+
343
463
  const transport = new StdioServerTransport();
344
464
  await server.connect(transport);