@ds9-solutions/mcp 0.1.0 → 0.2.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 (68) hide show
  1. package/content/agents/code-reviewer.agent.md +52 -22
  2. package/content/agents/db-analyser.agent.md +121 -0
  3. package/content/agents/security-reviewer.agent.md +119 -37
  4. package/content/agents/test-writer.agent.md +84 -35
  5. package/content/skills/dart/SKILL.md +139 -0
  6. package/content/skills/dart/examples/entity.dart +37 -0
  7. package/content/skills/dart/examples/error_handling.dart +36 -0
  8. package/content/skills/dart/examples/order_test.dart +34 -0
  9. package/content/skills/dart/examples/repository.dart +57 -0
  10. package/content/skills/dart/references/checklist.md +12 -0
  11. package/content/skills/dart/references/documentation.md +26 -0
  12. package/content/skills/dart/references/naming.md +27 -0
  13. package/content/skills/dart-frog/SKILL.md +208 -0
  14. package/content/skills/dart-frog/examples/auth_middleware.dart +50 -0
  15. package/content/skills/dart-frog/examples/controller.dart +75 -0
  16. package/content/skills/dart-frog/examples/dto.dart +41 -0
  17. package/content/skills/dart-frog/examples/error_response.dart +35 -0
  18. package/content/skills/dart-frog/examples/exceptions.dart +60 -0
  19. package/content/skills/dart-frog/examples/middleware_composition.dart +34 -0
  20. package/content/skills/dart-frog/examples/model.dart +20 -0
  21. package/content/skills/dart-frog/examples/port.dart +17 -0
  22. package/content/skills/dart-frog/examples/repository.dart +56 -0
  23. package/content/skills/dart-frog/examples/route_collection.dart +25 -0
  24. package/content/skills/dart-frog/examples/route_item.dart +24 -0
  25. package/content/skills/dart-frog/examples/route_test.dart +33 -0
  26. package/content/skills/dart-frog/examples/service.dart +51 -0
  27. package/content/skills/dart-frog/examples/service_test.dart +55 -0
  28. package/content/skills/dart-frog/references/checklist.md +31 -0
  29. package/content/skills/dart-frog/references/error-handling.md +48 -0
  30. package/content/skills/dart-frog/references/fpdart-taskeither.md +53 -0
  31. package/content/skills/dart-frog/references/layers.md +43 -0
  32. package/content/skills/dart-frog/references/middlewares-di.md +45 -0
  33. package/content/skills/dart-frog/references/project-structure.md +67 -0
  34. package/content/skills/dart-frog/references/routing.md +53 -0
  35. package/content/skills/dart-frog/references/testing.md +24 -0
  36. package/content/skills/docker/SKILL.md +3 -3
  37. package/content/skills/driftorm/SKILL.md +249 -0
  38. package/content/skills/flutter/SKILL.md +419 -0
  39. package/content/skills/hex-pattern-dart/SKILL.md +83 -0
  40. package/content/skills/hex-pattern-dart/examples/adapter-in.md +24 -0
  41. package/content/skills/hex-pattern-dart/examples/adapter-out.md +20 -0
  42. package/content/skills/hex-pattern-dart/examples/errors.md +25 -0
  43. package/content/skills/hex-pattern-dart/examples/port.md +11 -0
  44. package/content/skills/hex-pattern-dart/examples/service.md +20 -0
  45. package/content/skills/hex-pattern-dart/examples/structure-tree.md +28 -0
  46. package/content/skills/hex-pattern-dart/references/anti-patterns.md +16 -0
  47. package/content/skills/hex-pattern-dart/references/checklist.md +34 -0
  48. package/content/skills/hex-pattern-dart/references/dependency-injection.md +32 -0
  49. package/content/skills/hex-pattern-dart/references/error-layers.md +27 -0
  50. package/content/skills/hex-pattern-dart/references/layers-and-dependency-rule.md +29 -0
  51. package/content/skills/hex-pattern-dart/references/persistence.md +23 -0
  52. package/content/skills/hex-pattern-dart/references/ports-and-adapters.md +33 -0
  53. package/content/skills/hex-pattern-dart/references/testing.md +30 -0
  54. package/content/skills/mysql/SKILL.md +3 -3
  55. package/content/skills/typescript/SKILL.md +4 -4
  56. package/dist/src/content.js +10 -2
  57. package/dist/src/init.js +1 -0
  58. package/dist/src/search.js +20 -12
  59. package/dist/src/server.js +60 -4
  60. package/dist/src/skill-files.js +125 -0
  61. package/package.json +2 -2
  62. package/content/agents/db-specialist.agent.md +0 -78
  63. package/content/agents/nestjs-api-designer.agent.md +0 -61
  64. package/content/skills/hexagonal/SKILL.md +0 -156
  65. package/content/skills/jest-testing/SKILL.md +0 -88
  66. package/content/skills/nestjs/SKILL.md +0 -100
  67. package/content/skills/typeorm/SKILL.md +0 -119
  68. package/dist/test/mcp.test.js +0 -112
