santismm-knowledge-mcp 0.3.0 → 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 CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  [![santismm-knowledge-mcp MCP server](https://glama.ai/mcp/servers/santismm/santismm-knowledge-mcp/badges/card.svg)](https://glama.ai/mcp/servers/santismm/santismm-knowledge-mcp)
4
4
 
5
- > MCP server for the Santismm Knowledge Platform — harness engineering, agentic
6
- > AI patterns, reference architectures, AI governance and the agent taxonomy.
5
+ > MCP server for the Santismm Knowledge Platform — five core knowledge domains,
6
+ > first-party essays, Homeric Atlas datasets and epistemic claims.
7
7
 
8
8
  **This repository is generated** from the platform at [santismm.com](https://santismm.com).
9
9
  Do not edit it by hand; changes are overwritten on the next sync. Corrections go
@@ -68,7 +68,7 @@ is not a software licence and MIT is not a content licence.
68
68
 
69
69
  ## What it exposes
70
70
 
71
- 24 read-only tools over knowledge, patterns, architectures, governance, the
71
+ 30 read-only tools over knowledge, patterns, architectures, governance, the
72
72
  Harness Engineering Handbook and first-party Articles, each declaring an
73
73
  `outputSchema` and returning validated `structuredContent`. Every tool is
74
74
  annotated `readOnlyHint: true`, `destructiveHint: false` and
package/dist/articles.js CHANGED
@@ -30,7 +30,7 @@ function validArticle(value) {
30
30
  }
31
31
  async function fetchCorpus() {
32
32
  const response = await fetch(ARTICLES_API_URL, {
33
- headers: { Accept: "application/json", "User-Agent": "santismm-knowledge-mcp/0.3.0" },
33
+ headers: { Accept: "application/json", "User-Agent": "santismm-knowledge-mcp/0.4.0" },
34
34
  signal: AbortSignal.timeout(8_000),
35
35
  cache: "no-store",
36
36
  });
package/dist/labs.js ADDED
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Federated SANTISMM Labs catalogue and deterministic calculators.
3
+ *
4
+ * Labs owns the formulas and executes them. The knowledge MCP deliberately
5
+ * proxies the canonical API instead of copying arithmetic into this package:
6
+ * one formula version serves the interactive UI, REST callers and MCP agents.
7
+ */
8
+ export const LABS_API_URL = process.env.SANTISMM_LABS_API_URL ?? 'https://labs.santismm.com/api/labs';
9
+ const LABS_SERVICE_ORIGIN = new URL(LABS_API_URL).origin;
10
+ const LABS_CANONICAL_ORIGIN = 'https://labs.santismm.com';
11
+ const CACHE_TTL_MS = 5 * 60 * 1000;
12
+ let cache;
13
+ let pending;
14
+ function strings(value) {
15
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
16
+ }
17
+ function validLab(value) {
18
+ const lab = value;
19
+ return Boolean(lab &&
20
+ typeof lab.slug === 'string' &&
21
+ ['calculator', 'converter', 'experiment', 'educational-game'].includes(String(lab.kind)) &&
22
+ typeof lab.label === 'string' &&
23
+ typeof lab.title === 'string' &&
24
+ typeof lab.description === 'string' &&
25
+ strings(lab.inputs) &&
26
+ strings(lab.outputs) &&
27
+ (lab.formulas === undefined || strings(lab.formulas)) &&
28
+ strings(lab.assumptions) &&
29
+ typeof lab.version === 'string' &&
30
+ typeof lab.updated === 'string' &&
31
+ typeof lab.canonical_url === 'string' &&
32
+ lab.canonical_url.startsWith(`${LABS_CANONICAL_ORIGIN}/`) &&
33
+ typeof lab.api_url === 'string' &&
34
+ lab.api_url.startsWith(`${LABS_CANONICAL_ORIGIN}/api/labs/`) &&
35
+ (lab.calculation_url === undefined || lab.calculation_url.startsWith(`${LABS_CANONICAL_ORIGIN}/api/calculate/`)));
36
+ }
37
+ async function fetchCorpus() {
38
+ const response = await fetch(LABS_API_URL, {
39
+ headers: { Accept: 'application/json', 'User-Agent': 'santismm-knowledge-mcp/0.4.0' },
40
+ signal: AbortSignal.timeout(8_000),
41
+ cache: 'no-store',
42
+ });
43
+ if (!response.ok)
44
+ throw new Error(`Labs API returned HTTP ${response.status}`);
45
+ const raw = await response.json();
46
+ if (raw.source !== 'SANTISMM Labs' ||
47
+ typeof raw.canonical_url !== 'string' ||
48
+ !Array.isArray(raw.results) ||
49
+ !raw.results.every(validLab) ||
50
+ raw.count !== raw.results.length) {
51
+ throw new Error('Labs API returned an invalid catalogue contract');
52
+ }
53
+ return raw;
54
+ }
55
+ export async function loadLabs() {
56
+ if (cache && cache.expiresAt > Date.now())
57
+ return cache.corpus.results;
58
+ if (!pending) {
59
+ pending = fetchCorpus()
60
+ .then((corpus) => {
61
+ cache = { expiresAt: Date.now() + CACHE_TTL_MS, corpus };
62
+ return corpus;
63
+ })
64
+ .finally(() => {
65
+ pending = undefined;
66
+ });
67
+ }
68
+ return (await pending).results;
69
+ }
70
+ function normalise(value) {
71
+ return value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').toLowerCase();
72
+ }
73
+ export function searchLabCorpus(labs, query, limit) {
74
+ const terms = [...new Set(normalise(query).split(/[^a-z0-9]+/).filter((term) => term.length > 1))];
75
+ const phrase = normalise(query).trim();
76
+ if (terms.length === 0)
77
+ return [];
78
+ const fields = [
79
+ ['title', 8], ['slug', 7], ['description', 6], ['inputs', 4], ['outputs', 4],
80
+ ['formulas', 3], ['assumptions', 2], ['kind', 1],
81
+ ];
82
+ return labs
83
+ .map((lab) => {
84
+ const matchedFields = new Set();
85
+ const matchedTerms = new Set();
86
+ let score = 0;
87
+ for (const [field, weight] of fields) {
88
+ const raw = lab[field];
89
+ const value = normalise(Array.isArray(raw) ? raw.join(' ') : String(raw ?? ''));
90
+ for (const term of terms) {
91
+ if (!value.includes(term))
92
+ continue;
93
+ score += weight;
94
+ matchedFields.add(field);
95
+ matchedTerms.add(term);
96
+ }
97
+ if (phrase.length > 2 && value.includes(phrase))
98
+ score += weight * 2;
99
+ }
100
+ return { ...lab, score, matchedFields: [...matchedFields], matchedTerms: [...matchedTerms] };
101
+ })
102
+ .filter((lab) => lab.score > 0)
103
+ .sort((a, b) => b.score - a.score || b.updated.localeCompare(a.updated) || a.slug.localeCompare(b.slug))
104
+ .slice(0, limit);
105
+ }
106
+ export async function executeLabCalculator(slug, inputs) {
107
+ const response = await fetch(`${LABS_SERVICE_ORIGIN}/api/calculate/${slug}`, {
108
+ method: 'POST',
109
+ headers: {
110
+ Accept: 'application/json',
111
+ 'Content-Type': 'application/json',
112
+ 'User-Agent': 'santismm-knowledge-mcp/0.4.0',
113
+ },
114
+ body: JSON.stringify(inputs),
115
+ signal: AbortSignal.timeout(8_000),
116
+ cache: 'no-store',
117
+ });
118
+ const raw = await response.json();
119
+ if (!response.ok) {
120
+ const message = typeof raw.error === 'string' ? raw.error : `HTTP ${response.status}`;
121
+ throw new Error(`Labs calculator ${slug} failed: ${message}`);
122
+ }
123
+ if (raw.slug !== slug ||
124
+ typeof raw.version !== 'string' ||
125
+ typeof raw.canonical_url !== 'string' ||
126
+ typeof raw.api_url !== 'string' ||
127
+ typeof raw.inputs !== 'object' ||
128
+ typeof raw.results !== 'object' ||
129
+ !Array.isArray(raw.assumptions) ||
130
+ !Array.isArray(raw.warnings)) {
131
+ throw new Error(`Labs calculator ${slug} returned an invalid result contract`);
132
+ }
133
+ return raw;
134
+ }
package/dist/shape.js CHANGED
@@ -550,6 +550,49 @@ export function makeContent(loadAll, loadHandbook, loadHomeric, loadClaims) {
550
550
  license_url: LICENSE_INFO.url,
551
551
  total: domains.reduce((n, d) => n + d.count, 0),
552
552
  domains,
553
+ extensions: [
554
+ {
555
+ surface: "articles",
556
+ description: "Federated first-party long-form essays, read from their canonical Articles API at call time.",
557
+ tools: ["list_articles", "get_article", "search_articles"],
558
+ source: "https://articles.santismm.com/api/articles.json",
559
+ lookup: "article slug",
560
+ citation: "Each result carries canonical_url.",
561
+ },
562
+ {
563
+ surface: "homeric_atlas",
564
+ description: "Places, episodes and rival route reconstructions using the atlas identification vocabulary and 0–12 rubric.",
565
+ tools: [
566
+ "list_homeric_places", "get_homeric_place",
567
+ "list_homeric_episodes", "get_homeric_episode",
568
+ "list_homeric_routes", "get_homeric_route",
569
+ ],
570
+ source: `${SITE_URL}/api/homeric-atlas.json`,
571
+ lookup: "place, episode or route slug",
572
+ citation: "Each content result carries canonical_url and api_url.",
573
+ },
574
+ {
575
+ surface: "claims",
576
+ description: "The corpus's load-bearing claims with epistemic type, confidence, basis, limitations and falsification criteria.",
577
+ tools: ["list_claims", "get_claim"],
578
+ source: "bundled claim registry",
579
+ lookup: "claim id (for example HE-CLAIM-001) or slug",
580
+ citation: "Claims have no public page; cite their stable id and the MCP endpoint.",
581
+ },
582
+ {
583
+ surface: "labs",
584
+ description: "Interactive calculators, converters, experiments and educational games; three calculators execute deterministic, versioned formulas through the canonical Labs API.",
585
+ tools: [
586
+ "list_labs", "get_lab",
587
+ "calculate_agent_economics",
588
+ "calculate_evaluation_sample_size",
589
+ "calculate_human_supervision_capacity",
590
+ ],
591
+ source: "https://labs.santismm.com/api/labs",
592
+ lookup: "Lab slug; executable tools take typed numeric assumptions",
593
+ citation: "Definitions and calculation results carry canonical_url, api_url, version, assumptions and warnings.",
594
+ },
595
+ ],
553
596
  corpus: {
554
597
  newest_unit: newestUpdated([
555
598
  ...DOMAINS.flatMap((d) => loadAll(d)),
@@ -559,10 +602,12 @@ export function makeContent(loadAll, loadHandbook, loadHomeric, loadClaims) {
559
602
  "every content change; a published package carries the corpus frozen at publish " +
560
603
  "time. If total or newest_unit differ from that endpoint's, you are holding a snapshot.",
561
604
  },
562
- next: "search(query, locale) to answer a question across the whole corpus; " +
605
+ next: "search_all(query, locale) when a question may need core knowledge, an essay, a Lab calculation or a claim audit; " +
606
+ "search(query, locale) to search only the five-domain core; " +
563
607
  "list_<domain> to browse one; get_<domain>(slug) for a full unit with its " +
564
608
  "Evidence-First provenance; get_related(domain, slug) to traverse the graph. " +
565
- "Every result carries canonical_url and api_url, so cite the canonical_url.",
609
+ "Use the tools named in extensions for Articles, the Homeric Atlas and claims. " +
610
+ "Citable content results carry canonical_url; claim records use their stable id.",
566
611
  // For agents that would rather ingest the corpus than walk it.
567
612
  bulk: {
568
613
  llms_full_txt: `${SITE_URL}/llms-full.txt`,
package/dist/tools.js CHANGED
@@ -1,14 +1,16 @@
1
1
  import { z } from "zod";
2
2
  import { ARTICLES_API_URL, articleCard, articlesForLocale, loadArticles, searchArticleCorpus, } from "./articles.js";
3
+ import { LABS_API_URL, executeLabCalculator, loadLabs, searchLabCorpus, } from "./labs.js";
3
4
  /**
4
5
  * Single, framework-agnostic definition of the Santismm Knowledge MCP server:
5
6
  * its identity and the tool registry. Both transports — the stdio CLI
6
7
  * (`mcp/src/index.ts`) and the HTTP endpoint (`app/mcp/route.ts`) — call
7
8
  * `registerTools(server, content)`, so the exposed tools can never drift
8
- * between them. The data source is injected as `content` (an `McpContent`
9
- * provider); both providers read the same `content/{domain}/*.json` files.
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.
10
12
  */
11
- export const SERVER_INFO = { name: "santismm-knowledge", version: "0.3.0" };
13
+ export const SERVER_INFO = { name: "santismm-knowledge", version: "0.4.0" };
12
14
  /**
13
15
  * Core tools read a static local corpus, so all four hints are literally true:
14
16
  * nothing mutates, the same arguments produce the same answer, and no core
@@ -231,6 +233,7 @@ const ESPACIOS = [
231
233
  { domain: "homeric/routes", listTool: "list_homeric_routes", getTool: "get_homeric_route" },
232
234
  { domain: "claims", listTool: "list_claims", getTool: "get_claim" },
233
235
  { domain: "articles", listTool: "list_articles", getTool: "get_article" },
236
+ { domain: "labs", listTool: "list_labs", getTool: "get_lab" },
234
237
  ];
235
238
  /** Every identifier a card answers to: its id (handbook chapters) and its slug. */
236
239
  function identificadores(cards) {
@@ -248,9 +251,9 @@ function identificadores(cards) {
248
251
  }
249
252
  /** The cards of one space, whichever loader serves it. */
250
253
  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
+ // 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")
254
257
  return [];
255
258
  if (domain === "handbook")
256
259
  return content.listHandbook(locale);
@@ -473,6 +476,94 @@ const articleSearchOutput = {
473
476
  matchedTerms: z.array(z.string()),
474
477
  })),
475
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
+ };
476
567
  function articleFailure(error) {
477
568
  const body = {
478
569
  error: "articles_unavailable",
@@ -501,12 +592,83 @@ function articleNotFound(articles, slug) {
501
592
  isError: true,
502
593
  };
503
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
+ }
504
666
  export function registerTools(server, content) {
505
667
  // ── Orientation ────────────────────────────────────────────────────────────
506
668
  server.registerTool("get_overview", {
507
669
  title: "Corpus Overview — Start Here",
508
670
  annotations: READ_ONLY,
509
- description: "Get the corpus map — start here. Returns every domain (knowledge, patterns, architectures, governance and the Harness Engineering Handbook) with what it holds, the categories inside it, which tool retrieves a unit and what identifier that tool expects, plus the languages, licence and bulk-ingest URLs. One call is enough to know exactly where to go next.",
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.",
510
672
  inputSchema: z.object({}),
511
673
  outputSchema: z.object({
512
674
  source: z.string(),
@@ -526,6 +688,14 @@ export function registerTools(server, content) {
526
688
  url: z.string(),
527
689
  api_url: z.string(),
528
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
+ })),
529
699
  corpus: z
530
700
  .object({
531
701
  newest_unit: z.string().nullable().describe("Newest unit date in THIS copy (YYYY-MM-DD)."),
@@ -536,6 +706,111 @@ export function registerTools(server, content) {
536
706
  bulk: z.record(z.string(), z.string()),
537
707
  }),
538
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
+ });
539
814
  // ── Knowledge base ─────────────────────────────────────────────────────────
540
815
  server.registerTool("list_knowledge", {
541
816
  title: "List Agentic AI Knowledge Units",
@@ -711,6 +986,112 @@ export function registerTools(server, content) {
711
986
  return articleFailure(error);
712
987
  }
713
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
+ });
714
1095
  // ── Graph traversal ────────────────────────────────────────────────────────
715
1096
  server.registerTool("get_related", {
716
1097
  title: "Traverse the Knowledge Graph",
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "santismm-knowledge-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "license": "MIT",
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.",
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"