hegelion 0.4.0__py3-none-any.whl

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 (43) hide show
  1. hegelion/__init__.py +45 -0
  2. hegelion/core/__init__.py +29 -0
  3. hegelion/core/agent.py +166 -0
  4. hegelion/core/autocoding_state.py +293 -0
  5. hegelion/core/backends.py +442 -0
  6. hegelion/core/cache.py +92 -0
  7. hegelion/core/config.py +276 -0
  8. hegelion/core/core.py +649 -0
  9. hegelion/core/engine.py +865 -0
  10. hegelion/core/logging_utils.py +67 -0
  11. hegelion/core/models.py +293 -0
  12. hegelion/core/parsing.py +271 -0
  13. hegelion/core/personas.py +81 -0
  14. hegelion/core/prompt_autocoding.py +353 -0
  15. hegelion/core/prompt_dialectic.py +414 -0
  16. hegelion/core/prompts.py +127 -0
  17. hegelion/core/schema.py +67 -0
  18. hegelion/core/validation.py +68 -0
  19. hegelion/council.py +254 -0
  20. hegelion/examples_data/__init__.py +6 -0
  21. hegelion/examples_data/glm4_6_examples.jsonl +2 -0
  22. hegelion/judge.py +230 -0
  23. hegelion/mcp/__init__.py +3 -0
  24. hegelion/mcp/server.py +918 -0
  25. hegelion/scripts/hegelion_agent_cli.py +90 -0
  26. hegelion/scripts/hegelion_bench.py +117 -0
  27. hegelion/scripts/hegelion_cli.py +497 -0
  28. hegelion/scripts/hegelion_dataset.py +99 -0
  29. hegelion/scripts/hegelion_eval.py +137 -0
  30. hegelion/scripts/mcp_setup.py +150 -0
  31. hegelion/search_providers.py +151 -0
  32. hegelion/training/__init__.py +7 -0
  33. hegelion/training/datasets.py +123 -0
  34. hegelion/training/generator.py +232 -0
  35. hegelion/training/mlx_scu_trainer.py +379 -0
  36. hegelion/training/mlx_trainer.py +181 -0
  37. hegelion/training/unsloth_trainer.py +136 -0
  38. hegelion-0.4.0.dist-info/METADATA +295 -0
  39. hegelion-0.4.0.dist-info/RECORD +43 -0
  40. hegelion-0.4.0.dist-info/WHEEL +5 -0
  41. hegelion-0.4.0.dist-info/entry_points.txt +8 -0
  42. hegelion-0.4.0.dist-info/licenses/LICENSE +21 -0
  43. hegelion-0.4.0.dist-info/top_level.txt +1 -0