@@ -0,0 +1,34 @@
1
+ # Checklist de review — hexagonal DS9 (Dart)
2
+
3
+ ## Estrutura
4
+ - [ ] Organização por contexto com `domain/{models,ports,services}` e `adapter/{controllers,dtos,repository}`.
5
+ - [ ] Imports relativos dentro de `lib/`.
6
+ - [ ] Nenhum contexto importa `domain`/`adapter` de outro.
7
+
8
+ ## Dependência
9
+ - [ ] `domain` sem import de `adapter`, framework, driver ou tipos de protocolo.
10
+ - [ ] Ports como `abstract interface class` em `domain/ports`.
11
+ - [ ] Repositórios implementam ports e vivem em `adapter/repository`.
12
+
13
+ ## Erros
14
+ - [ ] `TaskEither` + `TaskEither.tryCatch` em toda operação falível.
15
+ - [ ] `BusinessException` → `ClientException` → `MiddlewareException`.
16
+ - [ ] 5xx com mensagem genérica; sem `toString()`/stack no cliente.
17
+ - [ ] Sem `catch (_)` genérico nem exceção lançada entre camadas.
18
+
19
+ ## Composição
20
+ - [ ] Dependências por construtor e montadas no composition root.
21
+ - [ ] Nada instanciado dentro de rota/handler/controller.
22
+ - [ ] `fpdart` presente no `pubspec.yaml` (ou usuário informado).
23
+
24
+ ## Responsabilidades
25
+ - [ ] Controller sem regra de negócio; repositório sem regra de negócio.
26
+ - [ ] Autorização de recurso no service.
27
+ - [ ] DTO fora do `domain`; mapper entre persistência e model.
28
+
29
+ ## Testes
30
+ - [ ] Service testado com fakes das ports (sucesso e cada erro).
31
+ - [ ] `test/` espelha `lib/`.
32
+
33
+ ## Dart
34
+ - [ ] Regras da skill `dart`: nomenclatura, `///` em APIs públicas, OOP, null safety.
@@ -0,0 +1,32 @@
1
+ # Injeção de dependência e composition root
2
+
3
+ ## Princípio
4
+
5
+ Existe **um** lugar onde o grafo de dependências é montado (composition root). O restante do código recebe dependências por construtor e só conhece abstrações (ports).
6
+
7
+ ## Regras
8
+
9
+ 1. Dependências entram por construtor, como `final` e por interface.
10
+ 2. Nunca instancie service, repositório ou controller dentro de rota/handler/controller.
11
+ 3. O composition root escolhe a implementação concreta de cada port.
12
+ 4. Ordem de montagem: infraestrutura → repositórios → services → controllers.
13
+ 5. Reaproveite instâncias (cache/singleton no composition root) quando o recurso for caro (conexão, cliente HTTP).
14
+ 6. Em frameworks com middleware ou provider, use o mecanismo do framework como composition root (ver a skill do framework, ex.: `dart-frog`).
15
+ 7. Em testes, o composition root é substituído por fakes das ports.
16
+
17
+ ## Pseudocódigo
18
+
19
+ ```text
20
+ compose():
21
+ dataSource = ConcreteDataSource(config)
22
+ repository = OrderRepository(dataSource)
23
+ service = OrderService(repository)
24
+ controller = OrderController(service)
25
+ return controller
26
+ ```
27
+
28
+ ## Evite
29
+
30
+ - Localizador global (`ServiceLocator.get()`) dentro do `domain`.
31
+ - Dependência criada com `new`/construtor direto dentro do método que a usa.
32
+ - Ler configuração/ambiente fora do composition root.
@@ -0,0 +1,27 @@
1
+ # Camadas de erro
2
+
3
+ Os nomes das camadas são fixos em todo projeto DS9.
4
+
5
+ | Camada | Tipo | Onde nasce | Campos | Quem consome |
6
+ |---|---|---|---|---|
7
+ | Negócio | `BusinessException` | service / repository | `domainName`, `statusCode`, `message` (segura), `error`, `stackTrace` | controller |
8
+ | Cliente | `ClientException` | controller | `statusCode`, `message` | borda (handler/middleware) |
9
+ | Borda | `MiddlewareException` | middleware / handler | `statusCode`, `message`, `stackTrace?` | resposta final |
10
+
11
+ ## Regras
12
+
13
+ 1. Falhas esperadas viajam como valor: `TaskEither.left(BusinessException(...))`.
14
+ 2. `tryCatch` converte qualquer exceção inesperada em `BusinessException`, preservando `error` e `stackTrace` para log.
15
+ 3. Mensagem para o cliente é sempre segura. Para 5xx use texto genérico; detalhes ficam no log.
16
+ 4. Nunca devolva `error.toString()` nem stack ao cliente.
17
+ 5. Evite `catch (_)` genérico sem converter para a camada correta.
18
+ 6. `domainName` identifica o contexto de origem e ajuda a rastrear logs.
19
+
20
+ ## Fluxo
21
+
22
+ ```text
23
+ repository.tryCatch(falha) ──► BusinessException
24
+ service repassa / enriquece ──► BusinessException
25
+ controller.mapLeft ──────────► ClientException (5xx genérico)
26
+ borda.match ─────────────────► resposta de protocolo
27
+ ```
@@ -0,0 +1,29 @@
1
+ # Camadas e regra de dependência
2
+
3
+ ## Sentido das dependências
4
+
5
+ ```text
6
+ adapter/controllers ─┐
7
+ adapter/dtos ────────┼──► domain/services ──► domain/ports ◄── adapter/repository
8
+ │ │
9
+ └──────────┴──► domain/models
10
+ ```
11
+
12
+ - `domain` conhece apenas `domain` e `common`.
13
+ - `adapter` conhece `domain` e `common`.
14
+ - O `domain` declara a port; o `adapter/repository` a implementa (inversão de dependência).
15
+
16
+ ## O que pertence a cada camada
17
+
18
+ - **models:** entidades, value objects, regras que valem em qualquer protocolo ou banco.
19
+ - **ports:** contratos de saída (persistência, mensageria, serviços externos). Nomeie `<Papel>Port`: `OrderRepositoryPort`, `PaymentGatewayPort`.
20
+ - **services:** um caso de uso por método público. Aplicam regra e autorização de recurso.
21
+ - **controllers/dtos:** tudo que depende de protocolo (HTTP, fila, CLI).
22
+ - **repository:** tudo que depende de infraestrutura (banco, API externa).
23
+
24
+ ## Sinais de violação
25
+
26
+ - `import` de pacote de framework, driver ou `adapter` dentro de `domain/`.
27
+ - DTO ou tipo de request/response aparecendo em assinatura de service.
28
+ - Service que monta SQL, chama HTTP ou lê variável de ambiente.
29
+ - Controller com `if` de regra de negócio.
@@ -0,0 +1,23 @@
1
+ # Persistência agnóstica
2
+
3
+ O padrão não prescreve banco nem ORM. Para a tecnologia do projeto, consulte a skill correspondente via `list_skills`/`recommend_for_task` e preserve o que já existe.
4
+
5
+ ## Contrato
6
+
7
+ - O `domain` declara a port (`OrderRepositoryPort`).
8
+ - O `adapter/repository` implementa a port usando a tecnologia do projeto.
9
+ - Opcionalmente, o repositório delega a um `DataSource` (abstração do acesso físico), facilitando trocar ou simular o banco.
10
+
11
+ ## Mapper
12
+
13
+ - Modelo de persistência (linha/documento) e model do domínio são tipos distintos.
14
+ - Um mapper converte nos dois sentidos e vive no `adapter/repository`.
15
+ - O `domain` nunca importa o tipo de persistência.
16
+
17
+ ## Regras
18
+
19
+ 1. Toda chamada de I/O dentro de `TaskEither.tryCatch`.
20
+ 2. Falha vira `BusinessException` com mensagem segura.
21
+ 3. "Não encontrado" é resultado explícito (`left` com status adequado ou `Option`), nunca `null` implícito.
22
+ 4. Transação: abra no adapter; o service expõe a operação de negócio completa, sem conhecer a transação.
23
+ 5. Migrations seguem a skill de persistência do projeto e não alteram estrutura existente sem pedido.
@@ -0,0 +1,33 @@
1
+ # Ports e adapters
2
+
3
+ ## Ports (saída)
4
+
5
+ - Declare como `abstract interface class` dentro de `domain/ports/`.
6
+ - Expresse a necessidade do domínio, não a tecnologia: `save(order)`, não `insertRow(map)`.
7
+ - Assinaturas usam apenas models e tipos de `common/`.
8
+ - Toda operação que pode falhar retorna `TaskEither<BusinessException, T>`.
9
+
10
+ ```text
11
+ abstract interface class OrderRepositoryPort
12
+ TaskEither<BusinessException, Order> findById(String id)
13
+ TaskEither<BusinessException, Unit> save(Order order)
14
+ ```
15
+
16
+ ## Adapters de entrada (controllers)
17
+
18
+ - Recebem o contexto do protocolo (request) a cada chamada de método.
19
+ - Validam o DTO, chamam o service e mapeiam o resultado para resposta.
20
+ - Mapeiam `BusinessException` → `ClientException`; 5xx com mensagem genérica.
21
+ - Recebem o service por construtor; nunca o criam.
22
+
23
+ ## Adapters de saída (repository)
24
+
25
+ - Implementam a port e escondem o detalhe de persistência.
26
+ - Mapeiam modelo de persistência ↔ model do domínio (mapper dedicado).
27
+ - Envolvem cada chamada externa em `TaskEither.tryCatch` e convertem a falha em `BusinessException`.
28
+
29
+ ## Nomenclatura
30
+
31
+ - Arquivos `snake_case`; classes `UpperCamelCase`.
32
+ - Port: `<Papel>Port` (`OrderRepositoryPort`). Implementação: nome do papel sem sufixo (`OrderRepository`), sem prefixo `I` e sem sufixo `Impl`.
33
+ - DTO: `<Acao><Entidade>Dto` / `<Entidade>Response`.
@@ -0,0 +1,30 @@
1
+ # Testes na arquitetura hexagonal
2
+
3
+ ## Estratégia
4
+
5
+ | Alvo | Como testar | Dublês |
6
+ |---|---|---|
7
+ | `model` | Unitário puro | Nenhum |
8
+ | `service` | Unitário com ports falsas | Fakes/mocks das ports |
9
+ | `controller` | Unitário com service falso e contexto de protocolo simulado | Service falso |
10
+ | `repository` | Integração com a tecnologia real ou em memória | Opcional |
11
+ | Borda (rota/handler) | Teste de contrato: status, corpo e método não suportado | Composition root de teste |
12
+
13
+ ## Regras
14
+
15
+ 1. A árvore de `test/` espelha `lib/`.
16
+ 2. Teste o caminho feliz **e** cada `left` (erro de negócio, erro inesperado, 5xx genérico).
17
+ 3. Verifique que o 5xx não vaza mensagem interna.
18
+ 4. Para `TaskEither`, execute com `.run()` e inspecione o `Either` resultante.
19
+ 5. Prefira fakes simples a mocks com muitas expectativas quando a port for pequena.
20
+ 6. Nenhum teste de `domain` pode precisar de rede, banco ou framework.
21
+ 7. Nome: `<comportamento> quando <condição>`.
22
+
23
+ ## Pseudocódigo
24
+
25
+ ```text
26
+ test "retorna Right quando o pedido existe":
27
+ repo = FakeOrderRepository(with: order)
28
+ result = await OrderService(repo).getById("1").run()
29
+ expect(result.isRight)
30
+ ```
@@ -5,7 +5,7 @@ description: Diretrizes de modelagem, indexação, migrations, performance e seg
5
5
 
