biaws-mcp 0.3.0 → 0.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
@@ -66,13 +66,13 @@ O pacote público expõe o executável `biaws-mcp`. Clientes configurados pelo C
66
66
  usam uma versão fixada por meio do cache local do npm:
67
67
 
68
68
  ```bash
69
- npx --yes biaws-mcp@0.3.0
69
+ npx --yes biaws-mcp@0.5.0
70
70
  ```
71
71
 
72
72
  Também é possível instalá-lo explicitamente:
73
73
 
74
74
  ```bash
75
- npm install --global biaws-mcp@0.3.0
75
+ npm install --global biaws-mcp@0.5.0
76
76
  biaws-mcp
77
77
  ```
78
78
 
@@ -91,7 +91,7 @@ npm start
91
91
  Em um cliente MCP, configure o comando:
92
92
 
93
93
  ```bash
94
- npx --yes biaws-mcp@0.3.0
94
+ npx --yes biaws-mcp@0.5.0
95
95
  ```
96
96
 
97
97
  O fluxo recomendado é gerar a configuração completa com:
@@ -105,6 +105,36 @@ node /caminho/para/biaws/biaws-cli/src/index.js \
105
105
  O processo de publicação e rollback está em
106
106
  [`docs/releasing.md`](docs/releasing.md).
107
107
 
108
+ ## Diagnóstico do transporte
109
+
110
+ A partir da versão 0.5.0, o processo escreve um evento JSON por linha em
111
+ `stderr`. O `stdout` permanece exclusivo para frames JSON-RPC do MCP. Cada
112
+ execução recebe um `executionId` e cada chamada de tool recebe um `requestId`,
113
+ permitindo correlacionar ciclo de vida, duração, tentativas HTTP, cancelamento e
114
+ falhas de processo sem registrar argumentos, respostas ou credenciais.
115
+
116
+ Os eventos principais são:
117
+
118
+ - `mcp_server_started`, `mcp_shutdown_requested` e `mcp_server_stopped`;
119
+ - `mcp_tool_call_started`, `mcp_tool_call_completed`,
120
+ `mcp_tool_call_failed` e `mcp_tool_call_cancelled`;
121
+ - `mcp_http_attempt_completed`, `mcp_http_attempt_failed` e
122
+ `mcp_http_retry_scheduled`;
123
+ - `mcp_input_error`, `mcp_stdout_error`, `mcp_uncaught_exception` e
124
+ `mcp_unhandled_rejection`.
125
+
126
+ `BIAWS_MCP_LOG_LEVEL` aceita `debug`, `info`, `warn` ou `error` e usa `info` por
127
+ padrão. O nível `debug` também registra o início de cada tentativa HTTP. O MCP
128
+ seleciona somente origem, método, status, duração e dados de correlação; URLs
129
+ completas, query strings, headers, argumentos, payloads e valores de ambiente
130
+ não são emitidos.
131
+
132
+ Para investigar `Transport closed`, preserve o `stderr` do processo configurado
133
+ pelo cliente MCP e procure, usando o mesmo `executionId`, por
134
+ `mcp_stdout_error`, eventos fatais, sinais ou encerramentos sem
135
+ `mcp_server_stopped`. Um `mcp_tool_call_failed` com resposta MCP válida indica
136
+ falha funcional ou da API, não fechamento do transporte.
137
+
108
138
  ## Ferramentas
109
139
 
110
140
  ### Catálogo e contexto de aplicações
@@ -213,6 +243,10 @@ conhecimento.
213
243
  - `issues_search`: busca issues com os mesmos filtros principais da
214
244
  `biaws-api`, inclusive por workspace, aplicação e componente afetado.
215
245
  - `issues_get`: obtém uma issue com comentários e metadados dos anexos.
246
+ - `issues_add_comment`: adiciona um comentário em Markdown a uma issue
247
+ existente; a autoria é atribuída pela identidade autenticada na API.
248
+ - `issues_update_comment`: atualiza o conteúdo e, opcionalmente, a data de um
249
+ comentário existente, preservando sua autoria original.
216
250
  - `issues_get_classification_catalog`: obtém a árvore taxonômica e os grupos de tags válidos; opcionalmente inclui listas achatadas de IDs e caminhos.
