santismm-knowledge-mcp 0.2.2 → 0.4.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 +11 -9
- 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 +402 -3
- package/content/maturity/harness-maturity-model.json +569 -0
- 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/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/labs.js +134 -0
- package/dist/shape.js +172 -5
- package/dist/tools.js +560 -7
- package/package.json +2 -2
package/dist/tools.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { ARTICLES_API_URL, articleCard, articlesForLocale, loadArticles, searchArticleCorpus, } from "./articles.js";
|
|
3
|
+
import { LABS_API_URL, executeLabCalculator, loadLabs, searchLabCorpus, } from "./labs.js";
|
|
2
4
|
/**
|
|
3
5
|
* Single, framework-agnostic definition of the Santismm Knowledge MCP server:
|
|
4
6
|
* its identity and the tool registry. Both transports — the stdio CLI
|
|
5
7
|
* (`mcp/src/index.ts`) and the HTTP endpoint (`app/mcp/route.ts`) — call
|
|
6
8
|
* `registerTools(server, content)`, so the exposed tools can never drift
|
|
7
|
-
* between them. The data source is injected as `content` (an
|
|
8
|
-
* provider); both providers read the same
|
|
9
|
+
* between them. The local data source is injected as `content` (an
|
|
10
|
+
* `McpContent` provider); both providers read the same canonical repository
|
|
11
|
+
* data, while Article tools deliberately read the first-party Articles API.
|
|
9
12
|
*/
|
|
10
|
-
export const SERVER_INFO = { name: "santismm-knowledge", version: "0.
|
|
13
|
+
export const SERVER_INFO = { name: "santismm-knowledge", version: "0.4.0" };
|
|
11
14
|
/**
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* Core tools read a static local corpus, so all four hints are literally true:
|
|
16
|
+
* nothing mutates, the same arguments produce the same answer, and no core
|
|
17
|
+
* tool reaches outside this corpus. Federated Article tools override only the
|
|
18
|
+
* open-world hint because they read another first-party origin.
|
|
15
19
|
*
|
|
16
20
|
* Declaring them matters because a client deciding whether a call needs
|
|
17
21
|
* confirmation, and an aggregator deciding how to present the server, both read
|
|
@@ -24,6 +28,8 @@ const READ_ONLY = {
|
|
|
24
28
|
idempotentHint: true,
|
|
25
29
|
openWorldHint: false,
|
|
26
30
|
};
|
|
31
|
+
/** Article tools read another first-party origin, so clients must know they are open-world. */
|
|
32
|
+
const READ_ONLY_REMOTE = { ...READ_ONLY, openWorldHint: true };
|
|
27
33
|
const localeSchema = z
|
|
28
34
|
.enum(["en", "es", "pt"])
|
|
29
35
|
.optional()
|
|
@@ -122,6 +128,8 @@ const unitOutput = z.object({
|
|
|
122
128
|
domain: z.string().optional(),
|
|
123
129
|
id: z.string().optional(),
|
|
124
130
|
slug: z.string().optional(),
|
|
131
|
+
/** Legacy identifiers that resolve to this canonical unit. */
|
|
132
|
+
aliases: z.array(z.string()).optional(),
|
|
125
133
|
category: z.string().optional(),
|
|
126
134
|
updated: z.string().optional(),
|
|
127
135
|
version: z.string().optional(),
|
|
@@ -223,6 +231,9 @@ const ESPACIOS = [
|
|
|
223
231
|
{ domain: "homeric/places", listTool: "list_homeric_places", getTool: "get_homeric_place" },
|
|
224
232
|
{ domain: "homeric/episodes", listTool: "list_homeric_episodes", getTool: "get_homeric_episode" },
|
|
225
233
|
{ domain: "homeric/routes", listTool: "list_homeric_routes", getTool: "get_homeric_route" },
|
|
234
|
+
{ domain: "claims", listTool: "list_claims", getTool: "get_claim" },
|
|
235
|
+
{ domain: "articles", listTool: "list_articles", getTool: "get_article" },
|
|
236
|
+
{ domain: "labs", listTool: "list_labs", getTool: "get_lab" },
|
|
226
237
|
];
|
|
227
238
|
/** Every identifier a card answers to: its id (handbook chapters) and its slug. */
|
|
228
239
|
function identificadores(cards) {
|
|
@@ -240,8 +251,14 @@ function identificadores(cards) {
|
|
|
240
251
|
}
|
|
241
252
|
/** The cards of one space, whichever loader serves it. */
|
|
242
253
|
function cardsDe(content, domain, locale) {
|
|
254
|
+
// Federated Articles and Labs are asynchronous and have their own recovery
|
|
255
|
+
// payloads; keep them out of the synchronous cross-space lookup for locals.
|
|
256
|
+
if (domain === "articles" || domain === "labs")
|
|
257
|
+
return [];
|
|
243
258
|
if (domain === "handbook")
|
|
244
259
|
return content.listHandbook(locale);
|
|
260
|
+
if (domain === "claims")
|
|
261
|
+
return content.listClaims(undefined, locale);
|
|
245
262
|
if (domain.startsWith("homeric/")) {
|
|
246
263
|
return content.listHomeric(domain.slice("homeric/".length), locale);
|
|
247
264
|
}
|
|
@@ -416,12 +433,242 @@ const homericListOutput = {
|
|
|
416
433
|
results: z.array(homericCard),
|
|
417
434
|
};
|
|
418
435
|
const homericUnitOutput = z.object({}).passthrough();
|
|
436
|
+
/**
|
|
437
|
+
* Claims get their own shape for the same reason the atlas does: forcing them
|
|
438
|
+
* into the corpus card would mean inventing an `evidence` block they do not
|
|
439
|
+
* have. A claim is not a unit with provenance — it IS the provenance, and what
|
|
440
|
+
* it carries instead is the rung of the ladder it sits on.
|
|
441
|
+
*/
|
|
442
|
+
const claimCard = z.object({
|
|
443
|
+
type: z.literal("claim"),
|
|
444
|
+
id: z.string(),
|
|
445
|
+
slug: z.string(),
|
|
446
|
+
claim_type: z.enum(["observed_fact", "industry_synthesis", "santismm_thesis", "strategic_hypothesis"]),
|
|
447
|
+
confidence_level: z.string(),
|
|
448
|
+
statement: z.string().optional(),
|
|
449
|
+
supports: z.array(z.string()),
|
|
450
|
+
reviewed: z.string(),
|
|
451
|
+
});
|
|
452
|
+
const claimListOutput = { count: z.number(), results: z.array(claimCard) };
|
|
453
|
+
const claimUnitOutput = z.object({}).passthrough();
|
|
454
|
+
const articleCardSchema = z.object({
|
|
455
|
+
slug: z.string(),
|
|
456
|
+
title: z.string(),
|
|
457
|
+
summary: z.string(),
|
|
458
|
+
language: z.string(),
|
|
459
|
+
published: z.string(),
|
|
460
|
+
modified: z.string(),
|
|
461
|
+
topics: z.array(z.string()),
|
|
462
|
+
translation_key: z.string().optional(),
|
|
463
|
+
canonical_url: z.string().describe("Cite this URL."),
|
|
464
|
+
api_url: z.string(),
|
|
465
|
+
});
|
|
466
|
+
const articleUnitOutput = articleCardSchema.extend({
|
|
467
|
+
body: z.string().describe("Full Markdown-like article body."),
|
|
468
|
+
});
|
|
469
|
+
const articleListOutput = { count: z.number(), results: z.array(articleCardSchema) };
|
|
470
|
+
const articleSearchOutput = {
|
|
471
|
+
query: z.string(),
|
|
472
|
+
count: z.number(),
|
|
473
|
+
results: z.array(articleCardSchema.extend({
|
|
474
|
+
score: z.number(),
|
|
475
|
+
matchedFields: z.array(z.string()),
|
|
476
|
+
matchedTerms: z.array(z.string()),
|
|
477
|
+
})),
|
|
478
|
+
};
|
|
479
|
+
const relatedContentSchema = z.object({
|
|
480
|
+
title: z.string(),
|
|
481
|
+
url: z.string(),
|
|
482
|
+
relationship: z.string(),
|
|
483
|
+
});
|
|
484
|
+
const labCardSchema = z.object({
|
|
485
|
+
slug: z.string(),
|
|
486
|
+
kind: z.enum(["calculator", "converter", "experiment", "educational-game"]),
|
|
487
|
+
label: z.string(),
|
|
488
|
+
title: z.string(),
|
|
489
|
+
description: z.string(),
|
|
490
|
+
inputs: z.array(z.string()),
|
|
491
|
+
outputs: z.array(z.string()),
|
|
492
|
+
formulas: z.array(z.string()).optional(),
|
|
493
|
+
assumptions: z.array(z.string()),
|
|
494
|
+
version: z.string(),
|
|
495
|
+
updated: z.string(),
|
|
496
|
+
canonical_url: z.string().describe("Cite this URL."),
|
|
497
|
+
api_url: z.string(),
|
|
498
|
+
calculation_url: z.string().optional(),
|
|
499
|
+
related_content: z.array(relatedContentSchema).optional(),
|
|
500
|
+
});
|
|
501
|
+
const labListOutput = { count: z.number(), results: z.array(labCardSchema) };
|
|
502
|
+
const calculationBaseSchema = z.object({
|
|
503
|
+
schema_version: z.string(),
|
|
504
|
+
source: z.string(),
|
|
505
|
+
language: z.string(),
|
|
506
|
+
slug: z.string(),
|
|
507
|
+
version: z.string(),
|
|
508
|
+
updated: z.string(),
|
|
509
|
+
canonical_url: z.string().describe("Cite this URL."),
|
|
510
|
+
api_url: z.string(),
|
|
511
|
+
methodology_url: z.string(),
|
|
512
|
+
inputs: z.record(z.string(), z.number()),
|
|
513
|
+
units: z.record(z.string(), z.string()),
|
|
514
|
+
interpretation: z.string(),
|
|
515
|
+
assumptions: z.array(z.string()),
|
|
516
|
+
formulas: z.array(z.string()),
|
|
517
|
+
warnings: z.array(z.string()),
|
|
518
|
+
license: z.object({ name: z.string(), spdx: z.string(), url: z.string() }),
|
|
519
|
+
});
|
|
520
|
+
const agentEconomicsOutput = calculationBaseSchema.extend({
|
|
521
|
+
results: z.object({
|
|
522
|
+
attempts: z.number(), executionCost: z.number(), reviewCost: z.number(), failedCases: z.number(),
|
|
523
|
+
reworkCost: z.number(), operatingCost: z.number(), manualCost: z.number(), savings: z.number(),
|
|
524
|
+
roi: z.number(), successfulOutcomes: z.number(), costPerSuccess: z.number(), costPerResolved: z.number(),
|
|
525
|
+
breakEvenSuccess: z.number(),
|
|
526
|
+
verdict: z.object({ tone: z.enum(["positive", "watch", "negative"]), title: z.string(), body: z.string() }),
|
|
527
|
+
}),
|
|
528
|
+
});
|
|
529
|
+
const evaluationSampleOutput = calculationBaseSchema.extend({
|
|
530
|
+
results: z.object({ detect: z.number(), estimate: z.number(), expected: z.number(), zero: z.number() }),
|
|
531
|
+
});
|
|
532
|
+
const humanSupervisionOutput = calculationBaseSchema.extend({
|
|
533
|
+
results: z.object({
|
|
534
|
+
routine: z.number(), escalations: z.number(), workload: z.number(), productivePerFte: z.number(),
|
|
535
|
+
requiredFte: z.number(), headroom: z.number(), cost: z.number(), backlogDays: z.number(),
|
|
536
|
+
sustainableVolume: z.number(),
|
|
537
|
+
}),
|
|
538
|
+
});
|
|
539
|
+
const globalSearchCard = z.object({
|
|
540
|
+
surface: z.enum(["core", "articles", "labs", "claims"]),
|
|
541
|
+
score: z.number(),
|
|
542
|
+
source_score: z.number(),
|
|
543
|
+
rank_within_surface: z.number(),
|
|
544
|
+
id: z.string().optional(),
|
|
545
|
+
slug: z.string(),
|
|
546
|
+
domain: z.string().optional(),
|
|
547
|
+
kind: z.string().optional(),
|
|
548
|
+
title: z.string(),
|
|
549
|
+
summary: z.string().optional(),
|
|
550
|
+
canonical_url: z.string().optional(),
|
|
551
|
+
api_url: z.string().optional(),
|
|
552
|
+
calculation_url: z.string().optional(),
|
|
553
|
+
suggested_tool: z.string(),
|
|
554
|
+
matchedFields: z.array(z.string()),
|
|
555
|
+
matchedTerms: z.array(z.string()),
|
|
556
|
+
}).passthrough();
|
|
557
|
+
const globalSearchOutput = {
|
|
558
|
+
query: z.string(),
|
|
559
|
+
count: z.number(),
|
|
560
|
+
results: z.array(globalSearchCard),
|
|
561
|
+
unavailable_surfaces: z.array(z.object({
|
|
562
|
+
surface: z.enum(["articles", "labs"]),
|
|
563
|
+
error: z.string(),
|
|
564
|
+
retry_tool: z.string(),
|
|
565
|
+
})),
|
|
566
|
+
};
|
|
567
|
+
function articleFailure(error) {
|
|
568
|
+
const body = {
|
|
569
|
+
error: "articles_unavailable",
|
|
570
|
+
source: ARTICLES_API_URL,
|
|
571
|
+
hint: "The first-party Articles API could not be read. Retry later or use its llms-full.txt corpus directly.",
|
|
572
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
576
|
+
isError: true,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function articleNotFound(articles, slug) {
|
|
580
|
+
const available = articles.map((article) => article.slug).sort();
|
|
581
|
+
const body = {
|
|
582
|
+
error: "not_found",
|
|
583
|
+
domain: "articles",
|
|
584
|
+
slug,
|
|
585
|
+
available_count: available.length,
|
|
586
|
+
available,
|
|
587
|
+
list_tool: "list_articles",
|
|
588
|
+
hint: "Call list_articles for every valid slug; article slugs are not interchangeable with core corpus identifiers.",
|
|
589
|
+
};
|
|
590
|
+
return {
|
|
591
|
+
content: [{ type: "text", text: JSON.stringify(body, null, 2) }],
|
|
592
|
+
isError: true,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function labsFailure(error) {
|
|
596
|
+
const body = {
|
|
597
|
+
error: "labs_unavailable",
|
|
598
|
+
source: LABS_API_URL,
|
|
599
|
+
hint: "The first-party Labs API could not be read. Retry later or bind directly to its OpenAPI document.",
|
|
600
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
601
|
+
};
|
|
602
|
+
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
|
|
603
|
+
}
|
|
604
|
+
function globalSearchFailure(error) {
|
|
605
|
+
const body = {
|
|
606
|
+
error: "federated_search_unavailable",
|
|
607
|
+
hint: "One federated source could not be read. Retry search_all with a restricted surfaces array, or use search for the local core corpus.",
|
|
608
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
609
|
+
};
|
|
610
|
+
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
|
|
611
|
+
}
|
|
612
|
+
function labNotFound(labs, slug) {
|
|
613
|
+
const available = labs.map((lab) => lab.slug).sort();
|
|
614
|
+
const body = {
|
|
615
|
+
error: "not_found",
|
|
616
|
+
domain: "labs",
|
|
617
|
+
slug,
|
|
618
|
+
available_count: available.length,
|
|
619
|
+
available,
|
|
620
|
+
list_tool: "list_labs",
|
|
621
|
+
hint: "Call list_labs for every valid slug. Only Labs with calculation_url can be executed.",
|
|
622
|
+
};
|
|
623
|
+
return { content: [{ type: "text", text: JSON.stringify(body, null, 2) }], isError: true };
|
|
624
|
+
}
|
|
625
|
+
function termsFor(query) {
|
|
626
|
+
return [...new Set(query.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().split(/[^a-z0-9]+/).filter((term) => term.length > 1))];
|
|
627
|
+
}
|
|
628
|
+
function claimSearch(content, query, locale, limit) {
|
|
629
|
+
const terms = termsFor(query);
|
|
630
|
+
return content.listClaims(undefined, locale)
|
|
631
|
+
.map((claim) => {
|
|
632
|
+
const fields = [["statement", 7], ["slug", 6], ["id", 5], ["claim_type", 4]];
|
|
633
|
+
let score = 0;
|
|
634
|
+
const matchedFields = new Set();
|
|
635
|
+
const matchedTerms = new Set();
|
|
636
|
+
for (const [field, weight] of fields) {
|
|
637
|
+
const value = String(claim[field] ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
|
|
638
|
+
for (const term of terms) {
|
|
639
|
+
if (!value.includes(term))
|
|
640
|
+
continue;
|
|
641
|
+
score += weight;
|
|
642
|
+
matchedFields.add(field);
|
|
643
|
+
matchedTerms.add(term);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { claim, score, matchedFields: [...matchedFields], matchedTerms: [...matchedTerms] };
|
|
647
|
+
})
|
|
648
|
+
.filter((hit) => hit.score > 0)
|
|
649
|
+
.sort((a, b) => b.score - a.score || String(a.claim.id).localeCompare(String(b.claim.id)))
|
|
650
|
+
.slice(0, limit);
|
|
651
|
+
}
|
|
652
|
+
const GET_TOOL_FOR_DOMAIN = {
|
|
653
|
+
knowledge: "get_knowledge", patterns: "get_pattern", architectures: "get_architecture",
|
|
654
|
+
governance: "get_governance", handbook: "get_handbook",
|
|
655
|
+
};
|
|
656
|
+
function intentBoost(query, surface) {
|
|
657
|
+
const normal = termsFor(query).join(" ");
|
|
658
|
+
if (surface === "labs" && /\b(calcul\w*|how many|cuant\w*|sample|muestra|roi|cost\w*|coste\w*|supervis\w*|fte|capacity|capacidad|break even|token\w*|pages|paginas)\b/.test(normal))
|
|
659
|
+
return 30;
|
|
660
|
+
if (surface === "claims" && /\b(claim|claims|evidence|fact|thesis|tesis|hypothesis|hipotesis|falsif|refut)\b/.test(normal))
|
|
661
|
+
return 25;
|
|
662
|
+
if (surface === "articles" && /\b(article|articles|essay|essays|articulo|artículo|ensayo|recent|latest|nuevo|reciente)\b/.test(normal))
|
|
663
|
+
return 20;
|
|
664
|
+
return 0;
|
|
665
|
+
}
|
|
419
666
|
export function registerTools(server, content) {
|
|
420
667
|
// ── Orientation ────────────────────────────────────────────────────────────
|
|
421
668
|
server.registerTool("get_overview", {
|
|
422
669
|
title: "Corpus Overview — Start Here",
|
|
423
670
|
annotations: READ_ONLY,
|
|
424
|
-
description: "Get the
|
|
671
|
+
description: "Get the complete MCP map — start here. Returns the five-domain core plus the separate Article, Labs, Homeric Atlas and claim-registry surfaces, with their tools, identifiers, citation rules, languages, licence and bulk-ingest URLs.",
|
|
425
672
|
inputSchema: z.object({}),
|
|
426
673
|
outputSchema: z.object({
|
|
427
674
|
source: z.string(),
|
|
@@ -441,6 +688,14 @@ export function registerTools(server, content) {
|
|
|
441
688
|
url: z.string(),
|
|
442
689
|
api_url: z.string(),
|
|
443
690
|
})),
|
|
691
|
+
extensions: z.array(z.object({
|
|
692
|
+
surface: z.string(),
|
|
693
|
+
description: z.string(),
|
|
694
|
+
tools: z.array(z.string()),
|
|
695
|
+
source: z.string(),
|
|
696
|
+
lookup: z.string(),
|
|
697
|
+
citation: z.string(),
|
|
698
|
+
})),
|
|
444
699
|
corpus: z
|
|
445
700
|
.object({
|
|
446
701
|
newest_unit: z.string().nullable().describe("Newest unit date in THIS copy (YYYY-MM-DD)."),
|
|
@@ -451,6 +706,111 @@ export function registerTools(server, content) {
|
|
|
451
706
|
bulk: z.record(z.string(), z.string()),
|
|
452
707
|
}),
|
|
453
708
|
}, async () => out({ ...content.overview() }));
|
|
709
|
+
server.registerTool("search_all", {
|
|
710
|
+
title: "Search every SANTISMM knowledge surface",
|
|
711
|
+
annotations: READ_ONLY_REMOTE,
|
|
712
|
+
description: "Search the core corpus, first-party essays, executable Labs and epistemic claims in one call. Use this first when a natural-language question might require a calculation, a long-form essay or a claim audit rather than only a core knowledge unit. Results name the next tool to call; calculator-shaped questions are routed toward Labs.",
|
|
713
|
+
inputSchema: z.object({
|
|
714
|
+
query: querySchema.describe("Question or topic, in English, Spanish or Portuguese."),
|
|
715
|
+
surfaces: z.array(z.enum(["core", "articles", "labs", "claims"])).min(1).optional()
|
|
716
|
+
.describe("Restrict the search. Omit to search all four surfaces."),
|
|
717
|
+
limit_per_surface: z.number().int().positive().max(10).optional().describe("Maximum hits from each surface. Default: 5."),
|
|
718
|
+
locale: localeSchema,
|
|
719
|
+
}),
|
|
720
|
+
outputSchema: z.object(globalSearchOutput),
|
|
721
|
+
}, async ({ query, surfaces, limit_per_surface, locale }) => {
|
|
722
|
+
try {
|
|
723
|
+
const selected = new Set(surfaces ?? ["core", "articles", "labs", "claims"]);
|
|
724
|
+
const limit = limit_per_surface ?? 5;
|
|
725
|
+
const lang = (locale ?? "en");
|
|
726
|
+
const [articleLoad, labLoad] = await Promise.allSettled([
|
|
727
|
+
selected.has("articles") ? loadArticles() : Promise.resolve([]),
|
|
728
|
+
selected.has("labs") ? loadLabs() : Promise.resolve([]),
|
|
729
|
+
]);
|
|
730
|
+
const unavailableSurfaces = [];
|
|
731
|
+
const articles = articleLoad.status === "fulfilled" ? articleLoad.value : [];
|
|
732
|
+
const labs = labLoad.status === "fulfilled" ? labLoad.value : [];
|
|
733
|
+
if (selected.has("articles") && articleLoad.status === "rejected") {
|
|
734
|
+
unavailableSurfaces.push({
|
|
735
|
+
surface: "articles",
|
|
736
|
+
error: articleLoad.reason instanceof Error ? articleLoad.reason.message : String(articleLoad.reason),
|
|
737
|
+
retry_tool: "search_articles",
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
if (selected.has("labs") && labLoad.status === "rejected") {
|
|
741
|
+
unavailableSurfaces.push({
|
|
742
|
+
surface: "labs",
|
|
743
|
+
error: labLoad.reason instanceof Error ? labLoad.reason.message : String(labLoad.reason),
|
|
744
|
+
retry_tool: "list_labs",
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
const hits = [];
|
|
748
|
+
if (selected.has("core")) {
|
|
749
|
+
const boost = intentBoost(query, "core");
|
|
750
|
+
for (const [index, raw] of content.search(query, undefined, limit, lang).entries()) {
|
|
751
|
+
const sourceScore = Number(raw.score ?? 0);
|
|
752
|
+
hits.push({
|
|
753
|
+
...raw,
|
|
754
|
+
surface: "core",
|
|
755
|
+
score: sourceScore + boost,
|
|
756
|
+
source_score: sourceScore,
|
|
757
|
+
rank_within_surface: index + 1,
|
|
758
|
+
title: String(raw.name ?? raw.slug ?? ""),
|
|
759
|
+
suggested_tool: GET_TOOL_FOR_DOMAIN[String(raw.domain)] ?? "search",
|
|
760
|
+
matchedFields: raw.matchedFields ?? [],
|
|
761
|
+
matchedTerms: raw.matchedTerms ?? [],
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
if (selected.has("articles")) {
|
|
766
|
+
const boost = intentBoost(query, "articles");
|
|
767
|
+
for (const [index, raw] of searchArticleCorpus(articles, query, limit).entries()) {
|
|
768
|
+
hits.push({
|
|
769
|
+
surface: "articles", score: raw.score + boost, source_score: raw.score,
|
|
770
|
+
rank_within_surface: index + 1, slug: raw.slug, title: raw.title, summary: raw.summary,
|
|
771
|
+
canonical_url: raw.canonical_url, api_url: raw.api_url, suggested_tool: "get_article",
|
|
772
|
+
matchedFields: raw.matchedFields, matchedTerms: raw.matchedTerms,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (selected.has("labs")) {
|
|
777
|
+
const boost = intentBoost(query, "labs");
|
|
778
|
+
const calculatorTools = {
|
|
779
|
+
"agent-economics": "calculate_agent_economics",
|
|
780
|
+
"evaluation-sample-size": "calculate_evaluation_sample_size",
|
|
781
|
+
"human-supervision-capacity": "calculate_human_supervision_capacity",
|
|
782
|
+
};
|
|
783
|
+
for (const [index, raw] of searchLabCorpus(labs, query, limit).entries()) {
|
|
784
|
+
hits.push({
|
|
785
|
+
surface: "labs", score: raw.score + boost, source_score: raw.score,
|
|
786
|
+
rank_within_surface: index + 1, slug: raw.slug, kind: raw.kind, title: raw.title,
|
|
787
|
+
summary: raw.description, canonical_url: raw.canonical_url, api_url: raw.api_url,
|
|
788
|
+
calculation_url: raw.calculation_url,
|
|
789
|
+
suggested_tool: calculatorTools[raw.slug] ?? "get_lab",
|
|
790
|
+
matchedFields: raw.matchedFields, matchedTerms: raw.matchedTerms,
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (selected.has("claims")) {
|
|
795
|
+
const boost = intentBoost(query, "claims");
|
|
796
|
+
for (const [index, raw] of claimSearch(content, query, lang, limit).entries()) {
|
|
797
|
+
const claim = raw.claim;
|
|
798
|
+
hits.push({
|
|
799
|
+
surface: "claims", score: raw.score + boost, source_score: raw.score,
|
|
800
|
+
rank_within_surface: index + 1, id: claim.id, slug: claim.slug,
|
|
801
|
+
kind: claim.claim_type, title: String(claim.statement ?? claim.slug),
|
|
802
|
+
summary: `Epistemic type: ${String(claim.claim_type)}; confidence: ${String(claim.confidence_level)}.`,
|
|
803
|
+
suggested_tool: "get_claim", matchedFields: raw.matchedFields, matchedTerms: raw.matchedTerms,
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
hits.sort((a, b) => Number(b.score) - Number(a.score) || Number(a.rank_within_surface) - Number(b.rank_within_surface));
|
|
808
|
+
return out({ query, count: hits.length, results: hits, unavailable_surfaces: unavailableSurfaces }, hits);
|
|
809
|
+
}
|
|
810
|
+
catch (error) {
|
|
811
|
+
return globalSearchFailure(error);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
454
814
|
// ── Knowledge base ─────────────────────────────────────────────────────────
|
|
455
815
|
server.registerTool("list_knowledge", {
|
|
456
816
|
title: "List Agentic AI Knowledge Units",
|
|
@@ -571,6 +931,167 @@ export function registerTools(server, content) {
|
|
|
571
931
|
const results = content.search(query, domains, limit ?? 20, (locale ?? "en"));
|
|
572
932
|
return outList(results, { query });
|
|
573
933
|
});
|
|
934
|
+
// ── First-party articles (federated) ─────────────────────────────────────
|
|
935
|
+
server.registerTool("list_articles", {
|
|
936
|
+
title: "List first-party essays",
|
|
937
|
+
annotations: READ_ONLY_REMOTE,
|
|
938
|
+
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.",
|
|
939
|
+
inputSchema: z.object({
|
|
940
|
+
locale: localeSchema.describe("Restrict to en, es or pt. Omit to return every language."),
|
|
941
|
+
}),
|
|
942
|
+
outputSchema: articleListOutput,
|
|
943
|
+
}, async ({ locale }) => {
|
|
944
|
+
try {
|
|
945
|
+
const articles = articlesForLocale(await loadArticles(), locale);
|
|
946
|
+
return outList(articles.map(articleCard));
|
|
947
|
+
}
|
|
948
|
+
catch (error) {
|
|
949
|
+
return articleFailure(error);
|
|
950
|
+
}
|
|
951
|
+
});
|
|
952
|
+
server.registerTool("get_article", {
|
|
953
|
+
title: "Get a first-party essay",
|
|
954
|
+
annotations: READ_ONLY_REMOTE,
|
|
955
|
+
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.",
|
|
956
|
+
inputSchema: z.object({
|
|
957
|
+
slug: slugSchema.describe("Article slug, e.g. 'the-stopwatch-and-the-exam'."),
|
|
958
|
+
}),
|
|
959
|
+
outputSchema: articleUnitOutput,
|
|
960
|
+
}, async ({ slug }) => {
|
|
961
|
+
try {
|
|
962
|
+
const articles = await loadArticles();
|
|
963
|
+
const article = articles.find((candidate) => candidate.slug === slug);
|
|
964
|
+
return article ? out(article) : articleNotFound(articles, slug);
|
|
965
|
+
}
|
|
966
|
+
catch (error) {
|
|
967
|
+
return articleFailure(error);
|
|
968
|
+
}
|
|
969
|
+
});
|
|
970
|
+
server.registerTool("search_articles", {
|
|
971
|
+
title: "Search first-party essays",
|
|
972
|
+
annotations: READ_ONLY_REMOTE,
|
|
973
|
+
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.",
|
|
974
|
+
inputSchema: z.object({
|
|
975
|
+
query: querySchema.describe("Keyword or phrase to search for in any supported language."),
|
|
976
|
+
locale: localeSchema.describe("Restrict to en, es or pt. Omit to search every language."),
|
|
977
|
+
limit: z.number().int().positive().max(20).optional().describe("Maximum results (default 10)."),
|
|
978
|
+
}),
|
|
979
|
+
outputSchema: articleSearchOutput,
|
|
980
|
+
}, async ({ query, locale, limit }) => {
|
|
981
|
+
try {
|
|
982
|
+
const articles = articlesForLocale(await loadArticles(), locale);
|
|
983
|
+
return outList(searchArticleCorpus(articles, query, limit ?? 10), { query });
|
|
984
|
+
}
|
|
985
|
+
catch (error) {
|
|
986
|
+
return articleFailure(error);
|
|
987
|
+
}
|
|
988
|
+
});
|
|
989
|
+
// ── SANTISMM Labs (federated metadata + deterministic execution) ─────────
|
|
990
|
+
server.registerTool("list_labs", {
|
|
991
|
+
title: "List calculators, converters, experiments and educational Labs",
|
|
992
|
+
annotations: READ_ONLY_REMOTE,
|
|
993
|
+
description: "List every SANTISMM Lab with its inputs, outputs, assumptions, formulas and citation URL. Use this to discover interactive and machine-readable tools; filter by kind when the user specifically asks for a calculator, converter, experiment or educational game.",
|
|
994
|
+
inputSchema: z.object({
|
|
995
|
+
kind: z.enum(["calculator", "converter", "experiment", "educational-game"]).optional(),
|
|
996
|
+
}),
|
|
997
|
+
outputSchema: z.object(labListOutput),
|
|
998
|
+
}, async ({ kind }) => {
|
|
999
|
+
try {
|
|
1000
|
+
const labs = await loadLabs();
|
|
1001
|
+
return outList(kind ? labs.filter((lab) => lab.kind === kind) : labs);
|
|
1002
|
+
}
|
|
1003
|
+
catch (error) {
|
|
1004
|
+
return labsFailure(error);
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
server.registerTool("get_lab", {
|
|
1008
|
+
title: "Get one SANTISMM Lab definition",
|
|
1009
|
+
annotations: READ_ONLY_REMOTE,
|
|
1010
|
+
description: "Get one Lab by slug, including formulas, assumptions, related SANTISMM content and its executable endpoint when one exists. Use this after list_labs or search_all; use the named calculate_* tool rather than reimplementing a published formula.",
|
|
1011
|
+
inputSchema: z.object({ slug: slugSchema.describe("Lab slug, e.g. 'evaluation-sample-size'.") }),
|
|
1012
|
+
outputSchema: labCardSchema,
|
|
1013
|
+
}, async ({ slug }) => {
|
|
1014
|
+
try {
|
|
1015
|
+
const labs = await loadLabs();
|
|
1016
|
+
const lab = labs.find((candidate) => candidate.slug === slug);
|
|
1017
|
+
return lab ? out(lab) : labNotFound(labs, slug);
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
return labsFailure(error);
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
server.registerTool("calculate_agent_economics", {
|
|
1024
|
+
title: "Calculate the operational economics of an AI agent",
|
|
1025
|
+
annotations: READ_ONLY_REMOTE,
|
|
1026
|
+
description: "Calculate monthly operating cost, cost per verified outcome, manual baseline, savings, ROI and break-even success rate from explicit assumptions. Use this for an agent business case or scenario comparison; keep every monetary input in the same currency and cite the returned canonical_url.",
|
|
1027
|
+
inputSchema: z.object({
|
|
1028
|
+
monthlyVolume: z.number().min(0).max(1_000_000_000).describe("Cases attempted per month."),
|
|
1029
|
+
manualMinutes: z.number().min(0).max(10_080).describe("Manual handling time per case."),
|
|
1030
|
+
hourlyCost: z.number().min(0).max(1_000_000).describe("Fully loaded human hourly cost, in the chosen currency."),
|
|
1031
|
+
inputTokens: z.number().min(0).max(100_000_000).describe("Input tokens per agent attempt."),
|
|
1032
|
+
outputTokens: z.number().min(0).max(100_000_000).describe("Output tokens per agent attempt."),
|
|
1033
|
+
inputPrice: z.number().min(0).max(1_000_000).describe("Model input price per million tokens, in the chosen currency."),
|
|
1034
|
+
outputPrice: z.number().min(0).max(1_000_000).describe("Model output price per million tokens, in the chosen currency."),
|
|
1035
|
+
toolCost: z.number().min(0).max(1_000_000).describe("External tool cost per attempt."),
|
|
1036
|
+
retryRate: z.number().min(0).max(500).describe("Extra attempts as a percentage of initial volume."),
|
|
1037
|
+
successRate: z.number().min(1).max(100).describe("Correctly verified outcomes as a percentage of cases."),
|
|
1038
|
+
reviewRate: z.number().min(0).max(100).describe("Share of cases reviewed by a person."),
|
|
1039
|
+
reviewMinutes: z.number().min(0).max(10_080).describe("Human review minutes per reviewed case."),
|
|
1040
|
+
reworkMinutes: z.number().min(0).max(10_080).describe("Human rework minutes per failed case."),
|
|
1041
|
+
}),
|
|
1042
|
+
outputSchema: agentEconomicsOutput,
|
|
1043
|
+
}, async (inputs) => {
|
|
1044
|
+
try {
|
|
1045
|
+
return out(await executeLabCalculator("agent-economics", inputs));
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
return labsFailure(error);
|
|
1049
|
+
}
|
|
1050
|
+
});
|
|
1051
|
+
server.registerTool("calculate_evaluation_sample_size", {
|
|
1052
|
+
title: "Calculate an agent evaluation sample size",
|
|
1053
|
+
annotations: READ_ONLY_REMOTE,
|
|
1054
|
+
description: "Calculate two different samples: how many independent evaluations are needed to detect at least one failure, and how many are needed to estimate its rate at a chosen margin. Use this when a user asks how many tests are enough; do not interpret zero observed failures as proof of zero risk.",
|
|
1055
|
+
inputSchema: z.object({
|
|
1056
|
+
failureRate: z.number().min(0.0001).max(99.9999).describe("Failure rate to detect, in percent."),
|
|
1057
|
+
confidence: z.union([z.literal(90), z.literal(95), z.literal(99)]).describe("Confidence level, in percent."),
|
|
1058
|
+
margin: z.number().min(0.1).max(50).describe("Margin for estimating the failure rate, in percentage points."),
|
|
1059
|
+
population: z.number().min(1).max(1_000_000_000).describe("Number of distinct evaluable cases."),
|
|
1060
|
+
}),
|
|
1061
|
+
outputSchema: evaluationSampleOutput,
|
|
1062
|
+
}, async (inputs) => {
|
|
1063
|
+
try {
|
|
1064
|
+
return out(await executeLabCalculator("evaluation-sample-size", inputs));
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
return labsFailure(error);
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
server.registerTool("calculate_human_supervision_capacity", {
|
|
1071
|
+
title: "Calculate human supervision capacity for an AI agent",
|
|
1072
|
+
annotations: READ_ONLY_REMOTE,
|
|
1073
|
+
description: "Calculate review and escalation workload, required FTE, available headroom or backlog, monthly labour cost and sustainable case volume. Use this before production rollout to test whether the stated human-oversight model is operationally credible; the result uses averages and is not a queueing simulation.",
|
|
1074
|
+
inputSchema: z.object({
|
|
1075
|
+
volume: z.number().min(0).max(1_000_000_000).describe("Agent cases per month."),
|
|
1076
|
+
sample: z.number().min(0).max(100).describe("Share of all cases selected for routine review, in percent."),
|
|
1077
|
+
reviewMinutes: z.number().min(0).max(10_080).describe("Minutes per routine review."),
|
|
1078
|
+
escalationRate: z.number().min(0).max(100).describe("Share of cases escalated, in percent."),
|
|
1079
|
+
escalationMinutes: z.number().min(0).max(10_080).describe("Minutes per escalation."),
|
|
1080
|
+
workdays: z.number().min(1).max(31).describe("Working days per month."),
|
|
1081
|
+
hoursDay: z.number().min(0.1).max(24).describe("Paid hours per working day."),
|
|
1082
|
+
utilization: z.number().min(1).max(100).describe("Share of paid time available for review and escalation, in percent."),
|
|
1083
|
+
reviewers: z.number().min(0.1).max(1_000_000).describe("Available reviewer FTE."),
|
|
1084
|
+
hourlyCost: z.number().min(0).max(1_000_000).describe("Fully loaded reviewer hourly cost, in the chosen currency."),
|
|
1085
|
+
}),
|
|
1086
|
+
outputSchema: humanSupervisionOutput,
|
|
1087
|
+
}, async (inputs) => {
|
|
1088
|
+
try {
|
|
1089
|
+
return out(await executeLabCalculator("human-supervision-capacity", inputs));
|
|
1090
|
+
}
|
|
1091
|
+
catch (error) {
|
|
1092
|
+
return labsFailure(error);
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
574
1095
|
// ── Graph traversal ────────────────────────────────────────────────────────
|
|
575
1096
|
server.registerTool("get_related", {
|
|
576
1097
|
title: "Traverse the Knowledge Graph",
|
|
@@ -655,6 +1176,38 @@ export function registerTools(server, content) {
|
|
|
655
1176
|
const entry = content.getHomeric("routes", slug, locale);
|
|
656
1177
|
return entry ? out(entry) : noEncontrado(content, "homeric/routes", slug, (locale ?? "en"));
|
|
657
1178
|
});
|
|
1179
|
+
// ── Claims (ADR 0003) ────────────────────────────────────────────────────
|
|
1180
|
+
// Everything else in this server answers "what does the corpus say?". These
|
|
1181
|
+
// two answer "how strongly, and on what?" — which of the corpus's statements
|
|
1182
|
+
// are observed fact, which are a reading of the industry, which are our own
|
|
1183
|
+
// position and which are a bet. Without them an agent has to infer the
|
|
1184
|
+
// epistemic status from prose, and prose does not distinguish those.
|
|
1185
|
+
server.registerTool("list_claims", {
|
|
1186
|
+
title: "List the corpus claims and their epistemic status",
|
|
1187
|
+
annotations: READ_ONLY,
|
|
1188
|
+
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.",
|
|
1189
|
+
inputSchema: z.object({
|
|
1190
|
+
claim_type: z
|
|
1191
|
+
.enum(["observed_fact", "industry_synthesis", "santismm_thesis", "strategic_hypothesis"])
|
|
1192
|
+
.optional()
|
|
1193
|
+
.describe("Restrict to one rung of the ladder. Omit for all."),
|
|
1194
|
+
locale: localeSchema,
|
|
1195
|
+
}),
|
|
1196
|
+
outputSchema: claimListOutput,
|
|
1197
|
+
}, async ({ claim_type, locale }) => outList(content.listClaims(claim_type, (locale ?? "en"))));
|
|
1198
|
+
server.registerTool("get_claim", {
|
|
1199
|
+
title: "Get one claim with its limits and what would refute it",
|
|
1200
|
+
annotations: READ_ONLY,
|
|
1201
|
+
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.",
|
|
1202
|
+
inputSchema: z.object({
|
|
1203
|
+
id: z.string().describe("Claim id (e.g. 'HE-CLAIM-001') or slug."),
|
|
1204
|
+
locale: localeSchema,
|
|
1205
|
+
}),
|
|
1206
|
+
outputSchema: claimUnitOutput,
|
|
1207
|
+
}, async ({ id, locale }) => {
|
|
1208
|
+
const c = content.getClaim(id, locale);
|
|
1209
|
+
return c ? out(c) : noEncontrado(content, "claims", id, (locale ?? "en"));
|
|
1210
|
+
});
|
|
658
1211
|
}
|
|
659
1212
|
/**
|
|
660
1213
|
* The tools this server exposes, as advertised metadata (name + description).
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "santismm-knowledge-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"description": "MCP server for the Santismm Knowledge Platform —
|
|
5
|
+
"description": "MCP server for the Santismm Knowledge Platform — core knowledge, first-party essays, Homeric Atlas datasets and epistemic claims. Ships the core corpus; the hosted endpoint at https://santismm.com/mcp is the always-fresh alternative.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"santismm-knowledge-mcp": "dist/index.js"
|