@shiftleftpt/sbd-toe-mcp 0.1.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.
Files changed (93) hide show
  1. package/.env.example +35 -0
  2. package/LICENSE +201 -0
  3. package/README.md +323 -0
  4. package/data/publish/algolia_docs_records.json +148847 -0
  5. package/data/publish/algolia_docs_records_enriched.json +194004 -0
  6. package/data/publish/algolia_entities_records.json +74715 -0
  7. package/data/publish/algolia_entities_records_enriched.json +177587 -0
  8. package/data/publish/algolia_index_settings.json +102 -0
  9. package/data/publish/sbd-toe-index-compact.json +111 -0
  10. package/data/reports/run_manifest.json +10 -0
  11. package/dist/backend/semantic-index-gateway.d.ts +25 -0
  12. package/dist/backend/semantic-index-gateway.js +555 -0
  13. package/dist/backend/semantic-index-gateway.js.map +1 -0
  14. package/dist/backend/semantic-index-gateway.test.d.ts +1 -0
  15. package/dist/backend/semantic-index-gateway.test.js +384 -0
  16. package/dist/backend/semantic-index-gateway.test.js.map +1 -0
  17. package/dist/bootstrap/checkout-backend.d.ts +31 -0
  18. package/dist/bootstrap/checkout-backend.js +136 -0
  19. package/dist/bootstrap/checkout-backend.js.map +1 -0
  20. package/dist/bootstrap/checkout-backend.test.d.ts +1 -0
  21. package/dist/bootstrap/checkout-backend.test.js +158 -0
  22. package/dist/bootstrap/checkout-backend.test.js.map +1 -0
  23. package/dist/bootstrap/release-checkout.d.ts +8 -0
  24. package/dist/bootstrap/release-checkout.js +168 -0
  25. package/dist/bootstrap/release-checkout.js.map +1 -0
  26. package/dist/bootstrap/release-checkout.test.d.ts +1 -0
  27. package/dist/bootstrap/release-checkout.test.js +137 -0
  28. package/dist/bootstrap/release-checkout.test.js.map +1 -0
  29. package/dist/config.d.ts +4 -0
  30. package/dist/config.js +81 -0
  31. package/dist/config.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.js +1063 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/orchestrator/ask-manual.d.ts +13 -0
  36. package/dist/orchestrator/ask-manual.js +202 -0
  37. package/dist/orchestrator/ask-manual.js.map +1 -0
  38. package/dist/prompt/build-answer-prompt.d.ts +2 -0
  39. package/dist/prompt/build-answer-prompt.js +51 -0
  40. package/dist/prompt/build-answer-prompt.js.map +1 -0
  41. package/dist/prompt/system-prompt.d.ts +1 -0
  42. package/dist/prompt/system-prompt.js +94 -0
  43. package/dist/prompt/system-prompt.js.map +1 -0
  44. package/dist/resources/sbd-toe-resources.d.ts +18 -0
  45. package/dist/resources/sbd-toe-resources.js +164 -0
  46. package/dist/resources/sbd-toe-resources.js.map +1 -0
  47. package/dist/resources/sbd-toe-resources.test.d.ts +1 -0
  48. package/dist/resources/sbd-toe-resources.test.js +134 -0
  49. package/dist/resources/sbd-toe-resources.test.js.map +1 -0
  50. package/dist/test-utils.d.ts +153 -0
  51. package/dist/test-utils.js +176 -0
  52. package/dist/test-utils.js.map +1 -0
  53. package/dist/tools/generate-document.d.ts +22 -0
  54. package/dist/tools/generate-document.js +392 -0
  55. package/dist/tools/generate-document.js.map +1 -0
  56. package/dist/tools/generate-document.test.d.ts +1 -0
  57. package/dist/tools/generate-document.test.js +189 -0
  58. package/dist/tools/generate-document.test.js.map +1 -0
  59. package/dist/tools/map-review-scope.d.ts +20 -0
  60. package/dist/tools/map-review-scope.js +299 -0
  61. package/dist/tools/map-review-scope.js.map +1 -0
  62. package/dist/tools/map-review-scope.test.d.ts +1 -0
  63. package/dist/tools/map-review-scope.test.js +204 -0
  64. package/dist/tools/map-review-scope.test.js.map +1 -0
  65. package/dist/tools/plan-repo-governance.d.ts +41 -0
  66. package/dist/tools/plan-repo-governance.js +509 -0
  67. package/dist/tools/plan-repo-governance.js.map +1 -0
  68. package/dist/tools/plan-repo-governance.test.d.ts +1 -0
  69. package/dist/tools/plan-repo-governance.test.js +237 -0
  70. package/dist/tools/plan-repo-governance.test.js.map +1 -0
  71. package/dist/tools/structured-tools.d.ts +5 -0
  72. package/dist/tools/structured-tools.js +310 -0
  73. package/dist/tools/structured-tools.js.map +1 -0
  74. package/dist/tools/structured-tools.test.d.ts +1 -0
  75. package/dist/tools/structured-tools.test.js +459 -0
  76. package/dist/tools/structured-tools.test.js.map +1 -0
  77. package/dist/types.d.ts +160 -0
  78. package/dist/types.js +2 -0
  79. package/dist/types.js.map +1 -0
  80. package/dist/upstream/backend-contract.d.ts +3 -0
  81. package/dist/upstream/backend-contract.js +37 -0
  82. package/dist/upstream/backend-contract.js.map +1 -0
  83. package/dist/validators/ai-disclosure.d.ts +39 -0
  84. package/dist/validators/ai-disclosure.js +183 -0
  85. package/dist/validators/ai-disclosure.js.map +1 -0
  86. package/dist/validators/ai-disclosure.test.d.ts +1 -0
  87. package/dist/validators/ai-disclosure.test.js +244 -0
  88. package/dist/validators/ai-disclosure.test.js.map +1 -0
  89. package/examples/claude-desktop.json +8 -0
  90. package/examples/vscode.mcp.json +9 -0
  91. package/package.json +50 -0
  92. package/prompts/sbd-toe-chat-system.md +71 -0
  93. package/smithery.yaml +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,1063 @@