6
6
  # MySQL — Diretrizes DS9
7
7
 
8
- Convenções obrigatórias para schema, queries e migrations em MySQL (compatível com MariaDB), usadas em conjunto com a skill `typeorm` para a camada de persistência em projetos NestJS/hexagonal.
8
+ Convenções obrigatórias para schema, queries e migrations em MySQL (compatível com MariaDB), usadas com a camada de persistência do projeto.
9
9
 
10
10
  ## Engine e charset
11
11
 
@@ -52,7 +52,7 @@ deleted_at DATETIME(3) NULL -- se o domínio usa soft delete
52
52
 
53
53
  ## Migrations
54
54
 
55
- - Toda alteração de schema é uma migration versionada (via TypeORM migrations — ver skill `typeorm`), nunca alteração manual direto no banco de produção.
55
+ - Toda alteração de schema é uma migration versionada (via o mecanismo de migrations do projeto), nunca alteração manual direto no banco de produção.
56
56
  - Migrations são **imutáveis** após merge/deploy: para corrigir, crie uma nova migration (nunca edite uma já aplicada em outro ambiente).
57
57
  - Toda migration precisa de `up` e `down` reversível. Se o `down` não for possível com segurança (ex.: `DROP COLUMN`), documente explicitamente no corpo da migration.
