biaws-mcp 0.4.0 → 0.6.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 +65 -26
- package/package.json +1 -1
- package/src/domains/knowledge/documentTypeCatalog.js +7 -0
- package/src/domains/knowledge/documentTypeCatalogData.js +104 -0
- package/src/domains/knowledge/service.js +24 -0
- package/src/domains/knowledge/tools.js +237 -36
- package/src/httpClient.js +59 -9
- package/src/index.js +109 -7
- package/src/logger.js +97 -0
- package/src/mcpServer.js +55 -6
- package/src/requestContext.js +4 -0
- package/src/tools.js +100 -1
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -9,19 +9,18 @@ O servidor separa os domínios:
|
|
|
9
9
|
topologia operacional.
|
|
10
10
|
- `issues_*`: chamados, incidentes, requisições operacionais, taxonomia e classificação.
|
|
11
11
|
- `demands_*`: melhorias, especificações, jornadas, prazos e contexto para desenvolvimento.
|
|
12
|
-
- `
|
|
13
|
-
|
|
14
|
-
relacionado a aplicações e componentes.
|
|
12
|
+
- `documents_*`: regras, decisões, guidelines, features, referências técnicas e
|
|
13
|
+
procedimentos relacionados ao workspace, a aplicações e a componentes.
|
|
15
14
|
- `knowledge_context_load`: regras ativas e decisões aceitas para o contexto
|
|
16
15
|
solicitado.
|
|
17
16
|
- `attachments_*`: envio, download, classificação e exclusão de arquivos de
|
|
18
|
-
chamados, melhorias, tarefas e
|
|
17
|
+
chamados, melhorias, tarefas e documentos.
|
|
19
18
|
- `secrets_*`: consulta e registro de metadados de segredos, sem acesso aos valores.
|
|
20
19
|
- `monitoring_templates_*`, `runtime_active_monitors_*` e
|
|
21
20
|
`runtime_monitoring_results_list`: administração versionada de templates,
|
|
22
21
|
configuração de monitores e consulta do histórico dos runtimes.
|
|
23
22
|
- `resource_collections_*` e `*_move_to_collection`: organização hierárquica
|
|
24
|
-
de aplicações,
|
|
23
|
+
de aplicações, documentos, melhorias, segredos, skills e servidores.
|
|
25
24
|
|
|
26
25
|
O carregamento de ambiente é autocontido no pacote. Em um checkout, ele preserva
|
|
27
26
|
o fallback para o `.env` da raiz; no pacote publicado, usa o `.env` local ou o
|
|
@@ -66,13 +65,13 @@ O pacote público expõe o executável `biaws-mcp`. Clientes configurados pelo C
|
|
|
66
65
|
usam uma versão fixada por meio do cache local do npm:
|
|
67
66
|
|
|
68
67
|
```bash
|
|
69
|
-
npx --yes biaws-mcp@0.
|
|
68
|
+
npx --yes biaws-mcp@0.6.0
|
|
70
69
|
```
|
|
71
70
|
|
|
72
71
|
Também é possível instalá-lo explicitamente:
|
|
73
72
|
|
|
74
73
|
```bash
|
|
75
|
-
npm install --global biaws-mcp@0.
|
|
74
|
+
npm install --global biaws-mcp@0.6.0
|
|
76
75
|
biaws-mcp
|
|
77
76
|
```
|
|
78
77
|
|
|
@@ -91,7 +90,7 @@ npm start
|
|
|
91
90
|
Em um cliente MCP, configure o comando:
|
|
92
91
|
|
|
93
92
|
```bash
|
|
94
|
-
npx --yes biaws-mcp@0.
|
|
93
|
+
npx --yes biaws-mcp@0.6.0
|
|
95
94
|
```
|
|
96
95
|
|
|
97
96
|
O fluxo recomendado é gerar a configuração completa com:
|
|
@@ -105,6 +104,36 @@ node /caminho/para/biaws/biaws-cli/src/index.js \
|
|
|
105
104
|
O processo de publicação e rollback está em
|
|
106
105
|
[`docs/releasing.md`](docs/releasing.md).
|
|
107
106
|
|
|
107
|
+
## Diagnóstico do transporte
|
|
108
|
+
|
|
109
|
+
A partir da versão 0.5.0, o processo escreve um evento JSON por linha em
|
|
110
|
+
`stderr`. O `stdout` permanece exclusivo para frames JSON-RPC do MCP. Cada
|
|
111
|
+
execução recebe um `executionId` e cada chamada de tool recebe um `requestId`,
|
|
112
|
+
permitindo correlacionar ciclo de vida, duração, tentativas HTTP, cancelamento e
|
|
113
|
+
falhas de processo sem registrar argumentos, respostas ou credenciais.
|
|
114
|
+
|
|
115
|
+
Os eventos principais são:
|
|
116
|
+
|
|
117
|
+
- `mcp_server_started`, `mcp_shutdown_requested` e `mcp_server_stopped`;
|
|
118
|
+
- `mcp_tool_call_started`, `mcp_tool_call_completed`,
|
|
119
|
+
`mcp_tool_call_failed` e `mcp_tool_call_cancelled`;
|
|
120
|
+
- `mcp_http_attempt_completed`, `mcp_http_attempt_failed` e
|
|
121
|
+
`mcp_http_retry_scheduled`;
|
|
122
|
+
- `mcp_input_error`, `mcp_stdout_error`, `mcp_uncaught_exception` e
|
|
123
|
+
`mcp_unhandled_rejection`.
|
|
124
|
+
|
|
125
|
+
`BIAWS_MCP_LOG_LEVEL` aceita `debug`, `info`, `warn` ou `error` e usa `info` por
|
|
126
|
+
padrão. O nível `debug` também registra o início de cada tentativa HTTP. O MCP
|
|
127
|
+
seleciona somente origem, método, status, duração e dados de correlação; URLs
|
|
128
|
+
completas, query strings, headers, argumentos, payloads e valores de ambiente
|
|
129
|
+
não são emitidos.
|
|
130
|
+
|
|
131
|
+
Para investigar `Transport closed`, preserve o `stderr` do processo configurado
|
|
132
|
+
pelo cliente MCP e procure, usando o mesmo `executionId`, por
|
|
133
|
+
`mcp_stdout_error`, eventos fatais, sinais ou encerramentos sem
|
|
134
|
+
`mcp_server_stopped`. Um `mcp_tool_call_failed` com resposta MCP válida indica
|
|
135
|
+
falha funcional ou da API, não fechamento do transporte.
|
|
136
|
+
|
|
108
137
|
## Ferramentas
|
|
109
138
|
|
|
110
139
|
### Catálogo e contexto de aplicações
|
|
@@ -190,19 +219,19 @@ monitor antes de provisionar o executor, crie-o com `enabled: false`.
|
|
|
190
219
|
- `resource_collections_delete`: exclui somente uma coleção vazia, sem
|
|
191
220
|
subcoleções nem itens vinculados;
|
|
192
221
|
- `applications_move_to_collection`, `servers_move_to_collection`,
|
|
193
|
-
`secrets_move_to_collection`, `skills_move_to_collection
|
|
194
|
-
`
|
|
222
|
+
`secrets_move_to_collection`, `skills_move_to_collection`,
|
|
223
|
+
`demands_move_to_collection` e `documents_move_to_collection`: movem um item
|
|
224
|
+
para uma coleção validada ou
|
|
195
225
|
para a raiz quando `collectionId` é vazio.
|
|
196
226
|
|
|
197
227
|
As quatro ferramentas `resource_collections_*` aceitam `resourceType` com os
|
|
198
|
-
valores `applications`, `
|
|
199
|
-
|
|
200
|
-
compartilhada por tipo. Todas as mutações mantêm as validações contra ciclos,
|
|
228
|
+
valores `applications`, `demands`, `documents`, `secrets`, `skills` ou
|
|
229
|
+
`servers`. Todas as mutações mantêm as validações contra ciclos,
|
|
201
230
|
escopo do workspace, permissões e auditoria da API.
|
|
202
231
|
|
|
203
232
|
`applications_get_context` entrega, em uma única consulta, a aplicação,
|
|
204
233
|
integrações, componentes, repositórios, deployments, runtimes, servidores referenciados e
|
|
205
|
-
resumos de issues, melhorias e
|
|
234
|
+
resumos de issues, melhorias e documentos associados. O argumento `limit`
|
|
206
235
|
vale separadamente para cada grupo, tem teto de 100 e padrão 25. O resultado
|
|
207
236
|
informa totais e truncamentos e não contém credenciais, endereços dos
|
|
208
237
|
servidores, metadata dos runtimes, anexos nem os textos extensos da base de
|
|
@@ -264,7 +293,7 @@ conhecimento.
|
|
|
264
293
|
conteúdo armazenado.
|
|
265
294
|
|
|
266
295
|
As quatro ferramentas aceitam `entityType` com `issue`, `demand`, `task` ou
|
|
267
|
-
`
|
|
296
|
+
`document`. `entityId` identifica o chamado, melhoria ou documento. Para
|
|
268
297
|
`task`, `entityId` deve ser o ID da melhoria pai e `taskId` deve identificar a
|
|
269
298
|
tarefa por ID ou código.
|
|
270
299
|
|
|
@@ -285,15 +314,25 @@ quando essas permissões tiverem sido removidas ou ainda não existirem, um
|
|
|
285
314
|
administrador deverá concedê-las. Operações no contexto de tarefa usam as
|
|
286
315
|
permissões de anexos da melhoria pai.
|
|
287
316
|
|
|
288
|
-
###
|
|
289
|
-
|
|
290
|
-
- `
|
|
291
|
-
|
|
292
|
-
- `
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
317
|
+
### Documentos
|
|
318
|
+
|
|
319
|
+
- `document_types_list`: consulta o contrato oficial de cada tipo, incluindo
|
|
320
|
+
estados, contexto obrigatório e campos específicos de `details`;
|
|
321
|
+
- `documents_search` e `documents_get`: localizam documentos e carregam seu
|
|
322
|
+
conteúdo completo sob demanda;
|
|
323
|
+
- `documents_create`: cria regras, decisões, guidelines, features, referências
|
|
324
|
+
técnicas e procedimentos usando um schema discriminado por `documentType`;
|
|
325
|
+
- `documents_update`: atualiza conteúdo, contexto e metadados, preservando o
|
|
326
|
+
tipo imutável;
|
|
327
|
+
- `documents_add_observation`: acrescenta uma observação imutável.
|
|
328
|
+
|
|
329
|
+
Regras de negócio, decisões arquiteturais e features exigem `applicationId`.
|
|
330
|
+
Referências técnicas podem pertencer ao workspace ou a uma aplicação.
|
|
331
|
+
Procedimentos também podem pertencer ao workspace ou ser vinculados a uma
|
|
332
|
+
aplicação e seus componentes; são documentos com `documentType=procedure`.
|
|
333
|
+
Guidelines usam `details.scope`: `workspace` proíbe `applicationId`,
|
|
334
|
+
`application` o exige e `component` exige também `affectedComponentIds`.
|
|
335
|
+
Componentes sempre devem estar ativos e pertencer à aplicação informada.
|
|
297
336
|
|
|
298
337
|
### Metadados de segredos
|
|
299
338
|
|
|
@@ -313,8 +352,8 @@ O grupo padrão `agent-operator` recebe `secrets.metadata.read` e
|
|
|
313
352
|
`secrets.value.reveal`.
|
|
314
353
|
|
|
315
354
|
Issues e melhorias criadas pelo MCP sempre pertencem a uma aplicação e podem
|
|
316
|
-
informar `affectedComponentIds`.
|
|
317
|
-
|
|
355
|
+
informar `affectedComponentIds`. Documentos seguem as regras de contexto
|
|
356
|
+
declaradas por `document_types_list`.
|
|
318
357
|
Nas ferramentas de consulta, os filtros comuns são `workspaceId`,
|
|
319
358
|
`applicationId` e `componentId`.
|
|
320
359
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Keep this published copy synchronized with shared/documentTypes.js. The MCP
|
|
2
|
+
// package cannot import files outside its package root; the parity test prevents
|
|
3
|
+
// the API and MCP contracts from drifting.
|
|
4
|
+
export {
|
|
5
|
+
DOCUMENT_TYPE_CATALOG,
|
|
6
|
+
DOCUMENT_TYPES,
|
|
7
|
+
} from "./documentTypeCatalogData.js";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export const DOCUMENT_TYPE_CATALOG = Object.freeze({
|
|
2
|
+
"business-rule": Object.freeze({
|
|
3
|
+
type: "business-rule",
|
|
4
|
+
label: "Regra de negócio",
|
|
5
|
+
description: "Condições e comportamentos esperados do domínio.",
|
|
6
|
+
defaultStatus: "draft",
|
|
7
|
+
statuses: Object.freeze(["draft", "active", "retired", "archived"]),
|
|
8
|
+
currentStatuses: Object.freeze(["active"]),
|
|
9
|
+
applicationRequired: true,
|
|
10
|
+
details: Object.freeze({
|
|
11
|
+
ruleCode: Object.freeze({ type: "string", maxLength: 80 }),
|
|
12
|
+
effectiveFrom: Object.freeze({ type: "date" }),
|
|
13
|
+
}),
|
|
14
|
+
}),
|
|
15
|
+
"architecture-decision": Object.freeze({
|
|
16
|
+
type: "architecture-decision",
|
|
17
|
+
label: "Decisão arquitetural",
|
|
18
|
+
description: "Escolhas técnicas, contexto, alternativas e consequências.",
|
|
19
|
+
defaultStatus: "proposed",
|
|
20
|
+
statuses: Object.freeze([
|
|
21
|
+
"proposed",
|
|
22
|
+
"accepted",
|
|
23
|
+
"rejected",
|
|
24
|
+
"superseded",
|
|
25
|
+
"archived",
|
|
26
|
+
]),
|
|
27
|
+
currentStatuses: Object.freeze(["accepted"]),
|
|
28
|
+
applicationRequired: true,
|
|
29
|
+
details: Object.freeze({
|
|
30
|
+
decidedAt: Object.freeze({ type: "date" }),
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
guideline: Object.freeze({
|
|
34
|
+
type: "guideline",
|
|
35
|
+
label: "Guideline",
|
|
36
|
+
description: "Diretrizes e padrões de desenvolvimento.",
|
|
37
|
+
defaultStatus: "draft",
|
|
38
|
+
statuses: Object.freeze(["draft", "published", "deprecated", "archived"]),
|
|
39
|
+
currentStatuses: Object.freeze(["published"]),
|
|
40
|
+
applicationRequired: false,
|
|
41
|
+
details: Object.freeze({
|
|
42
|
+
scope: Object.freeze({
|
|
43
|
+
type: "string",
|
|
44
|
+
enum: Object.freeze(["workspace", "application", "component"]),
|
|
45
|
+
default: "workspace",
|
|
46
|
+
}),
|
|
47
|
+
enforcement: Object.freeze({
|
|
48
|
+
type: "string",
|
|
49
|
+
enum: Object.freeze(["required", "recommended", "informative"]),
|
|
50
|
+
default: "recommended",
|
|
51
|
+
}),
|
|
52
|
+
}),
|
|
53
|
+
}),
|
|
54
|
+
feature: Object.freeze({
|
|
55
|
+
type: "feature",
|
|
56
|
+
label: "Feature",
|
|
57
|
+
description: "Descrição funcional e técnica aprofundada de uma capacidade.",
|
|
58
|
+
defaultStatus: "draft",
|
|
59
|
+
statuses: Object.freeze(["draft", "published", "deprecated", "archived"]),
|
|
60
|
+
currentStatuses: Object.freeze(["published"]),
|
|
61
|
+
applicationRequired: true,
|
|
62
|
+
details: Object.freeze({
|
|
63
|
+
maturity: Object.freeze({
|
|
64
|
+
type: "string",
|
|
65
|
+
enum: Object.freeze(["planned", "beta", "stable", "retired"]),
|
|
66
|
+
default: "stable",
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
}),
|
|
70
|
+
"technical-reference": Object.freeze({
|
|
71
|
+
type: "technical-reference",
|
|
72
|
+
label: "Referência técnica",
|
|
73
|
+
description: "Arquitetura atual, contratos, schemas e mecanismos.",
|
|
74
|
+
defaultStatus: "draft",
|
|
75
|
+
statuses: Object.freeze(["draft", "published", "deprecated", "archived"]),
|
|
76
|
+
currentStatuses: Object.freeze(["published"]),
|
|
77
|
+
applicationRequired: false,
|
|
78
|
+
details: Object.freeze({
|
|
79
|
+
referenceKind: Object.freeze({
|
|
80
|
+
type: "string",
|
|
81
|
+
enum: Object.freeze([
|
|
82
|
+
"architecture",
|
|
83
|
+
"contract",
|
|
84
|
+
"schema",
|
|
85
|
+
"protocol",
|
|
86
|
+
"mechanism",
|
|
87
|
+
]),
|
|
88
|
+
default: "architecture",
|
|
89
|
+
}),
|
|
90
|
+
}),
|
|
91
|
+
}),
|
|
92
|
+
procedure: Object.freeze({
|
|
93
|
+
type: "procedure",
|
|
94
|
+
label: "Procedimento",
|
|
95
|
+
description: "Instruções operacionais reutilizáveis.",
|
|
96
|
+
defaultStatus: "draft",
|
|
97
|
+
statuses: Object.freeze(["draft", "published", "deprecated", "archived"]),
|
|
98
|
+
currentStatuses: Object.freeze(["published"]),
|
|
99
|
+
applicationRequired: false,
|
|
100
|
+
details: Object.freeze({}),
|
|
101
|
+
}),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
export const DOCUMENT_TYPES = Object.freeze(Object.keys(DOCUMENT_TYPE_CATALOG));
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { cleanParams, fetchJson, sendJson } from "../../httpClient.js";
|
|
2
|
+
import { DOCUMENT_TYPE_CATALOG } from "./documentTypeCatalog.js";
|
|
2
3
|
|
|
3
4
|
const BASE_PATH = "/api/knowledge/documents";
|
|
4
5
|
|
|
5
6
|
function documentPayload(args = {}, current = {}) {
|
|
6
7
|
return {
|
|
8
|
+
identifier: args.identifier ?? current.identifier,
|
|
7
9
|
documentType: args.documentType ?? current.documentType,
|
|
8
10
|
title: args.title ?? current.title,
|
|
9
11
|
summary: args.summary ?? current.summary,
|
|
@@ -29,6 +31,28 @@ function documentPayload(args = {}, current = {}) {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
export function listDocumentTypes() {
|
|
35
|
+
return {
|
|
36
|
+
documentTypes: Object.values(DOCUMENT_TYPE_CATALOG).map((config) => ({
|
|
37
|
+
...config,
|
|
38
|
+
context: config.applicationRequired
|
|
39
|
+
? { applicationId: "required", affectedComponentIds: "optional" }
|
|
40
|
+
: config.type === "guideline"
|
|
41
|
+
? {
|
|
42
|
+
applicationId: "depends-on-details.scope",
|
|
43
|
+
affectedComponentIds: "required-when-scope-is-component",
|
|
44
|
+
}
|
|
45
|
+
: { applicationId: "optional", affectedComponentIds: "optional" },
|
|
46
|
+
})),
|
|
47
|
+
commonRules: {
|
|
48
|
+
workspaceId: "implicit-from-mcp-configuration",
|
|
49
|
+
affectedComponentIds: "requires-applicationId",
|
|
50
|
+
documentType: "immutable-after-creation",
|
|
51
|
+
repositorySource: "repositoryId-and-path-required",
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
32
56
|
export async function searchDocuments(args = {}) {
|
|
33
57
|
return fetchJson(
|
|
34
58
|
BASE_PATH,
|
|
@@ -2,78 +2,271 @@ import {
|
|
|
2
2
|
addDocumentObservation,
|
|
3
3
|
createDocument,
|
|
4
4
|
getDocument,
|
|
5
|
+
listDocumentTypes,
|
|
5
6
|
loadKnowledgeContext,
|
|
6
7
|
searchDocuments,
|
|
7
8
|
updateDocument,
|
|
8
9
|
} from "./service.js";
|
|
10
|
+
import {
|
|
11
|
+
DOCUMENT_TYPE_CATALOG,
|
|
12
|
+
DOCUMENT_TYPES,
|
|
13
|
+
} from "./documentTypeCatalog.js";
|
|
9
14
|
|
|
10
15
|
const ID = { type: "string", minLength: 1 };
|
|
16
|
+
const DATE = {
|
|
17
|
+
type: "string",
|
|
18
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
19
|
+
description: "Data no formato YYYY-MM-DD.",
|
|
20
|
+
};
|
|
11
21
|
const DOCUMENT_TYPE = {
|
|
12
22
|
type: "string",
|
|
13
|
-
enum:
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
"guideline",
|
|
17
|
-
"feature",
|
|
18
|
-
"technical-reference",
|
|
19
|
-
"procedure",
|
|
20
|
-
],
|
|
23
|
+
enum: DOCUMENT_TYPES,
|
|
24
|
+
description:
|
|
25
|
+
"Tipo imutável do documento. Consulte document_types_list para o contrato completo.",
|
|
21
26
|
};
|
|
22
27
|
const REFERENCES = {
|
|
23
28
|
type: "array",
|
|
24
29
|
maxItems: 100,
|
|
30
|
+
description: "Relações com outros documentos do mesmo workspace.",
|
|
25
31
|
items: {
|
|
26
32
|
type: "object",
|
|
27
33
|
required: ["targetDocumentId"],
|
|
28
34
|
additionalProperties: false,
|
|
29
35
|
properties: {
|
|
30
36
|
targetDocumentId: ID,
|
|
31
|
-
relationship: {
|
|
37
|
+
relationship: {
|
|
38
|
+
type: "string",
|
|
39
|
+
maxLength: 80,
|
|
40
|
+
description: "Tipo da relação; usa related quando omitido.",
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
const SOURCE = {
|
|
46
|
+
type: "object",
|
|
47
|
+
description:
|
|
48
|
+
"Origem canônica. Em mode=repository, repositoryId e path são obrigatórios.",
|
|
49
|
+
additionalProperties: false,
|
|
50
|
+
properties: {
|
|
51
|
+
mode: { type: "string", enum: ["native", "repository"] },
|
|
52
|
+
repositoryId: { type: "string", maxLength: 160 },
|
|
53
|
+
path: { type: "string", maxLength: 500 },
|
|
54
|
+
},
|
|
55
|
+
oneOf: [
|
|
56
|
+
{
|
|
57
|
+
properties: { mode: { const: "native" } },
|
|
58
|
+
required: ["mode"],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
properties: { mode: { const: "repository" } },
|
|
62
|
+
required: ["mode", "repositoryId", "path"],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const CLASSIFICATION = {
|
|
67
|
+
type: "object",
|
|
68
|
+
description: "Taxonomias e tags aplicáveis ao contexto do documento.",
|
|
69
|
+
additionalProperties: false,
|
|
70
|
+
properties: {
|
|
71
|
+
primaryTaxonomyId: { type: "string" },
|
|
72
|
+
secondaryTaxonomyIds: { type: "array", items: ID },
|
|
73
|
+
tags: {
|
|
74
|
+
type: "object",
|
|
75
|
+
additionalProperties: { type: "array", items: ID },
|
|
32
76
|
},
|
|
33
77
|
},
|
|
34
78
|
};
|
|
79
|
+
|
|
80
|
+
function detailProperty(definition) {
|
|
81
|
+
if (definition.type === "date") return DATE;
|
|
82
|
+
return {
|
|
83
|
+
type: definition.type,
|
|
84
|
+
...(definition.enum ? { enum: definition.enum } : {}),
|
|
85
|
+
...(definition.maxLength ? { maxLength: definition.maxLength } : {}),
|
|
86
|
+
...(definition.default !== undefined
|
|
87
|
+
? { default: definition.default }
|
|
88
|
+
: {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function detailsSchema(type, scope) {
|
|
93
|
+
const config = DOCUMENT_TYPE_CATALOG[type];
|
|
94
|
+
const properties = Object.fromEntries(
|
|
95
|
+
Object.entries(config.details).map(([field, definition]) => [
|
|
96
|
+
field,
|
|
97
|
+
detailProperty(definition),
|
|
98
|
+
]),
|
|
99
|
+
);
|
|
100
|
+
if (scope) properties.scope = { const: scope };
|
|
101
|
+
return {
|
|
102
|
+
type: "object",
|
|
103
|
+
additionalProperties: false,
|
|
104
|
+
properties,
|
|
105
|
+
...(scope ? { required: ["scope"] } : {}),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const ALL_STATUSES = [
|
|
110
|
+
...new Set(
|
|
111
|
+
Object.values(DOCUMENT_TYPE_CATALOG).flatMap(({ statuses }) => statuses),
|
|
112
|
+
),
|
|
113
|
+
];
|
|
114
|
+
|
|
35
115
|
const COMMON = {
|
|
116
|
+
identifier: {
|
|
117
|
+
type: "string",
|
|
118
|
+
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$",
|
|
119
|
+
maxLength: 80,
|
|
120
|
+
description:
|
|
121
|
+
"Identificador opcional e único no workspace, usando minúsculas, números e hífens simples.",
|
|
122
|
+
},
|
|
36
123
|
documentType: DOCUMENT_TYPE,
|
|
37
|
-
title: ID,
|
|
124
|
+
title: { ...ID, maxLength: 240 },
|
|
38
125
|
summary: { type: "string", minLength: 1, maxLength: 500 },
|
|
39
126
|
markdown: ID,
|
|
40
|
-
applicationId: {
|
|
41
|
-
|
|
127
|
+
applicationId: {
|
|
128
|
+
type: "string",
|
|
129
|
+
minLength: 1,
|
|
130
|
+
description:
|
|
131
|
+
"Obrigatório para regras, decisões, features e documentos cujo escopo seja aplicação ou componente; omita no escopo do workspace.",
|
|
132
|
+
},
|
|
133
|
+
affectedComponentIds: {
|
|
134
|
+
type: "array",
|
|
135
|
+
maxItems: 100,
|
|
136
|
+
items: ID,
|
|
137
|
+
description:
|
|
138
|
+
"Componentes ativos da applicationId. Não pode ser informado sem aplicação.",
|
|
139
|
+
},
|
|
42
140
|
collectionId: { type: "string" },
|
|
43
|
-
status: {
|
|
44
|
-
|
|
45
|
-
|
|
141
|
+
status: {
|
|
142
|
+
type: "string",
|
|
143
|
+
enum: ALL_STATUSES,
|
|
144
|
+
description:
|
|
145
|
+
"Estado válido para o documentType; quando omitido, usa o estado inicial do tipo.",
|
|
146
|
+
},
|
|
147
|
+
details: {
|
|
46
148
|
type: "object",
|
|
149
|
+
description:
|
|
150
|
+
"Metadados específicos do documentType, validados pelo ramo correspondente do schema.",
|
|
151
|
+
},
|
|
152
|
+
classification: CLASSIFICATION,
|
|
153
|
+
source: SOURCE,
|
|
154
|
+
references: REFERENCES,
|
|
155
|
+
definedAt: DATE,
|
|
156
|
+
lastReviewedAt: {
|
|
157
|
+
type: "string",
|
|
158
|
+
description: "YYYY-MM-DD ou vazio.",
|
|
159
|
+
},
|
|
160
|
+
nextReviewAt: { type: "string", description: "YYYY-MM-DD ou vazio." },
|
|
161
|
+
changeSummary: { type: "string" },
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function typeBranch(type) {
|
|
165
|
+
const config = DOCUMENT_TYPE_CATALOG[type];
|
|
166
|
+
return {
|
|
167
|
+
description: `${config.label}: ${config.description}`,
|
|
168
|
+
properties: {
|
|
169
|
+
documentType: { const: type },
|
|
170
|
+
status: { type: "string", enum: config.statuses },
|
|
171
|
+
details: detailsSchema(type),
|
|
172
|
+
},
|
|
173
|
+
required: [
|
|
174
|
+
"documentType",
|
|
175
|
+
...(config.applicationRequired ? ["applicationId"] : []),
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function optionalApplicationBranches(type) {
|
|
181
|
+
const config = DOCUMENT_TYPE_CATALOG[type];
|
|
182
|
+
const properties = {
|
|
183
|
+
documentType: { const: type },
|
|
184
|
+
status: { type: "string", enum: config.statuses },
|
|
185
|
+
details: detailsSchema(type),
|
|
186
|
+
};
|
|
187
|
+
return [
|
|
188
|
+
{
|
|
189
|
+
description: `${config.label} geral do workspace; applicationId e componentes devem ser omitidos.`,
|
|
190
|
+
properties: {
|
|
191
|
+
...properties,
|
|
192
|
+
applicationId: { const: null },
|
|
193
|
+
affectedComponentIds: { type: "array", maxItems: 0 },
|
|
194
|
+
},
|
|
195
|
+
required: ["documentType"],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
description: `${config.label} vinculada a uma aplicação e, opcionalmente, a seus componentes.`,
|
|
199
|
+
properties,
|
|
200
|
+
required: ["documentType", "applicationId"],
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const CREATE_VARIANTS = [
|
|
206
|
+
typeBranch("business-rule"),
|
|
207
|
+
typeBranch("architecture-decision"),
|
|
208
|
+
typeBranch("feature"),
|
|
209
|
+
...optionalApplicationBranches("technical-reference"),
|
|
210
|
+
...optionalApplicationBranches("procedure"),
|
|
211
|
+
{
|
|
212
|
+
description:
|
|
213
|
+
"Guideline geral do workspace; applicationId deve ser omitido.",
|
|
47
214
|
properties: {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
additionalProperties: { type: "array", items: ID },
|
|
215
|
+
documentType: { const: "guideline" },
|
|
216
|
+
status: {
|
|
217
|
+
type: "string",
|
|
218
|
+
enum: DOCUMENT_TYPE_CATALOG.guideline.statuses,
|
|
53
219
|
},
|
|
220
|
+
details: detailsSchema("guideline", "workspace"),
|
|
221
|
+
applicationId: { const: null },
|
|
222
|
+
affectedComponentIds: { type: "array", maxItems: 0 },
|
|
54
223
|
},
|
|
224
|
+
required: ["documentType", "details"],
|
|
55
225
|
},
|
|
56
|
-
|
|
57
|
-
|
|
226
|
+
{
|
|
227
|
+
description: "Guideline vinculada a uma aplicação.",
|
|
58
228
|
properties: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
229
|
+
documentType: { const: "guideline" },
|
|
230
|
+
status: {
|
|
231
|
+
type: "string",
|
|
232
|
+
enum: DOCUMENT_TYPE_CATALOG.guideline.statuses,
|
|
233
|
+
},
|
|
234
|
+
details: detailsSchema("guideline", "application"),
|
|
62
235
|
},
|
|
236
|
+
required: ["documentType", "details", "applicationId"],
|
|
63
237
|
},
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
238
|
+
{
|
|
239
|
+
description: "Guideline vinculada a ao menos um componente da aplicação.",
|
|
240
|
+
properties: {
|
|
241
|
+
documentType: { const: "guideline" },
|
|
242
|
+
status: {
|
|
243
|
+
type: "string",
|
|
244
|
+
enum: DOCUMENT_TYPE_CATALOG.guideline.statuses,
|
|
245
|
+
},
|
|
246
|
+
details: detailsSchema("guideline", "component"),
|
|
247
|
+
affectedComponentIds: {
|
|
248
|
+
type: "array",
|
|
249
|
+
minItems: 1,
|
|
250
|
+
maxItems: 100,
|
|
251
|
+
items: ID,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
required: [
|
|
255
|
+
"documentType",
|
|
256
|
+
"details",
|
|
257
|
+
"applicationId",
|
|
258
|
+
"affectedComponentIds",
|
|
259
|
+
],
|
|
260
|
+
},
|
|
261
|
+
];
|
|
70
262
|
|
|
71
|
-
function schema(properties, required = []) {
|
|
263
|
+
function schema(properties, required = [], extra = {}) {
|
|
72
264
|
return {
|
|
73
265
|
type: "object",
|
|
74
266
|
...(required.length ? { required } : {}),
|
|
75
267
|
additionalProperties: false,
|
|
76
268
|
properties,
|
|
269
|
+
...extra,
|
|
77
270
|
};
|
|
78
271
|
}
|
|
79
272
|
|
|
@@ -96,6 +289,12 @@ export const knowledgeTools = [
|
|
|
96
289
|
["applicationId"],
|
|
97
290
|
),
|
|
98
291
|
),
|
|
292
|
+
definition(
|
|
293
|
+
"document_types_list",
|
|
294
|
+
"Lista o contrato oficial de cada tipo de documento: contexto exigido, estados e campos details.",
|
|
295
|
+
listDocumentTypes,
|
|
296
|
+
schema({}),
|
|
297
|
+
),
|
|
99
298
|
definition(
|
|
100
299
|
"documents_search",
|
|
101
300
|
"Busca documentos por tipo, aplicação, componente, coleção, estado ou texto.",
|
|
@@ -106,7 +305,7 @@ export const knowledgeTools = [
|
|
|
106
305
|
applicationId: ID,
|
|
107
306
|
componentId: ID,
|
|
108
307
|
collectionId: ID,
|
|
109
|
-
status: { type: "string" },
|
|
308
|
+
status: { type: "string", enum: ALL_STATUSES },
|
|
110
309
|
currentOnly: { type: "boolean" },
|
|
111
310
|
includeWorkspace: { type: "boolean" },
|
|
112
311
|
includeArchived: { type: "boolean" },
|
|
@@ -122,13 +321,15 @@ export const knowledgeTools = [
|
|
|
122
321
|
),
|
|
123
322
|
definition(
|
|
124
323
|
"documents_create",
|
|
125
|
-
"Cria um documento
|
|
324
|
+
"Cria um documento usando o contrato discriminado pelo documentType. O workspace vem da configuração do MCP; applicationId e details seguem as regras de cada tipo.",
|
|
126
325
|
createDocument,
|
|
127
|
-
schema(COMMON, ["documentType", "title", "summary", "markdown"]
|
|
326
|
+
schema(COMMON, ["documentType", "title", "summary", "markdown"], {
|
|
327
|
+
oneOf: CREATE_VARIANTS,
|
|
328
|
+
}),
|
|
128
329
|
),
|
|
129
330
|
definition(
|
|
130
331
|
"documents_update",
|
|
131
|
-
"Atualiza
|
|
332
|
+
"Atualiza um documento existente. documentType é imutável; consulte documents_get antes de alterar contexto, status ou details.",
|
|
132
333
|
updateDocument,
|
|
133
334
|
schema({ documentId: ID, ...COMMON }, ["documentId"]),
|
|
134
335
|
),
|
package/src/httpClient.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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.
|
|
176
|
+
void operation.then(
|
|
177
|
+
() => inFlight.delete(operation),
|
|
178
|
+
() => inFlight.delete(operation),
|
|
179
|
+
);
|
|
131
180
|
return operation;
|
|
132
181
|
}
|
|
133
182
|
|
package/src/requestContext.js
CHANGED
package/src/tools.js
CHANGED
|
@@ -236,7 +236,7 @@ const tools = [
|
|
|
236
236
|
{
|
|
237
237
|
name: "issues_create_taxonomy_item",
|
|
238
238
|
description:
|
|
239
|
-
"Inclui um item na taxonomia compartilhada de issues e
|
|
239
|
+
"Inclui um item na taxonomia compartilhada de issues e documentos, na raiz ou sob um item pai.",
|
|
240
240
|
inputSchema: {
|
|
241
241
|
type: "object",
|
|
242
242
|
required: ["id", "label"],
|
|
@@ -831,6 +831,45 @@ function validateEnum(value, schema, path, fields) {
|
|
|
831
831
|
}
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
+
function validateConst(value, schema, path, fields) {
|
|
835
|
+
if (Object.hasOwn(schema, "const") && value !== schema.const) {
|
|
836
|
+
addValidationField(
|
|
837
|
+
fields,
|
|
838
|
+
path,
|
|
839
|
+
"invalid_const",
|
|
840
|
+
`${path} must be ${JSON.stringify(schema.const)}`,
|
|
841
|
+
);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
function validateString(value, schema, path, fields) {
|
|
846
|
+
if (typeof value !== "string") return;
|
|
847
|
+
if (schema.minLength !== undefined && value.length < schema.minLength) {
|
|
848
|
+
addValidationField(
|
|
849
|
+
fields,
|
|
850
|
+
path,
|
|
851
|
+
"min_length",
|
|
852
|
+
`${path} must contain at least ${schema.minLength} characters`,
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
if (schema.maxLength !== undefined && value.length > schema.maxLength) {
|
|
856
|
+
addValidationField(
|
|
857
|
+
fields,
|
|
858
|
+
path,
|
|
859
|
+
"max_length",
|
|
860
|
+
`${path} must contain at most ${schema.maxLength} characters`,
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
if (schema.pattern && !new RegExp(schema.pattern, "u").test(value)) {
|
|
864
|
+
addValidationField(
|
|
865
|
+
fields,
|
|
866
|
+
path,
|
|
867
|
+
"pattern",
|
|
868
|
+
`${path} must match ${schema.pattern}`,
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
834
873
|
function validateNumber(value, schema, path, fields) {
|
|
835
874
|
if (typeof value !== "number") return;
|
|
836
875
|
if (schema.minimum !== undefined && value < schema.minimum) {
|
|
@@ -921,13 +960,73 @@ function validateObject(value, schema, path, fields) {
|
|
|
921
960
|
validateObjectEntries(value, schema, path, fields);
|
|
922
961
|
}
|
|
923
962
|
|
|
963
|
+
function validateNot(value, schema, path, fields) {
|
|
964
|
+
if (!schema.not) return;
|
|
965
|
+
const nestedFields = [];
|
|
966
|
+
validateValue(value, schema.not, path, nestedFields);
|
|
967
|
+
if (!nestedFields.length) {
|
|
968
|
+
addValidationField(
|
|
969
|
+
fields,
|
|
970
|
+
path,
|
|
971
|
+
"not",
|
|
972
|
+
`${path || "arguments"} contains a forbidden combination of fields`,
|
|
973
|
+
);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function validateOneOf(value, schema, path, fields) {
|
|
978
|
+
if (!schema.oneOf?.length) return;
|
|
979
|
+
const candidates = schema.oneOf.map((candidate) => {
|
|
980
|
+
const candidateFields = [];
|
|
981
|
+
validateValue(value, candidate, path, candidateFields);
|
|
982
|
+
return candidateFields;
|
|
983
|
+
});
|
|
984
|
+
const matches = candidates.filter(
|
|
985
|
+
(candidateFields) => !candidateFields.length,
|
|
986
|
+
);
|
|
987
|
+
if (matches.length === 1) return;
|
|
988
|
+
if (!matches.length) {
|
|
989
|
+
const discriminatedIndexes = schema.oneOf.flatMap((candidate, index) => {
|
|
990
|
+
const matchesDiscriminator = Object.entries(
|
|
991
|
+
candidate.properties || {},
|
|
992
|
+
).some(
|
|
993
|
+
([property, definition]) =>
|
|
994
|
+
Object.hasOwn(definition, "const") &&
|
|
995
|
+
isPlainObject(value) &&
|
|
996
|
+
value[property] === definition.const,
|
|
997
|
+
);
|
|
998
|
+
return matchesDiscriminator ? [index] : [];
|
|
999
|
+
});
|
|
1000
|
+
const eligible = discriminatedIndexes.length
|
|
1001
|
+
? discriminatedIndexes.map((index) => candidates[index])
|
|
1002
|
+
: candidates;
|
|
1003
|
+
const best = eligible.reduce((selected, candidate) =>
|
|
1004
|
+
candidate.length < selected.length ? candidate : selected,
|
|
1005
|
+
);
|
|
1006
|
+
if (best.length) {
|
|
1007
|
+
fields.push(...best);
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
addValidationField(
|
|
1012
|
+
fields,
|
|
1013
|
+
path,
|
|
1014
|
+
"one_of",
|
|
1015
|
+
`${path || "arguments"} must match exactly one supported schema variant`,
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
924
1019
|
function validateValue(value, schema, path, fields) {
|
|
925
1020
|
if (!schema || value === undefined) return;
|
|
926
1021
|
if (!validateSchemaType(value, schema, path, fields)) return;
|
|
1022
|
+
validateConst(value, schema, path, fields);
|
|
927
1023
|
validateEnum(value, schema, path, fields);
|
|
1024
|
+
validateString(value, schema, path, fields);
|
|
928
1025
|
validateNumber(value, schema, path, fields);
|
|
929
1026
|
validateArray(value, schema, path, fields);
|
|
930
1027
|
validateObject(value, schema, path, fields);
|
|
1028
|
+
validateNot(value, schema, path, fields);
|
|
1029
|
+
validateOneOf(value, schema, path, fields);
|
|
931
1030
|
}
|
|
932
1031
|
|
|
933
1032
|
function validateArguments(tool, args) {
|
package/src/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export const SERVER_NAME = "biaws-mcp";
|
|
2
|
-
export const SERVER_VERSION = "0.
|
|
2
|
+
export const SERVER_VERSION = "0.6.0";
|