217
251
  - `issues_create_taxonomy_item`: inclui um item na raiz ou sob outro item,
218
252
  opcionalmente configurando seu escopo por aplicações.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biaws-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Servidor MCP de domínio para o Bondia Workspaces",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -130,6 +130,34 @@ export async function getIssueDetails(args = {}) {
130
130
  return fetchJson(`/api/issues/${encodeURIComponent(args.issueId)}`);
131
131
  }
132
132
 
133
+ export async function addIssueComment(args = {}) {
134
+ const issueId = String(args.issueId || "").trim();
135
+ const text = String(args.text || "").trim();
136
+ if (!issueId) throw new Error("issueId is required");
137
+ if (!text) throw new Error("text is required");
138
+
139
+ return sendJson(
140
+ `/api/issues/${encodeURIComponent(issueId)}/comments`,
141
+ cleanParams({ text, date: args.date }),
142
+ {},
143
+ "POST",
144
+ );
145
+ }
146
+
147
+ export async function updateIssueComment(args = {}) {
148
+ const issueId = String(args.issueId || "").trim();
149
+ const commentId = String(args.commentId || "").trim();
150
+ const text = String(args.text || "").trim();
151
+ if (!issueId) throw new Error("issueId is required");
152
+ if (!commentId) throw new Error("commentId is required");
153
+ if (!text) throw new Error("text is required");
154
+
155
+ return sendJson(
156
+ `/api/issues/${encodeURIComponent(issueId)}/comments/${encodeURIComponent(commentId)}`,
157
+ cleanParams({ text, date: args.date }),
158
+ );
159
+ }
160
+
133
161
  export async function getIssueClassificationCatalog(args = {}) {
134
162
  const payload = await fetchJson(
135
163
  "/api/issues/taxonomy",
package/src/httpClient.js CHANGED
@@ -1,4 +1,7 @@
1
- import { currentRequestSignal } from "./requestContext.js";
1
+ import {
2
+ currentRequestContext,
3
+ currentRequestSignal,
4
+ } from "./requestContext.js";
2
5
  import { setTimeout as delay } from "node:timers/promises";
3
6
 
4
7
  const DEFAULT_TIMEOUT_MS = 15_000;
@@ -145,9 +148,10 @@ function attachmentSizeError(maxBytes, actualBytes) {
145
148
  return error;
146
149
  }
147
150
 
148
- function createRequestContext(path, params, maxRetries) {
151
+ function createRequestContext(path, params, maxRetries, method) {
149
152
  const url = buildUrl(path, params);
150
153
  const externalSignal = currentRequestSignal();
154
+ const mcpContext = currentRequestContext();
151
155
  const timeoutSignal = AbortSignal.timeout(readTimeoutMs());
152
156
  return {
153
157
  url,
@@ -156,6 +160,10 @@ function createRequestContext(path, params, maxRetries) {
156
160
  ? AbortSignal.any([externalSignal, timeoutSignal])
157
161
  : timeoutSignal,
158
162
  maxRetries,
163
+ method: method || "GET",
164
+ logger: mcpContext?.logger,
165
+ requestId: mcpContext?.requestId,
166
+ tool: mcpContext?.tool,
159
167
  };
160
168
  }
161
169
 
@@ -174,8 +182,17 @@ function shouldRetryRequest(error, attempt, context) {
174
182
  }
175
183
 
176
184
  async function waitBeforeRetry(attempt, context) {
185
+ const backoffMs = 100 * 2 ** attempt;
186
+ context.logger?.warn("mcp_http_retry_scheduled", {
187
+ requestId: context.requestId,
188
+ tool: context.tool,
189
+ method: context.method,
190
+ origin: context.url.origin,
191
+ nextAttempt: attempt + 2,
192
+ backoffMs,
193
+ });
177
194
  try {
178
- await delay(100 * 2 ** attempt, undefined, { signal: context.signal });
195
+ await delay(backoffMs, undefined, { signal: context.signal });
179
196
  } catch (error) {
180
197
  throw transportError(error, context.url, context.externalSignal);
181
198
  }
@@ -183,11 +200,40 @@ async function waitBeforeRetry(attempt, context) {
183
200
 
184
201
  async function requestWithRetries(context, operation) {
185
202
  for (let attempt = 0; ; attempt += 1) {
203
+ const startedAt = Date.now();
204
+ context.logger?.debug("mcp_http_attempt_started", {
205
+ requestId: context.requestId,
206
+ tool: context.tool,
207
+ method: context.method,
208
+ origin: context.url.origin,
209
+ attempt: attempt + 1,
210
+ });
186
211
  try {
187
- return await operation(context);
212
+ const result = await operation(context);
213
+ context.logger?.info("mcp_http_attempt_completed", {
214
+ requestId: context.requestId,
215
+ tool: context.tool,
216
+ method: context.method,
217
+ origin: context.url.origin,
218
+ attempt: attempt + 1,
219
+ durationMs: Math.max(0, Date.now() - startedAt),
220
+ statusCode: context.responseStatus,
221
+ });
222
+ return result;
188
223
  } catch (cause) {
189
224
  const error = normalizeRequestError(cause, context);
190
- if (!shouldRetryRequest(error, attempt, context)) throw error;
225
+ const retry = shouldRetryRequest(error, attempt, context);
226
+ context.logger?.[retry ? "warn" : "error"]("mcp_http_attempt_failed", {
227
+ requestId: context.requestId,
228
+ tool: context.tool,
229
+ method: context.method,
230
+ origin: context.url.origin,
231
+ attempt: attempt + 1,
232
+ durationMs: Math.max(0, Date.now() - startedAt),
233
+ willRetry: retry,
234
+ error,
235
+ });
236
+ if (!retry) throw error;
191
237
  await waitBeforeRetry(attempt, context);
192
238
  }
193
239
  }
@@ -217,14 +263,16 @@ async function requestJson(
217
263
  { method, body, params = {}, headers = {} } = {},
218
264
  ) {
219
265
  const maxRetries = !method || method === "GET" ? readMaxRetries() : 0;
220
- const context = createRequestContext(path, params, maxRetries);
221
- return requestWithRetries(context, async ({ url, signal }) => {
266
+ const context = createRequestContext(path, params, maxRetries, method);
267
+ return requestWithRetries(context, async (requestContext) => {
268
+ const { url, signal } = requestContext;
222
269
  const response = await fetch(url, {
223
270
  ...(method ? { method } : {}),
224
271
  headers: { ...authenticationHeaders(), ...headers },
225
272
  body,
226
273
  signal,
227
274
  });
275
+ requestContext.responseStatus = response.status;
228
276
  const payload = await readPayload(response);
229
277
  if (!response.ok) throw responseError(response, payload);
230
278
  return payload;
@@ -235,12 +283,14 @@ async function requestBinary(
235
283
  path,
236
284
  { params = {}, headers = {}, maxBytes } = {},
237
285
  ) {
238
- const context = createRequestContext(path, params, readMaxRetries());
239
- return requestWithRetries(context, async ({ url, signal }) => {
286
+ const context = createRequestContext(path, params, readMaxRetries(), "GET");
287
+ return requestWithRetries(context, async (requestContext) => {
288
+ const { url, signal } = requestContext;
240
289
  const response = await fetch(url, {
241
290
  headers: { ...authenticationHeaders(), ...headers },
242
291
  signal,
243
292
  });
293
+ requestContext.responseStatus = response.status;
244
294
  if (!response.ok) {
245
295
  throw responseError(response, await readPayload(response));
246
296
  }
package/src/index.js CHANGED
@@ -1,9 +1,12 @@
1
+ import { randomUUID } from "node:crypto";
1
2
  import path from "node:path";
2
3
  import { fileURLToPath } from "node:url";
3
4
 
5
+ import { createLogger } from "./logger.js";
4
6
  import { createMcpMessageHandler, protocolError } from "./mcpServer.js";
5
7
  import { dispatchTool, listTools } from "./tools.js";
6
8
  import { loadEnv } from "./loadEnv.js";
9
+ import { SERVER_NAME, SERVER_VERSION } from "./version.js";
7
10
 
8
11
  const TOOL_DIR = path.resolve(
9
12
  path.dirname(fileURLToPath(import.meta.url)),
@@ -12,9 +15,27 @@ const TOOL_DIR = path.resolve(
12
15
 
13
16
  // O workspace pertence à configuração do projeto/cliente MCP. O arquivo da
14
17
  // instância fornece URL e credencial, mas não pode mudar esse escopo.
15
- loadEnv(TOOL_DIR, { preserve: ["BIAWS_WORKSPACE_ID"] });
18
+ const environment = loadEnv(TOOL_DIR, {
19
+ preserve: ["BIAWS_WORKSPACE_ID"],
20
+ });
21
+ const executionId = randomUUID();
22
+ const startedAt = Date.now();
23
+ const logger = createLogger({
24
+ service: SERVER_NAME,
25
+ version: SERVER_VERSION,
26
+ executionId,
27
+ });
28
+
29
+ // A broken stderr cannot be reported anywhere safely. Keep it from becoming a
30
+ // second fatal error while preserving stdout exclusively for MCP frames.
31
+ process.stderr.on("error", () => {});
16
32
 
17
33
  function writeMessage(message) {
34
+ if (process.stdout.destroyed || process.stdout.writableEnded) {
35
+ const error = new Error("MCP stdout is not writable");
36
+ error.code = "MCP_STDOUT_CLOSED";
37
+ throw error;
38
+ }
18
39
  process.stdout.write(`${JSON.stringify(message)}\n`);
19
40
  }
20
41
 
@@ -22,8 +43,20 @@ const server = createMcpMessageHandler({
22
43
  dispatchTool,
23
44
  listTools,
24
45
  writeMessage,
46
+ logger,
25
47
  });
26
48
  let buffer = "";
49
+ let shuttingDown = false;
50
+
51
+ logger.info("mcp_server_started", {
52
+ nodeVersion: process.versions.node,
53
+ pid: process.pid,
54
+ transport: "stdio",
55
+ environmentFilesLoaded: environment.loaded.length,
56
+ workspaceConfigured: Boolean(
57
+ String(process.env.BIAWS_WORKSPACE_ID || "").trim(),
58
+ ),
59
+ });
27
60
 
28
61
  process.stdin.setEncoding("utf8");
29
62
  process.stdin.on("data", (chunk) => {
@@ -35,23 +68,92 @@ process.stdin.on("data", (chunk) => {
35
68
  if (!line.trim()) continue;
36
69
  try {
37
70
  void server.accept(JSON.parse(line));
38
- } catch {
71
+ } catch (error) {
72
+ logger.warn("mcp_input_parse_failed", {
73
+ bytes: Buffer.byteLength(line),
74
+ error,
75
+ });
39
76
  writeMessage(protocolError(null, -32700, "Parse error"));
40
77
  }
41
78
  }
42
79
  });
43
80
 
44
81
  process.stdin.on("end", () => {
45
- void server.waitForIdle().finally(shutdown);
82
+ logger.info("mcp_input_closed", { bufferedBytes: Buffer.byteLength(buffer) });
83
+ void shutdown({ reason: "stdin_end" });
84
+ });
85
+
86
+ process.stdin.on("error", (error) => {
87
+ logger.error("mcp_input_error", { error });
88
+ void shutdown({ reason: "stdin_error", exitCode: 1, cancel: true });
89
+ });
90
+
91
+ process.stdout.on("error", (error) => {
92
+ logger.error("mcp_stdout_error", { error });
93
+ void shutdown({ reason: "stdout_error", exitCode: 1, cancel: true });
46
94
  });
47
95
 
48
- function shutdown() {
49
- process.exit(0);
96
+ async function shutdown({ reason, signal, exitCode = 0, cancel = false }) {
97
+ if (shuttingDown) return;
98
+ shuttingDown = true;
99
+ logger.info("mcp_shutdown_requested", {
100
+ reason,
101
+ signal,
102
+ exitCode,
103
+ cancelActiveRequests: cancel,
104
+ });
105
+ if (cancel) server.cancelAll();
106
+
107
+ const timeoutMs = 5_000;
108
+ let timedOut = false;
109
+ let timer;
110
+ await Promise.race([
111
+ server.waitForIdle(),
112
+ new Promise((resolve) => {
113
+ timer = setTimeout(() => {
114
+ timedOut = true;
115
+ resolve();
116
+ }, timeoutMs);
117
+ }),
118
+ ]);
119
+ clearTimeout(timer);
120
+ if (timedOut) {
121
+ logger.warn("mcp_shutdown_timed_out", { reason, timeoutMs });
122
+ }
123
+ logger.info("mcp_server_stopped", {
124
+ reason,
125
+ exitCode,
126
+ graceful: !timedOut,
127
+ uptimeMs: Math.max(0, Date.now() - startedAt),
128
+ });
129
+ process.exit(exitCode);
50
130
  }
51
131
 
52
132
  for (const signal of ["SIGINT", "SIGTERM"]) {
53
133
  process.on(signal, () => {
54
- server.cancelAll();
55
- void server.waitForIdle().finally(shutdown);
134
+ void shutdown({
135
+ reason: "signal",
136
+ signal,
137
+ exitCode: 0,
138
+ cancel: true,
139
+ });
56
140
  });
57
141
  }
142
+
143
+ process.on("uncaughtException", (error, origin) => {
144
+ logger.error("mcp_uncaught_exception", { origin, error });
145
+ void shutdown({
146
+ reason: "uncaught_exception",
147
+ exitCode: 1,
148
+ cancel: true,
149
+ });
150
+ });
151
+
152
+ process.on("unhandledRejection", (reason) => {
153
+ logger.error("mcp_unhandled_rejection", { error: reason });
154
+ void shutdown({
155
+ reason: "unhandled_rejection",
156
+ exitCode: 1,
157
+ cancel: true,
158
+ });
159
+ });
package/src/logger.js ADDED
@@ -0,0 +1,97 @@
1
+ const LEVELS = Object.freeze({ debug: 10, info: 20, warn: 30, error: 40 });
2
+ const SENSITIVE_KEY =
3
+ /authorization|cookie|password|passwd|token|secret|api.?key|connection.?string/iu;
4
+
5
+ function sanitizeText(value) {
6
+ return String(value)
7
+ .replace(/Bearer\s+[^\s,;]+/giu, "Bearer [REDACTED]")
8
+ .replace(/(https?:\/\/)[^/@\s]+@/giu, "$1[REDACTED]@")
9
+ .replace(
10
+ /([?&](?:token|key|secret|password)\s*=)[^&#\s]+/giu,
11
+ "$1[REDACTED]",
12
+ );
13
+ }
14
+
15
+ export function serializeError(error, depth = 0) {
16
+ if (!(error instanceof Error)) {
17
+ return { name: "Error", message: sanitizeText(error) };
18
+ }
19
+ const result = {
20
+ name: error.name,
21
+ message: sanitizeText(error.message),
22
+ ...(error.code === undefined ? {} : { code: String(error.code) }),
23
+ ...(Number.isInteger(error.statusCode)
24
+ ? { statusCode: error.statusCode }
25
+ : {}),
26
+ ...(typeof error.retryable === "boolean"
27
+ ? { retryable: error.retryable }
28
+ : {}),
29
+ ...(error.stack ? { stack: sanitizeText(error.stack) } : {}),
30
+ };
31
+ if (depth < 2 && error.cause !== undefined) {
32
+ result.cause = serializeError(error.cause, depth + 1);
33
+ }
34
+ return result;
35
+ }
36
+
37
+ function sanitize(value, key = "", seen = new WeakSet()) {
38
+ if (SENSITIVE_KEY.test(key)) return "[REDACTED]";
39
+ if (value instanceof Error) return serializeError(value);
40
+ if (typeof value === "string") return sanitizeText(value);
41
+ if (value === null || typeof value !== "object") return value;
42
+ if (seen.has(value)) return "[Circular]";
43
+ seen.add(value);
44
+ if (Array.isArray(value)) {
45
+ return value.map((entry) => sanitize(entry, "", seen));
46
+ }
47
+ return Object.fromEntries(
48
+ Object.entries(value).map(([entryKey, entry]) => [
49
+ entryKey,
50
+ sanitize(entry, entryKey, seen),
51
+ ]),
52
+ );
53
+ }
54
+
55
+ function configuredLevel(level) {
56
+ const normalized = String(level || "info").toLowerCase();
57
+ return LEVELS[normalized] === undefined ? "info" : normalized;
58
+ }
59
+
60
+ export function createLogger({
61
+ service,
62
+ version,
63
+ executionId,
64
+ level = process.env.BIAWS_MCP_LOG_LEVEL,
65
+ stream = process.stderr,
66
+ now = () => new Date().toISOString(),
67
+ } = {}) {
68
+ const minimumLevel = LEVELS[configuredLevel(level)];
69
+
70
+ function log(entryLevel, event, fields = {}) {
71
+ if (LEVELS[entryLevel] < minimumLevel) return false;
72
+ const record = sanitize({
73
+ timestamp: now(),
74
+ level: entryLevel,
75
+ service,
76
+ version,
77
+ executionId,
78
+ event,
79
+ ...fields,
80
+ });
81
+ try {
82
+ stream.write(`${JSON.stringify(record)}\n`);
83
+ return true;
84
+ } catch {
85
+ // stderr is the diagnostic fallback. Logging failures must never recurse
86
+ // or corrupt the MCP protocol on stdout.
87
+ return false;
88
+ }
89
+ }
90
+
91
+ return {
92
+ debug: (event, fields) => log("debug", event, fields),
93
+ info: (event, fields) => log("info", event, fields),
94
+ warn: (event, fields) => log("warn", event, fields),
95
+ error: (event, fields) => log("error", event, fields),
96
+ };
97
+ }
package/src/mcpServer.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { randomUUID } from "node:crypto";
2
+
1
3
  import { runWithRequestContext } from "./requestContext.js";
2
4
  import { SERVER_NAME, SERVER_VERSION } from "./version.js";
3
5
 
@@ -59,6 +61,9 @@ export function createMcpMessageHandler({
59
61
  dispatchTool,
60
62
  listTools,
61
63
  writeMessage,
64
+ logger,
65
+ createRequestId = randomUUID,
66
+ now = () => Date.now(),
62
67
  }) {
63
68
  const activeRequests = new Map();
64
69
  const inFlight = new Set();
@@ -66,13 +71,41 @@ export function createMcpMessageHandler({
66
71
  async function handleToolCall(id, params) {
67
72
  const controller = new AbortController();
68
73
  activeRequests.set(id, controller);
74
+ const requestId = createRequestId();
75
+ const tool = String(params.name || "");
76
+ const startedAt = now();
77
+ const logContext = {
78
+ requestId,
79
+ rpcRequestId: ["string", "number"].includes(typeof id) ? id : undefined,
80
+ tool,
81
+ };
82
+ logger?.info("mcp_tool_call_started", {
83
+ ...logContext,
84
+ inFlight: activeRequests.size,
85
+ });
69
86
  try {
70
87
  const result = await runWithRequestContext(
71
- { signal: controller.signal },
88
+ { signal: controller.signal, logger, requestId, tool },
72
89
  () => dispatchTool(params.name, params.arguments || {}),
73
90
  );
74
91
  writeMessage(success(id, toolResult(result)));
92
+ logger?.info("mcp_tool_call_completed", {
93
+ ...logContext,
94
+ durationMs: Math.max(0, now() - startedAt),
95
+ });
75
96
  } catch (error) {
97
+ const cancelled = error?.code === "REQUEST_CANCELLED";
98
+ const expectedFailure =
99
+ cancelled ||
100
+ (Number.isInteger(error?.statusCode) && error.statusCode < 500);
101
+ logger?.[expectedFailure ? "warn" : "error"](
102
+ cancelled ? "mcp_tool_call_cancelled" : "mcp_tool_call_failed",
103
+ {
104
+ ...logContext,
105
+ durationMs: Math.max(0, now() - startedAt),
106
+ error,
107
+ },
108
+ );
76
109
  writeMessage(success(id, toolErrorResult(error)));
77
110
  } finally {
78
111
  activeRequests.delete(id);
@@ -119,15 +152,31 @@ export function createMcpMessageHandler({
119
152
  }
120
153
 
121
154
  function accept(request) {
122
- const operation = handleMessage(request).catch(() => {
155
+ const operation = handleMessage(request).catch((error) => {
156
+ logger?.error("mcp_message_handling_failed", {
157
+ rpcRequestId: ["string", "number"].includes(typeof request?.id)
158
+ ? request.id
159
+ : undefined,
160
+ method: String(request?.method || ""),
161
+ error,
162
+ });
123
163
  if (request?.id !== undefined) {
124
- writeMessage(
125
- protocolError(request.id, -32603, "Internal JSON-RPC error"),
126
- );
164
+ try {
165
+ writeMessage(
166
+ protocolError(request.id, -32603, "Internal JSON-RPC error"),
167
+ );
168
+ } catch (writeError) {
169
+ logger?.error("mcp_error_response_write_failed", {
170
+ error: writeError,
171
+ });
172
+ }
127
173
  }
128
174
  });
129
175
  inFlight.add(operation);
130
- void operation.finally(() => inFlight.delete(operation));
176
+ void operation.then(
177
+ () => inFlight.delete(operation),
178
+ () => inFlight.delete(operation),
179
+ );
131
180
  return operation;
132
181
  }
133
182
 
@@ -9,3 +9,7 @@ export function runWithRequestContext(context, callback) {
9
9
  export function currentRequestSignal() {
10
10
  return requestContext.getStore()?.signal;
11
11
  }
12
+
13
+ export function currentRequestContext() {
14
+ return requestContext.getStore();
15
+ }
package/src/tools.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import {
2
+ addIssueComment,
2
3
  classifyIssue,
3
4
  createTaxonomyItem,
4
5
  createIssue,
@@ -10,6 +11,7 @@ import {
10
11
  summarizeIssuesForSupport,
11
12
  suggestTaxonomy,
12
13
  updateIssueState,
14
+ updateIssueComment,
13
15
  updateTaxonomyItem,
14
16
  } from "./domains/issues/service.js";
15
17
  import {
@@ -156,6 +158,58 @@ const tools = [
156
158
  },
157
159
  handler: getIssueDetails,
158
160
  },
161
+ {
162
+ name: "issues_add_comment",
163
+ description: "Adiciona um comentário em Markdown a uma issue existente.",
164
+ inputSchema: {
165
+ type: "object",
166
+ required: ["issueId", "text"],
167
+ additionalProperties: false,
168
+ properties: {
169
+ issueId: { type: "string", minLength: 1 },
170
+ text: {
171
+ type: "string",
172
+ minLength: 1,
173
+ description: "Conteúdo do comentário em Markdown.",
174
+ },
175
+ date: {
176
+ type: "string",
177
+ description:
178
+ "Data do comentário em formato aceito pela API; usa o momento atual quando omitida.",
179
+ },
180
+ },
181
+ },
182
+ handler: addIssueComment,
183
+ },
184
+ {
185
+ name: "issues_update_comment",
186
+ description:
187
+ "Atualiza o conteúdo e, opcionalmente, a data de um comentário de issue.",
188
+ inputSchema: {
189
+ type: "object",
190
+ required: ["issueId", "commentId", "text"],
191
+ additionalProperties: false,
192
+ properties: {
193
+ issueId: { type: "string", minLength: 1 },
194
+ commentId: {
195
+ type: "string",
196
+ minLength: 1,
197
+ description: "ID do comentário retornado por issues_get.",
198
+ },
199
+ text: {
200
+ type: "string",
201
+ minLength: 1,
202
+ description: "Novo conteúdo do comentário em Markdown.",
203
+ },
204
+ date: {
205
+ type: "string",
206
+ description:
207
+ "Nova data do comentário; usa o momento atual quando omitida.",
208
+ },
209
+ },
210
+ },
211
+ handler: updateIssueComment,
212
+ },
159
213
  {
160
214
  name: "issues_get_classification_catalog",
161
215
  description:
package/src/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export const SERVER_NAME = "biaws-mcp";
2
- export const SERVER_VERSION = "0.3.0";
2
+ export const SERVER_VERSION = "0.5.0";