58
58
  - Alterações em tabelas grandes (`ADD COLUMN`, `ADD INDEX`) em produção devem considerar lock de tabela: preferir `ALGORITHM=INPLACE, LOCK=NONE` quando suportado, e avaliar janela de manutenção para tabelas muito grandes.
@@ -66,7 +66,7 @@ deleted_at DATETIME(3) NULL -- se o domínio usa soft delete
66
66
 
67
67
  ## Queries
68
68
 
69
- - Nunca concatene valores de usuário diretamente em SQL — sempre use *prepared statements*/query builder parametrizado (via TypeORM). Isso é mandatório contra SQL Injection.
69
+ - Nunca concatene valores de usuário diretamente em SQL — sempre use *prepared statements*/query builder parametrizado (via a biblioteca de acesso a dados do projeto). Isso é mandatório contra SQL Injection.
70
70
  - Evite `SELECT *`; liste as colunas necessárias, especialmente em tabelas largas.
71
71
  - Pagine toda listagem (`LIMIT`/`OFFSET` ou keyset pagination para datasets grandes); nunca retorne uma tabela inteira sem limite.
72
72
  - Prefira keyset pagination (`WHERE id > :lastId ORDER BY id LIMIT :n`) a `OFFSET` alto em tabelas grandes (OFFSET grande é O(n)).
@@ -5,7 +5,7 @@ description: Convenções de TypeScript da DS9 — tipagem estrita, modelagem de
5
5
 
6
6
  # TypeScript — Diretrizes DS9
7
7
 
8
- Convenções de código TypeScript aplicáveis a qualquer projeto da empresa, incluindo os que usam NestJS (skill `nestjs`) e arquitetura hexagonal (skill `hexagonal`).
8
+ Convenções de código TypeScript aplicáveis a qualquer projeto da empresa, incluindo os que seguem arquitetura hexagonal em geral.
9
9
 
10
10
  ## Configuração do compilador
11
11
 
