santismm-knowledge-mcp 0.2.1

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 (182) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/content/CONVENTIONS.md +77 -0
  4. package/content/LICENSE +55 -0
  5. package/content/architectures/ai-workforce.json +280 -0
  6. package/content/architectures/customer-service-agent.json +292 -0
  7. package/content/architectures/enterprise-knowledge-assistant.json +292 -0
  8. package/content/architectures/operations-center.json +280 -0
  9. package/content/architectures/sales-copilot.json +280 -0
  10. package/content/governance/agentic-ai-governance-checklist.json +323 -0
  11. package/content/governance/audit-framework-for-agentic-systems.json +280 -0
  12. package/content/governance/enterprise-ai-governance-framework.json +277 -0
  13. package/content/governance/eu-ai-act.json +162 -0
  14. package/content/governance/human-oversight-and-accountability-policy.json +275 -0
  15. package/content/governance/iso-42001.json +161 -0
  16. package/content/governance/mitre-atlas.json +280 -0
  17. package/content/governance/nist-ai-rmf.json +161 -0
  18. package/content/governance/owasp-llm-top10.json +301 -0
  19. package/content/harness/HRN-001-definition-and-overview.es.md +76 -0
  20. package/content/harness/HRN-001-definition-and-overview.md +125 -0
  21. package/content/harness/HRN-001-definition-and-overview.pt.md +76 -0
  22. package/content/harness/HRN-002-a-brief-history-of-harness-engineering.es.md +83 -0
  23. package/content/harness/HRN-002-a-brief-history-of-harness-engineering.md +113 -0
  24. package/content/harness/HRN-002-a-brief-history-of-harness-engineering.pt.md +83 -0
  25. package/content/harness/HRN-003-the-harness-taxonomy.es.md +105 -0
  26. package/content/harness/HRN-003-the-harness-taxonomy.md +158 -0
  27. package/content/harness/HRN-003-the-harness-taxonomy.pt.md +105 -0
  28. package/content/harness/HRN-004-harness-engineering-principles.es.md +91 -0
  29. package/content/harness/HRN-004-harness-engineering-principles.md +135 -0
  30. package/content/harness/HRN-004-harness-engineering-principles.pt.md +91 -0
  31. package/content/harness/HRN-005-memory-in-agentic-systems.es.md +98 -0
  32. package/content/harness/HRN-005-memory-in-agentic-systems.md +145 -0
  33. package/content/harness/HRN-005-memory-in-agentic-systems.pt.md +98 -0
  34. package/content/harness/HRN-006-observability-for-agentic-systems.es.md +97 -0
  35. package/content/harness/HRN-006-observability-for-agentic-systems.md +139 -0
  36. package/content/harness/HRN-006-observability-for-agentic-systems.pt.md +97 -0
  37. package/content/harness/HRN-007-evaluation-of-agentic-systems.es.md +96 -0
  38. package/content/harness/HRN-007-evaluation-of-agentic-systems.md +145 -0
  39. package/content/harness/HRN-007-evaluation-of-agentic-systems.pt.md +96 -0
  40. package/content/harness/HRN-008-governance-within-the-harness.es.md +105 -0
  41. package/content/harness/HRN-008-governance-within-the-harness.md +146 -0
  42. package/content/harness/HRN-008-governance-within-the-harness.pt.md +105 -0
  43. package/content/harness/HRN-009-planning-and-goal-management.es.md +102 -0
  44. package/content/harness/HRN-009-planning-and-goal-management.md +142 -0
  45. package/content/harness/HRN-009-planning-and-goal-management.pt.md +102 -0
  46. package/content/harness/HRN-010-orchestration.es.md +107 -0
  47. package/content/harness/HRN-010-orchestration.md +149 -0
  48. package/content/harness/HRN-010-orchestration.pt.md +107 -0
  49. package/content/harness/HRN-011-security-for-agentic-systems.es.md +107 -0
  50. package/content/harness/HRN-011-security-for-agentic-systems.md +147 -0
  51. package/content/harness/HRN-011-security-for-agentic-systems.pt.md +107 -0
  52. package/content/harness/HRN-012-case-studies-in-harness-engineering.es.md +120 -0
  53. package/content/harness/HRN-012-case-studies-in-harness-engineering.md +157 -0
  54. package/content/harness/HRN-012-case-studies-in-harness-engineering.pt.md +120 -0
  55. package/content/harness/HRN-013-glossary.es.md +109 -0
  56. package/content/harness/HRN-013-glossary.md +124 -0
  57. package/content/harness/HRN-013-glossary.pt.md +109 -0
  58. package/content/harness/HRN-014-bibliography.es.md +89 -0
  59. package/content/harness/HRN-014-bibliography.md +109 -0
  60. package/content/harness/HRN-014-bibliography.pt.md +89 -0
  61. package/content/homeric/episodes/achilles-and-hector.json +139 -0
  62. package/content/homeric/episodes/aeolus-and-the-winds.json +131 -0
  63. package/content/homeric/episodes/agamemnons-murder.json +162 -0
  64. package/content/homeric/episodes/calypso-ogygia.json +157 -0
  65. package/content/homeric/episodes/catalogue-of-ships.json +177 -0
  66. package/content/homeric/episodes/cattle-of-the-sun.json +131 -0
  67. package/content/homeric/episodes/chryse-and-the-plague.json +131 -0
  68. package/content/homeric/episodes/cicones-at-ismarus.json +131 -0
  69. package/content/homeric/episodes/circe-on-aeaea.json +131 -0
  70. package/content/homeric/episodes/cyclops-polyphemus.json +162 -0
  71. package/content/homeric/episodes/laestrygonians.json +153 -0
  72. package/content/homeric/episodes/lotus-eaters.json +138 -0
  73. package/content/homeric/episodes/menelaus-and-proteus.json +130 -0
  74. package/content/homeric/episodes/nekyia.json +160 -0
  75. package/content/homeric/episodes/phaeacians-on-scheria.json +129 -0
  76. package/content/homeric/episodes/priams-ransom.json +131 -0
  77. package/content/homeric/episodes/return-to-ithaca.json +167 -0
  78. package/content/homeric/episodes/scylla-and-charybdis.json +131 -0
  79. package/content/homeric/episodes/suitors-ambush-at-asteris.json +131 -0
  80. package/content/homeric/episodes/telemachus-at-pylos.json +130 -0
  81. package/content/homeric/episodes/telemachus-in-sparta.json +130 -0
  82. package/content/homeric/episodes/the-achaean-camp.json +138 -0
  83. package/content/homeric/episodes/the-sirens.json +129 -0
  84. package/content/homeric/episodes/wooden-horse.json +168 -0
  85. package/content/homeric/places/aeaea.json +129 -0
  86. package/content/homeric/places/aeolia.json +161 -0
  87. package/content/homeric/places/asteris.json +120 -0
  88. package/content/homeric/places/aulis.json +122 -0
  89. package/content/homeric/places/cape-malea.json +126 -0
  90. package/content/homeric/places/chryse.json +120 -0
  91. package/content/homeric/places/dodona.json +129 -0
  92. package/content/homeric/places/dulichium.json +177 -0
  93. package/content/homeric/places/egypt.json +125 -0
  94. package/content/homeric/places/ephyra-acheron.json +127 -0
  95. package/content/homeric/places/hellespont.json +125 -0
  96. package/content/homeric/places/house-of-hades.json +91 -0
  97. package/content/homeric/places/ismarus.json +120 -0
  98. package/content/homeric/places/ithaca.json +240 -0
  99. package/content/homeric/places/knossos.json +132 -0
  100. package/content/homeric/places/laestrygonia.json +168 -0
  101. package/content/homeric/places/land-of-the-cyclopes.json +169 -0
  102. package/content/homeric/places/land-of-the-lotus-eaters.json +122 -0
  103. package/content/homeric/places/mount-ida.json +126 -0
  104. package/content/homeric/places/mycenae.json +152 -0
  105. package/content/homeric/places/ogygia.json +125 -0
  106. package/content/homeric/places/pharos.json +120 -0
  107. package/content/homeric/places/planctae.json +77 -0
  108. package/content/homeric/places/pylos.json +188 -0
  109. package/content/homeric/places/same.json +177 -0
  110. package/content/homeric/places/scheria.json +129 -0
  111. package/content/homeric/places/scylla-and-charybdis.json +135 -0
  112. package/content/homeric/places/sirens.json +127 -0
  113. package/content/homeric/places/sparta.json +179 -0
  114. package/content/homeric/places/tenedos.json +129 -0
  115. package/content/homeric/places/thrinacia.json +116 -0
  116. package/content/homeric/places/tiryns.json +123 -0
  117. package/content/homeric/places/troy.json +224 -0
  118. package/content/homeric/places/zacynthus.json +126 -0
  119. package/content/homeric/routes/achaean-expedition.json +132 -0
  120. package/content/homeric/routes/nostoi-of-the-others.json +205 -0
  121. package/content/homeric/routes/odysseus-nostos.json +307 -0
  122. package/content/homeric/routes/telemachy.json +134 -0
  123. package/content/knowledge/agent-memory.json +153 -0
  124. package/content/knowledge/agentic-ai.json +158 -0
  125. package/content/knowledge/agentic-evaluation.json +156 -0
  126. package/content/knowledge/agentic-threat-model.json +287 -0
  127. package/content/knowledge/ai-agent.json +153 -0
  128. package/content/knowledge/ai-cyberdefense.json +274 -0
  129. package/content/knowledge/ai-governance.json +155 -0
  130. package/content/knowledge/ai-observability.json +156 -0
  131. package/content/knowledge/context-engineering.json +153 -0
  132. package/content/knowledge/embeddings.json +153 -0
  133. package/content/knowledge/enterprise-rag.json +154 -0
  134. package/content/knowledge/fine-tuning.json +153 -0
  135. package/content/knowledge/foundation-models.json +154 -0
  136. package/content/knowledge/guardrails.json +153 -0
  137. package/content/knowledge/harness-engineering.json +158 -0
  138. package/content/knowledge/human-in-the-loop.json +153 -0
  139. package/content/knowledge/mcp-security.json +284 -0
  140. package/content/knowledge/model-context-protocol.json +154 -0
  141. package/content/knowledge/multi-agent-architecture.json +153 -0
  142. package/content/knowledge/prompt-engineering.json +153 -0
  143. package/content/knowledge/prompt-injection.json +138 -0
  144. package/content/knowledge/reasoning-models.json +153 -0
  145. package/content/knowledge/tool-use.json +156 -0
  146. package/content/library/cognitive-architecture-emergent-ai.md +15 -0
  147. package/content/library/devready-ep108-ai-customer-experiences.md +14 -0
  148. package/content/library/how-genai-impact-business.md +12 -0
  149. package/content/library/lmm-reshaping-industries-2024.md +12 -0
  150. package/content/library/rethinking-ai-pause.md +12 -0
  151. package/content/library/rise-of-agentic-ai.md +16 -0
  152. package/content/library/self-improving-autonomous-ai.md +16 -0
  153. package/content/library/the-stopwatch-and-the-exam.md +12 -0
  154. package/content/library/unlock-gpt4-secrets.md +12 -0
  155. package/content/library/vibe-coding-enterprise.md +15 -0
  156. package/content/library/video-transformando-negocios-genai.md +14 -0
  157. package/content/library/video-volando-alto-sky-airlines.md +13 -0
  158. package/content/matrix/agentic-control-matrix.json +967 -0
  159. package/content/patterns/attributed-memory.json +237 -0
  160. package/content/patterns/context-compression.json +304 -0
  161. package/content/patterns/egress-allowlist.json +310 -0
  162. package/content/patterns/evaluator-optimizer.json +180 -0
  163. package/content/patterns/goal-decomposition.json +290 -0
  164. package/content/patterns/human-approval-gate.json +311 -0
  165. package/content/patterns/human-escalation.json +288 -0
  166. package/content/patterns/least-privilege-tooling.json +333 -0
  167. package/content/patterns/long-term-memory.json +305 -0
  168. package/content/patterns/orchestrator-workers.json +202 -0
  169. package/content/patterns/parallelization.json +180 -0
  170. package/content/patterns/prompt-chaining.json +181 -0
  171. package/content/patterns/recovery-strategy.json +305 -0
  172. package/content/patterns/reflection.json +298 -0
  173. package/content/patterns/routing.json +294 -0
  174. package/content/patterns/sandboxed-execution.json +311 -0
  175. package/content/patterns/semantic-caching.json +201 -0
  176. package/content/patterns/supervisor-agent.json +290 -0
  177. package/content/patterns/task-prioritization.json +307 -0
  178. package/dist/content.js +181 -0
  179. package/dist/index.js +27 -0
  180. package/dist/shape.js +649 -0
  181. package/dist/tools.js +652 -0
  182. package/package.json +47 -0
