coherence-mcp-server 0.4.5 → 0.5.1

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.
@@ -1,6 +1,6 @@
1
1
  """Coherence Network MCP server — Python implementation.
2
2
 
3
- Exposes the Coherence Network API as 22 typed MCP tools.
3
+ Exposes the Coherence Network API as 60 typed MCP tools.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
@@ -90,6 +90,9 @@ TOOLS: list[Tool] = [
90
90
  "properties": {
91
91
  "limit": {"type": "number", "description": "Max ideas to return (default 20)", "default": 20},
92
92
  "search": {"type": "string", "description": "Search keyword to filter ideas"},
93
+ "workspace_id": {"type": "string", "description": "Optional workspace filter. Defaults to all workspaces."},
94
+ "pillar": {"type": "string", "description": "Optional pillar filter."},
95
+ "curated_only": {"type": "boolean", "description": "If true, only return curated super-ideas."},
93
96
  },
94
97
  },
95
98
  ),
@@ -142,6 +145,7 @@ TOOLS: list[Tool] = [
142
145
  "properties": {
143
146
  "limit": {"type": "number", "default": 20},
144
147
  "search": {"type": "string", "description": "Search keyword"},
148
+ "workspace_id": {"type": "string", "description": "Optional workspace filter. Defaults to all workspaces."},
145
149
  },
146
150
  },
147
151
  ),
@@ -252,12 +256,6 @@ TOOLS: list[Tool] = [
252
256
  "properties": {"window_days": {"type": "number", "default": 30}},
253
257
  },
254
258
  ),
255
- # Governance
256
- Tool(
257
- name="coherence_list_change_requests",
258
- description="List governance change requests.",
259
- inputSchema={"type": "object", "properties": {}},
260
- ),
261
259
  # Federation
262
260
  Tool(
263
261
  name="coherence_list_federation_nodes",
@@ -275,6 +273,7 @@ TOOLS: list[Tool] = [
275
273
  "task_type": {"type": "string", "description": "Filter by type: spec, test, impl, review, code-review"},
276
274
  "limit": {"type": "number", "description": "Max tasks to return (default 20)", "default": 20},
277
275
  "offset": {"type": "number", "description": "Pagination offset", "default": 0},
276
+ "workspace_id": {"type": "string", "description": "Optional workspace to filter by (via each task's linked idea)."},
278
277
  },
279
278
  },
280
279
  ),
@@ -351,7 +350,7 @@ TOOLS: list[Tool] = [
351
350
  # Ideas (Lifecycle)
352
351
  Tool(
353
352
  name="coherence_create_idea",
354
- description="Create a new idea in the portfolio.",
353
+ description="Create a new idea in the portfolio. Scoped to a workspace (default: coherence-network).",
355
354
  inputSchema={
356
355
  "type": "object",
357
356
  "required": ["id", "name", "description"],
@@ -361,8 +360,54 @@ TOOLS: list[Tool] = [
361
360
  "description": {"type": "string", "description": "Detailed vision and value proposition"},
362
361
  "potential_value": {"type": "number", "description": "Estimated CC value (0-1000)", "default": 100},
363
362
  "estimated_cost": {"type": "number", "description": "Estimated CC cost (0-1000)", "default": 50},
364
- "parent_idea_id": {"type": "string", "description": "Optional parent idea for hierarchy"},
363
+ "parent_idea_id": {"type": "string", "description": "Optional parent idea for hierarchy. Must belong to the same workspace."},
365
364
  "tags": {"type": "array", "items": {"type": "string"}, "description": "List of tags"},
365
+ "workspace_id": {"type": "string", "description": "Owning workspace slug. Defaults to 'coherence-network'."},
366
+ "pillar": {"type": "string", "description": "Top-level pillar (must be one of the workspace's declared pillars)."},
367
+ },
368
+ },
369
+ ),
370
+ # Workspaces (Tenant primitive)
371
+ Tool(
372
+ name="coherence_list_workspaces",
373
+ description="List all workspaces (tenants). Each workspace owns its own ideas, specs, pillars, and agent personas.",
374
+ inputSchema={"type": "object", "properties": {}},
375
+ ),
376
+ Tool(
377
+ name="coherence_get_workspace",
378
+ description="Get a workspace's full manifest (name, description, pillars, visibility).",
379
+ inputSchema={
380
+ "type": "object",
381
+ "required": ["workspace_id"],
382
+ "properties": {
383
+ "workspace_id": {"type": "string", "description": "Workspace slug (e.g. 'coherence-network')"},
384
+ },
385
+ },
386
+ ),
387
+ Tool(
388
+ name="coherence_create_workspace",
389
+ description="Create a new workspace (tenant) with its own pillar taxonomy. Use this to onboard a contributor team with isolated ideas/specs/agents.",
390
+ inputSchema={
391
+ "type": "object",
392
+ "required": ["id", "name"],
393
+ "properties": {
394
+ "id": {"type": "string", "description": "Slug: lowercase + hyphens (e.g. 'my-startup')"},
395
+ "name": {"type": "string", "description": "Human name"},
396
+ "description": {"type": "string", "description": "Workspace purpose"},
397
+ "pillars": {"type": "array", "items": {"type": "string"}, "description": "Top-level taxonomy for grouping ideas"},
398
+ "visibility": {"type": "string", "description": "public | federation | private", "default": "public"},
399
+ "owner_contributor_id": {"type": "string", "description": "Contributor who owns this workspace"},
400
+ },
401
+ },
402
+ ),
403
+ Tool(
404
+ name="coherence_get_workspace_pillars",
405
+ description="Get the pillar taxonomy declared by a workspace. Use this before setting `pillar` on a new idea.",
406
+ inputSchema={
407
+ "type": "object",
408
+ "required": ["workspace_id"],
409
+ "properties": {
410
+ "workspace_id": {"type": "string"},
366
411
  },
367
412
  },
368
413
  ),
@@ -713,6 +758,71 @@ TOOLS: list[Tool] = [
713
758
  },
714
759
  },
715
760
  ),
761
+ # Spec CRUD
762
+ Tool(
763
+ name="coherence_create_spec",
764
+ description="Create a new spec in the registry. Requires spec_id (slug), title, summary, and idea_id.",
765
+ inputSchema={
766
+ "type": "object",
767
+ "required": ["spec_id", "title", "summary"],
768
+ "properties": {
769
+ "spec_id": {"type": "string", "description": "Unique spec slug (e.g. '170-my-feature')"},
770
+ "title": {"type": "string", "description": "Spec title"},
771
+ "summary": {"type": "string", "description": "Spec summary"},
772
+ "idea_id": {"type": "string", "description": "Parent idea ID"},
773
+ "potential_value": {"type": "number", "description": "Estimated CC value", "default": 0},
774
+ "estimated_cost": {"type": "number", "description": "Estimated CC cost", "default": 0},
775
+ "content_path": {"type": "string", "description": "Path to spec content file"},
776
+ "implementation_summary": {"type": "string", "description": "Implementation summary text"},
777
+ },
778
+ },
779
+ ),
780
+ Tool(
781
+ name="coherence_update_spec",
782
+ description="Update an existing spec's properties (title, summary, value, cost, implementation summary).",
783
+ inputSchema={
784
+ "type": "object",
785
+ "required": ["spec_id"],
786
+ "properties": {
787
+ "spec_id": {"type": "string", "description": "The spec ID"},
788
+ "title": {"type": "string"},
789
+ "summary": {"type": "string"},
790
+ "potential_value": {"type": "number"},
791
+ "estimated_cost": {"type": "number"},
792
+ "actual_value": {"type": "number"},
793
+ "actual_cost": {"type": "number"},
794
+ "idea_id": {"type": "string"},
795
+ "implementation_summary": {"type": "string"},
796
+ "process_summary": {"type": "string"},
797
+ "content_path": {"type": "string"},
798
+ },
799
+ },
800
+ ),
801
+ # Workflow
802
+ Tool(
803
+ name="coherence_advance_idea",
804
+ description="Check prerequisites and advance an idea to its next lifecycle stage. Returns current state, prerequisite check results, and whether advancement succeeded.",
805
+ inputSchema={
806
+ "type": "object",
807
+ "required": ["idea_id"],
808
+ "properties": {
809
+ "idea_id": {"type": "string", "description": "The idea ID to advance"},
810
+ "force": {"type": "boolean", "description": "Skip prerequisite checks", "default": False},
811
+ },
812
+ },
813
+ ),
814
+ Tool(
815
+ name="coherence_trace",
816
+ description="Trace the full lineage of any entity — from idea to specs to tasks to source files. Provides navigation breadcrumbs across the entire system.",
817
+ inputSchema={
818
+ "type": "object",
819
+ "required": ["entity_id"],
820
+ "properties": {
821
+ "entity_id": {"type": "string", "description": "An idea slug, spec slug, or task ID"},
822
+ "entity_type": {"type": "string", "description": "idea, spec, or task", "default": "idea"},
823
+ },
824
+ },
825
+ ),
716
826
  ]
717
827
 
718
828
  TOOL_MAP: dict[str, Tool] = {t.name: t for t in TOOLS}
@@ -727,7 +837,14 @@ def dispatch(name: str, args: dict[str, Any]) -> Any:
727
837
  case "coherence_list_ideas":
728
838
  if args.get("search"):
729
839
  return api_get("/api/ideas/cards", {"search": args["search"], "limit": args.get("limit", 20)})
730
- return api_get("/api/ideas", {"limit": args.get("limit", 20)})
840
+ params = {"limit": args.get("limit", 20)}
841
+ if args.get("workspace_id"):
842
+ params["workspace_id"] = args["workspace_id"]
843
+ if args.get("pillar"):
844
+ params["pillar"] = args["pillar"]
845
+ if args.get("curated_only"):
846
+ params["curated_only"] = "true"
847
+ return api_get("/api/ideas", params)
731
848
  case "coherence_get_idea":
732
849
  return api_get(f"/api/ideas/{args['idea_id']}")
733
850
  case "coherence_idea_progress":
@@ -742,7 +859,10 @@ def dispatch(name: str, args: dict[str, Any]) -> Any:
742
859
  case "coherence_list_specs":
743
860
  if args.get("search"):
744
861
  return api_get("/api/spec-registry/cards", {"search": args["search"], "limit": args.get("limit", 20)})
745
- return api_get("/api/spec-registry", {"limit": args.get("limit", 20)})
862
+ params = {"limit": args.get("limit", 20)}
863
+ if args.get("workspace_id"):
864
+ params["workspace_id"] = args["workspace_id"]
865
+ return api_get("/api/spec-registry", params)
746
866
  case "coherence_get_spec":
747
867
  return api_get(f"/api/spec-registry/{args['spec_id']}")
748
868
  # Lineage
@@ -793,9 +913,6 @@ def dispatch(name: str, args: dict[str, Any]) -> Any:
793
913
  }
794
914
  case "coherence_friction_report":
795
915
  return api_get("/api/friction/report", {"window_days": args.get("window_days", 30)})
796
- # Governance
797
- case "coherence_list_change_requests":
798
- return api_get("/api/governance/change-requests")
799
916
  # Federation
800
917
  case "coherence_list_federation_nodes":
801
918
  nodes = api_get("/api/federation/nodes")
@@ -803,12 +920,15 @@ def dispatch(name: str, args: dict[str, Any]) -> Any:
803
920
  return {"nodes": nodes, "capabilities": caps}
804
921
  # Tasks
805
922
  case "coherence_list_tasks":
806
- return api_get("/api/agent/tasks", {
923
+ params = {
807
924
  "status": args.get("status"),
808
925
  "task_type": args.get("task_type"),
809
926
  "limit": args.get("limit", 20),
810
927
  "offset": args.get("offset", 0),
811
- })
928
+ }
929
+ if args.get("workspace_id"):
930
+ params["workspace_id"] = args["workspace_id"]
931
+ return api_get("/api/agent/tasks", params)
812
932
  case "coherence_get_task":
813
933
  return api_get(f"/api/agent/tasks/{args['task_id']}")
814
934
  case "coherence_task_next":
@@ -860,7 +980,25 @@ def dispatch(name: str, args: dict[str, Any]) -> Any:
860
980
  "estimated_cost": args.get("estimated_cost", 50),
861
981
  "parent_idea_id": args.get("parent_idea_id"),
862
982
  "tags": args.get("tags"),
983
+ "workspace_id": args.get("workspace_id"),
984
+ "pillar": args.get("pillar"),
985
+ })
986
+ # Workspaces
987
+ case "coherence_list_workspaces":
988
+ return api_get("/api/workspaces")
989
+ case "coherence_get_workspace":
990
+ return api_get(f"/api/workspaces/{args['workspace_id']}")
991
+ case "coherence_create_workspace":
992
+ return api_post("/api/workspaces", {
993
+ "id": args["id"],
994
+ "name": args["name"],
995
+ "description": args.get("description", ""),
996
+ "pillars": args.get("pillars", []),
997
+ "visibility": args.get("visibility", "public"),
998
+ "owner_contributor_id": args.get("owner_contributor_id"),
863
999
  })
1000
+ case "coherence_get_workspace_pillars":
1001
+ return api_get(f"/api/workspaces/{args['workspace_id']}/pillars")
864
1002
  case "coherence_update_idea":
865
1003
  return api_patch(f"/api/ideas/{args['idea_id']}", {
866
1004
  "name": args.get("name"),
@@ -1000,6 +1138,123 @@ def dispatch(name: str, args: dict[str, Any]) -> Any:
1000
1138
  "relationship_type": args["relationship_type"],
1001
1139
  "created_by": args.get("created_by", "mcp"),
1002
1140
  })
1141
+ # Spec CRUD
1142
+ case "coherence_create_spec":
1143
+ return api_post("/api/spec-registry", {
1144
+ "spec_id": args["spec_id"],
1145
+ "title": args["title"],
1146
+ "summary": args["summary"],
1147
+ "idea_id": args.get("idea_id"),
1148
+ "potential_value": args.get("potential_value", 0),
1149
+ "estimated_cost": args.get("estimated_cost", 0),
1150
+ "content_path": args.get("content_path"),
1151
+ "implementation_summary": args.get("implementation_summary"),
1152
+ })
1153
+ case "coherence_update_spec":
1154
+ body = {
1155
+ k: v for k, v in {
1156
+ "title": args.get("title"),
1157
+ "summary": args.get("summary"),
1158
+ "potential_value": args.get("potential_value"),
1159
+ "estimated_cost": args.get("estimated_cost"),
1160
+ "actual_value": args.get("actual_value"),
1161
+ "actual_cost": args.get("actual_cost"),
1162
+ "idea_id": args.get("idea_id"),
1163
+ "implementation_summary": args.get("implementation_summary"),
1164
+ "process_summary": args.get("process_summary"),
1165
+ "content_path": args.get("content_path"),
1166
+ }.items() if v is not None
1167
+ }
1168
+ return api_patch(f"/api/spec-registry/{args['spec_id']}", body)
1169
+ # Workflow
1170
+ case "coherence_advance_idea":
1171
+ idea_id = args["idea_id"]
1172
+ force = args.get("force", False)
1173
+ idea = api_get(f"/api/ideas/{idea_id}")
1174
+ if isinstance(idea, dict) and "error" in idea:
1175
+ return idea
1176
+ progress = api_get(f"/api/ideas/{idea_id}/progress")
1177
+ current_stage = idea.get("stage", "none") if isinstance(idea, dict) else "none"
1178
+ stage_order = ["none", "specced", "implementing", "testing", "reviewing", "complete"]
1179
+ idx = stage_order.index(current_stage) if current_stage in stage_order else 0
1180
+ if idx >= len(stage_order) - 1:
1181
+ return {"idea_id": idea_id, "current_stage": current_stage, "next_stage": None,
1182
+ "prerequisites_met": False, "prerequisite_details": {"reason": "already at final stage"},
1183
+ "advanced": False, "result": None}
1184
+ next_stage = stage_order[idx + 1]
1185
+ # Check prerequisites from progress tasks
1186
+ tasks_by_phase = progress.get("tasks_by_phase", {}) if isinstance(progress, dict) else {}
1187
+ prereq_checks = {
1188
+ "specced": ("spec", "at least 1 spec task completed"),
1189
+ "implementing": ("impl", "at least 1 impl task exists"),
1190
+ "testing": ("impl", "at least 1 impl task completed"),
1191
+ "reviewing": ("test", "at least 1 test task completed"),
1192
+ "complete": ("review", "at least 1 review task completed"),
1193
+ }
1194
+ prereqs_met = True
1195
+ prereq_details: dict[str, Any] = {}
1196
+ if next_stage in prereq_checks:
1197
+ phase_key, description = prereq_checks[next_stage]
1198
+ phase_tasks = tasks_by_phase.get(phase_key, [])
1199
+ if next_stage == "implementing":
1200
+ prereqs_met = len(phase_tasks) > 0
1201
+ prereq_details = {"check": description, "phase": phase_key, "task_count": len(phase_tasks)}
1202
+ else:
1203
+ completed = [t for t in phase_tasks if isinstance(t, dict) and t.get("status") == "completed"]
1204
+ prereqs_met = len(completed) > 0
1205
+ prereq_details = {"check": description, "phase": phase_key,
1206
+ "completed_count": len(completed), "total_count": len(phase_tasks)}
1207
+ advanced = False
1208
+ result = None
1209
+ if prereqs_met or force:
1210
+ result = api_patch(f"/api/ideas/{idea_id}", {"stage": next_stage})
1211
+ advanced = not (isinstance(result, dict) and "error" in result)
1212
+ return {"idea_id": idea_id, "current_stage": current_stage, "next_stage": next_stage,
1213
+ "prerequisites_met": prereqs_met, "prerequisite_details": prereq_details,
1214
+ "advanced": advanced, "result": result}
1215
+ case "coherence_trace":
1216
+ entity_id = args["entity_id"]
1217
+ entity_type = args.get("entity_type", "idea")
1218
+ if entity_type == "idea":
1219
+ idea = api_get(f"/api/ideas/{entity_id}")
1220
+ progress = api_get(f"/api/ideas/{entity_id}/progress")
1221
+ specs = api_get("/api/spec-registry/cards", {"q": entity_id, "limit": 50})
1222
+ all_tasks = api_get("/api/agent/tasks", {"status": None, "limit": 50})
1223
+ task_list = all_tasks.get("tasks", []) if isinstance(all_tasks, dict) else []
1224
+ idea_tasks = [t for t in task_list if isinstance(t, dict)
1225
+ and isinstance(t.get("context"), dict) and t["context"].get("idea_id") == entity_id]
1226
+ spec_items = specs if isinstance(specs, list) else specs.get("items", []) if isinstance(specs, dict) else []
1227
+ spec_ids = [s.get("spec_id", s.get("id", "")) for s in spec_items if isinstance(s, dict)]
1228
+ return {"entity_type": "idea", "idea": idea, "progress": progress,
1229
+ "specs": spec_items, "tasks": idea_tasks,
1230
+ "navigation": {"idea_file": f"ideas/{entity_id}.md",
1231
+ "spec_files": [f"specs/{s}.md" for s in spec_ids],
1232
+ "api": f"/api/ideas/{entity_id}",
1233
+ "cli": f"cc idea {entity_id}"}}
1234
+ elif entity_type == "spec":
1235
+ spec = api_get(f"/api/spec-registry/{entity_id}")
1236
+ spec_file = api_get("/api/content/file", {"path": f"specs/{entity_id}.md"})
1237
+ idea_id = spec.get("idea_id") if isinstance(spec, dict) else None
1238
+ parent_idea = api_get(f"/api/ideas/{idea_id}") if idea_id else None
1239
+ nav: dict[str, Any] = {"spec_file": f"specs/{entity_id}.md",
1240
+ "api": f"/api/spec-registry/{entity_id}",
1241
+ "cli": f"cc spec {entity_id}"}
1242
+ if idea_id:
1243
+ nav["idea_file"] = f"ideas/{idea_id}.md"
1244
+ return {"entity_type": "spec", "spec": spec, "spec_file_content": spec_file,
1245
+ "parent_idea": parent_idea, "navigation": nav}
1246
+ elif entity_type == "task":
1247
+ task = api_get(f"/api/agent/tasks/{entity_id}")
1248
+ events = api_get(f"/api/agent/tasks/{entity_id}/stream")
1249
+ context = task.get("context", {}) if isinstance(task, dict) else {}
1250
+ idea_id = context.get("idea_id") if isinstance(context, dict) else None
1251
+ parent_idea = api_get(f"/api/ideas/{idea_id}") if idea_id else None
1252
+ return {"entity_type": "task", "task": task, "events": events,
1253
+ "parent_idea": parent_idea,
1254
+ "navigation": {"api": f"/api/agent/tasks/{entity_id}",
1255
+ "cli": f"cc task {entity_id}"}}
1256
+ else:
1257
+ return {"error": f"Unknown entity_type: {entity_type}. Use 'idea', 'spec', or 'task'."}
1003
1258
  case _:
1004
1259
  return {"error": f"Unknown tool: {name}"}
1005
1260
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coherence-mcp-server",
3
- "version": "0.4.5",
3
+ "version": "0.5.1",
4
4
  "description": "Unified MCP server for the Coherence Network — 51 typed tools for AI agents to browse ideas, manage tasks, link identities, and navigate the universal graph.",
5
5
  "type": "module",
6
6
  "bin": {