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 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
- - `procedures_*`: procedimentos operacionais em Markdown, com tags e classificação taxonômica compartilhadas com issues.
13
- - `documents_*`: regras, decisões, guidelines, features e referências técnicas
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 procedimentos.
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, regras, decisões, procedimentos, segredos, skills e servidores.
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.3.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.3.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.3.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` e
194
- `procedures_move_to_collection`: movem um item para uma coleção validada ou
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`, `procedures`, `secrets`, `skills` ou `servers`.
199
- Procedimentos usam sua árvore própria na API; os demais recursos usam a árvore
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 procedimentos associados. O argumento `limit`
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
- `procedure`. `entityId` identifica o chamado, melhoria ou procedimento. Para
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
- ### Procedimentos
289
-
290
- - `procedures_search`: pesquisa por ID, texto (título, sumário e conteúdo),
291
- taxonomia (incluindo descendentes), tag e contexto.
292
- - `procedures_get_classification_catalog`: obtém a árvore taxonômica e os grupos de tags; opcionalmente inclui listas achatadas de IDs válidos.
293
- - `procedures_create`: cria um procedimento com título, sumário, conteúdo,
294
- classificação e contexto opcionais.
295
- - `procedures_update`: atualiza parcialmente título, sumário, conteúdo,
296
- classificação ou contexto.
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`. Procedimentos podem permanecer gerais ao
317
- workspace ou ser vinculados opcionalmente a uma aplicação e seus componentes.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "biaws-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Servidor MCP de domínio para o Bondia Workspaces",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -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
- "business-rule",
15
- "architecture-decision",
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: { type: "string" },
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: { type: "string" },
41
- affectedComponentIds: { type: "array", maxItems: 100, items: ID },
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: { type: "string" },
44
- details: { type: "object" },
45
- classification: {
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
- primaryTaxonomyId: { type: "string" },
49
- secondaryTaxonomyIds: { type: "array", items: ID },
50
- tags: {
51
- type: "object",
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
- source: {
57
- type: "object",
226
+ {
227
+ description: "Guideline vinculada a uma aplicação.",
58
228
  properties: {
59
- mode: { type: "string", enum: ["native", "repository"] },
60
- repositoryId: { type: "string" },
61
- path: { type: "string" },
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
- references: REFERENCES,
65
- definedAt: { type: "string", description: "YYYY-MM-DD" },
66
- lastReviewedAt: { type: "string", description: "YYYY-MM-DD ou vazio" },
67
- nextReviewAt: { type: "string", description: "YYYY-MM-DD ou vazio" },
68
- changeSummary: { type: "string" },
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 tipado no workspace ou em uma aplicação.",
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 conteúdo, contexto, metadados e relações de um documento.",
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 { 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
@@ -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 procedimentos, na raiz ou sob um item pai.",
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.4.0";
2
+ export const SERVER_VERSION = "0.6.0";