1
+ #!/usr/bin/env node
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import readline from "node:readline";
5
+ import { getConfig, resolveAppPath } from "./config.js";
6
+ import { formatSampledAnswerResult, inspectManualRetrieval, prepareManualAnsweringContext, searchManualQuestion } from "./orchestrator/ask-manual.js";
7
+ import { loadSystemPromptTemplate } from "./prompt/system-prompt.js";
8
+ import { getSnapshotCache, retrievePublishedContext } from "./backend/semantic-index-gateway.js";
9
+ import { handleGetSbdToeChapterBrief, handleListSbdToeChapters, handleMapSbdToeApplicability, handleQuerySbdToeEntities } from "./tools/structured-tools.js";
10
+ import { handleGenerateDocument } from "./tools/generate-document.js";
11
+ import { handleMapSbdToeReviewScope } from "./tools/map-review-scope.js";
12
+ import { handlePlanRepoGovernance } from "./tools/plan-repo-governance.js";
13
+ import { buildChapterApplicabilityJson, buildSetupAgentPrompt, buildSkillTemplateMarkdown } from "./resources/sbd-toe-resources.js";
14
+ const PROTOCOL_VERSION = "2025-03-26";
15
+ const LOG_LEVELS = [
16
+ "debug",
17
+ "info",
18
+ "notice",
19
+ "warning",
20
+ "error",
21
+ "critical",
22
+ "alert",
23
+ "emergency"
24
+ ];
25
+ class McpRuntime {
26
+ nextRequestId = 10_000;
27
+ pending = new Map();
28
+ clientCapabilities = {};
29
+ initialized = false;
30
+ logLevel = "info";
31
+ constructor() {
32
+ const rl = readline.createInterface({
33
+ input: process.stdin,
34
+ crlfDelay: Infinity
35
+ });
36
+ rl.on("line", (line) => {
37
+ void this.handleIncomingLine(line);
38
+ });
39
+ }
40
+ writeMessage(message) {
41
+ process.stdout.write(`${JSON.stringify(message)}\n`);
42
+ }
43
+ normalizeLogLevel(level) {
44
+ if (typeof level === "string") {
45
+ const normalized = LOG_LEVELS.find((candidate) => candidate === level);
46
+ if (normalized) {
47
+ return normalized;
48
+ }
49
+ }
50
+ return "info";
51
+ }
52
+ shouldLog(level) {
53
+ return LOG_LEVELS.indexOf(level) >= LOG_LEVELS.indexOf(this.logLevel);
54
+ }
55
+ getRequestId(id) {
56
+ return id === null || id === undefined ? undefined : String(id);
57
+ }
58
+ fingerprintQuestion(question) {
59
+ return createHash("sha256").update(question, "utf8").digest("hex").slice(0, 12);
60
+ }
61
+ getQuestionMetadata(args) {
62
+ const question = args.question;
63
+ if (typeof question !== "string") {
64
+ return {};
65
+ }
66
+ return {
67
+ question_length: question.length,
68
+ question_fingerprint: this.fingerprintQuestion(question)
69
+ };
70
+ }
71
+ summarizeError(error) {
72
+ if (error instanceof Error) {
73
+ return {
74
+ error_name: error.name,
75
+ message: error.message.split("\n", 1)[0] ?? "Unexpected error"
76
+ };
77
+ }
78
+ return {
79
+ message: String(error)
80
+ };
81
+ }
82
+ sendResponse(id, result) {
83
+ this.writeMessage({
84
+ jsonrpc: "2.0",
85
+ id,
86
+ result
87
+ });
88
+ }
89
+ sendError(id, code, message, data) {
90
+ this.writeMessage({
91
+ jsonrpc: "2.0",
92
+ id,
93
+ error: {
94
+ code,
95
+ message,
96
+ ...(data === undefined ? {} : { data })
97
+ }
98
+ });
99
+ }
100
+ sendNotification(method, params) {
101
+ this.writeMessage({
102
+ jsonrpc: "2.0",
103
+ method,
104
+ ...(params === undefined ? {} : { params })
105
+ });
106
+ }
107
+ async handleIncomingLine(line) {
108
+ const trimmed = line.trim();
109
+ if (trimmed.length === 0) {
110
+ return;
111
+ }
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(trimmed);
115
+ }
116
+ catch (error) {
117
+ await this.log("warning", {
118
+ event_type: "rpc.parse_error",
119
+ outcome: "failed",
120
+ ...this.summarizeError(error)
121
+ });
122
+ this.sendError(null, -32700, "Parse error", {
123
+ detail: error instanceof Error ? error.message : String(error)
124
+ });
125
+ return;
126
+ }
127
+ if (Array.isArray(parsed)) {
128
+ for (const item of parsed) {
129
+ await this.handleMessage(item);
130
+ }
131
+ return;
132
+ }
133
+ await this.handleMessage(parsed);
134
+ }
135
+ resolvePending(message) {
136
+ if (message.id === null) {
137
+ return false;
138
+ }
139
+ const pending = this.pending.get(message.id);
140
+ if (!pending) {
141
+ return false;
142
+ }
143
+ this.pending.delete(message.id);
144
+ if ("error" in message) {
145
+ pending.reject(new Error(message.error.message));
146
+ }
147
+ else {
148
+ pending.resolve(message.result);
149
+ }
150
+ return true;
151
+ }
152
+ async handleMessage(message) {
153
+ if ("id" in message && ("result" in message || "error" in message)) {
154
+ this.resolvePending(message);
155
+ return;
156
+ }
157
+ if (!("method" in message)) {
158
+ await this.log("warning", {
159
+ event_type: "rpc.invalid_request",
160
+ outcome: "failed",
161
+ message: "Received JSON-RPC message without method"
162
+ });
163
+ this.sendError(null, -32600, "Invalid Request");
164
+ return;
165
+ }
166
+ if (!("id" in message)) {
167
+ this.handleNotification(message);
168
+ return;
169
+ }
170
+ try {
171
+ await this.handleRequest(message);
172
+ }
173
+ catch (error) {
174
+ await this.log("error", {
175
+ event_type: "rpc.unhandled_error",
176
+ outcome: "failed",
177
+ request_id: this.getRequestId(message.id),
178
+ rpc_method: message.method,
179
+ ...this.summarizeError(error)
180
+ });
181
+ this.sendError(message.id, -32603, error instanceof Error ? error.message : "Internal error");
182
+ }
183
+ }
184
+ handleNotification(message) {
185
+ if (message.method === "notifications/initialized") {
186
+ this.initialized = true;
187
+ return;
188
+ }
189
+ if (message.method === "notifications/cancelled") {
190
+ return;
191
+ }
192
+ }
193
+ async handleRequest(request) {
194
+ switch (request.method) {
195
+ case "initialize":
196
+ this.handleInitialize(request);
197
+ return;
198
+ case "ping":
199
+ this.sendResponse(request.id, {});
200
+ return;
201
+ case "logging/setLevel":
202
+ this.handleSetLogLevel(request);
203
+ return;
204
+ case "tools/list":
205
+ this.handleToolsList(request);
206
+ return;
207
+ case "tools/call":
208
+ await this.handleToolsCall(request);
209
+ return;
210
+ case "prompts/list":
211
+ this.handlePromptsList(request);
212
+ return;
213
+ case "prompts/get":
214
+ this.handlePromptGet(request);
215
+ return;
216
+ case "resources/list":
217
+ this.handleResourcesList(request);
218
+ return;
219
+ case "resources/read":
220
+ await this.handleResourcesRead(request);
221
+ return;
222
+ default:
223
+ this.sendError(request.id, -32601, `Method not found: ${request.method}`);
224
+ }
225
+ }
226
+ handleInitialize(request) {
227
+ const params = request.params ?? {};
228
+ this.clientCapabilities =
229
+ typeof params.capabilities === "object" && params.capabilities !== null
230
+ ? params.capabilities
231
+ : {};
232
+ this.sendResponse(request.id, {
233
+ protocolVersion: PROTOCOL_VERSION,
234
+ capabilities: {
235
+ logging: {},
236
+ prompts: {
237
+ listChanged: false
238
+ },
239
+ resources: {
240
+ subscribe: false,
241
+ listChanged: false
242
+ },
243
+ tools: {
244
+ listChanged: false
245
+ }
246
+ },
247
+ serverInfo: {
248
+ name: "sbd-toe-mcp-poc",
249
+ version: "0.1.0"
250
+ },
251
+ instructions: "Use search_sbd_toe_manual to retrieve grounded SbD-ToE context before answering."
252
+ });
253
+ }
254
+ handleSetLogLevel(request) {
255
+ const previousLevel = this.logLevel;
256
+ const level = this.normalizeLogLevel(request.params?.level);
257
+ this.logLevel = level;
258
+ this.sendResponse(request.id, {});
259
+ void this.log("notice", {
260
+ event_type: "logging.level_changed",
261
+ outcome: "succeeded",
262
+ request_id: this.getRequestId(request.id),
263
+ rpc_method: request.method,
264
+ previous_level: previousLevel,
265
+ new_level: level,
266
+ message: "Updated runtime log level"
267
+ });
268
+ }
269
+ async log(level, event) {
270
+ if (!this.shouldLog(level)) {
271
+ return;
272
+ }
273
+ this.sendNotification("notifications/message", {
274
+ level,
275
+ logger: "sbd-toe-mcp-poc",
276
+ data: {
277
+ timestamp: new Date().toISOString(),
278
+ component: "mcp-runtime",
279
+ ...event
280
+ }
281
+ });
282
+ }
283
+ handleToolsList(request) {
284
+ this.sendResponse(request.id, {
285
+ tools: [
286
+ {
287
+ name: "search_sbd_toe_manual",
288
+ title: "Search SbD-ToE Manual",
289
+ description: "Recupera contexto grounded do manual SbD-ToE a partir do snapshot semântico local embutido.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ question: {
294
+ type: "string",
295
+ description: "Pergunta em linguagem natural sobre o manual."
296
+ },
297
+ debug: {
298
+ type: "boolean",
299
+ description: "Quando true, anexa o debug completo do retrieval."
300
+ },
301
+ topK: {
302
+ type: "integer",
303
+ minimum: 1,
304
+ maximum: 15,
305
+ description: "Número máximo de records usados como contexto."
306
+ }
307
+ },
308
+ required: ["question"],
309
+ additionalProperties: false
310
+ },
311
+ annotations: {
312
+ readOnlyHint: true
313
+ }
314
+ },
315
+ {
316
+ name: "answer_sbd_toe_manual",
317
+ title: "Answer SbD-ToE Manual",
318
+ description: "Faz retrieval do manual SbD-ToE e pede a resposta final ao modelo configurado no cliente via sampling MCP.",
319
+ inputSchema: {
320
+ type: "object",
321
+ properties: {
322
+ question: {
323
+ type: "string",
324
+ description: "Pergunta em linguagem natural sobre o manual."
325
+ },
326
+ debug: {
327
+ type: "boolean",
328
+ description: "Quando true, anexa o debug completo."
329
+ },
330
+ topK: {
331
+ type: "integer",
332
+ minimum: 1,
333
+ maximum: 15,
334
+ description: "Número máximo de records usados como contexto."
335
+ }
336
+ },
337
+ required: ["question"],
338
+ additionalProperties: false
339
+ },
340
+ annotations: {
341
+ readOnlyHint: true
342
+ }
343
+ },
344
+ {
345
+ name: "inspect_sbd_toe_retrieval",
346
+ title: "Inspect SbD-ToE Retrieval",
347
+ description: "Inspeciona retrieval, seleção de contexto e prompt final sem pedir resposta ao modelo do cliente.",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {
351
+ question: {
352
+ type: "string",
353
+ description: "Pergunta a usar na inspeção do retrieval."
354
+ },
355
+ topK: {
356
+ type: "integer",
357
+ minimum: 1,
358
+ maximum: 15,
359
+ description: "Número máximo de records selecionados para o prompt."
360
+ }
361
+ },
362
+ required: ["question"],
363
+ additionalProperties: false
364
+ },
365
+ annotations: {
366
+ readOnlyHint: true
367
+ }
368
+ },
369
+ {
370
+ name: "list_sbd_toe_chapters",
371
+ title: "List SbD-ToE Chapters",
372
+ description: "Lista os capítulos do manual SbD-ToE com id, título e aplicabilidade.",
373
+ inputSchema: {
374
+ type: "object",
375
+ properties: {
376
+ riskLevel: {
377
+ type: "string",
378
+ enum: ["L1", "L2", "L3"],
379
+ description: "Filtrar por nível de risco."
380
+ }
381
+ },
382
+ additionalProperties: false
383
+ },
384
+ annotations: { readOnlyHint: true }
385
+ },
386
+ {
387
+ name: "query_sbd_toe_entities",
388
+ title: "Query SbD-ToE Entities",
389
+ description: "Consulta entidades do manual por query, tipo, capítulo ou nível de risco.",
390
+ inputSchema: {
391
+ type: "object",
392
+ properties: {
393
+ query: { type: "string", minLength: 1, maxLength: 200 },
394
+ entityType: { type: "string" },
395
+ chapterId: { type: "string" },
396
+ riskLevel: { type: "string", enum: ["L1", "L2", "L3"] },
397
+ topK: { type: "integer", minimum: 1, maximum: 15 }
398
+ },
399
+ required: ["query"],
400
+ additionalProperties: false
401
+ },
402
+ annotations: { readOnlyHint: true }
403
+ },
404
+ {
405
+ name: "get_sbd_toe_chapter_brief",
406
+ title: "Get SbD-ToE Chapter Brief",
407
+ description: "Devolve resumo operacional de um capítulo: papel, fases, artefactos, intent_topics.",
408
+ inputSchema: {
409
+ type: "object",
410
+ properties: {
411
+ chapterId: { type: "string", minLength: 1 }
412
+ },
413
+ required: ["chapterId"],
414
+ additionalProperties: false
415
+ },
416
+ annotations: { readOnlyHint: true }
417
+ },
418
+ {
419
+ name: "plan_sbd_toe_repo_governance",
420
+ title: "Plan SbD-ToE Repo Governance",
421
+ description: "Dado o tipo de repositório, plataforma e nível de risco, devolve um plano de governança com controlos aplicáveis, checkpoints de baseline, checklist de evidências e recomendações de plataforma.",
422
+ inputSchema: {
423
+ type: "object",
424
+ properties: {
425
+ repoType: {
426
+ type: "string",
427
+ enum: ["library", "service", "webapp", "infrastructure", "pipeline", "monorepo"],
428
+ description: "Tipo de repositório."
429
+ },
430
+ platform: {
431
+ type: "string",
432
+ enum: ["github", "gitlab"],
433
+ description: "Plataforma de hosting do repositório."
434
+ },
435
+ riskLevel: {
436
+ type: "string",
437
+ enum: ["L1", "L2", "L3"],
438
+ description: "Nível de risco do projecto."
439
+ },
440
+ organizationContext: {
441
+ type: "object",
442
+ description: "Contexto organizacional opcional.",
443
+ properties: {
444
+ scale: { type: "string", enum: ["startup", "mid-size", "enterprise"] },
445
+ teamSize: { type: "integer", minimum: 1 },
446
+ enforcementLevel: { type: "string", enum: ["advisory", "enforced", "strict"] }
447
+ },
448
+ additionalProperties: false
449
+ }
450
+ },
451
+ required: ["repoType", "platform", "riskLevel"],
452
+ additionalProperties: false
453
+ },
454
+ annotations: { readOnlyHint: true }
455
+ },
456
+ {
457
+ name: "generate_document",
458
+ title: "Generate SbD-ToE Document",
459
+ description: "Gera o esqueleto estruturado de um documento SbD-ToE (secções, campos obrigatórios, critérios de aceitação) para um tipo e nível de risco.",
460
+ inputSchema: {
461
+ type: "object",
462
+ properties: {
463
+ type: {
464
+ type: "string",
465
+ enum: ["classification-template", "threat-model-template", "checklist", "training-plan", "secure-config"],
466
+ description: "Tipo de documento a gerar."
467
+ },
468
+ riskLevel: {
469
+ type: "string",
470
+ enum: ["L1", "L2", "L3"],
471
+ description: "Nível de risco do projecto."
472
+ },
473
+ context: {
474
+ type: "object",
475
+ description: "Contexto adicional do projecto (reservado, não usado na estrutura).",
476
+ additionalProperties: true
477
+ }
478
+ },
479
+ required: ["type", "riskLevel"],
480
+ additionalProperties: false
481
+ },
482
+ annotations: { readOnlyHint: true }
483
+ },
484
+ {
485
+ name: "map_sbd_toe_review_scope",
486
+ title: "Map SbD-ToE Review Scope",
487
+ description: "Dado um conjunto de ficheiros alterados, mapeia quais knowledge bundles SbD-ToE devem ser revistos, com reasoning explícito por path.",
488
+ inputSchema: {
489
+ type: "object",
490
+ properties: {
491
+ changedFiles: {
492
+ type: "array",
493
+ items: { type: "string" },
494
+ minItems: 1,
495
+ description: "Lista de paths relativos ao raiz do repositório."
496
+ },
497
+ riskLevel: {
498
+ type: "string",
499
+ enum: ["L1", "L2", "L3"],
500
+ description: "Nível de risco do projecto."
501
+ },
502
+ projectContext: {
503
+ type: "object",
504
+ description: "Contexto adicional do projecto (opcional).",
505
+ properties: {
506
+ repoRole: { type: "string" },
507
+ runtimeModel: { type: "string" },
508
+ distributionModel: { type: "string" },
509
+ hasCi: { type: "boolean" }
510
+ },
511
+ additionalProperties: false
512
+ },
513
+ diffSummary: {
514
+ type: "string",
515
+ description: "Resumo do diff (truncado a 500 chars)."
516
+ }
517
+ },
518
+ required: ["changedFiles", "riskLevel"],
519
+ additionalProperties: false
520
+ },
521
+ annotations: { readOnlyHint: true }
522
+ },
523
+ {
524
+ name: "map_sbd_toe_applicability",
525
+ title: "Map SbD-ToE Applicability",
526
+ description: "Mapeia capítulos/controlos activos, condicionais e excluídos para um nível de risco L1/L2/L3. Suporta contexto de projecto para activar bundles relevantes.",
527
+ inputSchema: {
528
+ type: "object",
529
+ properties: {
530
+ riskLevel: { type: "string", enum: ["L1", "L2", "L3"] },
531
+ technologies: {
532
+ type: "array",
533
+ items: {
534
+ type: "string",
535
+ enum: [
536
+ "containers", "serverless", "kubernetes", "ci-cd", "iac", "api-gateway",
537
+ "mobile", "spa", "microservices", "legacy-integration", "ml-ai", "data-pipeline",
538
+ "sca-sbom", "sast", "dast", "secrets-management", "monitoring", "iam",
539
+ "network-segmentation", "cryptography"
540
+ ]
541
+ },
542
+ description: "Tecnologias usadas no projecto."
543
+ },
544
+ hasPersonalData: {
545
+ type: "boolean",
546
+ description: "O projecto processa dados pessoais?"
547
+ },
548
+ isPublicFacing: {
549
+ type: "boolean",
550
+ description: "O projecto tem exposição pública?"
551
+ },
552
+ projectRole: {
553
+ type: "string",
554
+ enum: ["developer", "architect", "security", "devops", "manager"],
555
+ description: "Papel do utilizador no projecto."
556
+ }
557
+ },
558
+ required: ["riskLevel"],
559
+ additionalProperties: false
560
+ },
561
+ annotations: { readOnlyHint: true }
562
+ }
563
+ ]
564
+ });
565
+ }
566
+ getPromptDefinition() {
567
+ return {
568
+ name: "ask_sbd_toe_manual",
569
+ title: "Ask SbD-ToE Manual",
570
+ description: "Prompt MCP para orientar o chat do VS Code a responder perguntas sobre o manual SbD-ToE com grounding.",
571
+ arguments: [
572
+ {
573
+ name: "question",
574
+ description: "Pergunta sobre o manual SbD-ToE.",
575
+ required: true
576
+ }
577
+ ]
578
+ };
579
+ }
580
+ handlePromptsList(request) {
581
+ this.sendResponse(request.id, {
582
+ prompts: [
583
+ this.getPromptDefinition(),
584
+ {
585
+ name: "setup_sbd_toe_agent",
586
+ title: "Setup SbD-ToE Agent",
587
+ description: "Prompt MCP para configurar um agente com o contexto e regras do manual SbD-ToE para um nível de risco.",
588
+ arguments: [
589
+ {
590
+ name: "riskLevel",
591
+ description: "Nível de risco do projecto: L1, L2 ou L3.",
592
+ required: true
593
+ },
594
+ {
595
+ name: "projectRole",
596
+ description: "Papel ou descrição do projecto (opcional).",
597
+ required: false
598
+ }
599
+ ]
600
+ }
601
+ ]
602
+ });
603
+ }
604
+ handlePromptGet(request) {
605
+ const name = typeof request.params?.name === "string" ? request.params.name : "";
606
+ const args = typeof request.params?.arguments === "object" && request.params.arguments !== null
607
+ ? request.params.arguments
608
+ : {};
609
+ if (name === "ask_sbd_toe_manual") {
610
+ const question = typeof args.question === "string" ? args.question : "";
611
+ const promptText = `${loadSystemPromptTemplate()}\n\n` +
612
+ "Use a ferramenta `search_sbd_toe_manual` antes de responder.\n" +
613
+ `Question: ${question}`;
614
+ this.sendResponse(request.id, {
615
+ description: "Prompt grounded para perguntas sobre o manual SbD-ToE.",
616
+ messages: [
617
+ {
618
+ role: "user",
619
+ content: {
620
+ type: "text",
621
+ text: promptText
622
+ }
623
+ }
624
+ ]
625
+ });
626
+ return;
627
+ }
628
+ if (name === "setup_sbd_toe_agent") {
629
+ const riskLevel = args["riskLevel"];
630
+ if (typeof riskLevel !== "string" || !["L1", "L2", "L3"].includes(riskLevel)) {
631
+ this.sendError(request.id, -32602, 'O argumento "riskLevel" é obrigatório e deve ser L1, L2 ou L3.');
632
+ return;
633
+ }
634
+ const projectRole = typeof args["projectRole"] === "string" ? args["projectRole"] : undefined;
635
+ const promptText = buildSetupAgentPrompt(riskLevel, projectRole);
636
+ this.sendResponse(request.id, {
637
+ description: "Prompt para configurar um agente com o contexto SbD-ToE.",
638
+ messages: [
639
+ {
640
+ role: "user",
641
+ content: {
642
+ type: "text",
643
+ text: promptText
644
+ }
645
+ }
646
+ ]
647
+ });
648
+ return;
649
+ }
650
+ this.sendError(request.id, -32602, `Prompt desconhecida: ${name}`);
651
+ }
652
+ handleResourcesList(request) {
653
+ this.sendResponse(request.id, {
654
+ resources: [
655
+ {
656
+ uri: "sbd://toe/skill-template/{riskLevel}/{projectRole}",
657
+ name: "SbD-ToE Skill Template",
658
+ description: "Template de skill/instructions SbD-ToE para um nível de risco e papel de projecto.",
659
+ mimeType: "text/markdown"
660
+ },
661
+ {
662
+ uri: "sbd://toe/chapter-applicability/{riskLevel}",
663
+ name: "SbD-ToE Chapter Applicability",
664
+ description: "Capítulos activos, condicionais e excluídos para um nível de risco L1/L2/L3.",
665
+ mimeType: "application/json"
666
+ },
667
+ {
668
+ uri: "sbd://toe/index-compact",
669
+ name: "SbD-ToE Index Compact",
670
+ description: "Índice compacto do manual SbD-ToE. Injectável em system prompt para eliminar fase de descoberta exploratória.",
671
+ mimeType: "application/json"
672
+ }
673
+ ]
674
+ });
675
+ }
676
+ async handleResourcesRead(request) {
677
+ const uri = typeof request.params?.uri === "string" ? request.params.uri : "";
678
+ const applicabilityMatch = /^\/\/toe\/chapter-applicability\/([^/]+)$/.exec(uri.startsWith("sbd:") ? uri.slice(4) : "");
679
+ if (applicabilityMatch !== null) {
680
+ const riskLevel = applicabilityMatch[1] ?? "";
681
+ if (!["L1", "L2", "L3"].includes(riskLevel)) {
682
+ this.sendError(request.id, -32602, `riskLevel inválido: "${riskLevel}". Valores permitidos: L1, L2, L3.`);
683
+ return;
684
+ }
685
+ const data = buildChapterApplicabilityJson(riskLevel);
686
+ this.sendResponse(request.id, {
687
+ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(data, null, 2) }]
688
+ });
689
+ return;
690
+ }
691
+ const skillTemplateMatch = /^\/\/toe\/skill-template\/([^/]+)\/([^/]+)$/.exec(uri.startsWith("sbd:") ? uri.slice(4) : "");
692
+ if (skillTemplateMatch !== null) {
693
+ const riskLevel = skillTemplateMatch[1] ?? "";
694
+ const projectRole = skillTemplateMatch[2] ?? "";
695
+ if (!["L1", "L2", "L3"].includes(riskLevel)) {
696
+ this.sendError(request.id, -32602, `riskLevel inválido: "${riskLevel}". Valores permitidos: L1, L2, L3.`);
697
+ return;
698
+ }
699
+ const text = buildSkillTemplateMarkdown(riskLevel, projectRole);
700
+ this.sendResponse(request.id, {
701
+ contents: [{ uri, mimeType: "text/markdown", text }]
702
+ });
703
+ return;
704
+ }
705
+ if (uri === "sbd://toe/index-compact") {
706
+ const indexPath = resolveAppPath("data/publish/sbd-toe-index-compact.json");
707
+ let indexText;
708
+ try {
709
+ indexText = readFileSync(indexPath, "utf-8");
710
+ }
711
+ catch {
712
+ this.sendError(request.id, -32603, "Não foi possível ler o índice compacto SbD-ToE.");
713
+ return;
714
+ }
715
+ this.sendResponse(request.id, {
716
+ contents: [{ uri, mimeType: "application/json", text: indexText }]
717
+ });
718
+ return;
719
+ }
720
+ this.sendError(request.id, -32602, `URI de resource desconhecida: ${uri}`);
721
+ }
722
+ getStringArg(args, key) {
723
+ const value = args[key];
724
+ if (typeof value !== "string" || value.trim().length === 0) {
725
+ throw new Error(`O argumento "${key}" é obrigatório.`);
726
+ }
727
+ return value;
728
+ }
729
+ getOptionalBooleanArg(args, key) {
730
+ const value = args[key];
731
+ return typeof value === "boolean" ? value : undefined;
732
+ }
733
+ getOptionalIntegerArg(args, key) {
734
+ const value = args[key];
735
+ return typeof value === "number" && Number.isInteger(value) ? value : undefined;
736
+ }
737
+ supportsSampling() {
738
+ return Boolean(this.clientCapabilities.sampling &&
739
+ typeof this.clientCapabilities.sampling === "object");
740
+ }
741
+ async requestSampling(systemPrompt, userPrompt) {
742
+ if (!this.supportsSampling()) {
743
+ throw new Error("O cliente MCP atual não declarou suporte para sampling.");
744
+ }
745
+ const startedAt = Date.now();
746
+ await this.log("debug", {
747
+ event_type: "sampling.request",
748
+ outcome: "started",
749
+ sampling_max_tokens: getConfig().prompt.samplingMaxTokens,
750
+ message: "Requesting client-side sampling"
751
+ });
752
+ const id = this.nextRequestId++;
753
+ const promise = new Promise((resolve, reject) => {
754
+ this.pending.set(id, { resolve, reject });
755
+ });
756
+ this.writeMessage({
757
+ jsonrpc: "2.0",
758
+ id,
759
+ method: "sampling/createMessage",
760
+ params: {
761
+ messages: [
762
+ {
763
+ role: "user",
764
+ content: {
765
+ type: "text",
766
+ text: userPrompt
767
+ }
768
+ }
769
+ ],
770
+ systemPrompt,
771
+ temperature: 0.1,
772
+ maxTokens: getConfig().prompt.samplingMaxTokens
773
+ }
774
+ });
775
+ const result = (await promise);
776
+ const content = result.content;
777
+ const text = this.extractSamplingText(content);
778
+ const model = typeof result.model === "string" ? result.model : undefined;
779
+ await this.log("debug", {
780
+ event_type: "sampling.request",
781
+ outcome: "succeeded",
782
+ duration_ms: Date.now() - startedAt,
783
+ sampling_max_tokens: getConfig().prompt.samplingMaxTokens,
784
+ message: "Client-side sampling completed"
785
+ });
786
+ return model === undefined ? { text } : { model, text };
787
+ }
788
+ extractSamplingText(content) {
789
+ if (typeof content === "string") {
790
+ return content.trim();
791
+ }
792
+ if (Array.isArray(content)) {
793
+ const parts = content
794
+ .map((item) => {
795
+ if (!item || typeof item !== "object") {
796
+ return undefined;
797
+ }
798
+ const typed = item;
799
+ return typeof typed.text === "string" ? typed.text : undefined;
800
+ })
801
+ .filter((item) => Boolean(item));
802
+ if (parts.length > 0) {
803
+ return parts.join("\n").trim();
804
+ }
805
+ }
806
+ if (content && typeof content === "object") {
807
+ const typed = content;
808
+ if (typeof typed.text === "string") {
809
+ return typed.text.trim();
810
+ }
811
+ }
812
+ return JSON.stringify(content, null, 2);
813
+ }
814
+ async handleToolsCall(request) {
815
+ const params = request.params ?? {};
816
+ const name = typeof params.name === "string" ? params.name : "";
817
+ const args = typeof params.arguments === "object" && params.arguments !== null
818
+ ? params.arguments
819
+ : {};
820
+ const requestId = this.getRequestId(request.id);
821
+ const startedAt = Date.now();
822
+ const metadata = {
823
+ request_id: requestId,
824
+ rpc_method: request.method,
825
+ tool_name: name,
826
+ ...this.getQuestionMetadata(args),
827
+ ...(typeof args.debug === "boolean" ? { debug_enabled: args.debug } : {}),
828
+ ...(typeof args.topK === "number" && Number.isInteger(args.topK)
829
+ ? { top_k: args.topK }
830
+ : {})
831
+ };
832
+ await this.log("info", {
833
+ event_type: "tool.call",
834
+ outcome: "started",
835
+ ...metadata,
836
+ message: "Tool invocation started"
837
+ });
838
+ try {
839
+ switch (name) {
840
+ case "search_sbd_toe_manual": {
841
+ const question = this.getStringArg(args, "question");
842
+ const debug = this.getOptionalBooleanArg(args, "debug");
843
+ const topK = this.getOptionalIntegerArg(args, "topK");
844
+ const result = await searchManualQuestion(question, debug, topK);
845
+ this.sendResponse(request.id, {
846
+ content: [{ type: "text", text: result.text }]
847
+ });
848
+ await this.log("info", {
849
+ event_type: "tool.call",
850
+ outcome: "succeeded",
851
+ duration_ms: Date.now() - startedAt,
852
+ ...metadata,
853
+ message: "Tool invocation completed"
854
+ });
855
+ return;
856
+ }
857
+ case "inspect_sbd_toe_retrieval": {
858
+ const question = this.getStringArg(args, "question");
859
+ const topK = this.getOptionalIntegerArg(args, "topK");
860
+ const result = await inspectManualRetrieval(question, topK);
861
+ this.sendResponse(request.id, {
862
+ content: [{ type: "text", text: result.text }]
863
+ });
864
+ await this.log("info", {
865
+ event_type: "tool.call",
866
+ outcome: "succeeded",
867
+ duration_ms: Date.now() - startedAt,
868
+ ...metadata,
869
+ message: "Tool invocation completed"
870
+ });
871
+ return;
872
+ }
873
+ case "answer_sbd_toe_manual": {
874
+ const question = this.getStringArg(args, "question");
875
+ const debug = this.getOptionalBooleanArg(args, "debug");
876
+ const topK = this.getOptionalIntegerArg(args, "topK");
877
+ if (!this.supportsSampling()) {
878
+ // Fallback gracioso: devolver top-3 documentos sem sampling
879
+ const bundle = await retrievePublishedContext(question, 3);
880
+ const fallbackResult = {
881
+ sampling_unavailable: true,
882
+ note: "Sampling não disponível neste cliente. Apresentando os 3 documentos mais relevantes como contexto.",
883
+ results: bundle.retrieved
884
+ };
885
+ this.sendResponse(request.id, {
886
+ content: [{ type: "text", text: JSON.stringify(fallbackResult, null, 2) }]
887
+ });
888
+ await this.log("info", {
889
+ event_type: "tool.call",
890
+ outcome: "succeeded",
891
+ duration_ms: Date.now() - startedAt,
892
+ ...metadata,
893
+ message: "Tool invocation completed (sampling fallback)"
894
+ });
895
+ return;
896
+ }
897
+ const prepared = await prepareManualAnsweringContext(question, topK);
898
+ const sampled = await this.requestSampling(prepared.prompt.systemPrompt, prepared.prompt.userPrompt);
899
+ const result = formatSampledAnswerResult(question, prepared, sampled.text, sampled.model, debug);
900
+ this.sendResponse(request.id, {
901
+ content: [{ type: "text", text: result.text }]
902
+ });
903
+ await this.log("info", {
904
+ event_type: "tool.call",
905
+ outcome: "succeeded",
906
+ duration_ms: Date.now() - startedAt,
907
+ ...metadata,
908
+ message: "Tool invocation completed"
909
+ });
910
+ return;
911
+ }
912
+ case "list_sbd_toe_chapters": {
913
+ const cache = getSnapshotCache();
914
+ const result = handleListSbdToeChapters(args, cache);
915
+ this.sendResponse(request.id, {
916
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
917
+ });
918
+ await this.log("info", {
919
+ event_type: "tool.call",
920
+ outcome: "succeeded",
921
+ duration_ms: Date.now() - startedAt,
922
+ ...metadata,
923
+ message: "Tool invocation completed"
924
+ });
925
+ return;
926
+ }
927
+ case "query_sbd_toe_entities": {
928
+ const cache = getSnapshotCache();
929
+ const result = await handleQuerySbdToeEntities(args, cache);
930
+ this.sendResponse(request.id, {
931
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
932
+ });
933
+ await this.log("info", {
934
+ event_type: "tool.call",
935
+ outcome: "succeeded",
936
+ duration_ms: Date.now() - startedAt,
937
+ ...metadata,
938
+ message: "Tool invocation completed"
939
+ });
940
+ return;
941
+ }
942
+ case "get_sbd_toe_chapter_brief": {
943
+ const cache = getSnapshotCache();
944
+ const result = handleGetSbdToeChapterBrief(args, cache);
945
+ this.sendResponse(request.id, {
946
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
947
+ });
948
+ await this.log("info", {
949
+ event_type: "tool.call",
950
+ outcome: "succeeded",
951
+ duration_ms: Date.now() - startedAt,
952
+ ...metadata,
953
+ message: "Tool invocation completed"
954
+ });
955
+ return;
956
+ }
957
+ case "plan_sbd_toe_repo_governance": {
958
+ const result = handlePlanRepoGovernance(args);
959
+ this.sendResponse(request.id, {
960
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
961
+ });
962
+ await this.log("info", {
963
+ event_type: "tool.call",
964
+ outcome: "succeeded",
965
+ duration_ms: Date.now() - startedAt,
966
+ ...metadata,
967
+ message: "Tool invocation completed"
968
+ });
969
+ return;
970
+ }
971
+ case "generate_document": {
972
+ const result = handleGenerateDocument(args);
973
+ this.sendResponse(request.id, {
974
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
975
+ });
976
+ await this.log("info", {
977
+ event_type: "tool.call",
978
+ outcome: "succeeded",
979
+ duration_ms: Date.now() - startedAt,
980
+ ...metadata,
981
+ message: "Tool invocation completed"
982
+ });
983
+ return;
984
+ }
985
+ case "map_sbd_toe_review_scope": {
986
+ const result = handleMapSbdToeReviewScope(args);
987
+ this.sendResponse(request.id, {
988
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
989
+ });
990
+ await this.log("info", {
991
+ event_type: "tool.call",
992
+ outcome: "succeeded",
993
+ duration_ms: Date.now() - startedAt,
994
+ ...metadata,
995
+ message: "Tool invocation completed"
996
+ });
997
+ return;
998
+ }
999
+ case "map_sbd_toe_applicability": {
1000
+ const cache = getSnapshotCache();
1001
+ const result = handleMapSbdToeApplicability(args, cache);
1002
+ this.sendResponse(request.id, {
1003
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
1004
+ });
1005
+ await this.log("info", {
1006
+ event_type: "tool.call",
1007
+ outcome: "succeeded",
1008
+ duration_ms: Date.now() - startedAt,
1009
+ ...metadata,
1010
+ message: "Tool invocation completed"
1011
+ });
1012
+ return;
1013
+ }
1014
+ default:
1015
+ await this.log("warning", {
1016
+ event_type: "tool.call",
1017
+ outcome: "failed",
1018
+ duration_ms: Date.now() - startedAt,
1019
+ ...metadata,
1020
+ error_code: -32602,
1021
+ message: "Unknown tool requested"
1022
+ });
1023
+ this.sendError(request.id, -32602, `Tool desconhecida: ${name}`);
1024
+ }
1025
+ }
1026
+ catch (error) {
1027
+ // Erros com rpcError emitem JSON-RPC error (ex: -32602 para input inválido)
1028
+ if (error instanceof Error &&
1029
+ "rpcError" in error &&
1030
+ error.rpcError !== null &&
1031
+ typeof error.rpcError === "object") {
1032
+ const rpcError = error.rpcError;
1033
+ await this.log("warning", {
1034
+ event_type: "tool.call",
1035
+ outcome: "failed",
1036
+ duration_ms: Date.now() - startedAt,
1037
+ ...metadata,
1038
+ error_code: rpcError.code,
1039
+ message: rpcError.message
1040
+ });
1041
+ this.sendError(request.id, rpcError.code, rpcError.message, rpcError.data);
1042
+ return;
1043
+ }
1044
+ const message = error instanceof Error ? error.message : "Erro inesperado.";
1045
+ await this.log("error", {
1046
+ event_type: "tool.call",
1047
+ outcome: "failed",
1048
+ duration_ms: Date.now() - startedAt,
1049
+ ...metadata,
1050
+ ...this.summarizeError(error)
1051
+ });
1052
+ this.sendResponse(request.id, {
1053
+ isError: true,
1054
+ content: [{ type: "text", text: message }]
1055
+ });
1056
+ }
1057
+ }
1058
+ }
1059
+ function main() {
1060
+ new McpRuntime();
1061
+ }
1062
+ main();
1063
+ //# sourceMappingURL=index.js.map