@heretek-ai/epistemic-swarm 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/.agents/skills/brainstorming/SKILL.md +13 -0
  2. package/.agents/skills/code_audit/SKILL.md +13 -0
  3. package/.agents/skills/epistemic_search/SKILL.md +13 -0
  4. package/.agents/skills/grilling/SKILL.md +13 -0
  5. package/.agents/skills/oss_scout/SKILL.md +13 -0
  6. package/.agents/skills/research_cache/SKILL.md +13 -0
  7. package/.agents/skills/swarm_config/SKILL.md +13 -0
  8. package/.claude-plugin/plugin.json +15 -5
  9. package/.omp/README.md +39 -0
  10. package/.omp/SYSTEM.md +12 -0
  11. package/.omp/commands/audit.md +10 -0
  12. package/.omp/commands/brainstorming.md +12 -0
  13. package/.omp/commands/grill.md +9 -0
  14. package/.omp/commands/scout.md +10 -0
  15. package/.omp/commands/swarm-config.md +10 -0
  16. package/.omp/commands/swarm.md +9 -0
  17. package/.omp/hooks/post/epistemic-audit.ts +16 -0
  18. package/.omp/hooks/pre/epistemic-redirect.ts +21 -0
  19. package/.omp/prompts/brainstorming.md +8 -0
  20. package/.omp/prompts/swarm.md +8 -0
  21. package/MARKETPLACE.md +8 -0
  22. package/README.md +53 -8
  23. package/bin/cli.js +27 -3
  24. package/config/domain_packs/biopharma.json +23 -0
  25. package/config/domain_packs/legal.json +19 -0
  26. package/config/domain_packs/quant.json +19 -0
  27. package/config/mcp-research-servers.json +7 -0
  28. package/config/mcp_launcher.py +48 -137
  29. package/config/opencode-snippet.json +58 -3
  30. package/config/searxng_mcp.py +42 -83
  31. package/extensions/pi/index.js +196 -28
  32. package/install.sh +20 -4
  33. package/package.json +40 -5
  34. package/plugins/antigravity/README.md +28 -0
  35. package/plugins/antigravity/agents/alpha-thesis.md +6 -0
  36. package/plugins/antigravity/agents/beta-antithesis.md +7 -0
  37. package/plugins/antigravity/agents/brainstormer.md +7 -0
  38. package/plugins/antigravity/agents/epistemic-auditor.md +5 -0
  39. package/plugins/antigravity/hooks.json +23 -0
  40. package/plugins/antigravity/mcp_config.json +33 -0
  41. package/plugins/antigravity/plugin.json +21 -0
  42. package/plugins/antigravity/rules/epistemic-integrity.md +6 -0
  43. package/plugins/antigravity/skills/brainstorming/SKILL.md +13 -0
  44. package/plugins/antigravity/skills/code_audit/SKILL.md +13 -0
  45. package/plugins/antigravity/skills/epistemic_search/SKILL.md +13 -0
  46. package/plugins/antigravity/skills/grilling/SKILL.md +13 -0
  47. package/plugins/antigravity/skills/oss_scout/SKILL.md +13 -0
  48. package/plugins/antigravity/skills/research_cache/SKILL.md +13 -0
  49. package/plugins/antigravity/skills/swarm_config/SKILL.md +13 -0
  50. package/plugins/codex/AGENTS.md.snippet +10 -0
  51. package/plugins/codex/README.md +37 -0
  52. package/plugins/codex/config.toml.snippet +28 -0
  53. package/plugins/codex/openai.yaml +24 -0
  54. package/plugins/codex/skills/brainstorming/SKILL.md +13 -0
  55. package/plugins/codex/skills/code_audit/SKILL.md +13 -0
  56. package/plugins/codex/skills/epistemic_search/SKILL.md +13 -0
  57. package/plugins/codex/skills/grilling/SKILL.md +13 -0
  58. package/plugins/codex/skills/oss_scout/SKILL.md +13 -0
  59. package/plugins/codex/skills/research_cache/SKILL.md +13 -0
  60. package/plugins/codex/skills/swarm_config/SKILL.md +13 -0
  61. package/plugins/gemini/GEMINI.md +15 -0
  62. package/plugins/gemini/README.md +19 -0
  63. package/plugins/gemini/commands/audit.toml +6 -0
  64. package/plugins/gemini/commands/brainstorming.toml +10 -0
  65. package/plugins/gemini/commands/grill.toml +6 -0
  66. package/plugins/gemini/commands/scout.toml +7 -0
  67. package/plugins/gemini/commands/swarm-config.toml +7 -0
  68. package/plugins/gemini/commands/swarm.toml +8 -0
  69. package/plugins/gemini/gemini-extension.json +38 -0
  70. package/plugins/gemini/hooks/hooks.json +11 -0
  71. package/plugins/gemini/skills/brainstorming/SKILL.md +13 -0
  72. package/plugins/gemini/skills/code_audit/SKILL.md +13 -0
  73. package/plugins/gemini/skills/epistemic_search/SKILL.md +13 -0
  74. package/plugins/gemini/skills/grilling/SKILL.md +13 -0
  75. package/plugins/gemini/skills/oss_scout/SKILL.md +13 -0
  76. package/plugins/gemini/skills/research_cache/SKILL.md +13 -0
  77. package/plugins/gemini/skills/swarm_config/SKILL.md +13 -0
  78. package/plugins/opencode/index.js +335 -118
  79. package/prompts/agent_brainstormer.md +97 -0
  80. package/runner/__pycache__/__init__.cpython-311.pyc +0 -0
  81. package/runner/__pycache__/auctioneer.cpython-311.pyc +0 -0
  82. package/runner/__pycache__/auditor_engine.cpython-311.pyc +0 -0
  83. package/runner/__pycache__/claim_store.cpython-311.pyc +0 -0
  84. package/runner/__pycache__/claim_witness.cpython-311.pyc +0 -0
  85. package/runner/__pycache__/living_dossiers.cpython-311.pyc +0 -0
  86. package/runner/__pycache__/mcp_protocol.cpython-311.pyc +0 -0
  87. package/runner/__pycache__/mcp_server.cpython-311.pyc +0 -0
  88. package/runner/__pycache__/pcrb.cpython-311.pyc +0 -0
  89. package/runner/__pycache__/pcrb_verify.cpython-311.pyc +0 -0
  90. package/runner/__pycache__/refinement.cpython-311.pyc +0 -0
  91. package/runner/__pycache__/research_swarm.cpython-311.pyc +0 -0
  92. package/runner/__pycache__/state_machine.cpython-311.pyc +0 -0
  93. package/runner/auctioneer.py +169 -0
  94. package/runner/auditor_engine.py +127 -12
  95. package/runner/claim_store.py +361 -0
  96. package/runner/claim_witness.py +183 -0
  97. package/runner/living_dossiers.py +355 -0
  98. package/runner/mcp_protocol.py +188 -0
  99. package/runner/mcp_server.py +567 -0
  100. package/runner/pcrb.py +212 -0
  101. package/runner/pcrb_verify.py +237 -0
  102. package/runner/refinement.py +335 -0
  103. package/runner/research_swarm.py +378 -94
  104. package/runner/tests/__pycache__/test_auction_order.cpython-311.pyc +0 -0
  105. package/runner/tests/__pycache__/test_claim_store.cpython-311.pyc +0 -0
  106. package/runner/tests/__pycache__/test_claim_witness.cpython-311.pyc +0 -0
  107. package/runner/tests/__pycache__/test_domain_packs.cpython-311.pyc +0 -0
  108. package/runner/tests/__pycache__/test_fleet_seam.cpython-311.pyc +0 -0
  109. package/runner/tests/__pycache__/test_living_dossiers.cpython-311.pyc +0 -0
  110. package/runner/tests/__pycache__/test_mcp_server.cpython-311.pyc +0 -0
  111. package/runner/tests/__pycache__/test_pcrb.cpython-311.pyc +0 -0
  112. package/runner/tests/__pycache__/test_refinement.cpython-311.pyc +0 -0
  113. package/runner/tests/__pycache__/test_swarm.cpython-311.pyc +0 -0
  114. package/runner/tests/fixtures/auction_objective.json +35 -0
  115. package/runner/tests/fixtures/borderline_claims.json +27 -0
  116. package/runner/tests/fixtures/divergence_objectives.json +31 -0
  117. package/runner/tests/test_auction_order.py +173 -0
  118. package/runner/tests/test_claim_store.py +162 -0
  119. package/runner/tests/test_claim_witness.py +186 -0
  120. package/runner/tests/test_domain_packs.py +217 -0
  121. package/runner/tests/test_fleet_seam.py +113 -0
  122. package/runner/tests/test_living_dossiers.py +204 -0
  123. package/runner/tests/test_mcp_server.py +212 -0
  124. package/runner/tests/test_pcrb.py +240 -0
  125. package/runner/tests/test_refinement.py +255 -0
  126. package/runner/tests/test_swarm.py +173 -15
  127. package/scripts/auction_experiment.py +180 -0
  128. package/scripts/build_adapters.py +183 -0
  129. package/scripts/divergence_experiment.py +184 -0
  130. package/skills/brainstorming/SKILL.md +106 -0
  131. package/skills/brainstorming/__init__.py +1 -0
  132. package/skills/brainstorming/scripts/brainstorm.py +200 -0
  133. package/skills/research_cache/__pycache__/__init__.cpython-311.pyc +0 -0
  134. package/skills/research_cache/__pycache__/hasher.cpython-311.pyc +0 -0
  135. package/skills/swarm_config/SKILL.md +1 -1
  136. package/skills/swarm_config/__pycache__/__init__.cpython-311.pyc +0 -0
  137. package/skills/swarm_config/__pycache__/configure.cpython-311.pyc +0 -0
  138. package/skills/swarm_config/configure.py +45 -11