hegelion/mcp/server.py ADDED
@@ -0,0 +1,918 @@
1
+ """Model-agnostic MCP server for dialectical reasoning.
2
+
3
+ This version works with whatever LLM is calling the MCP server,
4
+ rather than making its own API calls. Perfect for Cursor, Claude Desktop,
5
+ VS Code, or any MCP-compatible environment.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ from typing import Any, Dict, List
13
+
14
+ from mcp.server import Server
15
+ from mcp.server.stdio import stdio_server
16
+ from mcp.types import CallToolResult, TextContent, Tool
17
+ import anyio
18
+
19
+ from hegelion.core.prompt_dialectic import (
20
+ create_dialectical_workflow,
21
+ create_single_shot_dialectic_prompt,
22
+ )
23
+ from hegelion.core.autocoding_state import AutocodingState, save_session, load_session
24
+ from hegelion.core.prompt_autocoding import PromptDrivenAutocoding
25
+
26
+ app = Server("hegelion-server")
27
+
28
+ # Compatibility: older anyio versions expose create_memory_object_stream as a plain function
29
+ # which cannot be subscripted (newer typing style uses subscripting). Patch a lightweight
30
+ # wrapper so the MCP server session setup does not crash when subscripting is attempted.
31
+ if not hasattr(anyio.create_memory_object_stream, "__getitem__"):
32
+ _create_stream = anyio.create_memory_object_stream
33
+
34
+ class _CreateStreamWrapper:
35
+ def __call__(self, *args, **kwargs):
36
+ return _create_stream(*args, **kwargs)
37
+
38
+ def __getitem__(self, _):
39
+ return self
40
+
41
+ anyio.create_memory_object_stream = _CreateStreamWrapper() # type: ignore[assignment]
42
+
43
+
44
+ @app.list_tools()
45
+ async def list_tools() -> list[Tool]:
46
+ """Return dialectical reasoning tools that work with any LLM."""
47
+ tools = [
48
+ Tool(
49
+ name="dialectical_workflow",
50
+ description=(
51
+ "Step-by-step prompts for dialectical reasoning (thesis → antithesis → synthesis). "
52
+ "Set response_style to control output: 'json' (structured, agent-friendly), "
53
+ "'sections' (full text), or 'synthesis_only' (just the resolution). "
54
+ "Example: {'query': 'Should AI be regulated?', 'response_style': 'json'}."
55
+ ),
56
+ inputSchema={
57
+ "type": "object",
58
+ "properties": {
59
+ "query": {
60
+ "type": "string",
61
+ "description": "The question or topic to analyze dialectically",
62
+ },
63
+ "use_search": {
64
+ "type": "boolean",
65
+ "description": "Include instructions to use search tools for real-world grounding",
66
+ "default": False,
67
+ },
68
+ "use_council": {
69
+ "type": "boolean",
70
+ "description": "Enable multi-perspective council critiques (Logician, Empiricist, Ethicist)",
71
+ "default": False,
72
+ },
73
+ "use_judge": {
74
+ "type": "boolean",
75
+ "description": "Include quality evaluation step",
76
+ "default": False,
77
+ },
78
+ "format": {
79
+ "type": "string",
80
+ "enum": ["workflow", "single_prompt"],
81
+ "description": "Return structured workflow or single comprehensive prompt",
82
+ "default": "workflow",
83
+ },
84
+ "response_style": {
85
+ "type": "string",
86
+ "enum": [
87
+ "sections",
88
+ "synthesis_only",
89
+ "json",
90
+ "conversational",
91
+ "bullet_points",
92
+ ],
93
+ "description": (
94
+ "Shape of the final output you want from the LLM: full thesis/antithesis/synthesis sections,"
95
+ " synthesis-only, or JSON with all fields."
96
+ ),
97
+ "default": "sections",
98
+ },
99
+ },
100
+ "required": ["query"],
101
+ },
102
+ ),
103
+ Tool(
104
+ name="dialectical_single_shot",
105
+ description=(
106
+ "One comprehensive prompt that makes the LLM do thesis → antithesis → synthesis in one go. "
107
+ "Use response_style: 'json' (structured), 'sections' (full text), or 'synthesis_only' (just the resolution)."
108
+ ),
109
+ inputSchema={
110
+ "type": "object",
111
+ "properties": {
112
+ "query": {
113
+ "type": "string",
114
+ "description": "The question or topic to analyze dialectically",
115
+ },
116
+ "use_search": {
117
+ "type": "boolean",
118
+ "description": "Include instructions to use search tools",
119
+ "default": False,
120
+ },
121
+ "use_council": {
122
+ "type": "boolean",
123
+ "description": "Enable multi-perspective council critiques",
124
+ "default": False,
125
+ },
126
+ "response_style": {
127
+ "type": "string",
128
+ "enum": [
129
+ "sections",
130
+ "synthesis_only",
131
+ "json",
132
+ "conversational",
133
+ "bullet_points",
134
+ ],
135
+ "description": (
136
+ "Format you want the model to return: full sections, synthesis-only, or JSON with thesis/antithesis/synthesis."
137
+ ),
138
+ "default": "sections",
139
+ },
140
+ },
141
+ "required": ["query"],
142
+ },
143
+ ),
144
+ Tool(
145
+ name="thesis_prompt",
146
+ description=(
147
+ "Generate just the thesis prompt for dialectical reasoning. "
148
+ "Use this when you want to execute dialectical reasoning step-by-step."
149
+ ),
150
+ inputSchema={
151
+ "type": "object",
152
+ "properties": {
153
+ "query": {
154
+ "type": "string",
155
+ "description": "The question or topic to analyze dialectically",
156
+ }
157
+ },
158
+ "required": ["query"],
159
+ },
160
+ ),
161
+ Tool(
162
+ name="antithesis_prompt",
163
+ description=(
164
+ "Generate the antithesis prompt for dialectical reasoning. "
165
+ "Requires the thesis output from a previous step."
166
+ ),
167
+ inputSchema={
168
+ "type": "object",
169
+ "properties": {
170
+ "query": {
171
+ "type": "string",
172
+ "description": "The original question or topic",
173
+ },
174
+ "thesis": {
175
+ "type": "string",
176
+ "description": "The thesis output to critique",
177
+ },
178
+ "use_search": {
179
+ "type": "boolean",
180
+ "description": "Include instructions to use search tools",
181
+ "default": False,
182
+ },
183
+ "use_council": {
184
+ "type": "boolean",
185
+ "description": "Use council-based multi-perspective critique",
186
+ "default": False,
187
+ },
188
+ },
189
+ "required": ["query", "thesis"],
190
+ },
191
+ ),
192
+ Tool(
193
+ name="synthesis_prompt",
194
+ description=(
195
+ "Generate the synthesis prompt for dialectical reasoning. "
196
+ "Requires thesis and antithesis outputs from previous steps."
197
+ ),
198
+ inputSchema={
199
+ "type": "object",
200
+ "properties": {
201
+ "query": {
202
+ "type": "string",
203
+ "description": "The original question or topic",
204
+ },
205
+ "thesis": {
206
+ "type": "string",
207
+ "description": "The thesis output",
208
+ },
209
+ "antithesis": {
210
+ "type": "string",
211
+ "description": "The antithesis critique output",
212
+ },
213
+ },
214
+ "required": ["query", "thesis", "antithesis"],
215
+ },
216
+ ),
217
+ # === AUTOCODING TOOLS (based on g3 paper) ===
218
+ Tool(
219
+ name="autocoding_init",
220
+ description=(
221
+ "Initialize a dialectical autocoding session with requirements. "
222
+ "Returns session state to pass to subsequent tool calls. "
223
+ "Based on the g3 paper's coach-player adversarial cooperation paradigm."
224
+ ),
225
+ inputSchema={
226
+ "type": "object",
227
+ "properties": {
228
+ "requirements": {
229
+ "type": "string",
230
+ "description": "The requirements document (source of truth). Should be structured as a checklist.",
231
+ },
232
+ "max_turns": {
233
+ "type": "integer",
234
+ "description": "Maximum turns before timeout (default: 10)",
235
+ "default": 10,
236
+ },
237
+ "approval_threshold": {
238
+ "type": "number",
239
+ "description": "Minimum compliance score for approval (0-1, default: 0.9)",
240
+ "default": 0.9,
241
+ },
242
+ },
243
+ "required": ["requirements"],
244
+ },
245
+ ),
246
+ Tool(
247
+ name="player_prompt",
248
+ description=(
249
+ "Generate the implementation prompt for the player agent in autocoding. "
250
+ "The player focuses on implementing requirements, NOT declaring success."
251
+ ),
252
+ inputSchema={
253
+ "type": "object",
254
+ "properties": {
255
+ "state": {
256
+ "type": "object",
257
+ "description": "AutocodingState dict from autocoding_init or autocoding_advance",
258
+ },
259
+ },
260
+ "required": ["state"],
261
+ },
262
+ ),
263
+ Tool(
264
+ name="coach_prompt",
265
+ description=(
266
+ "Generate the validation prompt for the coach agent in autocoding. "
267
+ "The coach verifies implementation against requirements, ignoring player's self-assessment."
268
+ ),
269
+ inputSchema={
270
+ "type": "object",
271
+ "properties": {
272
+ "state": {
273
+ "type": "object",
274
+ "description": "AutocodingState dict from player phase",
275
+ },
276
+ },
277
+ "required": ["state"],
278
+ },
279
+ ),
280
+ Tool(
281
+ name="autocoding_advance",
282
+ description=(
283
+ "Advance autocoding state after coach review. "
284
+ "Updates turn count, records feedback, and determines next phase."
285
+ ),
286
+ inputSchema={
287
+ "type": "object",
288
+ "properties": {
289
+ "state": {
290
+ "type": "object",
291
+ "description": "AutocodingState dict from coach phase",
292
+ },
293
+ "coach_feedback": {
294
+ "type": "string",
295
+ "description": "The coach's feedback text (compliance checklist and actions needed)",
296
+ },
297
+ "approved": {
298
+ "type": "boolean",
299
+ "description": "Whether the coach approved the implementation (look for 'COACH APPROVED')",
300
+ },
301
+ "compliance_score": {
302
+ "type": "number",
303
+ "description": "Optional compliance score (0-1) based on checklist items satisfied",
304
+ },
305
+ },
306
+ "required": ["state", "coach_feedback", "approved"],
307
+ },
308
+ ),
309
+ Tool(
310
+ name="autocoding_single_shot",
311
+ description=(
312
+ "Single comprehensive prompt for self-directed autocoding. "
313
+ "Combines player and coach roles with iterative implementation and self-verification."
314
+ ),
315
+ inputSchema={
316
+ "type": "object",
317
+ "properties": {
318
+ "requirements": {
319
+ "type": "string",
320
+ "description": "The requirements document (source of truth)",
321
+ },
322
+ "max_turns": {
323
+ "type": "integer",
324
+ "description": "Maximum iterations to attempt (default: 10)",
325
+ "default": 10,
326
+ },
327
+ },
328
+ "required": ["requirements"],
329
+ },
330
+ ),
331
+ Tool(
332
+ name="autocoding_save",
333
+ description=(
334
+ "Save an autocoding session to a JSON file. "
335
+ "Use this to persist session state for later resumption."
336
+ ),
337
+ inputSchema={
338
+ "type": "object",
339
+ "properties": {
340
+ "state": {
341
+ "type": "object",
342
+ "description": "AutocodingState dict to save",
343
+ },
344
+ "filepath": {
345
+ "type": "string",
346
+ "description": "Path to save the session JSON file",
347
+ },
348
+ },
349
+ "required": ["state", "filepath"],
350
+ },
351
+ ),
352
+ Tool(
353
+ name="autocoding_load",
354
+ description=(
355
+ "Load an autocoding session from a JSON file. "
356
+ "Use this to resume a previously saved session."
357
+ ),
358
+ inputSchema={
359
+ "type": "object",
360
+ "properties": {
361
+ "filepath": {
362
+ "type": "string",
363
+ "description": "Path to the session JSON file to load",
364
+ },
365
+ },
366
+ "required": ["filepath"],
367
+ },
368
+ ),
369
+ ]
370
+ return tools
371
+
372
+
373
+ def _response_style_summary(style: str) -> str:
374
+ """Short human-readable description of response style."""
375
+ match style:
376
+ case "json":
377
+ return "LLM should return a JSON object with thesis/antithesis/synthesis fields."
378
+ case "synthesis_only":
379
+ return "LLM should only return the synthesis (no thesis/antithesis sections)."
380
+ case "conversational":
381
+ return "LLM should return a natural, conversational response."
382
+ case "bullet_points":
383
+ return "LLM should return a concise bulleted list."
384
+ case _:
385
+ return "LLM should return full Thesis → Antithesis → Synthesis sections."
386
+
387
+
388
+ async def _send_progress(message: str, progress: float, total: float = 3.0) -> None:
389
+ """Send a progress notification if a progress token is available."""
390
+ try:
391
+ ctx = app.request_context
392
+ if ctx.meta and ctx.meta.progressToken:
393
+ await ctx.session.send_progress_notification(
394
+ ctx.meta.progressToken,
395
+ progress,
396
+ total=total,
397
+ message=message,
398
+ )
399
+ except (LookupError, AttributeError):
400
+ # No request context or progress token available
401
+ pass
402
+
403
+
404
+ @app.call_tool()
405
+ async def call_tool(name: str, arguments: Dict[str, Any]):
406
+ """Execute dialectical reasoning tools."""
407
+
408
+ if name == "dialectical_workflow":
409
+ query = arguments["query"]
410
+ use_search = arguments.get("use_search", False)
411
+ use_council = arguments.get("use_council", False)
412
+ use_judge = arguments.get("use_judge", False)
413
+ format_type = arguments.get("format", "workflow")
414
+ response_style = arguments.get("response_style", "sections")
415
+
416
+ # Send progress notification
417
+ await _send_progress("━━━ Preparing dialectical workflow ━━━", 1.0)
418
+
419
+ if format_type == "single_prompt":
420
+ await _send_progress("━━━ Generating single-shot prompt ━━━", 2.0)
421
+ prompt = create_single_shot_dialectic_prompt(
422
+ query=query,
423
+ use_search=use_search,
424
+ use_council=use_council,
425
+ response_style=response_style,
426
+ )
427
+ await _send_progress("━━━ Prompt ready ━━━", 3.0)
428
+ structured = {
429
+ "query": query,
430
+ "format": "single_prompt",
431
+ "use_search": use_search,
432
+ "use_council": use_council,
433
+ "response_style": response_style,
434
+ "prompt": prompt,
435
+ }
436
+ return ([TextContent(type="text", text=prompt)], structured)
437
+ else:
438
+ await _send_progress("━━━ THESIS prompt ready ━━━", 1.0)
439
+ await _send_progress("━━━ ANTITHESIS prompt ready ━━━", 2.0)
440
+ await _send_progress("━━━ SYNTHESIS prompt ready ━━━", 3.0)
441
+ workflow = create_dialectical_workflow(
442
+ query=query,
443
+ use_search=use_search,
444
+ use_council=use_council,
445
+ use_judge=use_judge,
446
+ )
447
+ workflow.setdefault("instructions", {})
448
+ workflow["instructions"]["response_style"] = response_style
449
+ workflow["instructions"]["response_style_note"] = _response_style_summary(
450
+ response_style
451
+ )
452
+
453
+ serialized = json.dumps(workflow, indent=2)
454
+ summary = (
455
+ "Hegelion dialectical workflow ready. Agents should read the structuredContent JSON. "
456
+ f"Human-readable summary: query='{query}', response_style='{response_style}'."
457
+ )
458
+ contents: List[TextContent] = [
459
+ TextContent(type="text", text=summary),
460
+ TextContent(type="text", text=serialized),
461
+ ]
462
+ return (contents, workflow)
463
+
464
+ elif name == "dialectical_single_shot":
465
+ query = arguments["query"]
466
+ use_search = arguments.get("use_search", False)
467
+ use_council = arguments.get("use_council", False)
468
+
469
+ response_style = arguments.get("response_style", "sections")
470
+ prompt = create_single_shot_dialectic_prompt(
471
+ query=query,
472
+ use_search=use_search,
473
+ use_council=use_council,
474
+ response_style=response_style,
475
+ )
476
+
477
+ structured = {
478
+ "query": query,
479
+ "use_search": use_search,
480
+ "use_council": use_council,
481
+ "response_style": response_style,
482
+ "prompt": prompt,
483
+ }
484
+ note = _response_style_summary(response_style)
485
+ contents = [
486
+ TextContent(type="text", text=f"{note}\n\n{prompt}"),
487
+ ]
488
+ return (contents, structured)
489
+
490
+ elif name == "thesis_prompt":
491
+ await _send_progress("━━━ THESIS ━━━ Generating prompt...", 1.0, 1.0)
492
+ from hegelion.core.prompt_dialectic import PromptDrivenDialectic
493
+
494
+ query = arguments["query"]
495
+ dialectic = PromptDrivenDialectic()
496
+ prompt_obj = dialectic.generate_thesis_prompt(query)
497
+
498
+ structured = {
499
+ "phase": prompt_obj.phase,
500
+ "prompt": prompt_obj.prompt,
501
+ "instructions": prompt_obj.instructions,
502
+ "expected_format": prompt_obj.expected_format,
503
+ }
504
+
505
+ response = f"""# THESIS PROMPT
506
+
507
+ {prompt_obj.prompt}
508
+
509
+ **Instructions:** {prompt_obj.instructions}
510
+ **Expected Format:** {prompt_obj.expected_format}"""
511
+
512
+ return ([TextContent(type="text", text=response)], structured)
513
+
514
+ elif name == "antithesis_prompt":
515
+ await _send_progress("━━━ ANTITHESIS ━━━ Generating prompt...", 1.0, 1.0)
516
+ from hegelion.core.prompt_dialectic import PromptDrivenDialectic
517
+
518
+ query = arguments["query"]
519
+ thesis = arguments["thesis"]
520
+ use_search = arguments.get("use_search", False)
521
+ use_council = arguments.get("use_council", False)
522
+
523
+ dialectic = PromptDrivenDialectic()
524
+
525
+ if use_council:
526
+ council_prompts = dialectic.generate_council_prompts(query, thesis)
527
+ response_parts = ["# COUNCIL ANTITHESIS PROMPTS\n"]
528
+ structured_prompts = []
529
+
530
+ for prompt_obj in council_prompts:
531
+ response_parts.append(f"## {prompt_obj.phase.replace('_', ' ').title()}")
532
+ response_parts.append(prompt_obj.prompt)
533
+ response_parts.append(f"**Instructions:** {prompt_obj.instructions}")
534
+ response_parts.append("")
535
+ structured_prompts.append(
536
+ {
537
+ "phase": prompt_obj.phase,
538
+ "prompt": prompt_obj.prompt,
539
+ "instructions": prompt_obj.instructions,
540
+ "expected_format": prompt_obj.expected_format,
541
+ }
542
+ )
543
+
544
+ structured = {"prompts": structured_prompts, "phase": "antithesis_council"}
545
+ response = "\n".join(response_parts)
546
+ else:
547
+ prompt_obj = dialectic.generate_antithesis_prompt(query, thesis, use_search)
548
+ structured = {
549
+ "phase": prompt_obj.phase,
550
+ "prompt": prompt_obj.prompt,
551
+ "instructions": prompt_obj.instructions,
552
+ "expected_format": prompt_obj.expected_format,
553
+ }
554
+ response = f"""# ANTITHESIS PROMPT
555
+
556
+ {prompt_obj.prompt}
557
+
558
+ **Instructions:** {prompt_obj.instructions}
559
+ **Expected Format:** {prompt_obj.expected_format}"""
560
+
561
+ return ([TextContent(type="text", text=response)], structured)
562
+
563
+ elif name == "synthesis_prompt":
564
+ await _send_progress("━━━ SYNTHESIS ━━━ Generating prompt...", 1.0, 1.0)
565
+ from hegelion.core.prompt_dialectic import PromptDrivenDialectic
566
+
567
+ query = arguments["query"]
568
+ thesis = arguments["thesis"]
569
+ antithesis = arguments["antithesis"]
570
+
571
+ dialectic = PromptDrivenDialectic()
572
+ prompt_obj = dialectic.generate_synthesis_prompt(query, thesis, antithesis)
573
+
574
+ structured = {
575
+ "phase": prompt_obj.phase,
576
+ "prompt": prompt_obj.prompt,
577
+ "instructions": prompt_obj.instructions,
578
+ "expected_format": prompt_obj.expected_format,
579
+ }
580
+
581
+ response = f"""# SYNTHESIS PROMPT
582
+
583
+ {prompt_obj.prompt}
584
+
585
+ **Instructions:** {prompt_obj.instructions}
586
+ **Expected Format:** {prompt_obj.expected_format}"""
587
+
588
+ return ([TextContent(type="text", text=response)], structured)
589
+
590
+ # === AUTOCODING TOOL HANDLERS ===
591
+
592
+ elif name == "autocoding_init":
593
+ await _send_progress("Initializing autocoding session...", 1.0, 2.0)
594
+
595
+ requirements = arguments["requirements"]
596
+ max_turns = arguments.get("max_turns", 10)
597
+ approval_threshold = arguments.get("approval_threshold", 0.9)
598
+
599
+ state = AutocodingState.create(
600
+ requirements=requirements,
601
+ max_turns=max_turns,
602
+ approval_threshold=approval_threshold,
603
+ )
604
+
605
+ await _send_progress("Session initialized", 2.0, 2.0)
606
+
607
+ structured = state.to_dict()
608
+ response = f"""# AUTOCODING SESSION INITIALIZED
609
+
610
+ **Session ID:** {state.session_id[:8]}...
611
+ **Max Turns:** {max_turns}
612
+ **Approval Threshold:** {approval_threshold:.0%}
613
+
614
+ The session is ready. Next step: call `player_prompt` with the returned state.
615
+
616
+ **Workflow:**
617
+ 1. Call `player_prompt` with state -> Execute returned prompt
618
+ 2. Call `coach_prompt` with state -> Execute returned prompt
619
+ 3. Call `autocoding_advance` with coach feedback
620
+ 4. Repeat until COACH APPROVED or timeout"""
621
+
622
+ return ([TextContent(type="text", text=response)], structured)
623
+
624
+ elif name == "player_prompt":
625
+ await _send_progress("Generating player prompt...", 1.0, 1.0)
626
+
627
+ state_dict = arguments["state"]
628
+ state = AutocodingState.from_dict(state_dict)
629
+
630
+ if state.phase != "player":
631
+ return CallToolResult(
632
+ content=[
633
+ TextContent(
634
+ type="text", text=f"Error: Expected player phase, got {state.phase}"
635
+ )
636
+ ],
637
+ structuredContent={"error": f"Invalid phase: {state.phase}", "expected": "player"},
638
+ isError=True,
639
+ )
640
+
641
+ autocoding = PromptDrivenAutocoding()
642
+ prompt_obj = autocoding.generate_player_prompt(
643
+ requirements=state.requirements,
644
+ coach_feedback=state.last_coach_feedback,
645
+ turn_number=state.current_turn + 1,
646
+ max_turns=state.max_turns,
647
+ )
648
+
649
+ # Advance state to coach phase for next call
650
+ new_state = state.advance_to_coach()
651
+
652
+ structured = {
653
+ **prompt_obj.to_dict(),
654
+ "state": new_state.to_dict(),
655
+ }
656
+
657
+ response = f"""# PLAYER PROMPT (Turn {state.current_turn + 1}/{state.max_turns})
658
+
659
+ {prompt_obj.prompt}
660
+
661
+ ---
662
+ **Instructions:** {prompt_obj.instructions}
663
+ **Next Step:** After executing this prompt, call `coach_prompt` with the updated state."""
664
+
665
+ return ([TextContent(type="text", text=response)], structured)
666
+
667
+ elif name == "coach_prompt":
668
+ await _send_progress("Generating coach prompt...", 1.0, 1.0)
669
+
670
+ state_dict = arguments["state"]
671
+ state = AutocodingState.from_dict(state_dict)
672
+
673
+ if state.phase != "coach":
674
+ return CallToolResult(
675
+ content=[
676
+ TextContent(type="text", text=f"Error: Expected coach phase, got {state.phase}")
677
+ ],
678
+ structuredContent={"error": f"Invalid phase: {state.phase}", "expected": "coach"},
679
+ isError=True,
680
+ )
681
+
682
+ autocoding = PromptDrivenAutocoding()
683
+ prompt_obj = autocoding.generate_coach_prompt(
684
+ requirements=state.requirements,
685
+ turn_number=state.current_turn + 1,
686
+ max_turns=state.max_turns,
687
+ )
688
+
689
+ structured = {
690
+ **prompt_obj.to_dict(),
691
+ "state": state.to_dict(),
692
+ }
693
+
694
+ response = f"""# COACH PROMPT (Turn {state.current_turn + 1}/{state.max_turns})
695
+
696
+ {prompt_obj.prompt}
697
+
698
+ ---
699
+ **Instructions:** {prompt_obj.instructions}
700
+ **Next Step:** After executing this prompt, call `autocoding_advance` with the coach's feedback."""
701
+
702
+ return ([TextContent(type="text", text=response)], structured)
703
+
704
+ elif name == "autocoding_advance":
705
+ await _send_progress("Advancing autocoding state...", 1.0, 2.0)
706
+
707
+ state_dict = arguments["state"]
708
+ coach_feedback = arguments["coach_feedback"]
709
+ approved = arguments["approved"]
710
+ compliance_score = arguments.get("compliance_score")
711
+
712
+ state = AutocodingState.from_dict(state_dict)
713
+
714
+ if state.phase != "coach":
715
+ return CallToolResult(
716
+ content=[
717
+ TextContent(type="text", text=f"Error: Expected coach phase, got {state.phase}")
718
+ ],
719
+ structuredContent={"error": f"Invalid phase: {state.phase}", "expected": "coach"},
720
+ isError=True,
721
+ )
722
+
723
+ new_state = state.advance_turn(
724
+ coach_feedback=coach_feedback,
725
+ approved=approved,
726
+ compliance_score=compliance_score,
727
+ )
728
+
729
+ await _send_progress("State advanced", 2.0, 2.0)
730
+
731
+ structured = new_state.to_dict()
732
+
733
+ if new_state.status == "approved":
734
+ response = f"""# AUTOCODING COMPLETE - APPROVED
735
+
736
+ **Session:** {new_state.session_id[:8]}...
737
+ **Turns Used:** {new_state.current_turn}/{new_state.max_turns}
738
+ **Final Status:** APPROVED
739
+
740
+ The implementation has been verified by the coach and meets all requirements."""
741
+
742
+ elif new_state.status == "timeout":
743
+ response = f"""# AUTOCODING COMPLETE - TIMEOUT
744
+
745
+ **Session:** {new_state.session_id[:8]}...
746
+ **Turns Used:** {new_state.current_turn}/{new_state.max_turns}
747
+ **Final Status:** TIMEOUT
748
+
749
+ Maximum turns reached without approval. Review the turn history for progress made."""
750
+
751
+ else:
752
+ response = f"""# AUTOCODING - CONTINUING
753
+
754
+ **Session:** {new_state.session_id[:8]}...
755
+ **Turn:** {new_state.current_turn + 1}/{new_state.max_turns}
756
+ **Status:** {new_state.status}
757
+
758
+ **Next Step:** Call `player_prompt` with the updated state to continue implementation."""
759
+
760
+ return ([TextContent(type="text", text=response)], structured)
761
+
762
+ elif name == "autocoding_single_shot":
763
+ await _send_progress("Generating single-shot autocoding prompt...", 1.0, 1.0)
764
+
765
+ requirements = arguments["requirements"]
766
+ max_turns = arguments.get("max_turns", 10)
767
+
768
+ autocoding = PromptDrivenAutocoding()
769
+ prompt_obj = autocoding.generate_single_shot_prompt(
770
+ requirements=requirements,
771
+ max_turns=max_turns,
772
+ )
773
+
774
+ structured = {
775
+ **prompt_obj.to_dict(),
776
+ "requirements": requirements,
777
+ "max_turns": max_turns,
778
+ }
779
+
780
+ response = f"""# SINGLE-SHOT AUTOCODING PROMPT
781
+
782
+ {prompt_obj.prompt}
783
+
784
+ ---
785
+ **Instructions:** {prompt_obj.instructions}
786
+ **Expected Format:** {prompt_obj.expected_format}"""
787
+
788
+ return ([TextContent(type="text", text=response)], structured)
789
+
790
+ elif name == "autocoding_save":
791
+ state_dict = arguments["state"]
792
+ filepath = arguments["filepath"]
793
+
794
+ state = AutocodingState.from_dict(state_dict)
795
+ save_session(state, filepath)
796
+
797
+ structured = {
798
+ "session_id": state.session_id,
799
+ "filepath": filepath,
800
+ "saved": True,
801
+ }
802
+ response = f"""# SESSION SAVED
803
+
804
+ **Session ID:** {state.session_id[:8]}...
805
+ **Saved to:** {filepath}
806
+ **Phase:** {state.phase}
807
+ **Turn:** {state.current_turn + 1}/{state.max_turns}
808
+
809
+ Session saved successfully. Use `autocoding_load` with the filepath to restore."""
810
+
811
+ return ([TextContent(type="text", text=response)], structured)
812
+
813
+ elif name == "autocoding_load":
814
+ filepath = arguments["filepath"]
815
+
816
+ try:
817
+ state = load_session(filepath)
818
+ except FileNotFoundError:
819
+ return CallToolResult(
820
+ content=[
821
+ TextContent(type="text", text=f"Error: Session file not found: {filepath}")
822
+ ],
823
+ structuredContent={"error": f"File not found: {filepath}"},
824
+ isError=True,
825
+ )
826
+ except json.JSONDecodeError as e:
827
+ return CallToolResult(
828
+ content=[
829
+ TextContent(type="text", text=f"Error: Invalid JSON in session file: {e}")
830
+ ],
831
+ structuredContent={"error": f"Invalid JSON: {str(e)}"},
832
+ isError=True,
833
+ )
834
+
835
+ structured = state.to_dict()
836
+ response = f"""# SESSION LOADED
837
+
838
+ **Session ID:** {state.session_id[:8]}...
839
+ **Loaded from:** {filepath}
840
+ **Phase:** {state.phase}
841
+ **Status:** {state.status}
842
+ **Turn:** {state.current_turn + 1}/{state.max_turns}
843
+
844
+ Session restored. Continue with the appropriate tool based on phase:
845
+ - If phase is "player": call `player_prompt`
846
+ - If phase is "coach": call `coach_prompt`"""
847
+
848
+ return ([TextContent(type="text", text=response)], structured)
849
+
850
+ else:
851
+ return CallToolResult(
852
+ content=[TextContent(type="text", text=f"Unknown tool: {name}")],
853
+ structuredContent={"error": f"Unknown tool: {name}"},
854
+ isError=True,
855
+ )
856
+
857
+
858
+ async def run_server() -> None:
859
+ """Run the prompt-driven MCP server using the standard stdio transport."""
860
+
861
+ init_options = app.create_initialization_options()
862
+
863
+ async with stdio_server() as (read_stream, write_stream):
864
+ await app.run(
865
+ read_stream,
866
+ write_stream,
867
+ init_options,
868
+ # Stateless keeps older MCP runtimes happy if they call tools/list immediately
869
+ # after initialization or skip explicit initialization notifications.
870
+ stateless=True,
871
+ )
872
+
873
+
874
+ def main() -> None:
875
+ """Main entry point for the prompt-driven MCP server."""
876
+ parser = argparse.ArgumentParser(
877
+ description="Hegelion Prompt-Driven MCP Server - Works with any LLM",
878
+ add_help=True,
879
+ )
880
+ parser.add_argument(
881
+ "--self-test",
882
+ action="store_true",
883
+ help="Run an in-process tool check (list tools + single-shot prompt) and exit",
884
+ )
885
+ args = parser.parse_args()
886
+
887
+ if args.self_test:
888
+ anyio.run(_self_test)
889
+ else:
890
+ anyio.run(run_server)
891
+
892
+
893
+ async def _self_test() -> None:
894
+ """Self-test: list tools and call a sample tool so users see output."""
895
+
896
+ print("šŸ”Ž Hegelion MCP self-test (in-process)")
897
+ tools = await list_tools()
898
+ tool_names = [t.name for t in tools]
899
+ print("Tools:", ", ".join(tool_names))
900
+
901
+ contents, structured = await call_tool(
902
+ name="dialectical_single_shot",
903
+ arguments={
904
+ "query": "Self-test: Can AI be genuinely creative?",
905
+ "use_council": True,
906
+ "response_style": "json",
907
+ },
908
+ )
909
+
910
+ print("\nresponse_style: json")
911
+ print("Structured keys:", list(structured.keys()))
912
+ prompt = structured.get("prompt", "")
913
+ print("Prompt preview:\n", prompt[:400], "...", sep="")
914
+ print("\nāœ… Self-test complete")
915
+
916
+
917
+ if __name__ == "__main__":
918
+ main()