santismm-knowledge-mcp 0.2.2 → 0.3.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 (35) hide show
  1. package/README.md +9 -7
  2. package/content/architectures/ai-workforce.json +3 -1
  3. package/content/claims/a-harness-can-degrade-the-agent.json +51 -0
  4. package/content/claims/harness-changes-outcomes.json +57 -0
  5. package/content/claims/harness-engineering-is-a-distinct-discipline.json +43 -0
  6. package/content/claims/harness-is-the-durable-asset.json +43 -0
  7. package/content/claims/maturity-levels-are-ordered-by-dependency.json +43 -0
  8. package/content/claims/practice-converges-on-eight-components.json +58 -0
  9. package/content/claims/verified-outcome-is-the-unit.json +50 -0
  10. package/content/governance/agentic-ai-governance-checklist.json +3 -1
  11. package/content/harness/HRN-001-definition-and-overview.es.md +6 -3
  12. package/content/harness/HRN-001-definition-and-overview.md +6 -3
  13. package/content/harness/HRN-001-definition-and-overview.pt.md +6 -3
  14. package/content/harness/HRN-013-glossary.es.md +1 -1
  15. package/content/harness/HRN-013-glossary.md +1 -1
  16. package/content/harness/HRN-013-glossary.pt.md +1 -1
  17. package/content/knowledge/foundation-models.json +2 -1
  18. package/content/knowledge/harness-engineering.json +3 -3
  19. package/content/library/the-agentic-enterprise-needs-an-immune-system.md +16 -0
  20. package/content/library/the-stopwatch-and-the-exam.md +5 -1
  21. package/content/matrix/agentic-control-matrix.json +402 -3
  22. package/content/maturity/harness-maturity-model.json +569 -0
  23. package/content/patterns/evaluator-optimizer.json +1 -0
  24. package/content/patterns/goal-decomposition.json +1 -0
  25. package/content/patterns/human-approval-gate.json +1 -0
  26. package/content/patterns/long-term-memory.json +1 -0
  27. package/content/patterns/orchestrator-workers.json +1 -0
  28. package/content/patterns/sandboxed-execution.json +1 -0
  29. package/content/patterns/supervisor-agent.json +1 -0
  30. package/content/scorecard/harness-scorecard.json +460 -0
  31. package/dist/articles.js +120 -0
  32. package/dist/content.js +14 -1
  33. package/dist/shape.js +125 -3
  34. package/dist/tools.js +176 -4
  35. package/package.json +1 -1
package/dist/shape.js CHANGED
@@ -184,6 +184,14 @@ function stem(token) {
184
184
  return token;
185
185
  }
186
186
  /** Relative importance of each field when scoring a search hit. */