@@ -0,0 +1,567 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ IUMBTEMS first-party MCP server: the canonical tool surface.
4
+
5
+ Wraps the existing Python APIs in-process (no spawnSync, no shell-outs):
6
+ - SwarmRunner (runner/research_swarm.py)
7
+ - SourceHasher (skills/research_cache/hasher.py)
8
+ - load_config / save_config (skills/swarm_config/configure.py)
9
+ - DesignTree (skills/grilling/socratic_tree.py)
10
+
11
+ Exposes the 7 canonical iumbtems_* tool names already registered by the
12
+ OpenCode plugin and Pi extension, so harness adapters can thin out to a
13
+ pointer at this server.
14
+
15
+ Two entry points:
16
+ python3 runner/mcp_server.py # stdio MCP server
17
+ python3 runner/mcp_server.py call <tool> '{}' # one-shot dispatch (JSON out)
18
+
19
+ Zero external dependencies by design: protocol loop is runner/mcp_protocol.py.
20
+ """
21
+
22
+ import argparse
23
+ import io
24
+ import json
25
+ import os
26
+ import sys
27
+ from contextlib import redirect_stdout
28
+ from pathlib import Path
29
+ from typing import Any, Callable, Dict, List, Optional
30
+
31
+ # Bootstrap imports when launched as `python3 runner/mcp_server.py`
32
+ SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
33
+ PROJECT_ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, ".."))
34
+ if PROJECT_ROOT not in sys.path:
35
+ sys.path.insert(0, PROJECT_ROOT)
36
+
37
+ from runner.mcp_protocol import PROTOCOL_VERSION, StdioJsonRpcServer, ToolSpec # noqa: E402
38
+
39
+ SERVER_NAME = "iumbtems"
40
+ SERVER_VERSION = "0.3.0"
41
+
42
+
43
+ # ---------------------------------------------------------------------------
44
+ # helpers
45
+ # ---------------------------------------------------------------------------
46
+
47
+ def _tool_text(payload: Any) -> str:
48
+ """Normalize a handler return into the MCP text content block body."""
49
+ if isinstance(payload, str):
50
+ return payload
51
+ return json.dumps(payload, indent=2, default=str)
52
+
53
+
54
+ def _capture_stdout(fn: Callable[..., Any], *args: Any, **kwargs: Any) -> str:
55
+ """Run a print-based CLI function and return what it wrote to stdout."""
56
+ buf = io.StringIO()
57
+ with redirect_stdout(buf):
58
+ fn(*args, **kwargs)
59
+ return buf.getvalue()
60
+
61
+
62
+ # ---------------------------------------------------------------------------
63
+ # tool handlers
64
+ # ---------------------------------------------------------------------------
65
+
66
+ def _handle_config(args: Dict[str, Any]) -> str:
67
+ """Inspect or modify swarm parameters in .research/config.json."""
68
+ from skills.swarm_config.configure import (
69
+ display_config,
70
+ load_config,
71
+ save_config,
72
+ )
73
+
74
+ base_dir = args.get("base_dir") or args.get("dir") or ".research"
75
+ cfg = load_config(base_dir)
76
+
77
+ updates = {}
78
+ for key in ("search_engine", "engine", "max_iterations", "depth", "mode", "divergence_threshold"):
79
+ if key in args and args[key] is not None:
80
+ canon = {
81
+ "engine": "search_engine",
82
+ "depth": "max_iterations",
83
+ "divergence": "divergence_threshold",
84
+ }.get(key, key)
85
+ updates[canon] = args[key]
86
+
87
+ if updates:
88
+ cfg.update(updates)
89
+ save_config(cfg, base_dir)
90
+ return _tool_text({"status": "updated", "config": cfg, "written_to": str(Path(base_dir) / "config.json")})
91
+
92
+ return _tool_text({"status": "current", "config": cfg})
93
+
94
+
95
+ def _run_swarm_mode(mode: Optional[str], args: Dict[str, Any]) -> str:
96
+ """Shared dispatch for the four swarm-shaped tools."""
97
+ from runner.research_swarm import SwarmRunner
98
+
99
+ objective = args.get("objective") or args.get("target") or args.get("feature")
100
+ if not objective:
101
+ raise ValueError("objective is required")
102
+
103
+ frontier = args.get("frontier") or args.get("frontier_file")
104
+ runner = SwarmRunner(
105
+ base_dir=Path(args.get("base_dir") or args.get("dir") or ".research"),
106
+ mock_mode=bool(args.get("mock_claude") or args.get("mock_mode")),
107
+ mode=mode or args.get("mode"),
108
+ engine=args.get("engine"),
109
+ depth=args.get("depth"),
110
+ domain_pack=args.get("domain_pack"),
111
+ )
112
+ buf = io.StringIO()
113
+ with redirect_stdout(buf):
114
+ runner.run_swarm(objective, frontier_file=Path(frontier) if frontier else None)
115
+
116
+ report = runner.base_dir / (
117
+ {"audit": "code_audit_report.md", "scout": "oss_scout_report.md", "brainstorm": "brainstorm_report.md"}.get(
118
+ mode or args.get("mode") or "research", "final_synthesis.md"
119
+ )
120
+ )
121
+ return _tool_text({
122
+ "status": "completed",
123
+ "mode": mode or args.get("mode") or "research",
124
+ "report": str(report),
125
+ "log": buf.getvalue()[-4000:],
126
+ })
127
+
128
+
129
+ def _handle_swarm_research(args: Dict[str, Any]) -> str:
130
+ """Run the dialectic multi-agent research swarm (thesis vs antithesis)."""
131
+ return _run_swarm_mode(None, args)
132
+
133
+
134
+ def _handle_code_audit(args: Dict[str, Any]) -> str:
135
+ """Run a dialectic codebase architecture & security audit."""
136
+ return _run_swarm_mode("audit", args)
137
+
138
+
139
+ def _handle_oss_scout(args: Dict[str, Any]) -> str:
140
+ """Scout open-source software, libraries, and clean-room blueprints."""
141
+ return _run_swarm_mode("scout", args)
142
+
143
+
144
+ def _handle_brainstorm(args: Dict[str, Any]) -> str:
145
+ """Run lateral brainstorming (feature vectors + paradigm moves + spikes)."""
146
+ return _run_swarm_mode("brainstorm", args)
147
+
148
+
149
+ def _handle_verify_quote(args: Dict[str, Any]) -> str:
150
+ """Verify a verbatim quote against the content-addressed source cache."""
151
+ from skills.research_cache.hasher import SourceHasher
152
+
153
+ content_hash = args.get("hash") or args.get("content_hash") or args.get("source_hash")
154
+ quote = args.get("quote")
155
+ if not content_hash or quote is None:
156
+ raise ValueError("hash and quote are required")
157
+
158
+ hasher = SourceHasher(Path(args.get("base_dir") or args.get("dir") or ".research"))
159
+ is_verified, confidence, message = hasher.verify_quote(content_hash, quote)
160
+ return _tool_text({
161
+ "verified": bool(is_verified),
162
+ "confidence": float(confidence),
163
+ "message": message,
164
+ "content_hash": content_hash,
165
+ })
166
+
167
+
168
+ def _handle_report_retraction(args: Dict[str, Any]) -> str:
169
+ """Record a RETRACTED/REVISED event for a cached source (Living Dossiers)."""
170
+ from runner.living_dossiers import write_retraction
171
+
172
+ source_hash = args.get("hash") or args.get("source_hash")
173
+ event = args.get("event")
174
+ if not source_hash or not event:
175
+ raise ValueError("hash and event are required")
176
+ base_dir = Path(args.get("base_dir") or args.get("dir") or ".research")
177
+ path = write_retraction(
178
+ base_dir,
179
+ source_hash,
180
+ str(event).upper(),
181
+ note=args.get("note") or "",
182
+ supersedes=args.get("supersedes"),
183
+ )
184
+ return _tool_text({"status": "recorded", "path": str(path)})
185
+
186
+
187
+ def _handle_check_staleness(args: Dict[str, Any]) -> str:
188
+ """Run one claim-degradation pass (retractions -> STALE/SUSPECT + requeue)."""
189
+ from runner.living_dossiers import check_staleness
190
+
191
+ base_dir = Path(args.get("base_dir") or args.get("dir") or ".research")
192
+ scope_ids = args.get("scope_ids")
193
+ return _tool_text(check_staleness(base_dir, scope_ids=scope_ids))
194
+
195
+
196
+ def _handle_set_domain_pack(args: Dict[str, Any]) -> str:
197
+ """Activate a Regulated Domain Pack (epistemic constitution) in config."""
198
+ from runner.refinement import load_domain_pack
199
+ from skills.swarm_config.configure import load_config, save_config
200
+
201
+ pack = args.get("pack") or args.get("domain_pack") or args.get("pack_id")
202
+ if not pack:
203
+ raise ValueError("pack is required")
204
+ constitution = load_domain_pack(pack) # raises if unknown — fail loudly
205
+ base_dir = args.get("base_dir") or args.get("dir") or ".research"
206
+ cfg = load_config(base_dir)
207
+ cfg["domain_pack"] = pack
208
+ save_config(cfg, base_dir)
209
+ return _tool_text({
210
+ "status": "activated",
211
+ "domain_pack": pack,
212
+ "accept_threshold": constitution.accept_threshold,
213
+ "retraction_policy": constitution.retraction_policy,
214
+ "banned_domains": constitution.banned_domains,
215
+ })
216
+
217
+
218
+ def _handle_export_brief(args: Dict[str, Any]) -> str:
219
+ """Export a proof-carrying research brief (signed, self-contained bundle)."""
220
+ from runner.pcrb import export_brief
221
+
222
+ base_dir = Path(args.get("base_dir") or args.get("dir") or ".research")
223
+ key = args.get("key")
224
+ key_path = Path(args["key_file"]) if args.get("key_file") else None
225
+ path = export_brief(
226
+ base_dir,
227
+ out_path=Path(args["out"]) if args.get("out") else None,
228
+ scope_ids=args.get("scope_ids"),
229
+ objective=args.get("objective"),
230
+ key=key,
231
+ key_path=key_path,
232
+ key_id=args.get("key_id") or "local",
233
+ )
234
+ return _tool_text({"status": "exported", "path": str(path)})
235
+
236
+
237
+ def _handle_verify_brief(args: Dict[str, Any]) -> str:
238
+ """Verify a proof-carrying brief against its bundled sources (no trust in the LLM)."""
239
+ import os
240
+
241
+ from runner.pcrb import ENV_KEY
242
+ from runner.pcrb_verify import verify_brief
243
+
244
+ brief = args.get("brief") or args.get("path")
245
+ if not brief:
246
+ raise ValueError("brief path is required")
247
+ p = Path(brief)
248
+ if not p.exists():
249
+ raise FileNotFoundError(f"no such brief: {p}")
250
+ bundle = json.loads(p.read_text(encoding="utf-8"))
251
+
252
+ key = None
253
+ if args.get("key"):
254
+ key = args["key"].encode("utf-8")
255
+ elif args.get("key_file") and Path(args["key_file"]).exists():
256
+ key = Path(args["key_file"]).read_bytes().strip()
257
+ elif os.environ.get(ENV_KEY):
258
+ key = os.environ[ENV_KEY].encode("utf-8")
259
+
260
+ return _tool_text(verify_brief(bundle, key=key))
261
+
262
+
263
+ def _handle_reindex_claims(args: Dict[str, Any]) -> str:
264
+ """Rebuild the derived claims.sqlite index from .research flat files."""
265
+ from runner.claim_store import reindex
266
+
267
+ base_dir = Path(args.get("base_dir") or args.get("dir") or ".research")
268
+ return _tool_text(reindex(base_dir))
269
+
270
+
271
+ def _handle_socratic_frontier(args: Dict[str, Any]) -> str:
272
+ """Advance or inspect the Socratic decision-tree frontier."""
273
+ from skills.grilling.socratic_tree import DesignTree
274
+
275
+ file_path = Path(args.get("file") or args.get("frontier_file") or ".research/frontier.json")
276
+
277
+ if args.get("settle"):
278
+ node_id, answer = args["settle"][0], args["settle"][1]
279
+ if file_path.exists():
280
+ tree = DesignTree.load_from_json(file_path)
281
+ else:
282
+ tree = DesignTree(objective=args.get("objective", ""))
283
+ tree.settle_node(node_id, answer)
284
+ tree.export_frontier_json(file_path)
285
+ return _tool_text({"status": "settled", "node_id": node_id, "file": str(file_path)})
286
+
287
+ if not file_path.exists():
288
+ return _tool_text({
289
+ "status": "no_frontier",
290
+ "message": f"No frontier file at {file_path}. Run /grilling first.",
291
+ "file": str(file_path),
292
+ })
293
+
294
+ tree = DesignTree.load_from_json(file_path)
295
+ frontier = tree.compute_frontier()
296
+ return _tool_text({
297
+ "status": "frontier",
298
+ "objective": tree.objective,
299
+ "is_complete": tree.is_complete(),
300
+ "total_nodes": len(tree.nodes),
301
+ "settled": sum(1 for n in tree.nodes.values() if n.is_settled()),
302
+ "frontier": [{"node_id": n.node_id, "question": n.question} for n in frontier],
303
+ "file": str(file_path),
304
+ })
305
+
306
+
307
+ # ---------------------------------------------------------------------------
308
+ # registry
309
+ # ---------------------------------------------------------------------------
310
+
311
+ def build_tools() -> List[ToolSpec]:
312
+ """The 7 canonical iumbtems_* tools, in-process wrappers only."""
313
+ return [
314
+ ToolSpec(
315
+ name="iumbtems_config",
316
+ description="Inspect or dynamically adjust Epistemic Swarm parameters (search engine, depth, mode) in .research/config.json.",
317
+ input_schema={
318
+ "type": "object",
319
+ "properties": {
320
+ "base_dir": {"type": "string", "description": "Path to .research workspace (default .research)"},
321
+ "search_engine": {"type": "string", "enum": ["duckduckgo", "brave", "firecrawl", "searxng"]},
322
+ "max_iterations": {"type": "integer", "description": "Dialectic depth 1-4"},
323
+ "mode": {"type": "string", "enum": ["research", "audit", "scout", "hybrid", "brainstorm"]},
324
+ "divergence_threshold": {"type": "number"},
325
+ },
326
+ },
327
+ handler=_handle_config,
328
+ ),
329
+ ToolSpec(
330
+ name="iumbtems_swarm_research",
331
+ description="Dispatch the dialectic researcher pair (Agent Alpha thesis vs Agent Beta antithesis) with epistemic auditing. Long-running; blocks until the swarm completes.",
332
+ input_schema={
333
+ "type": "object",
334
+ "properties": {
335
+ "objective": {"type": "string", "description": "Research question or objective"},
336
+ "base_dir": {"type": "string"},
337
+ "frontier_file": {"type": "string", "description": "Settled frontier.json from grilling"},
338
+ "engine": {"type": "string", "enum": ["duckduckgo", "brave", "firecrawl", "searxng"]},
339
+ "depth": {"type": "integer"},
340
+ "mock_claude": {"type": "boolean", "description": "Synthetic run, zero API cost"},
341
+ },
342
+ "required": ["objective"],
343
+ },
344
+ handler=_handle_swarm_research,
345
+ ),
346
+ ToolSpec(
347
+ name="iumbtems_code_audit",
348
+ description="Dispatch a dialectic codebase review (structural architect vs vulnerability red-team).",
349
+ input_schema={
350
+ "type": "object",
351
+ "properties": {
352
+ "target": {"type": "string", "description": "Codebase target or audit focus"},
353
+ "base_dir": {"type": "string"},
354
+ "mock_claude": {"type": "boolean"},
355
+ },
356
+ "required": ["target"],
357
+ },
358
+ handler=_handle_code_audit,
359
+ ),
360
+ ToolSpec(
361
+ name="iumbtems_oss_scout",
362
+ description="Scout open-source libraries, audit licenses, and build clean-room blueprints.",
363
+ input_schema={
364
+ "type": "object",
365
+ "properties": {
366
+ "feature": {"type": "string", "description": "Feature or library to scout"},
367
+ "base_dir": {"type": "string"},
368
+ "mock_claude": {"type": "boolean"},
369
+ },
370
+ "required": ["feature"],
371
+ },
372
+ handler=_handle_oss_scout,
373
+ ),
374
+ ToolSpec(
375
+ name="iumbtems_brainstorm",
376
+ description="Lateral ideation portfolio (feature vectors + paradigm moves + falsifiable spikes). Never bug fixes.",
377
+ input_schema={
378
+ "type": "object",
379
+ "properties": {
380
+ "objective": {"type": "string", "description": "Ambiguous prompt to ideate on"},
381
+ "base_dir": {"type": "string"},
382
+ "mock_claude": {"type": "boolean"},
383
+ },
384
+ "required": ["objective"],
385
+ },
386
+ handler=_handle_brainstorm,
387
+ ),
388
+ ToolSpec(
389
+ name="iumbtems_verify_quote",
390
+ description="Audit a verbatim citation against the SHA-256 source cache (.research/sources/<hash>.md).",
391
+ input_schema={
392
+ "type": "object",
393
+ "properties": {
394
+ "hash": {"type": "string", "description": "Content-addressed SHA-256 of the cached source"},
395
+ "quote": {"type": "string", "description": "Verbatim quote to verify"},
396
+ "base_dir": {"type": "string"},
397
+ },
398
+ "required": ["hash", "quote"],
399
+ },
400
+ handler=_handle_verify_quote,
401
+ ),
402
+ ToolSpec(
403
+ name="iumbtems_reindex_claims",
404
+ description="Rebuild the derived claims.sqlite index from .research flat files (idempotent; flat files are source of truth).",
405
+ input_schema={
406
+ "type": "object",
407
+ "properties": {
408
+ "base_dir": {"type": "string", "description": "Path to .research workspace"},
409
+ },
410
+ },
411
+ handler=_handle_reindex_claims,
412
+ ),
413
+ ToolSpec(
414
+ name="iumbtems_report_retraction",
415
+ description="Record a RETRACTED/REVISED event for a cached source; dependent VERIFIED claims degrade to STALE/SUSPECT on the next audit (Living Dossiers).",
416
+ input_schema={
417
+ "type": "object",
418
+ "properties": {
419
+ "hash": {"type": "string", "description": "Content-addressed SHA-256 of the affected source"},
420
+ "event": {"type": "string", "enum": ["RETRACTED", "REVISED"]},
421
+ "note": {"type": "string", "description": "Why the source was retracted/revised"},
422
+ "base_dir": {"type": "string"},
423
+ },
424
+ "required": ["hash", "event"],
425
+ },
426
+ handler=_handle_report_retraction,
427
+ ),
428
+ ToolSpec(
429
+ name="iumbtems_check_staleness",
430
+ description="Run one claim-degradation pass: join claims against retraction events, write the status ledger, and queue re-runs for degraded scopes. Never mutates dossiers.",
431
+ input_schema={
432
+ "type": "object",
433
+ "properties": {
434
+ "base_dir": {"type": "string"},
435
+ "scope_ids": {
436
+ "type": "array",
437
+ "items": {"type": "string"},
438
+ "description": "Limit the pass to specific scopes",
439
+ },
440
+ },
441
+ },
442
+ handler=_handle_check_staleness,
443
+ ),
444
+ ToolSpec(
445
+ name="iumbtems_set_domain_pack",
446
+ description="Activate a Regulated Domain Pack (biopharma/quant/legal epistemic constitution) for subsequent audits.",
447
+ input_schema={
448
+ "type": "object",
449
+ "properties": {
450
+ "pack": {"type": "string", "description": "Pack id: biopharma | quant | legal (or a path)"},
451
+ "base_dir": {"type": "string"},
452
+ },
453
+ "required": ["pack"],
454
+ },
455
+ handler=_handle_set_domain_pack,
456
+ ),
457
+ ToolSpec(
458
+ name="iumbtems_export_brief",
459
+ description="Export a proof-carrying research brief (PCRB): self-contained signed bundle of synthesis, claims, quote witnesses, and full source texts.",
460
+ input_schema={
461
+ "type": "object",
462
+ "properties": {
463
+ "base_dir": {"type": "string"},
464
+ "out": {"type": "string", "description": "Output path (default <base_dir>/brief.pcrb.json)"},
465
+ "objective": {"type": "string"},
466
+ "key": {"type": "string", "description": "HMAC key (prefer key_file or env IUMBTEMS_PCRB_KEY)"},
467
+ "key_file": {"type": "string"},
468
+ },
469
+ },
470
+ handler=_handle_export_brief,
471
+ ),
472
+ ToolSpec(
473
+ name="iumbtems_verify_brief",
474
+ description="Verify a proof-carrying brief against its BUNDLED sources: manifest integrity, HMAC signature, and every quote re-checked. Exit-fail semantics; no trust in the producing LLM.",
475
+ input_schema={
476
+ "type": "object",
477
+ "properties": {
478
+ "brief": {"type": "string", "description": "Path to brief.pcrb.json"},
479
+ "key": {"type": "string"},
480
+ "key_file": {"type": "string"},
481
+ },
482
+ "required": ["brief"],
483
+ },
484
+ handler=_handle_verify_brief,
485
+ ),
486
+ ToolSpec(
487
+ name="iumbtems_socratic_frontier",
488
+ description="Inspect or advance the Socratic decision-tree frontier (.research/frontier.json).",
489
+ input_schema={
490
+ "type": "object",
491
+ "properties": {
492
+ "file": {"type": "string", "description": "Path to frontier.json"},
493
+ "objective": {"type": "string"},
494
+ "settle": {
495
+ "type": "array",
496
+ "items": {"type": "string"},
497
+ "description": "Two-element [node_id, answer] to settle a frontier node",
498
+ },
499
+ },
500
+ },
501
+ handler=_handle_socratic_frontier,
502
+ ),
503
+ ]
504
+
505
+
506
+ def build_server() -> StdioJsonRpcServer:
507
+ return StdioJsonRpcServer(
508
+ server_name=SERVER_NAME,
509
+ version=SERVER_VERSION,
510
+ tools=build_tools(),
511
+ protocol_version=PROTOCOL_VERSION,
512
+ log_prefix="[iumbtems-mcp]",
513
+ )
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # entry points
518
+ # ---------------------------------------------------------------------------
519
+
520
+ def call_once(tool_name: str, arguments: Dict[str, Any]) -> int:
521
+ """One-shot dispatch: run a single tool and print its text payload."""
522
+ server = build_server()
523
+ spec = server._by_name.get(tool_name)
524
+ if spec is None:
525
+ json.dump(
526
+ {"error": f"Tool {tool_name} not found", "available": [t.name for t in server.tools]},
527
+ sys.stdout,
528
+ indent=2,
529
+ )
530
+ sys.stdout.write("\n")
531
+ return 1
532
+ try:
533
+ text = spec.handler(arguments)
534
+ except Exception as exc: # noqa: BLE001 - surface as a JSON error payload
535
+ json.dump({"error": f"{type(exc).__name__}: {exc}"}, sys.stdout, indent=2)
536
+ sys.stdout.write("\n")
537
+ return 1
538
+ sys.stdout.write(text if text.endswith("\n") else text + "\n")
539
+ return 0
540
+
541
+
542
+ def main(argv: Optional[List[str]] = None) -> int:
543
+ parser = argparse.ArgumentParser(description="IUMBTEMS MCP server")
544
+ sub = parser.add_subparsers(dest="command")
545
+
546
+ call_p = sub.add_parser("call", help="One-shot tool dispatch (JSON out)")
547
+ call_p.add_argument("tool", help="Tool name, e.g. iumbtems_verify_quote")
548
+ call_p.add_argument("arguments", help="JSON object of tool arguments")
549
+
550
+ sub.add_parser("serve", help="Run stdio MCP server (default)")
551
+
552
+ args = parser.parse_args(argv)
553
+
554
+ if args.command == "call":
555
+ try:
556
+ arguments = json.loads(args.arguments) if args.arguments else {}
557
+ except json.JSONDecodeError as exc:
558
+ print(json.dumps({"error": f"arguments must be valid JSON: {exc}"}))
559
+ return 1
560
+ return call_once(args.tool, arguments)
561
+
562
+ build_server().serve_forever()
563
+ return 0
564
+
565
+
566
+ if __name__ == "__main__":
567
+ sys.exit(main())