coherence-mcp-server 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.
@@ -0,0 +1,966 @@
1
+ """Coherence Network MCP server — Python implementation.
2
+
3
+ Exposes the Coherence Network API as 22 typed MCP tools.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ import os
11
+ from typing import Any
12
+
13
+ import httpx
14
+ from mcp.server import Server
15
+ from mcp.server.stdio import stdio_server
16
+ from mcp.types import TextContent, Tool
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ API_BASE = os.environ.get("COHERENCE_API_URL", "https://api.coherencycoin.com").rstrip("/")
21
+ API_KEY = os.environ.get("COHERENCE_API_KEY", "")
22
+
23
+ # ---------------------------------------------------------------------------
24
+ # HTTP helpers
25
+ # ---------------------------------------------------------------------------
26
+
27
+ def _headers() -> dict[str, str]:
28
+ h = {"Content-Type": "application/json"}
29
+ if API_KEY:
30
+ h["X-API-Key"] = API_KEY
31
+ return h
32
+
33
+
34
+ def api_get(path: str, params: dict[str, Any] | None = None) -> Any:
35
+ url = f"{API_BASE}{path}"
36
+ filtered = {k: v for k, v in (params or {}).items() if v is not None}
37
+ try:
38
+ r = httpx.get(url, params=filtered, headers=_headers(), timeout=15.0)
39
+ r.raise_for_status()
40
+ return r.json()
41
+ except httpx.HTTPStatusError as exc:
42
+ return {"error": f"{exc.response.status_code} {exc.response.reason_phrase}"}
43
+ except Exception as exc:
44
+ return {"error": str(exc)}
45
+
46
+
47
+ def api_post(path: str, body: dict[str, Any]) -> Any:
48
+ url = f"{API_BASE}{path}"
49
+ try:
50
+ r = httpx.post(url, json=body, headers=_headers(), timeout=15.0)
51
+ r.raise_for_status()
52
+ return r.json()
53
+ except httpx.HTTPStatusError as exc:
54
+ try:
55
+ detail = exc.response.json().get("detail", exc.response.reason_phrase)
56
+ except Exception:
57
+ detail = exc.response.reason_phrase
58
+ return {"error": detail}
59
+ except Exception as exc:
60
+ return {"error": str(exc)}
61
+
62
+
63
+ def api_patch(path: str, body: dict[str, Any]) -> Any:
64
+ url = f"{API_BASE}{path}"
65
+ try:
66
+ r = httpx.patch(url, json=body, headers=_headers(), timeout=15.0)
67
+ r.raise_for_status()
68
+ return r.json()
69
+ except httpx.HTTPStatusError as exc:
70
+ try:
71
+ detail = exc.response.json().get("detail", exc.response.reason_phrase)
72
+ except Exception:
73
+ detail = exc.response.reason_phrase
74
+ return {"error": detail}
75
+ except Exception as exc:
76
+ return {"error": str(exc)}
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # Tool definitions
81
+ # ---------------------------------------------------------------------------
82
+
83
+ TOOLS: list[Tool] = [
84
+ # Ideas
85
+ Tool(
86
+ name="coherence_list_ideas",
87
+ description="Browse the idea portfolio ranked by ROI and free-energy score. Returns ideas with scores, manifestation status, and selection weights.",
88
+ inputSchema={
89
+ "type": "object",
90
+ "properties": {
91
+ "limit": {"type": "number", "description": "Max ideas to return (default 20)", "default": 20},
92
+ "search": {"type": "string", "description": "Search keyword to filter ideas"},
93
+ },
94
+ },
95
+ ),
96
+ Tool(
97
+ name="coherence_get_idea",
98
+ description="Get full details for a single idea including scores, open questions, value gap, and linked tasks.",
99
+ inputSchema={
100
+ "type": "object",
101
+ "properties": {
102
+ "idea_id": {"type": "string", "description": "The idea ID"},
103
+ },
104
+ "required": ["idea_id"],
105
+ },
106
+ ),
107
+ Tool(
108
+ name="coherence_idea_progress",
109
+ description="Get progress for an idea: stage, tasks by phase, CC staked/spent, contributors.",
110
+ inputSchema={
111
+ "type": "object",
112
+ "properties": {"idea_id": {"type": "string"}},
113
+ "required": ["idea_id"],
114
+ },
115
+ ),
116
+ Tool(
117
+ name="coherence_select_idea",
118
+ description="Let the portfolio engine select the next highest-ROI idea to work on. Temperature controls exploration vs exploitation.",
119
+ inputSchema={
120
+ "type": "object",
121
+ "properties": {
122
+ "temperature": {"type": "number", "description": "0=deterministic, >1=explore (default 0.5)", "default": 0.5},
123
+ },
124
+ },
125
+ ),
126
+ Tool(
127
+ name="coherence_showcase",
128
+ description="List validated, shipped ideas that have proven their value.",
129
+ inputSchema={"type": "object", "properties": {}},
130
+ ),
131
+ Tool(
132
+ name="coherence_resonance",
133
+ description="Show which ideas are generating the most energy and activity right now.",
134
+ inputSchema={"type": "object", "properties": {}},
135
+ ),
136
+ # Specs
137
+ Tool(
138
+ name="coherence_list_specs",
139
+ description="List feature specs with ROI metrics, value gaps, and implementation summaries.",
140
+ inputSchema={
141
+ "type": "object",
142
+ "properties": {
143
+ "limit": {"type": "number", "default": 20},
144
+ "search": {"type": "string", "description": "Search keyword"},
145
+ },
146
+ },
147
+ ),
148
+ Tool(
149
+ name="coherence_get_spec",
150
+ description="Get full spec detail including implementation summary, pseudocode, and ROI.",
151
+ inputSchema={
152
+ "type": "object",
153
+ "properties": {"spec_id": {"type": "string"}},
154
+ "required": ["spec_id"],
155
+ },
156
+ ),
157
+ # Lineage
158
+ Tool(
159
+ name="coherence_list_lineage",
160
+ description="List value lineage chains connecting ideas to specs, implementations, and payouts.",
161
+ inputSchema={
162
+ "type": "object",
163
+ "properties": {"limit": {"type": "number", "default": 20}},
164
+ },
165
+ ),
166
+ Tool(
167
+ name="coherence_lineage_valuation",
168
+ description="Get ROI valuation for a lineage chain — measured value, estimated cost, and ROI ratio.",
169
+ inputSchema={
170
+ "type": "object",
171
+ "properties": {"lineage_id": {"type": "string"}},
172
+ "required": ["lineage_id"],
173
+ },
174
+ ),
175
+ # Identity
176
+ Tool(
177
+ name="coherence_list_providers",
178
+ description="List all 37 supported identity providers grouped by category (Social, Dev, Crypto/Web3, Professional, Identity, Custom).",
179
+ inputSchema={"type": "object", "properties": {}},
180
+ ),
181
+ Tool(
182
+ name="coherence_link_identity",
183
+ description="Link a provider identity (GitHub, Discord, Ethereum, etc.) to a contributor. No registration required.",
184
+ inputSchema={
185
+ "type": "object",
186
+ "properties": {
187
+ "contributor_id": {"type": "string", "description": "Contributor name"},
188
+ "provider": {"type": "string", "description": "Provider key (github, discord, ethereum, solana, ...)"},
189
+ "provider_id": {"type": "string", "description": "Handle, address, or username on that provider"},
190
+ },
191
+ "required": ["contributor_id", "provider", "provider_id"],
192
+ },
193
+ ),
194
+ Tool(
195
+ name="coherence_lookup_identity",
196
+ description="Find which contributor owns a specific provider identity. Reverse lookup.",
197
+ inputSchema={
198
+ "type": "object",
199
+ "properties": {
200
+ "provider": {"type": "string"},
201
+ "provider_id": {"type": "string"},
202
+ },
203
+ "required": ["provider", "provider_id"],
204
+ },
205
+ ),
206
+ Tool(
207
+ name="coherence_get_identities",
208
+ description="Get all linked identities for a contributor.",
209
+ inputSchema={
210
+ "type": "object",
211
+ "properties": {"contributor_id": {"type": "string"}},
212
+ "required": ["contributor_id"],
213
+ },
214
+ ),
215
+ # Contributions
216
+ Tool(
217
+ name="coherence_record_contribution",
218
+ description="Record a contribution. Identify by contributor_id OR by provider+provider_id (no registration needed).",
219
+ inputSchema={
220
+ "type": "object",
221
+ "properties": {
222
+ "contributor_id": {"type": "string", "description": "Contributor name (optional if provider+provider_id given)"},
223
+ "provider": {"type": "string", "description": "Identity provider (optional)"},
224
+ "provider_id": {"type": "string", "description": "Identity handle (optional)"},
225
+ "type": {"type": "string", "description": "Contribution type: code, docs, review, design, community, other"},
226
+ "amount_cc": {"type": "number", "description": "CC value (default 1)", "default": 1},
227
+ "idea_id": {"type": "string", "description": "Related idea ID (optional)"},
228
+ },
229
+ "required": ["type"],
230
+ },
231
+ ),
232
+ Tool(
233
+ name="coherence_contributor_ledger",
234
+ description="Get a contributor's CC balance and contribution history.",
235
+ inputSchema={
236
+ "type": "object",
237
+ "properties": {"contributor_id": {"type": "string"}},
238
+ "required": ["contributor_id"],
239
+ },
240
+ ),
241
+ # Status
242
+ Tool(
243
+ name="coherence_status",
244
+ description="Get network health: API status, uptime, federation nodes, idea count.",
245
+ inputSchema={"type": "object", "properties": {}},
246
+ ),
247
+ Tool(
248
+ name="coherence_friction_report",
249
+ description="Get friction report — where the pipeline struggles.",
250
+ inputSchema={
251
+ "type": "object",
252
+ "properties": {"window_days": {"type": "number", "default": 30}},
253
+ },
254
+ ),
255
+ # Governance
256
+ Tool(
257
+ name="coherence_list_change_requests",
258
+ description="List governance change requests.",
259
+ inputSchema={"type": "object", "properties": {}},
260
+ ),
261
+ # Federation
262
+ Tool(
263
+ name="coherence_list_federation_nodes",
264
+ description="List federated nodes and their capabilities.",
265
+ inputSchema={"type": "object", "properties": {}},
266
+ ),
267
+ # Tasks (Agent Work Protocol)
268
+ Tool(
269
+ name="coherence_list_tasks",
270
+ description="List tasks in the agent work pipeline with optional filters (pending, running, completed, failed, needs_decision).",
271
+ inputSchema={
272
+ "type": "object",
273
+ "properties": {
274
+ "status": {"type": "string", "description": "Filter by status: pending, running, completed, failed, needs_decision, timed_out"},
275
+ "task_type": {"type": "string", "description": "Filter by type: spec, test, impl, review, code-review"},
276
+ "limit": {"type": "number", "description": "Max tasks to return (default 20)", "default": 20},
277
+ "offset": {"type": "number", "description": "Pagination offset", "default": 0},
278
+ },
279
+ },
280
+ ),
281
+ Tool(
282
+ name="coherence_get_task",
283
+ description="Get full detail for a single task including direction, context, worker_id, and result/output.",
284
+ inputSchema={
285
+ "type": "object",
286
+ "required": ["task_id"],
287
+ "properties": {
288
+ "task_id": {"type": "string", "description": "The task ID"},
289
+ },
290
+ },
291
+ ),
292
+ Tool(
293
+ name="coherence_task_next",
294
+ description="Claim the highest-priority pending task from the queue. This is the entry point for agents to start working.",
295
+ inputSchema={
296
+ "type": "object",
297
+ "properties": {
298
+ "worker_id": {"type": "string", "description": "Identity of the agent/node claiming the task (defaults to 'mcp-agent')"},
299
+ },
300
+ },
301
+ ),
302
+ Tool(
303
+ name="coherence_task_claim",
304
+ description="Claim a specific task by ID.",
305
+ inputSchema={
306
+ "type": "object",
307
+ "required": ["task_id"],
308
+ "properties": {
309
+ "task_id": {"type": "string", "description": "The task ID"},
310
+ "worker_id": {"type": "string", "description": "Identity of the agent/node (defaults to 'mcp-agent')"},
311
+ },
312
+ },
313
+ ),
314
+ Tool(
315
+ name="coherence_task_report",
316
+ description="Report the result of a claimed task (completed or failed).",
317
+ inputSchema={
318
+ "type": "object",
319
+ "required": ["task_id", "status"],
320
+ "properties": {
321
+ "task_id": {"type": "string", "description": "The task ID"},
322
+ "status": {"type": "string", "description": "Result status: completed or failed"},
323
+ "output": {"type": "string", "description": "The final work product (code, spec text, review notes, etc.)"},
324
+ },
325
+ },
326
+ ),
327
+ Tool(
328
+ name="coherence_task_seed",
329
+ description="Create a new task from an existing idea (e.g. seed a 'spec' task for idea X).",
330
+ inputSchema={
331
+ "type": "object",
332
+ "required": ["idea_id"],
333
+ "properties": {
334
+ "idea_id": {"type": "string", "description": "Target idea ID"},
335
+ "task_type": {"type": "string", "description": "Type of task: spec, test, impl, review (default: spec)", "default": "spec"},
336
+ "direction": {"type": "string", "description": "Optional custom instruction for the task"},
337
+ },
338
+ },
339
+ ),
340
+ Tool(
341
+ name="coherence_task_events",
342
+ description="Get the activity log/event stream for a specific task.",
343
+ inputSchema={
344
+ "type": "object",
345
+ "required": ["task_id"],
346
+ "properties": {
347
+ "task_id": {"type": "string", "description": "The task ID"},
348
+ },
349
+ },
350
+ ),
351
+ # Ideas (Lifecycle)
352
+ Tool(
353
+ name="coherence_create_idea",
354
+ description="Create a new idea in the portfolio.",
355
+ inputSchema={
356
+ "type": "object",
357
+ "required": ["id", "name", "description"],
358
+ "properties": {
359
+ "id": {"type": "string", "description": "Unique idea slug (e.g. 'my-new-feature')"},
360
+ "name": {"type": "string", "description": "Short, descriptive name"},
361
+ "description": {"type": "string", "description": "Detailed vision and value proposition"},
362
+ "potential_value": {"type": "number", "description": "Estimated CC value (0-1000)", "default": 100},
363
+ "estimated_cost": {"type": "number", "description": "Estimated CC cost (0-1000)", "default": 50},
364
+ "parent_idea_id": {"type": "string", "description": "Optional parent idea for hierarchy"},
365
+ "tags": {"type": "array", "items": {"type": "string"}, "description": "List of tags"},
366
+ },
367
+ },
368
+ ),
369
+ Tool(
370
+ name="coherence_update_idea",
371
+ description="Update an existing idea's properties (stage, status, metadata).",
372
+ inputSchema={
373
+ "type": "object",
374
+ "required": ["idea_id"],
375
+ "properties": {
376
+ "idea_id": {"type": "string", "description": "The idea ID"},
377
+ "name": {"type": "string"},
378
+ "description": {"type": "string"},
379
+ "stage": {"type": "string", "description": "New stage: draft, proposed, active, completed, archived"},
380
+ "manifestation_status": {"type": "string", "description": "none, spec, implemented, validated"},
381
+ "potential_value": {"type": "number"},
382
+ "estimated_cost": {"type": "number"},
383
+ },
384
+ },
385
+ ),
386
+ # Universal Graph (Navigation & Edges)
387
+ Tool(
388
+ name="coherence_list_edges",
389
+ description="List relationship edges in the universal graph with optional filters.",
390
+ inputSchema={
391
+ "type": "object",
392
+ "properties": {
393
+ "type": {"type": "string", "description": "Edge type: blocks, enables, depends-on, related-to, etc."},
394
+ "from_id": {"type": "string", "description": "Source node ID"},
395
+ "to_id": {"type": "string", "description": "Target node ID"},
396
+ "limit": {"type": "number", "default": 50},
397
+ },
398
+ },
399
+ ),
400
+ Tool(
401
+ name="coherence_get_entity_edges",
402
+ description="Get all incoming and outgoing edges for any entity (Idea, Spec, Contributor, Asset).",
403
+ inputSchema={
404
+ "type": "object",
405
+ "required": ["entity_id"],
406
+ "properties": {
407
+ "entity_id": {"type": "string", "description": "The entity ID"},
408
+ "type": {"type": "string", "description": "Filter by edge type"},
409
+ "direction": {"type": "string", "description": "both, outgoing, incoming", "default": "both"},
410
+ },
411
+ },
412
+ ),
413
+ Tool(
414
+ name="coherence_create_edge",
415
+ description="Create a typed relationship edge between two entities in the graph.",
416
+ inputSchema={
417
+ "type": "object",
418
+ "required": ["from_id", "to_id", "type"],
419
+ "properties": {
420
+ "from_id": {"type": "string", "description": "Source entity ID"},
421
+ "to_id": {"type": "string", "description": "Target entity ID"},
422
+ "type": {"type": "string", "description": "Edge type: blocks, enables, depends-on, contains, transforms, opposes, etc."},
423
+ "strength": {"type": "number", "description": "Edge strength (0.0-1.0)", "default": 1.0},
424
+ "created_by": {"type": "string", "description": "Author/agent creating this edge", "default": "mcp-agent"},
425
+ },
426
+ },
427
+ ),
428
+ # Assets
429
+ Tool(
430
+ name="coherence_list_assets",
431
+ description="List tracked assets (code, docs, endpoints) with pagination.",
432
+ inputSchema={
433
+ "type": "object",
434
+ "properties": {
435
+ "limit": {"type": "number", "default": 100},
436
+ "offset": {"type": "number", "default": 0},
437
+ },
438
+ },
439
+ ),
440
+ Tool(
441
+ name="coherence_get_asset",
442
+ description="Get detail for a specific asset by UUID.",
443
+ inputSchema={
444
+ "type": "object",
445
+ "required": ["asset_id"],
446
+ "properties": {
447
+ "asset_id": {"type": "string", "description": "The asset UUID"},
448
+ },
449
+ },
450
+ ),
451
+ Tool(
452
+ name="coherence_create_asset",
453
+ description="Register a new tracked asset.",
454
+ inputSchema={
455
+ "type": "object",
456
+ "required": ["type", "description"],
457
+ "properties": {
458
+ "type": {"type": "string", "description": "CODE, DOCS, ENDPOINT, etc."},
459
+ "description": {"type": "string", "description": "Asset description"},
460
+ "total_cost": {"type": "number", "description": "Initial CC cost", "default": 0},
461
+ },
462
+ },
463
+ ),
464
+ # News
465
+ Tool(
466
+ name="coherence_get_news_feed",
467
+ description="Latest news items from configured RSS sources with optional POV ranking.",
468
+ inputSchema={
469
+ "type": "object",
470
+ "properties": {
471
+ "limit": {"type": "number", "default": 50},
472
+ "source": {"type": "string", "description": "Filter by source name"},
473
+ "pov": {"type": "string", "description": "Point-of-view lens ID to rank items by affinity"},
474
+ "refresh": {"type": "boolean", "default": False},
475
+ },
476
+ },
477
+ ),
478
+ Tool(
479
+ name="coherence_get_news_resonance",
480
+ description="News items matched to ideas with resonance scores and explanations.",
481
+ inputSchema={
482
+ "type": "object",
483
+ "properties": {
484
+ "limit": {"type": "number", "default": 100},
485
+ "top_n": {"type": "number", "default": 5},
486
+ "refresh": {"type": "boolean", "default": False},
487
+ },
488
+ },
489
+ ),
490
+ Tool(
491
+ name="coherence_list_news_sources",
492
+ description="List all configured news sources.",
493
+ inputSchema={
494
+ "type": "object",
495
+ "properties": {
496
+ "active_only": {"type": "boolean", "default": False},
497
+ },
498
+ },
499
+ ),
500
+ Tool(
501
+ name="coherence_add_news_source",
502
+ description="Add a new news source (RSS).",
503
+ inputSchema={
504
+ "type": "object",
505
+ "required": ["id", "url"],
506
+ "properties": {
507
+ "id": {"type": "string", "description": "Unique source ID"},
508
+ "name": {"type": "string"},
509
+ "url": {"type": "string", "description": "RSS feed URL"},
510
+ },
511
+ },
512
+ ),
513
+ Tool(
514
+ name="coherence_get_trending_news",
515
+ description="Trending keywords extracted from recent news items.",
516
+ inputSchema={
517
+ "type": "object",
518
+ "properties": {
519
+ "top_n": {"type": "number", "default": 20},
520
+ "refresh": {"type": "boolean", "default": False},
521
+ },
522
+ },
523
+ ),
524
+ # Treasury
525
+ Tool(
526
+ name="coherence_get_treasury_info",
527
+ description="Treasury wallet addresses, conversion rates, and total CC balance.",
528
+ inputSchema={"type": "object", "properties": {}},
529
+ ),
530
+ Tool(
531
+ name="coherence_record_deposit",
532
+ description="Record a crypto deposit and convert to CC for a contributor.",
533
+ inputSchema={
534
+ "type": "object",
535
+ "required": ["contributor_id", "asset", "amount", "tx_hash"],
536
+ "properties": {
537
+ "contributor_id": {"type": "string"},
538
+ "asset": {"type": "string", "description": "eth or btc"},
539
+ "amount": {"type": "number"},
540
+ "tx_hash": {"type": "string"},
541
+ "wallet_address": {"type": "string"},
542
+ },
543
+ },
544
+ ),
545
+ Tool(
546
+ name="coherence_get_deposit_history",
547
+ description="Get deposit history for a contributor.",
548
+ inputSchema={
549
+ "type": "object",
550
+ "required": ["contributor_id"],
551
+ "properties": {
552
+ "contributor_id": {"type": "string"},
553
+ },
554
+ },
555
+ ),
556
+ # Governance
557
+ Tool(
558
+ name="coherence_list_change_requests",
559
+ description="List open governance change proposals.",
560
+ inputSchema={
561
+ "type": "object",
562
+ "properties": {
563
+ "limit": {"type": "number", "default": 200},
564
+ },
565
+ },
566
+ ),
567
+ Tool(
568
+ name="coherence_get_change_request",
569
+ description="Get detail for a specific change proposal.",
570
+ inputSchema={
571
+ "type": "object",
572
+ "required": ["change_request_id"],
573
+ "properties": {
574
+ "change_request_id": {"type": "string"},
575
+ },
576
+ },
577
+ ),
578
+ Tool(
579
+ name="coherence_vote_governance",
580
+ description="Cast a vote on a governance change proposal.",
581
+ inputSchema={
582
+ "type": "object",
583
+ "required": ["change_request_id", "voter_id", "vote"],
584
+ "properties": {
585
+ "change_request_id": {"type": "string"},
586
+ "voter_id": {"type": "string"},
587
+ "vote": {"type": "string", "description": "yes or no"},
588
+ "rationale": {"type": "string"},
589
+ },
590
+ },
591
+ ),
592
+ Tool(
593
+ name="coherence_propose_governance",
594
+ description="Create a new governance change proposal.",
595
+ inputSchema={
596
+ "type": "object",
597
+ "required": ["title", "description", "proposer_id"],
598
+ "properties": {
599
+ "title": {"type": "string"},
600
+ "description": {"type": "string"},
601
+ "proposer_id": {"type": "string"},
602
+ "idea_id": {"type": "string"},
603
+ },
604
+ },
605
+ ),
606
+ # DIF (Decentralized Identity Foundation)
607
+ Tool(
608
+ name="coherence_get_dif_stats",
609
+ description="DIF accuracy statistics — true/false positive rates, accuracy by language.",
610
+ inputSchema={"type": "object", "properties": {}},
611
+ ),
612
+ Tool(
613
+ name="coherence_get_recent_dif",
614
+ description="Recent DIF verification entries with scores and outcomes.",
615
+ inputSchema={
616
+ "type": "object",
617
+ "properties": {
618
+ "limit": {"type": "number", "default": 20},
619
+ },
620
+ },
621
+ ),
622
+ # Concepts (Living Codex ontology)
623
+ Tool(
624
+ name="coherence_list_concepts",
625
+ description="Browse the Living Codex ontology — 184 universal concepts with typed relationships and 53 axes.",
626
+ inputSchema={
627
+ "type": "object",
628
+ "properties": {
629
+ "limit": {"type": "number", "description": "Max concepts to return (default 50, max 500)", "default": 50},
630
+ "offset": {"type": "number", "description": "Pagination offset", "default": 0},
631
+ "search": {"type": "string", "description": "Search query to filter concepts"},
632
+ },
633
+ },
634
+ ),
635
+ Tool(
636
+ name="coherence_get_concept",
637
+ description="Get full details for a single concept from the Living Codex ontology.",
638
+ inputSchema={
639
+ "type": "object",
640
+ "required": ["concept_id"],
641
+ "properties": {
642
+ "concept_id": {"type": "string", "description": "Concept ID (e.g. 'activity', 'knowledge', 'resonance')"},
643
+ "include_edges": {"type": "boolean", "description": "Include typed relationship edges", "default": False},
644
+ },
645
+ },
646
+ ),
647
+ Tool(
648
+ name="coherence_link_concepts",
649
+ description="Create a typed relationship edge between two concepts in the Living Codex ontology.",
650
+ inputSchema={
651
+ "type": "object",
652
+ "required": ["from_id", "relationship_type", "to_id"],
653
+ "properties": {
654
+ "from_id": {"type": "string", "description": "Source concept ID"},
655
+ "relationship_type": {"type": "string", "description": "Relationship type (transforms, contains, enables, opposes, ...)"},
656
+ "to_id": {"type": "string", "description": "Target concept ID"},
657
+ "created_by": {"type": "string", "description": "Author/agent creating this edge", "default": "mcp"},
658
+ },
659
+ },
660
+ ),
661
+ ]
662
+
663
+ TOOL_MAP: dict[str, Tool] = {t.name: t for t in TOOLS}
664
+
665
+ # ---------------------------------------------------------------------------
666
+ # Tool dispatch
667
+ # ---------------------------------------------------------------------------
668
+
669
+ def dispatch(name: str, args: dict[str, Any]) -> Any:
670
+ match name:
671
+ # Ideas
672
+ case "coherence_list_ideas":
673
+ if args.get("search"):
674
+ return api_get("/api/ideas/cards", {"search": args["search"], "limit": args.get("limit", 20)})
675
+ return api_get("/api/ideas", {"limit": args.get("limit", 20)})
676
+ case "coherence_get_idea":
677
+ return api_get(f"/api/ideas/{args['idea_id']}")
678
+ case "coherence_idea_progress":
679
+ return api_get(f"/api/ideas/{args['idea_id']}/progress")
680
+ case "coherence_select_idea":
681
+ return api_post("/api/ideas/select", {"temperature": args.get("temperature", 0.5)})
682
+ case "coherence_showcase":
683
+ return api_get("/api/ideas/showcase")
684
+ case "coherence_resonance":
685
+ return api_get("/api/ideas/resonance")
686
+ # Specs
687
+ case "coherence_list_specs":
688
+ if args.get("search"):
689
+ return api_get("/api/spec-registry/cards", {"search": args["search"], "limit": args.get("limit", 20)})
690
+ return api_get("/api/spec-registry", {"limit": args.get("limit", 20)})
691
+ case "coherence_get_spec":
692
+ return api_get(f"/api/spec-registry/{args['spec_id']}")
693
+ # Lineage
694
+ case "coherence_list_lineage":
695
+ return api_get("/api/value-lineage/links", {"limit": args.get("limit", 20)})
696
+ case "coherence_lineage_valuation":
697
+ return api_get(f"/api/value-lineage/links/{args['lineage_id']}/valuation")
698
+ # Identity
699
+ case "coherence_list_providers":
700
+ return api_get("/api/identity/providers")
701
+ case "coherence_link_identity":
702
+ return api_post("/api/identity/link", {
703
+ "contributor_id": args["contributor_id"],
704
+ "provider": args["provider"],
705
+ "provider_id": args["provider_id"],
706
+ "display_name": args["provider_id"],
707
+ })
708
+ case "coherence_lookup_identity":
709
+ from urllib.parse import quote
710
+ return api_get(f"/api/identity/lookup/{quote(args['provider'])}/{quote(args['provider_id'])}")
711
+ case "coherence_get_identities":
712
+ from urllib.parse import quote
713
+ return api_get(f"/api/identity/{quote(args['contributor_id'])}")
714
+ # Contributions
715
+ case "coherence_record_contribution":
716
+ return api_post("/api/contributions/record", {
717
+ k: v for k, v in {
718
+ "contributor_id": args.get("contributor_id"),
719
+ "provider": args.get("provider"),
720
+ "provider_id": args.get("provider_id"),
721
+ "type": args["type"],
722
+ "amount_cc": args.get("amount_cc", 1),
723
+ "idea_id": args.get("idea_id"),
724
+ }.items() if v is not None
725
+ })
726
+ case "coherence_contributor_ledger":
727
+ from urllib.parse import quote
728
+ return api_get(f"/api/contributions/ledger/{quote(args['contributor_id'])}")
729
+ # Status
730
+ case "coherence_status":
731
+ health = api_get("/api/health")
732
+ count = api_get("/api/ideas/count")
733
+ nodes = api_get("/api/federation/nodes")
734
+ return {
735
+ "health": health,
736
+ "ideas": count,
737
+ "federation_nodes": len(nodes) if isinstance(nodes, list) else 0,
738
+ }
739
+ case "coherence_friction_report":
740
+ return api_get("/api/friction/report", {"window_days": args.get("window_days", 30)})
741
+ # Governance
742
+ case "coherence_list_change_requests":
743
+ return api_get("/api/governance/change-requests")
744
+ # Federation
745
+ case "coherence_list_federation_nodes":
746
+ nodes = api_get("/api/federation/nodes")
747
+ caps = api_get("/api/federation/nodes/capabilities")
748
+ return {"nodes": nodes, "capabilities": caps}
749
+ # Tasks
750
+ case "coherence_list_tasks":
751
+ return api_get("/api/agent/tasks", {
752
+ "status": args.get("status"),
753
+ "task_type": args.get("task_type"),
754
+ "limit": args.get("limit", 20),
755
+ "offset": args.get("offset", 0),
756
+ })
757
+ case "coherence_get_task":
758
+ return api_get(f"/api/agent/tasks/{args['task_id']}")
759
+ case "coherence_task_next":
760
+ # Claim next available pending task
761
+ data = api_get("/api/agent/tasks", {"status": "pending", "limit": 1})
762
+ tasks = data.get("tasks", []) if isinstance(data, dict) else []
763
+ if not tasks:
764
+ return {"error": "No pending tasks available"}
765
+ task_id = tasks[0]["id"]
766
+ return api_patch(f"/api/agent/tasks/{task_id}", {
767
+ "status": "running",
768
+ "worker_id": args.get("worker_id", "mcp-agent"),
769
+ })
770
+ case "coherence_task_claim":
771
+ return api_patch(f"/api/agent/tasks/{args['task_id']}", {
772
+ "status": "running",
773
+ "worker_id": args.get("worker_id", "mcp-agent"),
774
+ })
775
+ case "coherence_task_report":
776
+ return api_patch(f"/api/agent/tasks/{args['task_id']}", {
777
+ "status": args["status"],
778
+ "output": args.get("output", f"Task {args['status']} via mcp-agent"),
779
+ })
780
+ case "coherence_task_seed":
781
+ idea_id = args["idea_id"]
782
+ # Fetch idea for name if not provided in direction
783
+ idea = api_get(f"/api/ideas/{idea_id}")
784
+ idea_name = idea.get("name", "Unknown Idea") if isinstance(idea, dict) else "Unknown Idea"
785
+ task_type = args.get("task_type", "spec")
786
+ direction = args.get("direction") or f"{task_type} for '{idea_name}' ({idea_id})"
787
+ return api_post("/api/agent/tasks", {
788
+ "task_type": task_type,
789
+ "direction": direction,
790
+ "context": {
791
+ "idea_id": idea_id,
792
+ "idea_name": idea_name,
793
+ "seeded_by": "mcp-agent",
794
+ },
795
+ })
796
+ case "coherence_task_events":
797
+ return api_get(f"/api/agent/tasks/{args['task_id']}/stream")
798
+ # Ideas
799
+ case "coherence_create_idea":
800
+ return api_post("/api/ideas", {
801
+ "id": args["id"],
802
+ "name": args["name"],
803
+ "description": args["description"],
804
+ "potential_value": args.get("potential_value", 100),
805
+ "estimated_cost": args.get("estimated_cost", 50),
806
+ "parent_idea_id": args.get("parent_idea_id"),
807
+ "tags": args.get("tags"),
808
+ })
809
+ case "coherence_update_idea":
810
+ return api_patch(f"/api/ideas/{args['idea_id']}", {
811
+ "name": args.get("name"),
812
+ "description": args.get("description"),
813
+ "stage": args.get("stage"),
814
+ "manifestation_status": args.get("manifestation_status"),
815
+ "potential_value": args.get("potential_value"),
816
+ "estimated_cost": args.get("estimated_cost"),
817
+ })
818
+ # Graph / Edges
819
+ case "coherence_list_edges":
820
+ return api_get("/api/edges", {
821
+ "type": args.get("type"),
822
+ "from_id": args.get("from_id"),
823
+ "to_id": args.get("to_id"),
824
+ "limit": args.get("limit", 50),
825
+ })
826
+ case "coherence_get_entity_edges":
827
+ return api_get(f"/api/entities/{args['entity_id']}/edges", {
828
+ "type": args.get("type"),
829
+ "direction": args.get("direction", "both"),
830
+ })
831
+ case "coherence_create_edge":
832
+ return api_post("/api/edges", {
833
+ "from_id": args["from_id"],
834
+ "to_id": args["to_id"],
835
+ "type": args["type"],
836
+ "strength": args.get("strength", 1.0),
837
+ "created_by": args.get("created_by", "mcp-agent"),
838
+ })
839
+ # Assets
840
+ case "coherence_list_assets":
841
+ return api_get("/api/assets", {"limit": args.get("limit", 100), "offset": args.get("offset", 0)})
842
+ case "coherence_get_asset":
843
+ return api_get(f"/api/assets/{args['asset_id']}")
844
+ case "coherence_create_asset":
845
+ return api_post("/api/assets", {
846
+ "type": args["type"],
847
+ "description": args["description"],
848
+ "total_cost": args.get("total_cost", 0),
849
+ })
850
+ # News
851
+ case "coherence_get_news_feed":
852
+ return api_get("/api/news/feed", {
853
+ "limit": args.get("limit", 50),
854
+ "source": args.get("source"),
855
+ "pov": args.get("pov"),
856
+ "refresh": args.get("refresh", False),
857
+ })
858
+ case "coherence_get_news_resonance":
859
+ return api_get("/api/news/resonance", {
860
+ "limit": args.get("limit", 100),
861
+ "top_n": args.get("top_n", 5),
862
+ "refresh": args.get("refresh", False),
863
+ })
864
+ case "coherence_list_news_sources":
865
+ return api_get("/api/news/sources", {"active_only": args.get("active_only", False)})
866
+ case "coherence_add_news_source":
867
+ return api_post("/api/news/sources", {
868
+ "id": args["id"],
869
+ "url": args["url"],
870
+ "name": args.get("name"),
871
+ })
872
+ case "coherence_get_trending_news":
873
+ return api_get("/api/news/trending", {
874
+ "top_n": args.get("top_n", 20),
875
+ "refresh": args.get("refresh", False),
876
+ })
877
+ # Treasury
878
+ case "coherence_get_treasury_info":
879
+ return api_get("/api/treasury")
880
+ case "coherence_record_deposit":
881
+ return api_post("/api/treasury/deposit", {
882
+ "contributor_id": args["contributor_id"],
883
+ "asset": args["asset"],
884
+ "amount": args["amount"],
885
+ "tx_hash": args["tx_hash"],
886
+ "wallet_address": args.get("wallet_address"),
887
+ })
888
+ case "coherence_get_deposit_history":
889
+ return api_get(f"/api/treasury/deposits/{args['contributor_id']}")
890
+ # Governance
891
+ case "coherence_list_change_requests":
892
+ return api_get("/api/governance/change-requests", {"limit": args.get("limit", 200)})
893
+ case "coherence_get_change_request":
894
+ return api_get(f"/api/governance/change-requests/{args['change_request_id']}")
895
+ case "coherence_vote_governance":
896
+ return api_post(f"/api/governance/change-requests/{args['change_request_id']}/votes", {
897
+ "voter_id": args["voter_id"],
898
+ "vote": args["vote"],
899
+ "rationale": args.get("rationale"),
900
+ })
901
+ case "coherence_propose_governance":
902
+ return api_post("/api/governance/change-requests", {
903
+ "title": args["title"],
904
+ "description": args["description"],
905
+ "proposer_id": args["proposer_id"],
906
+ "idea_id": args.get("idea_id"),
907
+ })
908
+ # DIF
909
+ case "coherence_get_dif_stats":
910
+ return api_get("/api/dif/stats")
911
+ case "coherence_get_recent_dif":
912
+ return api_get("/api/dif/recent", {"limit": args.get("limit", 20)})
913
+ # Concepts
914
+ case "coherence_list_concepts":
915
+ if args.get("search"):
916
+ return api_get("/api/concepts/search", {"q": args["search"], "limit": args.get("limit", 20)})
917
+ return api_get("/api/concepts", {"limit": args.get("limit", 50), "offset": args.get("offset", 0)})
918
+ case "coherence_get_concept":
919
+ concept = api_get(f"/api/concepts/{args['concept_id']}")
920
+ if args.get("include_edges"):
921
+ edges = api_get(f"/api/concepts/{args['concept_id']}/edges")
922
+ if isinstance(concept, dict):
923
+ concept["edges"] = edges
924
+ return concept
925
+ case "coherence_link_concepts":
926
+ return api_post(f"/api/concepts/{args['from_id']}/edges", {
927
+ "from_id": args["from_id"],
928
+ "to_id": args["to_id"],
929
+ "relationship_type": args["relationship_type"],
930
+ "created_by": args.get("created_by", "mcp"),
931
+ })
932
+ case _:
933
+ return {"error": f"Unknown tool: {name}"}
934
+
935
+
936
+ # ---------------------------------------------------------------------------
937
+ # MCP server
938
+ # ---------------------------------------------------------------------------
939
+
940
+ server = Server("coherence-network")
941
+
942
+
943
+ @server.list_tools()
944
+ async def handle_list_tools() -> list[Tool]:
945
+ return TOOLS
946
+
947
+
948
+ @server.call_tool()
949
+ async def handle_call_tool(name: str, arguments: dict | None) -> list[TextContent]:
950
+ args = arguments or {}
951
+ try:
952
+ result = dispatch(name, args)
953
+ text = json.dumps(result, default=str)
954
+ except Exception as exc:
955
+ logger.exception("Tool %s failed", name)
956
+ text = json.dumps({"error": str(exc)})
957
+ return [TextContent(type="text", text=text)]
958
+
959
+
960
+ async def run() -> None:
961
+ async with stdio_server() as (read_stream, write_stream):
962
+ await server.run(
963
+ read_stream,
964
+ write_stream,
965
+ server.create_initialization_options(),
966
+ )