package/dist/tools.js ADDED
@@ -0,0 +1,652 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Single, framework-agnostic definition of the Santismm Knowledge MCP server:
4
+ * its identity and the tool registry. Both transports — the stdio CLI
5
+ * (`mcp/src/index.ts`) and the HTTP endpoint (`app/mcp/route.ts`) — call
6
+ * `registerTools(server, content)`, so the exposed tools can never drift
7
+ * between them. The data source is injected as `content` (an `McpContent`
8
+ * provider); both providers read the same `content/{domain}/*.json` files.
9
+ */
10
+ export const SERVER_INFO = { name: "santismm-knowledge", version: "0.2.1" };
11
+ /**
12
+ * Every tool here reads a static corpus and nothing else, so all four hints are
13
+ * literally true rather than aspirational: nothing mutates, the same arguments
14
+ * always produce the same answer, and no tool reaches outside this corpus.
15
+ *
16
+ * Declaring them matters because a client deciding whether a call needs
17
+ * confirmation, and an aggregator deciding how to present the server, both read
18
+ * these and must otherwise assume the cautious default — that an undeclared
19
+ * tool might destroy something.
20
+ */
21
+ const READ_ONLY = {
22
+ readOnlyHint: true,
23
+ destructiveHint: false,
24
+ idempotentHint: true,
25
+ openWorldHint: false,
26
+ };
27
+ const localeSchema = z
28
+ .enum(["en", "es", "pt"])
29
+ .optional()
30
+ .describe("Language of the returned body. Default: en.");
31
+ // ── Output schemas ──────────────────────────────────────────────────────────
32
+ // Declared so every tool advertises an `outputSchema` and returns validated
33
+ // `structuredContent`: callers get typed data instead of having to JSON.parse
34
+ // a text blob.
35
+ //
36
+ // Shapes that carry CONTENT (cards, units) are open: a plain `z.object`
37
+ // serializes to `additionalProperties: false`, which silently turned every
38
+ // field the corpus gained into a contract violation for clients that validate.
39
+ // Shapes that are pure ENVELOPE (`{count, results}`, the overview) stay closed
40
+ // — the code defines them exactly, so a field added there without being
41
+ // declared is a genuine mistake, and `npm run test:mcp` validates live
42
+ // responses against these schemas to catch it.
43
+ const evidenceSchema = z
44
+ .object({
45
+ evidenceLevel: z.string().optional(),
46
+ confidenceLevel: z.string().optional(),
47
+ sourceType: z.array(z.string()).optional(),
48
+ })
49
+ .optional()
50
+ .describe("Evidence-First provenance: weight claims by this.");
51
+ /**
52
+ * Compact card returned by list/search/graph tools. Open, for the same reason
53
+ * `unitOutput` is: cards have gained fields before (`canonical_url`, `score`)
54
+ * and will again, and a closed schema turns that into a client-side rejection.
55
+ *
56
+ * `.passthrough()` rather than `z.looseObject`: both exist in zod 4 and mean the
57
+ * same thing here, and `.passthrough()` keeps the openness attached to the
58
+ * schema it opens instead of to the constructor, which reads better next to the
59
+ * closed schemas around it.
60
+ *
61
+ * (This file used to be compiled against two different zod copies — the site's
62
+ * v4 and the CLI's v3 — which restricted it to API present in both. That is
63
+ * over: `@modelcontextprotocol/server` v2 registers tools through Standard
64
+ * Schema and needs `~standard.jsonSchema`, which zod 3 does not implement, so
65
+ * the CLI moved to zod 4 with it. One zod, one SDK.)
66
+ */
67
+ /**
68
+ * Bounded free-text inputs.
69
+ *
70
+ * `slug` and `query` were unbounded strings, so a caller could send a megabyte
71
+ * where a word belongs and the server would hash it, search it and log it
72
+ * before deciding it matched nothing. The limits are set well above anything
73
+ * the corpus can legitimately need — the longest slug is a fraction of this —
74
+ * so they refuse abuse without ever refusing a real request.
75
+ */
76
+ const slugSchema = z.string().min(1).max(128);
77
+ const querySchema = z.string().min(1).max(512);
78
+ const cardSchema = z.object({
79
+ domain: z.string(),
80
+ id: z.string().optional(),
81
+ slug: z.string(),
82
+ category: z.string().optional(),
83
+ name: z.string(),
84
+ summary: z.string().optional(),
85
+ status: z.string().optional(),
86
+ evidence: evidenceSchema,
87
+ updated: z.string().optional(),
88
+ locale: z.string().optional(),
89
+ canonical_url: z.string().optional().describe("Cite this URL."),
90
+ api_url: z.string().optional(),
91
+ }).passthrough();
92
+ const listOutput = {
93
+ count: z.number(),
94
+ results: z.array(cardSchema),
95
+ };
96
+ const searchOutput = {
97
+ query: z.string(),
98
+ count: z.number(),
99
+ results: z.array(cardSchema.extend({
100
+ score: z.number().describe("Relevance; higher is better."),
101
+ matchedFields: z.array(z.string()).describe("Which fields the query hit."),
102
+ matchedTerms: z.array(z.string()),
103
+ })),
104
+ };
105
+ /**
106
+ * A full unit.
107
+ *
108
+ * Open, and that is the whole point: a bare (non-passthrough)
109
+ * `z.object` serializes to `additionalProperties: false`, so every field the
110
+ * corpus actually returns but the schema never named — `locales`, `related`,
111
+ * `patterns`, `knowledge`, `frameworks`, `technologies`, `featured`, and the
112
+ * handbook's own `requested_locale`/`resolved_locale`/`fallback` — made the
113
+ * payload INVALID against the contract the tool advertised. A client that
114
+ * validates strictly, exactly as the MCP spec invites it to, would reject
115
+ * perfectly good results.
116
+ *
117
+ * So: name every field that exists today, so clients get real typing, and stay
118
+ * open, so adding a field to a content unit can never invalidate a response.
119
+ */
120
+ const referenceSchema = z.object({ title: z.string().optional(), url: z.string().optional() });
121
+ const unitOutput = z.object({
122
+ domain: z.string().optional(),
123
+ id: z.string().optional(),
124
+ slug: z.string().optional(),
125
+ category: z.string().optional(),
126
+ updated: z.string().optional(),
127
+ version: z.string().optional(),
128
+ locale: z.string().optional(),
129
+ evidence: evidenceSchema,
130
+ canonical_url: z.string().optional(),
131
+ api_url: z.string().optional(),
132
+ references: z.array(referenceSchema).optional(),
133
+ featured: z.boolean().optional(),
134
+ /** Cross-references, by slug (or HRN id inside the handbook). */
135
+ related: z.array(z.string()).optional(),
136
+ patterns: z.array(z.string()).optional(),
137
+ knowledge: z.array(z.string()).optional(),
138
+ frameworks: z.array(z.string()).optional(),
139
+ technologies: z.array(z.string()).optional(),
140
+ tags: z.array(z.string()).optional(),
141
+ /** Every locale, when no `locale` argument narrowed the request. */
142
+ locales: z.record(z.string(), z.unknown()).optional(),
143
+ /** The single requested locale's body, when `locale` was given. */
144
+ body: z.unknown().optional(),
145
+ // Handbook only. Added in order to be honest about the English-only bodies,
146
+ // then left undeclared — so honesty broke the contract.
147
+ requested_locale: z.string().optional(),
148
+ resolved_locale: z.string().optional(),
149
+ fallback: z.boolean().optional(),
150
+ status: z.string().optional(),
151
+ name: z.string().optional(),
152
+ summary: z.string().optional(),
153
+ }).passthrough();
154
+ /**
155
+ * The four unit getters, tightened into an actual contract.
156
+ *
157
+ * `unitOutput` is a superset covering five domains, so every field is optional
158
+ * and it ends in `.passthrough()` — the schema validated the response and
159
+ * promised nothing. An audit put it plainly: structured, but not typed.
160
+ *
161
+ * The required set below is the INTERSECTION of what every unit in the corpus
162
+ * actually returns, measured across all 55 of them rather than chosen by eye.
163
+ * `featured`, `technologies`, `id` and the domain-specific cross-reference
164
+ * arrays stay optional because they are genuinely absent on some units;
165
+ * requiring them would be a contract the server breaks on its own data.
166
+ *
167
+ * `locales` is optional for the same reason, and it is worth recording why the
168
+ * first version of this got it wrong: measuring only `get_knowledge({slug})`
169
+ * showed `locales` on every unit, so it went into the required set — and
170
+ * `get_knowledge({slug, locale})` returns `body` INSTEAD of `locales` and
171
+ * started failing its own output validation. The intersection has to be taken
172
+ * across every call SHAPE, not only across every unit.
173
+ *
174
+ * `.strict()` closes the object: an undeclared field becomes a failure instead
175
+ * of a silent addition, which is what makes the schema worth reading. The
176
+ * handbook keeps the permissive base — its shape really is different (a body,
177
+ * a resolved locale, no `locales` map and no `version`).
178
+ */
179
+ const UNIT_REQUIRED = {
180
+ domain: true,
181
+ slug: true,
182
+ category: true,
183
+ updated: true,
184
+ version: true,
185
+ evidence: true,
186
+ canonical_url: true,
187
+ api_url: true,
188
+ references: true,
189
+ related: true,
190
+ };
191
+ const unitGetOutput = unitOutput.required(UNIT_REQUIRED).strict();
192
+ /**
193
+ * Emit a result on both channels: `structuredContent` (validated against the
194
+ * tool's outputSchema, for clients that support it) and the serialized JSON in
195
+ * `content`, which the MCP spec recommends keeping for backward compatibility.
196
+ */
197
+ function out(structured, text = structured) {
198
+ return {
199
+ content: [{ type: "text", text: JSON.stringify(text, null, 2) }],
200
+ structuredContent: structured,
201
+ };
202
+ }
203
+ /** A list/search payload: the array stays the `content` shape callers already parse. */
204
+ function outList(results, extra = {}) {
205
+ return out({ count: results.length, results, ...extra }, results);
206
+ }
207
+ /**
208
+ * Where each retrievable space lives and which tools reach it.
209
+ *
210
+ * Written out rather than derived by string surgery on the domain name: the
211
+ * getters are not a regular transformation of it (`patterns` → `get_pattern`,
212
+ * `knowledge` → `get_knowledge`), and a rule with exceptions would break
213
+ * silently the next time a space is added. `npm run validate` checks every name
214
+ * here against the `server.registerTool(` calls below, so the table cannot
215
+ * drift from the server it describes.
216
+ */
217
+ const ESPACIOS = [
218
+ { domain: "knowledge", listTool: "list_knowledge", getTool: "get_knowledge" },
219
+ { domain: "patterns", listTool: "list_patterns", getTool: "get_pattern" },
220
+ { domain: "architectures", listTool: "list_architectures", getTool: "get_architecture" },
221
+ { domain: "governance", listTool: "list_governance", getTool: "get_governance" },
222
+ { domain: "handbook", listTool: "list_handbook", getTool: "get_handbook" },
223
+ { domain: "homeric/places", listTool: "list_homeric_places", getTool: "get_homeric_place" },
224
+ { domain: "homeric/episodes", listTool: "list_homeric_episodes", getTool: "get_homeric_episode" },
225
+ { domain: "homeric/routes", listTool: "list_homeric_routes", getTool: "get_homeric_route" },
226
+ ];
227
+ /** Every identifier a card answers to: its id (handbook chapters) and its slug. */
228
+ function identificadores(cards) {
229
+ const vistos = new Set();
230
+ for (const card of cards) {
231
+ const o = card;
232
+ if (!o)
233
+ continue;
234
+ for (const campo of [o.id, o.slug]) {
235
+ if (typeof campo === "string" && campo.length > 0)
236
+ vistos.add(campo);
237
+ }
238
+ }
239
+ return [...vistos];
240
+ }
241
+ /** The cards of one space, whichever loader serves it. */
242
+ function cardsDe(content, domain, locale) {
243
+ if (domain === "handbook")
244
+ return content.listHandbook(locale);
245
+ if (domain.startsWith("homeric/")) {
246
+ return content.listHomeric(domain.slice("homeric/".length), locale);
247
+ }
248
+ return content.listDomain(domain, locale);
249
+ }
250
+ /**
251
+ * Levenshtein distance, bounded by `tope`: it stops as soon as the whole row
252
+ * exceeds the budget, so a long identifier costs nothing when it is obviously
253
+ * unrelated. Only ever run over the few dozen short slugs of one space.
254
+ */
255
+ function distancia(a, b, tope) {
256
+ if (Math.abs(a.length - b.length) > tope)
257
+ return tope + 1;
258
+ let anterior = Array.from({ length: b.length + 1 }, (_, i) => i);
259
+ for (let i = 1; i <= a.length; i++) {
260
+ const fila = [i];
261
+ let minima = i;
262
+ for (let j = 1; j <= b.length; j++) {
263
+ const coste = a[i - 1] === b[j - 1] ? 0 : 1;
264
+ const v = Math.min(fila[j - 1] + 1, anterior[j] + 1, anterior[j - 1] + coste);
265
+ fila.push(v);
266
+ if (v < minima)
267
+ minima = v;
268
+ }
269
+ if (minima > tope)
270
+ return tope + 1;
271
+ anterior = fila;
272
+ }
273
+ return anterior[b.length];
274
+ }
275
+ /** Identifiers are compared case- and separator-insensitively: agents guess both. */
276
+ function normalizar(s) {
277
+ return s.toLowerCase().replace(/[\s_]+/g, "-");
278
+ }
279
+ /** Most identifiers a not-found payload will spell out before summarising. */
280
+ const MUESTRA_MAXIMA = 30;
281
+ /**
282
+ * Tool-level failure that an agent can recover from.
283
+ *
284
+ * The bare `{error, domain, slug}` this used to return was a dead end: 90 days
285
+ * of traffic show `get_handbook` answering 2 of 29 calls, the other 27 asking
286
+ * for ids invented by analogy (`arch-001`, `hom-r-001`) against a handbook that
287
+ * only has the `HRN-###` series — and eight more calls asking for
288
+ * `customer-service-agent` in three domains that do not hold it while
289
+ * `architectures` does. Nothing in the response said so, so the caller retried
290
+ * the same invention. Now the error carries the valid identifiers, the tool
291
+ * that lists them, the nearest match, and the space that does hold the slug.
292
+ *
293
+ * `isError` is required here: the SDK skips output validation for error
294
+ * results, which is what lets a not-found response omit `structuredContent`
295
+ * while the tool still declares an outputSchema.
296
+ */
297
+ function noEncontrado(content, domain, pedido, locale) {
298
+ const espacio = ESPACIOS.find((e) => e.domain === domain);
299
+ const cuerpo = { error: "not_found", domain, slug: pedido };
300
+ const pistas = [];
301
+ let validos = [];
302
+ try {
303
+ validos = identificadores(cardsDe(content, domain, locale));
304
+ }
305
+ catch {
306
+ validos = [];
307
+ }
308
+ // Lo mismo, en otro sitio: el fallo más común no es que falte el contenido.
309
+ const buscado = normalizar(pedido);
310
+ for (const otro of ESPACIOS) {
311
+ if (otro.domain === domain)
312
+ continue;
313
+ let suyos = [];
314
+ try {
315
+ suyos = identificadores(cardsDe(content, otro.domain, locale));
316
+ }
317
+ catch {
318
+ continue;
319
+ }
320
+ const acierto = suyos.find((id) => normalizar(id) === buscado);
321
+ if (acierto) {
322
+ cuerpo.found_in = { domain: otro.domain, id: acierto, tool: otro.getTool };
323
+ pistas.push(`"${acierto}" exists in ${otro.domain}: call ${otro.getTool}.`);
324
+ break;
325
+ }
326
+ }
327
+ if (validos.length > 0) {
328
+ const tope = Math.max(2, Math.floor(buscado.length / 3));
329
+ const cerca = validos
330
+ .map((id) => ({ id, d: distancia(buscado, normalizar(id), tope) }))
331
+ .filter((c) => c.d <= tope)
332
+ .sort((a, b) => a.d - b.d)
333
+ .slice(0, 3)
334
+ .map((c) => c.id);
335
+ if (cerca.length > 0) {
336
+ cuerpo.did_you_mean = cerca;
337
+ pistas.push(`Did you mean ${cerca.map((c) => `"${c}"`).join(", ")}?`);
338
+ }
339
+ const orden = [...validos].sort();
340
+ cuerpo.available_count = orden.length;
341
+ cuerpo.available = orden.slice(0, MUESTRA_MAXIMA);
342
+ if (orden.length > MUESTRA_MAXIMA) {
343
+ cuerpo.available_truncated = true;
344
+ pistas.push(`${domain} has ${orden.length} identifiers; ${MUESTRA_MAXIMA} are listed here.`);
345
+ }
346
+ }
347
+ if (espacio) {
348
+ cuerpo.list_tool = espacio.listTool;
349
+ pistas.push(`${espacio.listTool} returns every identifier this tool accepts.`);
350
+ }
351
+ pistas.push("Identifiers are not interchangeable between spaces; do not invent one by analogy.");
352
+ cuerpo.hint = pistas.join(" ");
353
+ return {
354
+ content: [{ type: "text", text: JSON.stringify(cuerpo, null, 2) }],
355
+ isError: true,
356
+ };
357
+ }
358
+ /**
359
+ * Atlas cards and units.
360
+ *
361
+ * They keep the two things an agent has to weigh before quoting the atlas — the
362
+ * identification class and the rubric — at the top level, rather than buried
363
+ * inside a locale body.
364
+ */
365
+ const homericConfidence = z
366
+ .object({
367
+ textual: z.number(),
368
+ archaeological: z.number(),
369
+ scholarly: z.number(),
370
+ geographic: z.number(),
371
+ score: z.number(),
372
+ band: z.string(),
373
+ })
374
+ .partial()
375
+ .passthrough();
376
+ const homericCard = z
377
+ .object({
378
+ kind: z.string(),
379
+ id: z.string().optional(),
380
+ slug: z.string(),
381
+ name: z.string(),
382
+ summary: z.string().optional(),
383
+ identification: z.string().optional(),
384
+ region: z.string().optional(),
385
+ work: z.string().optional(),
386
+ citation: z.string().optional(),
387
+ unlocated: z.boolean().optional(),
388
+ hypotheses: z.number().optional(),
389
+ confidence: homericConfidence.nullable().optional(),
390
+ canonical_url: z.string().optional().describe("Cite this URL."),
391
+ api_url: z.string().optional(),
392
+ })
393
+ .passthrough();
394
+ const homericListOutput = {
395
+ count: z.number(),
396
+ results: z.array(homericCard),
397
+ };
398
+ const homericUnitOutput = z.object({}).passthrough();
399
+ export function registerTools(server, content) {
400
+ // ── Orientation ────────────────────────────────────────────────────────────
401
+ server.registerTool("get_overview", {
402
+ title: "Corpus Overview — Start Here",
403
+ annotations: READ_ONLY,
404
+ description: "Get the corpus map — start here. Returns every domain (knowledge, patterns, architectures, governance and the Harness Engineering Handbook) with what it holds, the categories inside it, which tool retrieves a unit and what identifier that tool expects, plus the languages, licence and bulk-ingest URLs. One call is enough to know exactly where to go next.",
405
+ inputSchema: z.object({}),
406
+ outputSchema: z.object({
407
+ source: z.string(),
408
+ site: z.string(),
409
+ locales: z.array(z.string()),
410
+ license: z.string(),
411
+ license_spdx: z.string().describe("SPDX id — check this, not the prose."),
412
+ license_url: z.string(),
413
+ total: z.number(),
414
+ domains: z.array(z.object({
415
+ domain: z.string(),
416
+ count: z.number(),
417
+ description: z.string(),
418
+ categories: z.array(z.string()),
419
+ tools: z.object({ list: z.string(), get: z.string() }),
420
+ lookup: z.string(),
421
+ url: z.string(),
422
+ api_url: z.string(),
423
+ })),
424
+ next: z.string(),
425
+ bulk: z.record(z.string(), z.string()),
426
+ }),
427
+ }, async () => out({ ...content.overview() }));
428
+ // ── Knowledge base ─────────────────────────────────────────────────────────
429
+ server.registerTool("list_knowledge", {
430
+ title: "List Agentic AI Knowledge Units",
431
+ annotations: READ_ONLY,
432
+ description: "List all knowledge units (concepts on agentic & enterprise AI) with slug, category, title, summary and Evidence-First provenance. Use this to browse the domain; use `search` when you have a question rather than a slug.",
433
+ inputSchema: z.object({ locale: localeSchema }),
434
+ outputSchema: listOutput,
435
+ }, async ({ locale }) => outList(content.listDomain("knowledge", (locale ?? "en"))));
436
+ server.registerTool("get_knowledge", {
437
+ title: "Get an Agentic AI Knowledge Unit",
438
+ annotations: READ_ONLY,
439
+ description: "Get one knowledge unit by slug. Returns the full entry, or a single-locale body if locale is given. Use this once `search` or `list_knowledge` has given you a slug.",
440
+ inputSchema: z.object({ slug: slugSchema.describe("Knowledge unit slug, e.g. 'harness-engineering'."), locale: localeSchema }),
441
+ outputSchema: unitGetOutput,
442
+ }, async ({ slug, locale }) => {
443
+ const entry = content.getEntry("knowledge", slug, locale);
444
+ return entry ? out(entry) : noEncontrado(content, "knowledge", slug, (locale ?? "en"));
445
+ });
446
+ // ── Patterns ───────────────────────────────────────────────────────────────
447
+ server.registerTool("list_patterns", {
448
+ title: "List Enterprise AI Patterns",
449
+ annotations: READ_ONLY,
450
+ description: "List all Enterprise AI patterns (reusable agentic design patterns) with slug, category, name, summary and provenance. Use this to browse the catalogue; use `search` when you are looking for a pattern that solves a problem.",
451
+ inputSchema: z.object({ locale: localeSchema }),
452
+ outputSchema: listOutput,
453
+ }, async ({ locale }) => outList(content.listDomain("patterns", (locale ?? "en"))));
454
+ server.registerTool("get_pattern", {
455
+ title: "Get an Enterprise AI Pattern",
456
+ annotations: READ_ONLY,
457
+ description: "Get one Enterprise AI pattern by slug (includes problem, solution, KPIs, failure modes, lessons). Use this once `search` or `list_patterns` has given you a slug.",
458
+ inputSchema: z.object({ slug: slugSchema.describe("Pattern slug, e.g. 'human-approval-gate'."), locale: localeSchema }),
459
+ outputSchema: unitGetOutput,
460
+ }, async ({ slug, locale }) => {
461
+ const entry = content.getEntry("patterns", slug, locale);
462
+ return entry ? out(entry) : noEncontrado(content, "patterns", slug, (locale ?? "en"));
463
+ });
464
+ // ── Reference architectures ────────────────────────────────────────────────
465
+ server.registerTool("list_architectures", {
466
+ title: "List Agentic Reference Architectures",
467
+ annotations: READ_ONLY,
468
+ description: "List all reference architectures (end-to-end enterprise agentic blueprints) with id, slug, category, name, summary and provenance. Use this to browse the blueprints; use `search` when you have a use case rather than a name.",
469
+ inputSchema: z.object({ locale: localeSchema }),
470
+ outputSchema: listOutput,
471
+ }, async ({ locale }) => outList(content.listDomain("architectures", (locale ?? "en"))));
472
+ server.registerTool("get_architecture", {
473
+ title: "Get an Agentic Reference Architecture",
474
+ annotations: READ_ONLY,
475
+ description: "Get one reference architecture by slug (includes the request flow, reference scenario, KPIs, cost & scaling, and the patterns/knowledge it composes). Use this once `search` or `list_architectures` has given you a slug.",
476
+ inputSchema: z.object({ slug: slugSchema.describe("Architecture slug, e.g. 'customer-service-agent'."), locale: localeSchema }),
477
+ outputSchema: unitGetOutput,
478
+ }, async ({ slug, locale }) => {
479
+ const entry = content.getEntry("architectures", slug, locale);
480
+ return entry ? out(entry) : noEncontrado(content, "architectures", slug, (locale ?? "en"));
481
+ });
482
+ // ── Governance ─────────────────────────────────────────────────────────────
483
+ server.registerTool("list_governance", {
484
+ title: "List AI Governance Units",
485
+ annotations: READ_ONLY,
486
+ description: "List all AI governance units (regulations, standards, frameworks, playbooks — EU AI Act, ISO 42001, NIST AI RMF, agentic checklist) with id, slug, category, name and summary. Use this to browse the regulations and standards; use `search` for an obligation or control.",
487
+ inputSchema: z.object({ locale: localeSchema }),
488
+ outputSchema: listOutput,
489
+ }, async ({ locale }) => outList(content.listDomain("governance", (locale ?? "en"))));
490
+ server.registerTool("get_governance", {
491
+ title: "Get an AI Governance Unit",
492
+ annotations: READ_ONLY,
493
+ description: "Get one AI governance unit by slug (includes scope, key requirements, implementable controls, a checklist and common pitfalls). Use this once `search` or `list_governance` has given you a slug.",
494
+ inputSchema: z.object({ slug: slugSchema.describe("Governance unit slug, e.g. 'eu-ai-act'."), locale: localeSchema }),
495
+ outputSchema: unitGetOutput,
496
+ }, async ({ slug, locale }) => {
497
+ const entry = content.getEntry("governance", slug, locale);
498
+ return entry ? out(entry) : noEncontrado(content, "governance", slug, (locale ?? "en"));
499
+ });
500
+ // ── Harness Engineering Handbook ───────────────────────────────────────────
501
+ server.registerTool("list_handbook", {
502
+ title: "List Harness Engineering Handbook Chapters",
503
+ annotations: READ_ONLY,
504
+ description: "List the Harness Engineering Handbook chapters (HRN-001…): the canonical long-form corpus on harness engineering — definition, history, taxonomy, principles, memory, observability, evaluation, governance, planning, orchestration, security, case studies, glossary and bibliography. Use this to see the chapter map; use `search` when you have a topic rather than a chapter.",
505
+ inputSchema: z.object({ locale: localeSchema }),
506
+ outputSchema: listOutput,
507
+ }, async ({ locale }) => outList(content.listHandbook((locale ?? "en"))));
508
+ server.registerTool("get_handbook", {
509
+ title: "Get a Harness Engineering Handbook Chapter",
510
+ annotations: READ_ONLY,
511
+ description: "Get one Harness Engineering Handbook chapter, by id (e.g. 'HRN-001') or slug. Returns the full Markdown body plus its provenance and related ids. Use this once `search` or `list_handbook` has given you an id.",
512
+ inputSchema: z.object({
513
+ id: slugSchema.describe("Chapter id like 'HRN-001', or its slug."),
514
+ locale: localeSchema,
515
+ }),
516
+ outputSchema: unitOutput.extend({
517
+ body: z.string().optional().describe("Full Markdown body of the chapter."),
518
+ requested_locale: z.string().optional().describe("The locale that was asked for."),
519
+ resolved_locale: z
520
+ .string()
521
+ .optional()
522
+ .describe("The locale actually returned. Chapters are authored in English and translated; where a translation exists this equals the requested locale, and `fallback` says when it does not."),
523
+ fallback: z.boolean().optional().describe("True when the body is not in the requested locale."),
524
+ }),
525
+ }, async ({ id, locale }) => {
526
+ const entry = content.getHandbookChapter(id, (locale ?? "en"));
527
+ return entry ? out(entry) : noEncontrado(content, "handbook", id, (locale ?? "en"));
528
+ });
529
+ // ── Search ─────────────────────────────────────────────────────────────────
530
+ server.registerTool("search", {
531
+ title: "Search Harness Engineering & Agentic AI",
532
+ annotations: READ_ONLY,
533
+ description: "Ranked keyword search across the whole corpus (knowledge, patterns, architectures, governance and the handbook). Matches every language and ignores accents, so query in the user's own words. Each hit carries a relevance score and the fields it matched; follow up with the matching get_* tool for full detail. Use this before any `get_*` tool whenever you have a question rather than an identifier.",
534
+ inputSchema: z.object({
535
+ query: querySchema.describe("Keyword or phrase to search for, in any of en/es/pt."),
536
+ domains: z
537
+ .array(z.enum(["knowledge", "patterns", "architectures", "governance", "handbook"]))
538
+ .optional()
539
+ .describe("Restrict to these domains. Omit to search everything, including the handbook."),
540
+ limit: z.number().int().positive().max(100).optional().describe("Max results (default 20)."),
541
+ locale: localeSchema,
542
+ }),
543
+ outputSchema: searchOutput,
544
+ }, async ({ query, domains, limit, locale }) => {
545
+ const results = content.search(query, domains, limit ?? 20, (locale ?? "en"));
546
+ return outList(results, { query });
547
+ });
548
+ // ── Graph traversal ────────────────────────────────────────────────────────
549
+ server.registerTool("get_related", {
550
+ title: "Traverse the Knowledge Graph",
551
+ annotations: READ_ONLY,
552
+ description: "Traverse the knowledge graph: given a unit, return its neighbours — the units it links to (outgoing) and the units that reference it (incoming), each with the relationship type. Use this after a `get_*` call to widen an answer with adjacent units.",
553
+ inputSchema: z.object({
554
+ domain: z.enum(["knowledge", "patterns", "architectures", "governance", "handbook"]),
555
+ slug: z
556
+ .string()
557
+ .describe("Slug of the unit to start from (or an HRN id when domain is 'handbook')."),
558
+ locale: localeSchema,
559
+ }),
560
+ outputSchema: z.object({
561
+ unit: cardSchema,
562
+ outgoing: z.array(cardSchema.extend({ type: z.string() })),
563
+ incoming: z.array(cardSchema.extend({ type: z.string() })),
564
+ }),
565
+ }, async ({ domain, slug, locale }) => {
566
+ const result = content.related(domain, slug, (locale ?? "en"));
567
+ return result ? out(result) : noEncontrado(content, domain, slug, (locale ?? "en"));
568
+ });
569
+ // ── Homeric Atlas (Labs) ───────────────────────────────────────────────────
570
+ const atlasNote = "Identification classes: accepted (an excavated site with consensus), plausible (a real place, contested), speculative (a minority reading or a later tradition), mythical (the poem places it outside the mappable world). Confidence is a published 0-12 rubric - textual, archaeological, scholarly and geographic, 0-3 each - and is an editorial judgement, not a probability.";
571
+ server.registerTool("list_homeric_places", {
572
+ title: "List Homeric Atlas places",
573
+ annotations: READ_ONLY,
574
+ description: "List every place in the Homeric Atlas with its identification class, its confidence score and how many competing identifications it carries. Use this to browse the atlas; use `get_homeric_place` once you have a slug. " +
575
+ atlasNote,
576
+ inputSchema: z.object({ locale: localeSchema }),
577
+ outputSchema: homericListOutput,
578
+ }, async ({ locale }) => outList(content.listHomeric("places", (locale ?? "en"))));
579
+ server.registerTool("get_homeric_place", {
580
+ title: "Get a Homeric Atlas place",
581
+ annotations: READ_ONLY,
582
+ description: "Get one place by slug: every identification proposed for it, each with its own coordinates, class, 0-12 rubric and sources, plus the attested passages. Use this when you need to weigh the evidence for a location, or to cite it; a place the poem does not locate carries no coordinates at all.",
583
+ inputSchema: z.object({
584
+ slug: z.string().describe("Place slug, e.g. 'ithaca'."),
585
+ locale: localeSchema,
586
+ }),
587
+ outputSchema: homericUnitOutput,
588
+ }, async ({ slug, locale }) => {
589
+ const entry = content.getHomeric("places", slug, locale);
590
+ return entry ? out(entry) : noEncontrado(content, "homeric/places", slug, (locale ?? "en"));
591
+ });
592
+ server.registerTool("list_homeric_episodes", {
593
+ title: "List Homeric Atlas episodes",
594
+ annotations: READ_ONLY,
595
+ description: "List every episode of the Iliad and the Odyssey held in the atlas, in reading order, with its passage, the places it involves and how firmly it can be located. Use this to find the episode you want; use `get_homeric_episode` for its theories and sources.",
596
+ inputSchema: z.object({ locale: localeSchema }),
597
+ outputSchema: homericListOutput,
598
+ }, async ({ locale }) => outList(content.listHomeric("episodes", (locale ?? "en"))));
599
+ server.registerTool("get_homeric_episode", {
600
+ title: "Get a Homeric Atlas episode",
601
+ annotations: READ_ONLY,
602
+ description: "Get one episode by slug: the passage, the narrative, the competing theories about where it happened (each with its proponent and sources), the confidence rubric and the FAQs. Use this when the question is where an episode took place and who argued for it.",
603
+ inputSchema: z.object({
604
+ slug: z.string().describe("Episode slug, e.g. 'nekyia'."),
605
+ locale: localeSchema,
606
+ }),
607
+ outputSchema: homericUnitOutput,
608
+ }, async ({ slug, locale }) => {
609
+ const entry = content.getHomeric("episodes", slug, locale);
610
+ return entry ? out(entry) : noEncontrado(content, "homeric/episodes", slug, (locale ?? "en"));
611
+ });
612
+ server.registerTool("list_homeric_routes", {
613
+ title: "List Homeric Atlas routes",
614
+ annotations: READ_ONLY,
615
+ description: "List the reconstructed itineraries (the nostos of Odysseus and the others), each with its rival reconstructions scored separately. Use this to see which voyages the atlas reconstructs before fetching one.",
616
+ inputSchema: z.object({ locale: localeSchema }),
617
+ outputSchema: homericListOutput,
618
+ }, async ({ locale }) => outList(content.listHomeric("routes", (locale ?? "en"))));
619
+ server.registerTool("get_homeric_route", {
620
+ title: "Get a Homeric Atlas route",
621
+ annotations: READ_ONLY,
622
+ description: "Get one route by slug: each reconstruction variant with its ordered stops, the hypothesis chosen at each stop, its confidence rubric and its sources. Use this to compare rival reconstructions of a voyage: they are returned side by side rather than merged.",
623
+ inputSchema: z.object({
624
+ slug: z.string().describe("Route slug, e.g. 'odysseus-nostos'."),
625
+ locale: localeSchema,
626
+ }),
627
+ outputSchema: homericUnitOutput,
628
+ }, async ({ slug, locale }) => {
629
+ const entry = content.getHomeric("routes", slug, locale);
630
+ return entry ? out(entry) : noEncontrado(content, "homeric/routes", slug, (locale ?? "en"));
631
+ });
632
+ }
633
+ /**
634
+ * The tools this server exposes, as advertised metadata (name + description).
635
+ * Derived by running the same `registerTools` registry against a capturing
636
+ * stub, so discovery surfaces (ai-index.json, llms.txt, the /mcp docs page) can
637
+ * list the tools without hardcoding — the advertised set can never drift from
638
+ * what the server actually registers. Handlers are never invoked here, so the
639
+ * injected content provider is a no-op.
640
+ */
641
+ export function toolManifest() {
642
+ const tools = [];
643
+ const capture = {
644
+ registerTool(name, config) {
645
+ tools.push({ name, description: config.description ?? "" });
646
+ return undefined;
647
+ },
648
+ };
649
+ const noopContent = new Proxy({}, { get: () => () => undefined });
650
+ registerTools(capture, noopContent);
651
+ return tools;
652
+ }