187
+ /**
188
+ * Cuánto puede sumar como mucho la centralidad. Por debajo del peso de un
189
+ * acierto en título (6) a propósito: desempata y matiza dentro de un empate,
190
+ * nunca adelanta a una unidad que acertó más términos de la consulta.
191
+ */
192
+ const CENTRALITY_NUDGE = 1.5;
193
+ /** Satura: pasar de 0 a 4 enlaces importa; de 40 a 44, ya no. */
194
+ const centralityBonus = (inbound) => CENTRALITY_NUDGE * (inbound / (inbound + 4));
187
195
  const FIELD_WEIGHTS = {
188
196
  name: 6,
189
197
  slug: 5,
@@ -231,6 +239,7 @@ function buildSearchFields(entry) {
231
239
  name: norm(names.join(" ")),
232
240
  slug: norm(entry.slug ?? ""),
233
241
  id: norm(str(raw.id)),
242
+ aliases: norm(arr(raw.aliases)),
234
243
  summary: norm(summaries.join(" ")),
235
244
  category: norm(entry.category ?? ""),
236
245
  keyConcepts: norm(concepts.join(" ")),
@@ -448,8 +457,70 @@ function summarizeHomeric(kind, e, locale) {
448
457
  api_url: `${SITE_URL}/api/homeric/${kind}/${e.slug}`,
449
458
  };
450
459
  }
451
- export function makeContent(loadAll, loadHandbook, loadHomeric) {
460
+ export function makeContent(loadAll, loadHandbook, loadHomeric, loadClaims) {
452
461
  const getOne = (domain, slug) => loadAll(domain).find((e) => e.slug === slug);
462
+ /**
463
+ * Enlaces entrantes por unidad — la única señal de centralidad que este
464
+ * corpus ya tiene, y la que decide un empate de puntuación.
465
+ *
466
+ * Para una consulta de una palabra, toda unidad que la lleve en el título
467
+ * saca exactamente la misma nota: "memory" empataba 4 unidades y "harness" 7,
468
+ * y el orden lo resolvía `localeCompare`. Cuál es la canónica no está en el
469
+ * texto — está en quién apunta a quién.
470
+ *
471
+ * Cuenta TODAS las aristas declaradas, incluidas las del manual, que
472
+ * referencia por identificador (`HRN-003`, `GOV-001`, `ARCH-001`) y no por
473
+ * slug: medirlo solo sobre los cuatro dominios JSON dejaba a los 14 capítulos
474
+ * a cero por artefacto y habría hundido el manual entero fingiendo que era
475
+ * una señal.
476
+ */
477
+ let inboundCache = null;
478
+ const inboundLinks = () => {
479
+ if (inboundCache)
480
+ return inboundCache;
481
+ const bySlug = new Map(); // ARCH-001 / HRN-003 -> slug
482
+ const entries = [];
483
+ for (const domain of DOMAINS) {
484
+ for (const e of loadAll(domain)) {
485
+ entries.push(e);
486
+ const id = e.id;
487
+ if (typeof id === "string")
488
+ bySlug.set(id.toUpperCase(), e.slug);
489
+ const aliases = e.aliases;
490
+ if (Array.isArray(aliases)) {
491
+ for (const alias of aliases)
492
+ if (typeof alias === "string")
493
+ bySlug.set(alias.toUpperCase(), e.slug);
494
+ }
495
+ }
496
+ }
497
+ if (loadHandbook) {
498
+ for (const c of loadHandbook()) {
499
+ entries.push(c);
500
+ if (c.id)
501
+ bySlug.set(String(c.id).toUpperCase(), c.slug);
502
+ }
503
+ }
504
+ const inb = new Map();
505
+ for (const e of entries) {
506
+ for (const field of ["related", "patterns", "knowledge", "architectures", "governance"]) {
507
+ const refs = e[field];
508
+ if (!Array.isArray(refs))
509
+ continue;
510
+ for (const r of refs) {
511
+ if (typeof r !== "string")
512
+ continue;
513
+ // Una referencia que no resuelve acaba bajo una clave que ninguna
514
+ // unidad tiene, así que no confiere centralidad a nadie. Quien avisa
515
+ // de que existen es `npm run validate`, no esto.
516
+ const slug = bySlug.get(r.toUpperCase()) ?? r;
517
+ inb.set(slug, (inb.get(slug) ?? 0) + 1);
518
+ }
519
+ }
520
+ }
521
+ inboundCache = inb;
522
+ return inb;
523
+ };
453
524
  const card = (d, e, locale, type) => ({
454
525
  type,
455
526
  ...summarize(d, e, locale),
@@ -497,6 +568,12 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
497
568
  llms_full_txt: `${SITE_URL}/llms-full.txt`,
498
569
  graph: `${SITE_URL}/api/graph.json`,
499
570
  homeric_atlas: `${SITE_URL}/api/homeric-atlas.json`,
571
+ articles: "https://articles.santismm.com/llms-full.txt",
572
+ articles_index: "https://articles.santismm.com/ai-index.json",
573
+ articles_api: "https://articles.santismm.com/api/articles.json",
574
+ labs: "https://labs.santismm.com/llms-full.txt",
575
+ labs_api: "https://labs.santismm.com/api/labs",
576
+ labs_openapi: "https://labs.santismm.com/openapi.json",
500
577
  },
501
578
  };
502
579
  },
@@ -538,11 +615,13 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
538
615
  // measured across the corpus actually being searched.
539
616
  const wantHandbook = domains.includes("handbook");
540
617
  const structured = domains.filter((d) => d !== "handbook");
618
+ const inb = inboundLinks();
541
619
  const docs = [];
542
620
  for (const domain of structured) {
543
621
  for (const entry of loadAll(domain)) {
544
622
  docs.push({
545
623
  fields: searchFields(entry),
624
+ slug: entry.slug,
546
625
  card: () => summarize(domain, entry, locale),
547
626
  });
548
627
  }
@@ -553,6 +632,7 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
553
632
  for (const e of loadHandbook()) {
554
633
  docs.push({
555
634
  fields: handbookFields(e),
635
+ slug: e.slug,
556
636
  card: () => summarizeHandbook(e, locale),
557
637
  });
558
638
  }
@@ -571,12 +651,54 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
571
651
  const scored = [];
572
652
  for (const d of docs) {
573
653
  const hit = scoreEntry(d.fields, tokens, idf);
574
- if (hit)
575
- scored.push({ ...d.card(), ...hit });
654
+ if (!hit)
655
+ continue;
656
+ // La centralidad se suma DESPUÉS de puntuar el texto: decide entre
657
+ // unidades que ya empataron por relevancia, no compite con ella.
658
+ const score = Math.round((hit.score + centralityBonus(inb.get(d.slug) ?? 0)) * 100) / 100;
659
+ scored.push({ ...d.card(), ...hit, score });
576
660
  }
577
661
  scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
578
662
  return scored.slice(0, limit);
579
663
  },
664
+ /**
665
+ * Claims (ADR 0003). The card carries the type and the confidence, because
666
+ * that is the whole point: an agent must be able to tell an observed fact
667
+ * from a bet without reading the prose and guessing.
668
+ */
669
+ listClaims(type, locale = "en") {
670
+ if (!loadClaims)
671
+ return [];
672
+ return loadClaims()
673
+ .filter((c) => !type || c.claimType === type)
674
+ // Orden estable por id: el del directorio no lo es, y un listado que
675
+ // cambia de orden entre llamadas es un listado en el que no se confía.
676
+ .sort((a, b) => String(a.id).localeCompare(String(b.id)))
677
+ .map((c) => {
678
+ const L = (c.locales ?? {})[locale] ?? {};
679
+ return {
680
+ type: "claim",
681
+ id: c.id,
682
+ slug: c.slug,
683
+ claim_type: c.claimType,
684
+ confidence_level: c.confidenceLevel,
685
+ statement: L.statement,
686
+ supports: c.supports,
687
+ reviewed: c.reviewed,
688
+ };
689
+ });
690
+ },
691
+ /** By `HE-CLAIM-nnn` or slug: an agent that saw either can come back. */
692
+ getClaim(id, locale = "en") {
693
+ if (!loadClaims)
694
+ return undefined;
695
+ const key = String(id).toUpperCase();
696
+ const c = loadClaims().find((x) => String(x.id).toUpperCase() === key || x.slug === id);
697
+ if (!c)
698
+ return undefined;
699
+ const L = (c.locales ?? {})[locale] ?? {};
700
+ return { type: "claim", ...c, body: L };
701
+ },
580
702
  listHomeric(kind, locale = "en") {
581
703
  if (!loadHomeric)
582
704
  return [];
package/dist/tools.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { ARTICLES_API_URL, articleCard, articlesForLocale, loadArticles, searchArticleCorpus, } from "./articles.js";
2
3
  /**
3
4
  * Single, framework-agnostic definition of the Santismm Knowledge MCP server:
4
5
  * its identity and the tool registry. Both transports — the stdio CLI
@@ -7,11 +8,12 @@ import { z } from "zod";
7
8
  * between them. The data source is injected as `content` (an `McpContent`
8
9
  * provider); both providers read the same `content/{domain}/*.json` files.
9
10
  */
10
- export const SERVER_INFO = { name: "santismm-knowledge", version: "0.2.2" };
11
+ export const SERVER_INFO = { name: "santismm-knowledge", version: "0.3.0" };
11
12
  /**
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.
13
+ * Core tools read a static local corpus, so all four hints are literally true:
14
+ * nothing mutates, the same arguments produce the same answer, and no core
15
+ * tool reaches outside this corpus. Federated Article tools override only the
16
+ * open-world hint because they read another first-party origin.
15
17
  *
16
18
  * Declaring them matters because a client deciding whether a call needs
17
19
  * confirmation, and an aggregator deciding how to present the server, both read
@@ -24,6 +26,8 @@ const READ_ONLY = {
24
26
  idempotentHint: true,
25
27
  openWorldHint: false,
26
28
  };
29
+ /** Article tools read another first-party origin, so clients must know they are open-world. */
30
+ const READ_ONLY_REMOTE = { ...READ_ONLY, openWorldHint: true };
27
31
  const localeSchema = z
28
32
  .enum(["en", "es", "pt"])
29
33
  .optional()
@@ -122,6 +126,8 @@ const unitOutput = z.object({
122
126
  domain: z.string().optional(),
123
127
  id: z.string().optional(),
124
128
  slug: z.string().optional(),
129
+ /** Legacy identifiers that resolve to this canonical unit. */
130
+ aliases: z.array(z.string()).optional(),
125
131
  category: z.string().optional(),
126
132
  updated: z.string().optional(),
127
133
  version: z.string().optional(),
@@ -223,6 +229,8 @@ const ESPACIOS = [
223
229
  { domain: "homeric/places", listTool: "list_homeric_places", getTool: "get_homeric_place" },
224
230
  { domain: "homeric/episodes", listTool: "list_homeric_episodes", getTool: "get_homeric_episode" },
225
231
  { domain: "homeric/routes", listTool: "list_homeric_routes", getTool: "get_homeric_route" },
232
+ { domain: "claims", listTool: "list_claims", getTool: "get_claim" },
233
+ { domain: "articles", listTool: "list_articles", getTool: "get_article" },
226
234
  ];
227
235
  /** Every identifier a card answers to: its id (handbook chapters) and its slug. */
228
236
  function identificadores(cards) {
@@ -240,8 +248,14 @@ function identificadores(cards) {
240
248
  }
241
249
  /** The cards of one space, whichever loader serves it. */
242
250
  function cardsDe(content, domain, locale) {
251
+ // Federated Articles are asynchronous and have their own recovery payload;
252
+ // keep them out of the synchronous cross-space lookup used by local units.
253
+ if (domain === "articles")
254
+ return [];
243
255
  if (domain === "handbook")
244
256
  return content.listHandbook(locale);
257
+ if (domain === "claims")
258
+ return content.listClaims(undefined, locale);
245
259
  if (domain.startsWith("homeric/")) {
246
260
  return content.listHomeric(domain.slice("homeric/".length), locale);
247
261
  }
@@ -416,6 +430,77 @@ const homericListOutput = {
416
430
  results: z.array(homericCard),
417
431
  };
418
432
  const homericUnitOutput = z.object({}).passthrough();
433
+ /**
434
+ * Claims get their own shape for the same reason the atlas does: forcing them
435
+ * into the corpus card would mean inventing an `evidence` block they do not
436
+ * have. A claim is not a unit with provenance — it IS the provenance, and what
437
+ * it carries instead is the rung of the ladder it sits on.
438
+ */
439
+ const claimCard = z.object({
440
+ type: z.literal("claim"),
441
+ id: z.string(),
442
+ slug: z.string(),
443
+ claim_type: z.enum(["observed_fact", "industry_synthesis", "santismm_thesis", "strategic_hypothesis"]),
444
+ confidence_level: z.string(),
445
+ statement: z.string().optional(),
446
+ supports: z.array(z.string()),
447
+ reviewed: z.string(),
448
+ });
449
+ const claimListOutput = { count: z.number(), results: z.array(claimCard) };
450
+ const claimUnitOutput = z.object({}).passthrough();
451
+ const articleCardSchema = z.object({
452
+ slug: z.string(),
453
+ title: z.string(),
454
+ summary: z.string(),
455
+ language: z.string(),
456
+ published: z.string(),
457
+ modified: z.string(),
458
+ topics: z.array(z.string()),
459
+ translation_key: z.string().optional(),
460
+ canonical_url: z.string().describe("Cite this URL."),
461
+ api_url: z.string(),
462
+ });
463
+ const articleUnitOutput = articleCardSchema.extend({
464
+ body: z.string().describe("Full Markdown-like article body."),
465
+ });
466
+ const articleListOutput = { count: z.number(), results: z.array(articleCardSchema) };
467
+ const articleSearchOutput = {
468
+ query: z.string(),
469
+ count: z.number(),
470
+ results: z.array(articleCardSchema.extend({
471
+ score: z.number(),
472
+ matchedFields: z.array(z.string()),
473
+ matchedTerms: z.array(z.string()),
474
+ })),
475
+ };
476
+ function articleFailure(error) {
477
+ const body = {
478
+ error: "articles_unavailable",
479
+ source: ARTICLES_API_URL,
480
+ hint: "The first-party Articles API could not be read. Retry later or use its llms-full.txt corpus directly.",
481
+ detail: error instanceof Error ? error.message : String(error),
482
+ };
483
+ return {
484
+ content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
485
+ isError: true,
486
+ };
487
+ }
488
+ function articleNotFound(articles, slug) {
489
+ const available = articles.map((article) => article.slug).sort();
490
+ const body = {
491
+ error: "not_found",
492
+ domain: "articles",
493
+ slug,
494
+ available_count: available.length,
495
+ available,
496
+ list_tool: "list_articles",
497
+ hint: "Call list_articles for every valid slug; article slugs are not interchangeable with core corpus identifiers.",
498
+ };
499
+ return {
500
+ content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
501
+ isError: true,
502
+ };
503
+ }
419
504
  export function registerTools(server, content) {
420
505
  // ── Orientation ────────────────────────────────────────────────────────────
421
506
  server.registerTool("get_overview", {
@@ -571,6 +656,61 @@ export function registerTools(server, content) {
571
656
  const results = content.search(query, domains, limit ?? 20, (locale ?? "en"));
572
657
  return outList(results, { query });
573
658
  });
659
+ // ── First-party articles (federated) ─────────────────────────────────────
660
+ server.registerTool("list_articles", {
661
+ title: "List first-party essays",
662
+ annotations: READ_ONLY_REMOTE,
663
+ description: "List every long-form essay published on articles.santismm.com, with language, dates, topics and citable canonical URLs. Use this to browse the essay catalogue; use `search_articles` when you have a topic rather than a slug.",
664
+ inputSchema: z.object({
665
+ locale: localeSchema.describe("Restrict to en, es or pt. Omit to return every language."),
666
+ }),
667
+ outputSchema: articleListOutput,
668
+ }, async ({ locale }) => {
669
+ try {
670
+ const articles = articlesForLocale(await loadArticles(), locale);
671
+ return outList(articles.map(articleCard));
672
+ }
673
+ catch (error) {
674
+ return articleFailure(error);
675
+ }
676
+ });
677
+ server.registerTool("get_article", {
678
+ title: "Get a first-party essay",
679
+ annotations: READ_ONLY_REMOTE,
680
+ description: "Get one complete essay by slug, including its clean Markdown-like body, metadata, licence context and canonical URL. Use this after `list_articles` or `search_articles` has returned the slug you need.",
681
+ inputSchema: z.object({
682
+ slug: slugSchema.describe("Article slug, e.g. 'the-stopwatch-and-the-exam'."),
683
+ }),
684
+ outputSchema: articleUnitOutput,
685
+ }, async ({ slug }) => {
686
+ try {
687
+ const articles = await loadArticles();
688
+ const article = articles.find((candidate) => candidate.slug === slug);
689
+ return article ? out(article) : articleNotFound(articles, slug);
690
+ }
691
+ catch (error) {
692
+ return articleFailure(error);
693
+ }
694
+ });
695
+ server.registerTool("search_articles", {
696
+ title: "Search first-party essays",
697
+ annotations: READ_ONLY_REMOTE,
698
+ description: "Ranked, accent-insensitive full-text search over every first-party essay, including titles, summaries, topics and bodies. Use this when you need long-form analysis about a topic; follow with `get_article` for the complete essay.",
699
+ inputSchema: z.object({
700
+ query: querySchema.describe("Keyword or phrase to search for in any supported language."),
701
+ locale: localeSchema.describe("Restrict to en, es or pt. Omit to search every language."),
702
+ limit: z.number().int().positive().max(20).optional().describe("Maximum results (default 10)."),
703
+ }),
704
+ outputSchema: articleSearchOutput,
705
+ }, async ({ query, locale, limit }) => {
706
+ try {
707
+ const articles = articlesForLocale(await loadArticles(), locale);
708
+ return outList(searchArticleCorpus(articles, query, limit ?? 10), { query });
709
+ }
710
+ catch (error) {
711
+ return articleFailure(error);
712
+ }
713
+ });
574
714
  // ── Graph traversal ────────────────────────────────────────────────────────
575
715
  server.registerTool("get_related", {
576
716
  title: "Traverse the Knowledge Graph",
@@ -655,6 +795,38 @@ export function registerTools(server, content) {
655
795
  const entry = content.getHomeric("routes", slug, locale);
656
796
  return entry ? out(entry) : noEncontrado(content, "homeric/routes", slug, (locale ?? "en"));
657
797
  });
798
+ // ── Claims (ADR 0003) ────────────────────────────────────────────────────
799
+ // Everything else in this server answers "what does the corpus say?". These
800
+ // two answer "how strongly, and on what?" — which of the corpus's statements
801
+ // are observed fact, which are a reading of the industry, which are our own
802
+ // position and which are a bet. Without them an agent has to infer the
803
+ // epistemic status from prose, and prose does not distinguish those.
804
+ server.registerTool("list_claims", {
805
+ title: "List the corpus claims and their epistemic status",
806
+ annotations: READ_ONLY,
807
+ description: "List the load-bearing claims of the corpus, each tagged as observed_fact, industry_synthesis, santismm_thesis or strategic_hypothesis, with its confidence and the units it underpins. Use this before quoting the handbook to know whether a statement is evidence, a reading of the industry, or a position taken. Filter by `claim_type` to get only what is checkable.",
808
+ inputSchema: z.object({
809
+ claim_type: z
810
+ .enum(["observed_fact", "industry_synthesis", "santismm_thesis", "strategic_hypothesis"])
811
+ .optional()
812
+ .describe("Restrict to one rung of the ladder. Omit for all."),
813
+ locale: localeSchema,
814
+ }),
815
+ outputSchema: claimListOutput,
816
+ }, async ({ claim_type, locale }) => outList(content.listClaims(claim_type, (locale ?? "en"))));
817
+ server.registerTool("get_claim", {
818
+ title: "Get one claim with its limits and what would refute it",
819
+ annotations: READ_ONLY,
820
+ description: "Get one claim by id (HE-CLAIM-001) or slug: the statement, what it rests on, its structured sources, and — always present — what it does NOT establish and the observation that would retire it. Use this to cite the corpus honestly, or to check whether a result you have just measured confirms or falsifies a claim it makes.",
821
+ inputSchema: z.object({
822
+ id: z.string().describe("Claim id (e.g. 'HE-CLAIM-001') or slug."),
823
+ locale: localeSchema,
824
+ }),
825
+ outputSchema: claimUnitOutput,
826
+ }, async ({ id, locale }) => {
827
+ const c = content.getClaim(id, locale);
828
+ return c ? out(c) : noEncontrado(content, "claims", id, (locale ?? "en"));
829
+ });
658
830
  }
659
831
  /**
660
832
  * The tools this server exposes, as advertised metadata (name + description).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santismm-knowledge-mcp",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "description": "MCP server for the Santismm Knowledge Platform — harness engineering, agentic AI patterns, reference architectures and AI governance. Ships the corpus; the hosted endpoint at https://santismm.com/mcp is the always-fresh alternative.",
6
6
  "type": "module",