santismm-knowledge-mcp 0.2.1 → 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.
- package/README.md +35 -15
- package/content/architectures/ai-workforce.json +3 -1
- package/content/claims/a-harness-can-degrade-the-agent.json +51 -0
- package/content/claims/harness-changes-outcomes.json +57 -0
- package/content/claims/harness-engineering-is-a-distinct-discipline.json +43 -0
- package/content/claims/harness-is-the-durable-asset.json +43 -0
- package/content/claims/maturity-levels-are-ordered-by-dependency.json +43 -0
- package/content/claims/practice-converges-on-eight-components.json +58 -0
- package/content/claims/verified-outcome-is-the-unit.json +50 -0
- package/content/governance/agentic-ai-governance-checklist.json +3 -1
- package/content/harness/HRN-001-definition-and-overview.es.md +6 -3
- package/content/harness/HRN-001-definition-and-overview.md +6 -3
- package/content/harness/HRN-001-definition-and-overview.pt.md +6 -3
- package/content/harness/HRN-013-glossary.es.md +1 -1
- package/content/harness/HRN-013-glossary.md +1 -1
- package/content/harness/HRN-013-glossary.pt.md +1 -1
- package/content/knowledge/foundation-models.json +2 -1
- package/content/knowledge/harness-engineering.json +3 -3
- package/content/library/the-agentic-enterprise-needs-an-immune-system.md +16 -0
- package/content/library/the-stopwatch-and-the-exam.md +5 -1
- package/content/matrix/agentic-control-matrix.json +408 -5
- package/content/maturity/harness-maturity-model.json +569 -0
- package/content/patterns/correlated-run-trace.json +329 -0
- package/content/patterns/egress-allowlist.json +4 -3
- package/content/patterns/evaluator-optimizer.json +1 -0
- package/content/patterns/goal-decomposition.json +1 -0
- package/content/patterns/human-approval-gate.json +1 -0
- package/content/patterns/long-term-memory.json +1 -0
- package/content/patterns/orchestrator-workers.json +1 -0
- package/content/patterns/output-boundary-encoding.json +320 -0
- package/content/patterns/recovery-strategy.json +8 -3
- package/content/patterns/sandboxed-execution.json +1 -0
- package/content/patterns/supervisor-agent.json +1 -0
- package/content/scorecard/harness-scorecard.json +460 -0
- package/dist/articles.js +120 -0
- package/dist/content.js +14 -1
- package/dist/shape.js +150 -3
- package/dist/tools.js +202 -4
- package/package.json +1 -1
package/dist/shape.js
CHANGED
|
@@ -78,6 +78,22 @@ const DOMAIN_INFO = {
|
|
|
78
78
|
lookup: "HRN id (e.g. HRN-001) or slug",
|
|
79
79
|
},
|
|
80
80
|
};
|
|
81
|
+
/**
|
|
82
|
+
* Newest `updated` across a set of units, or null if none carries one.
|
|
83
|
+
*
|
|
84
|
+
* Dates are ISO `YYYY-MM-DD`, so a string comparison is the date comparison.
|
|
85
|
+
* Derived from the loaded content rather than stamped at build time, so it is
|
|
86
|
+
* correct in every deployment without anyone remembering to update it.
|
|
87
|
+
*/
|
|
88
|
+
function newestUpdated(items) {
|
|
89
|
+
let newest = null;
|
|
90
|
+
for (const it of items) {
|
|
91
|
+
const u = it.updated;
|
|
92
|
+
if (typeof u === "string" && (newest === null || u > newest))
|
|
93
|
+
newest = u;
|
|
94
|
+
}
|
|
95
|
+
return newest;
|
|
96
|
+
}
|
|
81
97
|
/** Distinct, sorted category values — read off the content, never hand-listed. */
|
|
82
98
|
function categoriesOf(items) {
|
|
83
99
|
return [...new Set(items.map((e) => e.category).filter((c) => Boolean(c)))].sort();
|
|
@@ -168,6 +184,14 @@ function stem(token) {
|
|
|
168
184
|
return token;
|
|
169
185
|
}
|
|
170
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));
|
|
171
195
|
const FIELD_WEIGHTS = {
|
|
172
196
|
name: 6,
|
|
173
197
|
slug: 5,
|
|
@@ -215,6 +239,7 @@ function buildSearchFields(entry) {
|
|
|
215
239
|
name: norm(names.join(" ")),
|
|
216
240
|
slug: norm(entry.slug ?? ""),
|
|
217
241
|
id: norm(str(raw.id)),
|
|
242
|
+
aliases: norm(arr(raw.aliases)),
|
|
218
243
|
summary: norm(summaries.join(" ")),
|
|
219
244
|
category: norm(entry.category ?? ""),
|
|
220
245
|
keyConcepts: norm(concepts.join(" ")),
|
|
@@ -432,8 +457,70 @@ function summarizeHomeric(kind, e, locale) {
|
|
|
432
457
|
api_url: `${SITE_URL}/api/homeric/${kind}/${e.slug}`,
|
|
433
458
|
};
|
|
434
459
|
}
|
|
435
|
-
export function makeContent(loadAll, loadHandbook, loadHomeric) {
|
|
460
|
+
export function makeContent(loadAll, loadHandbook, loadHomeric, loadClaims) {
|
|
436
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
|
+
};
|
|
437
524
|
const card = (d, e, locale, type) => ({
|
|
438
525
|
type,
|
|
439
526
|
...summarize(d, e, locale),
|
|
@@ -463,6 +550,15 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
|
|
|
463
550
|
license_url: LICENSE_INFO.url,
|
|
464
551
|
total: domains.reduce((n, d) => n + d.count, 0),
|
|
465
552
|
domains,
|
|
553
|
+
corpus: {
|
|
554
|
+
newest_unit: newestUpdated([
|
|
555
|
+
...DOMAINS.flatMap((d) => loadAll(d)),
|
|
556
|
+
...(loadHandbook ? loadHandbook() : []),
|
|
557
|
+
]),
|
|
558
|
+
freshness: `This describes the copy answering the call. ${SITE_URL}/mcp is redeployed on ` +
|
|
559
|
+
"every content change; a published package carries the corpus frozen at publish " +
|
|
560
|
+
"time. If total or newest_unit differ from that endpoint's, you are holding a snapshot.",
|
|
561
|
+
},
|
|
466
562
|
next: "search(query, locale) to answer a question across the whole corpus; " +
|
|
467
563
|
"list_<domain> to browse one; get_<domain>(slug) for a full unit with its " +
|
|
468
564
|
"Evidence-First provenance; get_related(domain, slug) to traverse the graph. " +
|
|
@@ -472,6 +568,12 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
|
|
|
472
568
|
llms_full_txt: `${SITE_URL}/llms-full.txt`,
|
|
473
569
|
graph: `${SITE_URL}/api/graph.json`,
|
|
474
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",
|
|
475
577
|
},
|
|
476
578
|
};
|
|
477
579
|
},
|
|
@@ -513,11 +615,13 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
|
|
|
513
615
|
// measured across the corpus actually being searched.
|
|
514
616
|
const wantHandbook = domains.includes("handbook");
|
|
515
617
|
const structured = domains.filter((d) => d !== "handbook");
|
|
618
|
+
const inb = inboundLinks();
|
|
516
619
|
const docs = [];
|
|
517
620
|
for (const domain of structured) {
|
|
518
621
|
for (const entry of loadAll(domain)) {
|
|
519
622
|
docs.push({
|
|
520
623
|
fields: searchFields(entry),
|
|
624
|
+
slug: entry.slug,
|
|
521
625
|
card: () => summarize(domain, entry, locale),
|
|
522
626
|
});
|
|
523
627
|
}
|
|
@@ -528,6 +632,7 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
|
|
|
528
632
|
for (const e of loadHandbook()) {
|
|
529
633
|
docs.push({
|
|
530
634
|
fields: handbookFields(e),
|
|
635
|
+
slug: e.slug,
|
|
531
636
|
card: () => summarizeHandbook(e, locale),
|
|
532
637
|
});
|
|
533
638
|
}
|
|
@@ -546,12 +651,54 @@ export function makeContent(loadAll, loadHandbook, loadHomeric) {
|
|
|
546
651
|
const scored = [];
|
|
547
652
|
for (const d of docs) {
|
|
548
653
|
const hit = scoreEntry(d.fields, tokens, idf);
|
|
549
|
-
if (hit)
|
|
550
|
-
|
|
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 });
|
|
551
660
|
}
|
|
552
661
|
scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
553
662
|
return scored.slice(0, limit);
|
|
554
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
|
+
},
|
|
555
702
|
listHomeric(kind, locale = "en") {
|
|
556
703
|
if (!loadHomeric)
|
|
557
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.
|
|
11
|
+
export const SERVER_INFO = { name: "santismm-knowledge", version: "0.3.0" };
|
|
11
12
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
}
|
|
@@ -349,6 +363,26 @@ function noEncontrado(content, domain, pedido, locale) {
|
|
|
349
363
|
pistas.push(`${espacio.listTool} returns every identifier this tool accepts.`);
|
|
350
364
|
}
|
|
351
365
|
pistas.push("Identifiers are not interchangeable between spaces; do not invent one by analogy.");
|
|
366
|
+
/**
|
|
367
|
+
* The third reason a lookup fails, and the one this answer could not express.
|
|
368
|
+
*
|
|
369
|
+
* A published package carries the corpus frozen at publish time, so an agent
|
|
370
|
+
* holding one gets `not_found` for a unit that exists — and everything above
|
|
371
|
+
* tells it, truthfully but misleadingly, that the identifier is not in the
|
|
372
|
+
* corpus. Stating the date of this copy turns that into something the caller
|
|
373
|
+
* can check instead of a dead end wearing a helpful face.
|
|
374
|
+
*/
|
|
375
|
+
try {
|
|
376
|
+
const fecha = content.overview().corpus.newest_unit;
|
|
377
|
+
if (fecha) {
|
|
378
|
+
cuerpo.corpus_newest_unit = fecha;
|
|
379
|
+
pistas.push(`This copy holds nothing newer than ${fecha}. If the unit was added after that, ` +
|
|
380
|
+
"a package snapshot is the reason and the hosted endpoint has it.");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
// La recuperación nunca puede ser la que rompa la respuesta.
|
|
385
|
+
}
|
|
352
386
|
cuerpo.hint = pistas.join(" ");
|
|
353
387
|
return {
|
|
354
388
|
content: [{ type: "text", text: JSON.stringify(cuerpo, null, 2) }],
|
|
@@ -396,6 +430,77 @@ const homericListOutput = {
|
|
|
396
430
|
results: z.array(homericCard),
|
|
397
431
|
};
|
|
398
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
|
+
}
|
|
399
504
|
export function registerTools(server, content) {
|
|
400
505
|
// ── Orientation ────────────────────────────────────────────────────────────
|
|
401
506
|
server.registerTool("get_overview", {
|
|
@@ -421,6 +526,12 @@ export function registerTools(server, content) {
|
|
|
421
526
|
url: z.string(),
|
|
422
527
|
api_url: z.string(),
|
|
423
528
|
})),
|
|
529
|
+
corpus: z
|
|
530
|
+
.object({
|
|
531
|
+
newest_unit: z.string().nullable().describe("Newest unit date in THIS copy (YYYY-MM-DD)."),
|
|
532
|
+
freshness: z.string(),
|
|
533
|
+
})
|
|
534
|
+
.describe("Whether this copy is current. Compare against the hosted endpoint: a lower total or an older newest_unit means you are holding a snapshot, not that the corpus lacks what you asked for."),
|
|
424
535
|
next: z.string(),
|
|
425
536
|
bulk: z.record(z.string(), z.string()),
|
|
426
537
|
}),
|
|
@@ -545,6 +656,61 @@ export function registerTools(server, content) {
|
|
|
545
656
|
const results = content.search(query, domains, limit ?? 20, (locale ?? "en"));
|
|
546
657
|
return outList(results, { query });
|
|
547
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
|
+
});
|
|
548
714
|
// ── Graph traversal ────────────────────────────────────────────────────────
|
|
549
715
|
server.registerTool("get_related", {
|
|
550
716
|
title: "Traverse the Knowledge Graph",
|
|
@@ -629,6 +795,38 @@ export function registerTools(server, content) {
|
|
|
629
795
|
const entry = content.getHomeric("routes", slug, locale);
|
|
630
796
|
return entry ? out(entry) : noEncontrado(content, "homeric/routes", slug, (locale ?? "en"));
|
|
631
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
|
+
});
|
|
632
830
|
}
|
|
633
831
|
/**
|
|
634
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.
|
|
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",
|