@@ -31,7 +31,7 @@ function handle(payload: OrderPayload) { return payload.items.map((i) => i.id);
31
31
 
32
32
  ## Modelagem de domínio
33
33
 
34
- - Use *value objects* para conceitos com regras próprias (ex.: `Email`, `Money`), em vez de `string`/`number` soltos, quando o projeto já segue esse padrão (ver skill `hexagonal`).
34
+ - Use *value objects* para conceitos com regras próprias (ex.: `Email`, `Money`), em vez de `string`/`number` soltos, quando o projeto já segue esse padrão.
35
35
  - Invariantes de negócio são validadas no construtor/factory da entidade (`Order.create(...)`), lançando erro de domínio em vez de permitir estado inválido existir.
36
36
  - Prefira classes/funções imutáveis (métodos retornam uma nova instância) para entidades de domínio sensíveis a concorrência, salvo quando o projeto adota mutação controlada por padrão.
37
37
 
@@ -44,7 +44,7 @@ function handle(payload: OrderPayload) { return payload.items.map((i) => i.id);
44
44
  ## Imports e organização de módulos
45
45
 
46
46
  - Imports absolutos/com alias (`@app/domain/...`) para cruzar camadas, imports relativos (`./`, `../`) dentro do mesmo módulo/camada — configurar via `paths` no `tsconfig.json`.
47
- - Nunca import circular entre módulos; se ocorrer, é sinal de fronteira de módulo mal definida (revisar com a skill `hexagonal`).
47
+ - Nunca import circular entre módulos; se ocorrer, é sinal de fronteira de módulo mal definida (revisar a organização dos módulos).
48
48
  - Um export principal por arquivo quando possível (facilita navegação e testes); evite arquivos "barril" (`index.ts`) gigantes que reexportam dezenas de símbolos sem necessidade.
49
49
 
50
50
  ## Nulidade e valores opcionais
@@ -57,7 +57,7 @@ function handle(payload: OrderPayload) { return payload.items.map((i) => i.id);
57
57
  - Classes/interfaces/tipos: `PascalCase`. Variáveis/funções: `camelCase`. Constantes verdadeiramente globais/imutáveis: `UPPER_SNAKE_CASE`.
58
58
  - Nomes de função descrevem a ação (`calculateTotal`, não `total` ou `doIt`); booleanos começam com `is`/`has`/`should` (`isValid`, `hasDiscount`).
59
59
  - Evite abreviações não óbvias (`qty` ok se for convenção do domínio; `tmp`, `data2` não).
60
- - Nome de **arquivo**: `kebab-case`, com sufixo que indica o papel do arquivo quando aplicável (`order-repository.port.ts`, `create-order.use-case.ts`, `typeorm-order.repository.ts`, `order.entity.ts`, `order.controller.ts`, `order.module.ts`). Arquivo de teste usa o mesmo nome base + `.spec.ts`/`.e2e-spec.ts` (ver skill `jest-testing`).
60
+ - Nome de **arquivo**: `kebab-case`, com sufixo que indica o papel do arquivo quando aplicável (`order-repository.port.ts`, `create-order.use-case.ts`, `order.repository.ts`, `order.entity.ts`, `order.controller.ts`, `order.module.ts`). Arquivo de teste usa o mesmo nome base + `.spec.ts`/`.e2e-spec.ts` (seguindo a convenção de testes do projeto).
61
61
 
62
62
  ## Checklist ao escrever/revisar código TypeScript
63
63
 
@@ -9,6 +9,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { fileURLToPath } from "node:url";
11
11
  import { parse as parseYaml } from "yaml";
12
+ import { scanSkillFiles } from "./skill-files.js";
12
13
  // Frontmatter = bloco YAML entre dois `---` no topo do arquivo. Aceita \r\n porque os arquivos
13
14
  // podem ter sido salvos no Windows.
14
15
  const FRONTMATTER = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
@@ -50,9 +51,12 @@ export function resolveContentRoot() {
50
51
  * Skill = `skills/<pasta>/SKILL.md` (id = nome da pasta).
51
52
  * Agente = `agents/<nome>.agent.md` (id = nome do arquivo; estrutura plana, sem subpastas).
52
53
  */
53
- export function loadEntries(root = resolveContentRoot()) {
54
+ export function loadEntries(root = resolveContentRoot(),
55
+ // Por padrão os avisos vão para stderr; testes podem injetar um coletor.
56
+ onWarning = (w) => console.error(`[ds9-mcp] aviso: ${w}`)) {
54
57
  const entries = [];
55
58
  const warnings = [];
59
+ const fileWarnings = [];
56
60
  const skillsDir = join(root, "skills");
57
61
  for (const dirent of readdirSync(skillsDir, { withFileTypes: true })) {
58
62
  const file = join(skillsDir, dirent.name, "SKILL.md");
@@ -71,7 +75,10 @@ export function loadEntries(root = resolveContentRoot()) {
71
75
  name: String(data.name ?? dirent.name),
72
76
  description: String(data.description ?? ""),
73
77
  body,
78
+ files: scanSkillFiles(join(skillsDir, dirent.name), fileWarnings),
74
79
  });
80
+ for (const w of fileWarnings.splice(0))
81
+ warnings.push(`skill "${dirent.name}": ${w}`);
75
82
  }
76
83
  const agentsDir = join(root, "agents");
77
84
  if (existsSync(agentsDir)) {
@@ -87,12 +94,13 @@ export function loadEntries(root = resolveContentRoot()) {
87
94
  name: String(data.name ?? id),
88
95
  description: String(data.description ?? ""),
89
96
  body,
97
+ files: [],
90
98
  });
91
99
  }
92
100
  }
93
101
  // stderr, nunca stdout: no transporte stdio o stdout é o canal do protocolo MCP, e qualquer
94
102
  // texto extra nele corromperia a comunicação com a IDE.
95
103
  for (const w of warnings)
96
- console.error(`[ds9-mcp] aviso: ${w}`);
104
+ onWarning(w);
97
105
  return entries;
98
106
  }
package/dist/src/init.js CHANGED
@@ -78,6 +78,7 @@ Antes de implementar, revisar ou testar código:
78
78
  1. Chame a tool \`recommend_for_task\` com a descrição da tarefa.
79
79
  2. Leia as skills e agentes recomendados com \`get_skill\` / \`get_agent\` e siga as diretrizes.
80
80
  3. Use \`search_guidelines\` para dúvidas pontuais.
81
+ 4. Se a skill tiver arquivos complementares, leia-os com \`list_skill_files\` / \`get_skill_file\`.
81
82
  ${BLOCK_END}`;
82
83
  /**
83
84
  * Insere o bloco DS9 num AGENTS.md ou, se já existir, substitui apenas o trecho entre os
@@ -68,18 +68,26 @@ export function searchBodies(entries, query, limit = 8) {
68
68
  return [];
69
69
  const hits = [];
70
70
  for (const entry of entries) {
71
- entry.body.split(/\r?\n/).forEach((line, i) => {
72
- const score = count(line, tokens);
73
- if (score > 0 && line.trim()) {
74
- hits.push({
75
- kind: entry.kind,
76
- id: entry.id,
77
- line: i + 1,
78
- text: line.trim().slice(0, 300), // limita linhas gigantes
79
- score,
80
- });
81
- }
82
- });
71
+ // Corpo principal + arquivos complementares (exemplos, referências) da skill.
72
+ const sources = [
73
+ { file: undefined, content: entry.body },
74
+ ...entry.files.map((f) => ({ file: f.path, content: f.text })),
75
+ ];
76
+ for (const src of sources) {
77
+ src.content.split(/\r?\n/).forEach((line, i) => {
78
+ const score = count(line, tokens);
79
+ if (score > 0 && line.trim()) {
80
+ hits.push({
81
+ kind: entry.kind,
82
+ id: entry.id,
83
+ ...(src.file ? { file: src.file } : {}),
84
+ line: i + 1,
85
+ text: line.trim().slice(0, 300), // limita linhas gigantes
86
+ score,
87
+ });
88
+ }
89
+ });
90
+ }
83
91
  }
84
92
  return hits
85
93
  .sort((a, b) => b.score - a.score)
@@ -9,12 +9,14 @@
9
9
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
10
10
  import { z } from "zod";
11
11
  import { rankByMetadata, searchBodies } from "./search.js";
12
+ import { isSafeRelativePath, MAX_PATH_LENGTH, SKILL_ID } from "./skill-files.js";
12
13
  // Enviado ao host no handshake. Hosts sem suporte nativo a skills/agentes dependem disto (e do
13
14
  // bloco no AGENTS.md) para saber que devem consultar o servidor antes de implementar.
14
15
  const INSTRUCTIONS = [
15
16
  "Servidor de diretrizes oficiais da DS9 (skills por tecnologia e subagentes especializados).",
16
17
  "Antes de implementar, revisar ou testar código, chame `recommend_for_task` com a descrição da tarefa",
17
18
  "e depois `get_skill` / `get_agent` para ler as diretrizes recomendadas e segui-las.",
19
+ "Se a skill indicar arquivos complementares, use `list_skill_files` e `get_skill_file` para ler exemplos e referências sob demanda.",
18
20
  ].join(" ");
19
21
  // Monta a resposta padrão de uma tool. `isError` sinaliza ao LLM que a chamada falhou, mas sem
20
22
  // derrubar o protocolo (erro de uso != erro do servidor).
@@ -31,8 +33,21 @@ export function createServer(entries, version = "0.0.0") {
31
33
  // Helpers de consulta sobre as entradas.
32
34
  const byKind = (kind) => entries.filter((e) => e.kind === kind);
33
35
  const find = (kind, id) => entries.find((e) => e.kind === kind && e.id === id);
34
- const render = (e) => `# ${e.kind === "skill" ? "Skill" : "Agente"}: ${e.id}\n\n> ${e.description}\n\n${e.body}`;
35
- const listing = (items) => items.map((e) => `- ${e.id}: ${e.description}`).join("\n") || "(nenhum resultado)";
36
+ const render = (e) => `# ${e.kind === "skill" ? "Skill" : "Agente"}: ${e.id}\n\n> ${e.description}\n\n${e.body}` +
37
+ (e.files.length
38
+ ? `\n\n---\nArquivos complementares (${e.files.length}): use \`list_skill_files\` e \`get_skill_file\` com skill="${e.id}" para exemplos e referências sob demanda.`
39
+ : "");
40
+ const listing = (items) => items
41
+ .map((e) => `- ${e.id}: ${e.description}${e.files.length ? ` (+${e.files.length} arquivos)` : ""}`)
42
+ .join("\n") || "(nenhum resultado)";
43
+ // Erros de skill/arquivo nunca citam caminhos do disco: só ids e caminhos do índice.
44
+ const skillOrError = (skill) => {
45
+ const entry = find("skill", skill);
46
+ return entry
47
+ ? { entry }
48
+ : { error: text(`skill "${skill}" não encontrada. Disponíveis:\n${listing(byKind("skill"))}`, true) };
49
+ };
50
+ const fileList = (e) => e.files.map((f) => `- ${f.path} (${f.size} bytes)${f.description ? `: ${f.description}` : ""}`).join("\n");
36
51
  // Skills e agentes têm exatamente a mesma API (list/get/prompt/resource). Em vez de duplicar o
37
52
  // código, registramos os dois em um laço; só mudam os nomes e os rótulos.
38
53
  for (const [kind, plural, label] of [
@@ -63,7 +78,7 @@ export function createServer(entries, version = "0.0.0") {
63
78
  }
64
79
  return text(render(entry));
65
80
  });
66
- // Um prompt por entrada (ex.: `skill-nestjs`). Fica estático porque o catálogo é conhecido na
81
+ // Um prompt por entrada (ex.: `skill-hex-pattern-dart`). Fica estático porque o catálogo é conhecido na
67
82
  // inicialização do servidor.
68
83
  for (const entry of byKind(kind)) {
69
84
  server.registerPrompt(`${kind}-${entry.id}`, { description: entry.description }, () => ({
@@ -91,6 +106,45 @@ export function createServer(entries, version = "0.0.0") {
91
106
  return { contents: [{ uri: uri.href, mimeType: "text/markdown", text: render(entry) }] };
92
107
  });
93
108
  }
109
+ // list_skill_files / get_skill_file — carga sob demanda dos arquivos complementares de uma skill.
110
+ // Só leem o índice em memória montado no carregamento; o cliente nunca informa caminho de disco.
111
+ const skillSchema = z
112
+ .string()
113
+ .regex(SKILL_ID, "id de skill inválido")
114
+ .describe("Id da skill (veja list_skills)");
115
+ server.registerTool("list_skill_files", {
116
+ description: "Lista os arquivos complementares (exemplos, referências) de uma skill DS9, com tamanho e descrição.",
117
+ inputSchema: { skill: skillSchema },
118
+ annotations: { readOnlyHint: true },
119
+ }, async ({ skill }) => {
120
+ const { entry, error } = skillOrError(skill);
121
+ if (!entry)
122
+ return error;
123
+ return text(entry.files.length
124
+ ? `Arquivos da skill "${skill}" (leia com get_skill_file):\n${fileList(entry)}`
125
+ : `A skill "${skill}" não possui arquivos complementares.`);
126
+ });
127
+ server.registerTool("get_skill_file", {
128
+ description: "Retorna o conteúdo de um arquivo complementar de uma skill DS9. Use o caminho exato de list_skill_files.",
129
+ inputSchema: {
130
+ skill: skillSchema,
131
+ path: z
132
+ .string()
133
+ .max(MAX_PATH_LENGTH)
134
+ .describe("Caminho relativo listado por list_skill_files, ex.: examples/entity.dart"),
135
+ },
136
+ annotations: { readOnlyHint: true },
137
+ }, async ({ skill, path }) => {
138
+ const { entry, error } = skillOrError(skill);
139
+ if (!entry)
140
+ return error;
141
+ // Busca exata no índice: caminhos fora dele (inclusive `..`, absolutos etc.) simplesmente não existem.
142
+ const file = isSafeRelativePath(path) ? entry.files.find((f) => f.path === path) : undefined;
143
+ if (!file) {
144
+ return text(`Arquivo não encontrado na skill "${skill}". Arquivos válidos:\n${fileList(entry) || "(nenhum)"}`, true);
145
+ }
146
+ return text(`# ${skill}/${file.path}\n\n${file.text}`);
147
+ });
94
148
  // search_guidelines — para dúvidas pontuais ("como nomear migrations?") sem ler arquivos inteiros.
95
149
  server.registerTool("search_guidelines", {
96
150
  description: "Busca um termo no conteúdo de todas as skills e agentes DS9 e devolve as linhas mais relevantes.",
@@ -99,7 +153,9 @@ export function createServer(entries, version = "0.0.0") {
99
153
  }, async ({ query, limit }) => {
100
154
  const hits = searchBodies(entries, query, limit ?? 8);
101
155
  return text(hits.length
102
- ? hits.map((h) => `[${h.kind}:${h.id}:L${h.line}] ${h.text}`).join("\n")
156
+ ? hits
157
+ .map((h) => `[${h.kind}:${h.id}${h.file ? `:${h.file}` : ""}:L${h.line}] ${h.text}`)
158
+ .join("\n")
103
159
  : "Nenhum trecho encontrado.");
104
160
  });
105
161
  // recommend_for_task — é a "ativação automática" possível via MCP: compensa a falta de roteamento
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Arquivos complementares de uma skill (`skills/<id>/examples/`, `references/` etc.).
3
+ *
4
+ * Segurança — por que é uma allowlist e não uma sanitização de caminho:
5
+ * - tudo é indexado UMA vez, no carregamento, pelo próprio servidor;
6
+ * - `get_skill_file` só encontra caminhos que estão nesse índice (comparação exata de string);
7
+ * - o texto vem da memória: nenhuma chamada de tool toca o disco nem monta caminho com entrada do
8
+ * cliente. Não há como escapar da pasta da skill porque o cliente nunca fornece um caminho de
9
+ * arquivo, apenas uma chave de busca.
10
+ * A varredura ainda aplica limites e ignora symlinks, ocultos e extensões fora da lista.
11
+ */
12
+ import { lstatSync, readdirSync, readFileSync, realpathSync } from "node:fs";
13
+ import { join, sep } from "node:path";
14
+ export const ALLOWED_EXTENSIONS = new Set([
15
+ ".md",
16
+ ".dart",
17
+ ".yaml",
18
+ ".yml",
19
+ ".json",
20
+ ".txt",
21
+ ]);
22
+ export const MAX_FILE_BYTES = 100 * 1024;
23
+ export const MAX_FILES_PER_SKILL = 200;
24
+ export const MAX_DEPTH = 4;
25
+ export const MAX_PATH_LENGTH = 200;
26
+ // SKILL.md é servido por `get_skill`; README.md não é copiado pelo `sync-content`, então ignorá-lo
27
+ // aqui mantém o comportamento igual em desenvolvimento e em produção.
28
+ const IGNORED_NAMES = new Set(["skill.md", "readme.md", "node_modules"]);
29
+ /** Id de skill válido: é o nome da pasta, em kebab-case. */
30
+ export const SKILL_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
31
+ // eslint-disable-next-line no-control-regex
32
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f]/;
33
+ /**
34
+ * Valida o formato do caminho informado pelo cliente. É uma barreira extra de entrada; a proteção
35
+ * real é a busca exata no índice. Rejeita tudo que não seja um caminho relativo simples com `/`.
36
+ */
37
+ export function isSafeRelativePath(path) {
38
+ if (path.length === 0 || path.length > MAX_PATH_LENGTH)
39
+ return false;
40
+ if (CONTROL_CHARS.test(path))
41
+ return false;
42
+ if (path.includes("\\") || path.includes(":") || path.includes("%"))
43
+ return false;
44
+ if (path.startsWith("/"))
45
+ return false;
46
+ return path.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
47
+ }
48
+ const UTF8 = new TextDecoder("utf-8", { fatal: true });
49
+ /** Primeira linha útil do arquivo: título Markdown ou comentário `///`/`//`. */
50
+ function describe(text) {
51
+ for (const raw of text.split(/\r?\n/)) {
52
+ const line = raw.trim();
53
+ if (!line)
54
+ continue;
55
+ const cleaned = line.replace(/^(#+|\/\/\/?|\*|>)\s*/, "").trim();
56
+ if (cleaned && cleaned !== "---")
57
+ return cleaned.slice(0, 160);
58
+ }
59
+ return "";
60
+ }
61
+ /**
62
+ * Varre a pasta de uma skill e devolve os arquivos complementares permitidos, em ordem estável.
63
+ * Problemas (arquivo grande, extensão proibida, symlink etc.) viram avisos e o arquivo é ignorado:
64
+ * um item ruim nunca derruba o carregamento das demais skills.
65
+ */
66
+ export function scanSkillFiles(skillDir, warnings) {
67
+ const files = [];
68
+ const rootReal = realpathSync(skillDir);
69
+ const walk = (dir, prefix, depth) => {
70
+ for (const name of readdirSync(dir).sort()) {
71
+ const rel = prefix ? `${prefix}/${name}` : name;
72
+ const abs = join(dir, name);
73
+ if (name.startsWith(".") || IGNORED_NAMES.has(name.toLowerCase()))
74
+ continue;
75
+ const stat = lstatSync(abs);
76
+ if (stat.isSymbolicLink()) {
77
+ warnings.push(`"${rel}" ignorado: link simbólico`);
78
+ continue;
79
+ }
80
+ if (stat.isDirectory()) {
81
+ if (depth >= MAX_DEPTH)
82
+ warnings.push(`"${rel}" ignorado: profundidade acima de ${MAX_DEPTH}`);
83
+ else
84
+ walk(abs, rel, depth + 1);
85
+ continue;
86
+ }
87
+ if (!stat.isFile())
88
+ continue;
89
+ const ext = name.includes(".") ? name.slice(name.lastIndexOf(".")).toLowerCase() : "";
90
+ if (!ALLOWED_EXTENSIONS.has(ext)) {
91
+ warnings.push(`"${rel}" ignorado: extensão não permitida`);
92
+ continue;
93
+ }
94
+ if (stat.size > MAX_FILE_BYTES) {
95
+ warnings.push(`"${rel}" ignorado: maior que ${MAX_FILE_BYTES} bytes`);
96
+ continue;
97
+ }
98
+ if (files.length >= MAX_FILES_PER_SKILL) {
99
+ warnings.push(`"${rel}" ignorado: limite de ${MAX_FILES_PER_SKILL} arquivos por skill`);
100
+ continue;
101
+ }
102
+ if (!rel.split("/").every((s) => isSafeRelativePath(s))) {
103
+ warnings.push(`"${rel}" ignorado: nome de arquivo inválido`);
104
+ continue;
105
+ }
106
+ // Confinamento: o arquivo real precisa estar dentro da pasta da skill.
107
+ const real = realpathSync(abs);
108
+ if (!real.startsWith(rootReal + sep)) {
109
+ warnings.push(`"${rel}" ignorado: fora da pasta da skill`);
110
+ continue;
111
+ }
112
+ let text;
113
+ try {
114
+ text = UTF8.decode(readFileSync(real));
115
+ }
116
+ catch {
117
+ warnings.push(`"${rel}" ignorado: não é texto UTF-8 válido`);
118
+ continue;
119
+ }
120
+ files.push({ path: rel, size: stat.size, description: describe(text), text });
121
+ }
122
+ };
123
+ walk(skillDir, "", 1);
124
+ return files;
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ds9-solutions/mcp",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Servidor MCP da DS9: expõe skills e subagentes oficiais para qualquer IDE/LLM com suporte a MCP",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -8,7 +8,7 @@
8
8
  "ds9-mcp": "dist/src/index.js"
9
9
  },
10
10
  "files": [
11
- "dist",
11
+ "dist/src",
12
12
  "content"
13
13
  ],
14
14
  "engines": {