devtorch-core 3.0.1__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 (193) hide show
  1. devtorch_core/__init__.py +158 -0
  2. devtorch_core/aggphi_textual.py +275 -0
  3. devtorch_core/alerts/__init__.py +23 -0
  4. devtorch_core/alerts/base.py +46 -0
  5. devtorch_core/alerts/config.py +60 -0
  6. devtorch_core/alerts/dispatcher.py +110 -0
  7. devtorch_core/alerts/jira.py +96 -0
  8. devtorch_core/alerts/linear.py +72 -0
  9. devtorch_core/alerts/pagerduty.py +66 -0
  10. devtorch_core/alerts/slack.py +81 -0
  11. devtorch_core/alerts/teams.py +70 -0
  12. devtorch_core/audit/__init__.py +43 -0
  13. devtorch_core/audit/exporter.py +297 -0
  14. devtorch_core/audit/privacy.py +101 -0
  15. devtorch_core/audit/scrubber.py +149 -0
  16. devtorch_core/audit/service.py +67 -0
  17. devtorch_core/audit/signing.py +127 -0
  18. devtorch_core/broadcast/__init__.py +4 -0
  19. devtorch_core/broadcast/broadcaster.py +100 -0
  20. devtorch_core/broadcast/watcher.py +71 -0
  21. devtorch_core/capability.py +639 -0
  22. devtorch_core/cloud/__init__.py +1 -0
  23. devtorch_core/cloud/client_config.py +472 -0
  24. devtorch_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. devtorch_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. devtorch_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. devtorch_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. devtorch_core/cloud/client_configs/.opencode.json +15 -0
  29. devtorch_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. devtorch_core/cloud/devtorch-mcp-bridge.js +357 -0
  31. devtorch_core/cloud/mcp_client.py +229 -0
  32. devtorch_core/cloud/setup.py +144 -0
  33. devtorch_core/cloud/sync.py +143 -0
  34. devtorch_core/cloud/sync_bundle.py +603 -0
  35. devtorch_core/cloud/sync_conflicts.py +159 -0
  36. devtorch_core/cloud/sync_state.py +159 -0
  37. devtorch_core/cloud/team_sync.py +283 -0
  38. devtorch_core/codex/__init__.py +9 -0
  39. devtorch_core/codex/__main__.py +97 -0
  40. devtorch_core/codex/capture.py +208 -0
  41. devtorch_core/codex/proxy.py +412 -0
  42. devtorch_core/concept_catalog.py +209 -0
  43. devtorch_core/consolidation/__init__.py +3 -0
  44. devtorch_core/consolidation/synthesizer.py +87 -0
  45. devtorch_core/consolidation/workflow.py +175 -0
  46. devtorch_core/daemon/__init__.py +27 -0
  47. devtorch_core/daemon/supervisor.py +293 -0
  48. devtorch_core/daemon/watcher.py +244 -0
  49. devtorch_core/dashboard_api.py +2012 -0
  50. devtorch_core/deltaf.py +97 -0
  51. devtorch_core/disclosure.py +50 -0
  52. devtorch_core/divergence/__init__.py +3 -0
  53. devtorch_core/divergence/detector.py +166 -0
  54. devtorch_core/gateway/__init__.py +32 -0
  55. devtorch_core/gateway/key_manager.py +124 -0
  56. devtorch_core/gateway/metrics_webhook.py +252 -0
  57. devtorch_core/gateway/policy.py +262 -0
  58. devtorch_core/gateway/server.py +727 -0
  59. devtorch_core/gateway/sso.py +233 -0
  60. devtorch_core/gcc.py +1246 -0
  61. devtorch_core/github/__init__.py +35 -0
  62. devtorch_core/github/app.py +240 -0
  63. devtorch_core/github/comment_builder.py +113 -0
  64. devtorch_core/github/pat.py +76 -0
  65. devtorch_core/github/pr_parser.py +82 -0
  66. devtorch_core/github/pr_reporter.py +555 -0
  67. devtorch_core/gitlab/__init__.py +177 -0
  68. devtorch_core/hitl/__init__.py +4 -0
  69. devtorch_core/hitl/channels.py +129 -0
  70. devtorch_core/hitl/orchestrator.py +95 -0
  71. devtorch_core/hooks/__init__.py +17 -0
  72. devtorch_core/hooks/claude_code.py +228 -0
  73. devtorch_core/hooks/git_capture.py +341 -0
  74. devtorch_core/hooks/git_commit.py +182 -0
  75. devtorch_core/hooks/installer.py +733 -0
  76. devtorch_core/hooks/pre_commit.py +157 -0
  77. devtorch_core/hooks/runner.py +344 -0
  78. devtorch_core/identity/__init__.py +4 -0
  79. devtorch_core/identity/agent.py +86 -0
  80. devtorch_core/identity/providers.py +85 -0
  81. devtorch_core/invariants.py +182 -0
  82. devtorch_core/mcp/__init__.py +10 -0
  83. devtorch_core/mcp/auth.py +177 -0
  84. devtorch_core/mcp/server.py +1049 -0
  85. devtorch_core/metrics/__init__.py +35 -0
  86. devtorch_core/metrics/aggregate.py +215 -0
  87. devtorch_core/metrics/calibrate.py +198 -0
  88. devtorch_core/metrics/calibration.py +125 -0
  89. devtorch_core/metrics/credibility.py +288 -0
  90. devtorch_core/metrics/delivery_time.py +70 -0
  91. devtorch_core/metrics/dhs.py +126 -0
  92. devtorch_core/metrics/mcs.py +96 -0
  93. devtorch_core/metrics/roi.py +88 -0
  94. devtorch_core/metrics/session_writer.py +81 -0
  95. devtorch_core/metrics/shadow_ai.py +117 -0
  96. devtorch_core/metrics/sprint_writer.py +243 -0
  97. devtorch_core/observability/__init__.py +78 -0
  98. devtorch_core/observability/datadog.py +157 -0
  99. devtorch_core/observability/formatter.py +119 -0
  100. devtorch_core/observability/report.py +264 -0
  101. devtorch_core/observability/servicenow.py +147 -0
  102. devtorch_core/observability/splunk.py +218 -0
  103. devtorch_core/observability/webhook.py +227 -0
  104. devtorch_core/parser/__init__.py +30 -0
  105. devtorch_core/parser/blocks.py +216 -0
  106. devtorch_core/parser/inference.py +159 -0
  107. devtorch_core/parser/thinking.py +112 -0
  108. devtorch_core/projects.py +169 -0
  109. devtorch_core/prompt_artifact.py +76 -0
  110. devtorch_core/proxy/__init__.py +9 -0
  111. devtorch_core/proxy/routes/__init__.py +1 -0
  112. devtorch_core/proxy/routes/anthropic.py +264 -0
  113. devtorch_core/proxy/routes/azure_openai.py +336 -0
  114. devtorch_core/proxy/routes/gemini.py +331 -0
  115. devtorch_core/proxy/routes/groq.py +284 -0
  116. devtorch_core/proxy/routes/ollama.py +279 -0
  117. devtorch_core/proxy/routes/openai.py +287 -0
  118. devtorch_core/proxy/server.py +356 -0
  119. devtorch_core/query/__init__.py +15 -0
  120. devtorch_core/query/grep.py +181 -0
  121. devtorch_core/query/hybrid.py +86 -0
  122. devtorch_core/query/semantic.py +157 -0
  123. devtorch_core/rdp.py +105 -0
  124. devtorch_core/reasoning/__init__.py +4 -0
  125. devtorch_core/reasoning/entry.py +31 -0
  126. devtorch_core/reasoning/store.py +122 -0
  127. devtorch_core/reasoning_plus/__init__.py +70 -0
  128. devtorch_core/reasoning_plus/augmenter.py +326 -0
  129. devtorch_core/reasoning_plus/capture.py +51 -0
  130. devtorch_core/reasoning_plus/config.py +256 -0
  131. devtorch_core/reasoning_plus/context.py +262 -0
  132. devtorch_core/reasoning_plus/learning/__init__.py +72 -0
  133. devtorch_core/reasoning_plus/learning/analytics.py +141 -0
  134. devtorch_core/reasoning_plus/learning/api.py +313 -0
  135. devtorch_core/reasoning_plus/learning/chain.py +285 -0
  136. devtorch_core/reasoning_plus/learning/composer.py +74 -0
  137. devtorch_core/reasoning_plus/learning/cross_project.py +234 -0
  138. devtorch_core/reasoning_plus/learning/embeddings.py +209 -0
  139. devtorch_core/reasoning_plus/learning/extractor.py +207 -0
  140. devtorch_core/reasoning_plus/learning/models.py +116 -0
  141. devtorch_core/reasoning_plus/learning/provenance.py +126 -0
  142. devtorch_core/reasoning_plus/learning/recorder.py +81 -0
  143. devtorch_core/reasoning_plus/learning/relevance.py +122 -0
  144. devtorch_core/reasoning_plus/learning/state.py +86 -0
  145. devtorch_core/reasoning_plus/learning/store.py +160 -0
  146. devtorch_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  147. devtorch_core/reasoning_plus/prompt.py +90 -0
  148. devtorch_core/rep.py +134 -0
  149. devtorch_core/rep_network/__init__.py +25 -0
  150. devtorch_core/rep_network/merge.py +70 -0
  151. devtorch_core/rep_network/node.py +137 -0
  152. devtorch_core/rep_network/server.py +140 -0
  153. devtorch_core/rep_network/sync.py +207 -0
  154. devtorch_core/sensitivity.py +182 -0
  155. devtorch_core/serve.py +258 -0
  156. devtorch_core/session/__init__.py +39 -0
  157. devtorch_core/session/disagreement.py +188 -0
  158. devtorch_core/session/models.py +114 -0
  159. devtorch_core/session/orchestrator.py +182 -0
  160. devtorch_core/session/planner.py +169 -0
  161. devtorch_core/session/simulator.py +132 -0
  162. devtorch_core/signing.py +290 -0
  163. devtorch_core/sis.py +197 -0
  164. devtorch_core/storage.py +308 -0
  165. devtorch_core/templates/__init__.py +6 -0
  166. devtorch_core/templates/engine.py +122 -0
  167. devtorch_core/templates/go.py +18 -0
  168. devtorch_core/templates/infra.py +19 -0
  169. devtorch_core/templates/library/__init__.py +18 -0
  170. devtorch_core/templates/library/api_design.md +27 -0
  171. devtorch_core/templates/library/bug_fix.md +27 -0
  172. devtorch_core/templates/library/decision_record.md +27 -0
  173. devtorch_core/templates/library/engine.py +228 -0
  174. devtorch_core/templates/library/security_review.md +30 -0
  175. devtorch_core/templates/python.py +19 -0
  176. devtorch_core/templates/react.py +18 -0
  177. devtorch_core/templates/typescript.py +18 -0
  178. devtorch_core/theta.py +221 -0
  179. devtorch_core/theta_synthesis.py +268 -0
  180. devtorch_core/topics.py +320 -0
  181. devtorch_core/variance.py +219 -0
  182. devtorch_core/wrapper/__init__.py +52 -0
  183. devtorch_core/wrapper/anthropic.py +487 -0
  184. devtorch_core/wrapper/base.py +562 -0
  185. devtorch_core/wrapper/bedrock.py +342 -0
  186. devtorch_core/wrapper/gemini.py +422 -0
  187. devtorch_core/wrapper/ollama.py +527 -0
  188. devtorch_core/wrapper/openai.py +461 -0
  189. devtorch_core-3.0.1.dist-info/METADATA +867 -0
  190. devtorch_core-3.0.1.dist-info/RECORD +193 -0
  191. devtorch_core-3.0.1.dist-info/WHEEL +5 -0
  192. devtorch_core-3.0.1.dist-info/entry_points.txt +2 -0
  193. devtorch_core-3.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1049 @@
1
+ """
2
+ S7 MCP Server — DevTorch primitives exposed via JSON-RPC 2.0 / MCP protocol.
3
+
4
+ Supports two transports:
5
+ - If the `mcp` SDK is installed: uses mcp.server.Server + stdio_server
6
+ - Fallback: raw JSON-RPC 2.0 line-by-line stdio loop
7
+
8
+ Tools exposed:
9
+ devtorch_commit — Commit a reasoning milestone to .GCC/
10
+ devtorch_sensitivity_add — Record a sensitivity signal
11
+ devtorch_context — Retrieve bounded context bundle
12
+ devtorch_theta_read — Read coordination vector Θ
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+ from typing import Any, Dict, List, Optional
21
+
22
+ # FastAPI / Starlette imports — only needed for SSE transport, but must be at
23
+ # module level so that FastAPI can resolve string annotations (PEP 563) when
24
+ # building the route dependency graph.
25
+ try:
26
+ from fastapi import FastAPI as _FastAPI, Request as _Request, HTTPException as _HTTPException
27
+ from fastapi.responses import StreamingResponse as _StreamingResponse, Response as _Response
28
+ _FASTAPI_AVAILABLE = True
29
+ except ImportError: # pragma: no cover
30
+ _FASTAPI_AVAILABLE = False
31
+ _FastAPI = None # type: ignore[assignment,misc]
32
+ _Request = None # type: ignore[assignment]
33
+ _HTTPException = None # type: ignore[assignment]
34
+ _StreamingResponse = None # type: ignore[assignment]
35
+ _Response = None # type: ignore[assignment]
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Tool definitions (shared between MCP-SDK path and fallback path)
39
+ # ---------------------------------------------------------------------------
40
+
41
+ TOOLS: List[Dict[str, Any]] = [
42
+ {
43
+ "name": "devtorch_commit",
44
+ "description": "Commit a reasoning milestone to the .GCC/ store",
45
+ "inputSchema": {
46
+ "type": "object",
47
+ "properties": {
48
+ "message": {"type": "string", "description": "Commit message"},
49
+ "branch": {
50
+ "type": "string",
51
+ "description": "Branch name (optional)",
52
+ },
53
+ "concepts": {
54
+ "type": "string",
55
+ "description": "Comma-separated concept tags (optional, e.g. auth,security)",
56
+ },
57
+ },
58
+ "required": ["message"],
59
+ },
60
+ },
61
+ {
62
+ "name": "devtorch_sensitivity_add",
63
+ "description": "Record what this decision depends on (sensitivity signal)",
64
+ "inputSchema": {
65
+ "type": "object",
66
+ "properties": {
67
+ "concept": {"type": "string", "description": "Target concept"},
68
+ "signal": {
69
+ "type": "string",
70
+ "description": "Signal / counterfactual text",
71
+ },
72
+ "confidence": {
73
+ "type": "number",
74
+ "description": "Confidence in [0, 1]",
75
+ },
76
+ "disclosure": {
77
+ "type": "string",
78
+ "description": "Disclosure level: PUBLIC, PROTECTED (default), or PRIVATE",
79
+ },
80
+ },
81
+ "required": ["concept", "signal", "confidence"],
82
+ },
83
+ },
84
+ {
85
+ "name": "devtorch_context",
86
+ "description": "Retrieve bounded context bundle from .GCC/ (use this at session start)",
87
+ "inputSchema": {
88
+ "type": "object",
89
+ "properties": {
90
+ "k": {
91
+ "type": "integer",
92
+ "description": "Token budget (default 8000)",
93
+ },
94
+ },
95
+ },
96
+ },
97
+ {
98
+ "name": "devtorch_theta_read",
99
+ "description": "Read current coordination vector Θ (shared sensitivity state)",
100
+ "inputSchema": {
101
+ "type": "object",
102
+ "properties": {
103
+ "concepts": {
104
+ "type": "array",
105
+ "items": {"type": "string"},
106
+ "description": "Filter to specific concepts (optional)",
107
+ },
108
+ },
109
+ },
110
+ },
111
+ {
112
+ "name": "devtorch_reasoning_query",
113
+ "description": "Query an agent's reasoning about a specific concept (use at session start to understand prior decisions)",
114
+ "inputSchema": {
115
+ "type": "object",
116
+ "properties": {
117
+ "concept": {"type": "string", "description": "Concept to query (e.g. auth, schema, payments)"},
118
+ "agent_id": {"type": "string", "description": "Filter to a specific agent ID (optional)"},
119
+ "scope": {"type": "string", "description": "branch | session | org (default: branch)"},
120
+ "k": {"type": "integer", "description": "Token budget (default 4000)"},
121
+ },
122
+ "required": ["concept"],
123
+ },
124
+ },
125
+ {
126
+ "name": "devtorch_divergence_detect",
127
+ "description": "Detect reasoning divergence between agents for a concept",
128
+ "inputSchema": {
129
+ "type": "object",
130
+ "properties": {
131
+ "concept": {"type": "string"},
132
+ },
133
+ "required": ["concept"],
134
+ },
135
+ },
136
+ {
137
+ "name": "devtorch_consolidation_status",
138
+ "description": "Get consolidation record status for a concept",
139
+ "inputSchema": {
140
+ "type": "object",
141
+ "properties": {
142
+ "concept": {"type": "string"},
143
+ },
144
+ "required": ["concept"],
145
+ },
146
+ },
147
+ {
148
+ "name": "devtorch_hitl_respond",
149
+ "description": "Provide a human or agent resolution to a pending HITL consolidation",
150
+ "inputSchema": {
151
+ "type": "object",
152
+ "properties": {
153
+ "record_id": {"type": "string", "description": "ConsolidationRecord ID"},
154
+ "author": {"type": "string", "description": "Responder name or agent_id"},
155
+ "comment": {"type": "string", "description": "Explanation or reasoning"},
156
+ "decision": {"type": "string", "description": "The consolidated decision text"},
157
+ },
158
+ "required": ["record_id", "author", "decision"],
159
+ },
160
+ },
161
+ {
162
+ "name": "devtorch_reasoning_plus_config",
163
+ "description": "Read current Reasoning Plus settings and their source",
164
+ "inputSchema": {
165
+ "type": "object",
166
+ "properties": {},
167
+ },
168
+ },
169
+ {
170
+ "name": "devtorch_reasoning_plus_set",
171
+ "description": "Update Reasoning Plus settings for this project",
172
+ "inputSchema": {
173
+ "type": "object",
174
+ "properties": {
175
+ "enabled": {"type": "boolean", "description": "Enable or disable Reasoning Plus"},
176
+ "smart_top_n": {"type": "integer", "description": "Number of smart-context files to include"},
177
+ "smart_max_lines": {"type": "integer", "description": "Max lines per smart-context file"},
178
+ "context_k": {"type": "integer", "description": "Context token budget (used by benchmark runners)"},
179
+ "require_thinking": {"type": "boolean", "description": "Require <thinking> block in responses"},
180
+ "max_keywords": {"type": "integer", "description": "Max keywords for smart context extraction"},
181
+ "cache_ttl_secs": {"type": "integer", "description": "Smart context cache TTL in seconds"},
182
+ "learning_enabled": {"type": "boolean", "description": "Enable or disable Reasoning Plus Learning"},
183
+ "learning_top_n": {"type": "integer", "description": "Number of learnings to inject"},
184
+ "learning_max_lines": {"type": "integer", "description": "Max lines for the injected learnings block"},
185
+ "learning_embedding_backend": {"type": "string", "description": "Embedding backend for relevance: keyword, sentence_transformers, openai"},
186
+ "learning_embedding_model": {"type": "string", "description": "Model name for sentence_transformers or openai backend"},
187
+ "learning_extraction_strategy": {"type": "string", "description": "Learning extraction strategy: rule or llm"},
188
+ "learning_relevance_cache_ttl": {"type": "integer", "description": "Relevance cache TTL in seconds"},
189
+ },
190
+ },
191
+ },
192
+ {
193
+ "name": "devtorch_reasoning_plus_learning_query",
194
+ "description": "Query relevant learnings from the Reasoning Plus Learning store",
195
+ "inputSchema": {
196
+ "type": "object",
197
+ "properties": {
198
+ "context": {"type": "string", "description": "Current task or prompt context to match learnings against"},
199
+ "top_n": {"type": "integer", "description": "Maximum number of learnings to return (default: project setting)"},
200
+ "session_id": {"type": "string", "description": "Optional session filter"},
201
+ "state_hash": {"type": "string", "description": "Optional workspace state hash for freshness boost"},
202
+ },
203
+ "required": ["context"],
204
+ },
205
+ },
206
+ {
207
+ "name": "devtorch_reasoning_plus_learning_purge",
208
+ "description": "Purge stale or deprecated learnings from the Reasoning Plus Learning store",
209
+ "inputSchema": {
210
+ "type": "object",
211
+ "properties": {
212
+ "scope": {"type": "string", "description": "Scope to purge: stale, deprecated, or all"},
213
+ "session_id": {"type": "string", "description": "Optional session filter"},
214
+ },
215
+ },
216
+ },
217
+ {
218
+ "name": "devtorch_reasoning_plus_learning_feedback",
219
+ "description": "Apply outcome feedback to a learning (success, failure, or stale)",
220
+ "inputSchema": {
221
+ "type": "object",
222
+ "properties": {
223
+ "learning_id": {"type": "string", "description": "Learning ID to update"},
224
+ "outcome": {"type": "string", "description": "Outcome: success, failure, or stale"},
225
+ },
226
+ "required": ["learning_id", "outcome"],
227
+ },
228
+ },
229
+ {
230
+ "name": "devtorch_topics_list",
231
+ "description": "List all topics with captured reasoning",
232
+ "inputSchema": {
233
+ "type": "object",
234
+ "properties": {
235
+ "all": {"type": "boolean", "description": "Include topics with no data (default: false)"}
236
+ },
237
+ },
238
+ },
239
+ {
240
+ "name": "devtorch_reasoning_history",
241
+ "description": "Get reasoning history for a topic (expands to all concepts) or a single concept",
242
+ "inputSchema": {
243
+ "type": "object",
244
+ "properties": {
245
+ "topic": {"type": "string", "description": "Topic name (expands to multiple concepts)"},
246
+ "concept": {"type": "string", "description": "Single concept name (alternative to topic)"},
247
+ "max_entries": {"type": "integer", "description": "Max total entries across all concepts (default: 50)"},
248
+ "agent_id": {"type": "string", "description": "Filter by agent ID"}
249
+ },
250
+ },
251
+ },
252
+ {
253
+ "name": "devtorch_concepts_list",
254
+ "description": "List all concepts from all sources (I3, theta, sensitivity, learnings, commits)",
255
+ "inputSchema": {
256
+ "type": "object",
257
+ "properties": {
258
+ "learned_only": {"type": "boolean", "description": "Only concepts from learning interactions"}
259
+ },
260
+ },
261
+ },
262
+ {
263
+ "name": "devtorch_concepts_related",
264
+ "description": "List concepts related to a given topic",
265
+ "inputSchema": {
266
+ "type": "object",
267
+ "properties": {
268
+ "topic": {"type": "string"}
269
+ },
270
+ "required": ["topic"],
271
+ },
272
+ },
273
+ {
274
+ "name": "devtorch_sync_pull",
275
+ "description": "Pull delta from server — returns items the client doesn't have based on sync state",
276
+ "inputSchema": {
277
+ "type": "object",
278
+ "properties": {
279
+ "sync_state": {
280
+ "type": "object",
281
+ "description": "Client's current sync state (what it already has)",
282
+ },
283
+ "chunk_size": {
284
+ "type": "integer",
285
+ "description": "Max items per data type in response (default: 500)",
286
+ },
287
+ },
288
+ },
289
+ },
290
+ {
291
+ "name": "devtorch_sync_push",
292
+ "description": "Push delta to server — sends items the server doesn't have",
293
+ "inputSchema": {
294
+ "type": "object",
295
+ "properties": {
296
+ "bundle": {
297
+ "type": "string",
298
+ "description": "Base64-encoded gzip-compressed delta bundle",
299
+ },
300
+ "batch_id": {
301
+ "type": "string",
302
+ "description": "Batch identifier for chunking",
303
+ },
304
+ "is_last": {
305
+ "type": "boolean",
306
+ "description": "True if this is the final batch (default: true)",
307
+ },
308
+ },
309
+ },
310
+ },
311
+ ]
312
+
313
+ # ---------------------------------------------------------------------------
314
+ # Tool implementations
315
+ # ---------------------------------------------------------------------------
316
+
317
+
318
+ def _tool_devtorch_commit(repo: Any, args: dict) -> str:
319
+ message = args.get("message", "")
320
+ concepts_raw = args.get("concepts", "")
321
+ concepts = [c.strip() for c in concepts_raw.split(",") if c.strip()] if concepts_raw else None
322
+ repo.commit(message=message, concepts=concepts)
323
+ parts = [f"Committed to .GCC/"]
324
+ if concepts:
325
+ parts.append(f"[concepts: {', '.join(concepts)}]")
326
+ return " ".join(parts)
327
+
328
+
329
+ def _tool_devtorch_sensitivity_add(repo: Any, args: dict) -> str:
330
+ from devtorch_core.sensitivity import SensitivityEvent
331
+
332
+ concept = args["concept"]
333
+ signal = args["signal"]
334
+ confidence = float(args["confidence"])
335
+ disclosure = args.get("disclosure", "PROTECTED").upper()
336
+ if disclosure not in ("PUBLIC", "PROTECTED", "PRIVATE"):
337
+ disclosure = "PROTECTED"
338
+
339
+ ev = SensitivityEvent(
340
+ source_node="mcp-client",
341
+ target_concept=concept,
342
+ counterfactuals=signal,
343
+ confidence=confidence,
344
+ disclosure_level=disclosure,
345
+ message=signal,
346
+ )
347
+ repo.add_sensitivity(ev)
348
+ return f"Sensitivity recorded: {concept} @ {confidence}"
349
+
350
+
351
+ def _tool_devtorch_context(repo: Any, args: dict) -> str:
352
+ k = int(args.get("k", 8000))
353
+ bundle = repo.context_bundle(
354
+ k_tokens=k, policy={"sensitivity_policy": "default"}
355
+ )
356
+ # bundle is a dict; format it as a readable string
357
+ artifacts = bundle.get("artifacts", [])
358
+ if not artifacts:
359
+ return f"Context bundle (budget={k} tokens): no artifacts found."
360
+
361
+ lines = [f"Context bundle (budget={k} tokens, {len(artifacts)} artifacts):"]
362
+ for art in artifacts:
363
+ # Support both "name" (test mock) and "path" (real GCCRepository output)
364
+ name = art.get("name", art.get("path", art.get("id", "unknown")))
365
+ reason = art.get("reason", "")
366
+ # Support both "tokens" (test mock) and "approx_tokens" (real GCCRepository output)
367
+ tokens = art.get("tokens", art.get("approx_tokens", "?"))
368
+ lines.append(f" - {name} ({tokens} tokens): {reason}")
369
+ return "\n".join(lines)
370
+
371
+
372
+ def _tool_devtorch_theta_read(repo: Any, args: dict) -> str:
373
+ theta = repo.get_theta().get("coordination_vector", {})
374
+ concepts = args.get("concepts")
375
+ if concepts:
376
+ theta = {k: v for k, v in theta.items() if k in concepts}
377
+ return json.dumps(theta, indent=2)
378
+
379
+
380
+ def _tool_devtorch_reasoning_query(repo: Any, args: dict) -> str:
381
+ from devtorch_core.reasoning import ReasoningStore
382
+
383
+ concept = args["concept"]
384
+ agent_id = args.get("agent_id")
385
+ scope = args.get("scope", "branch")
386
+ k = int(args.get("k", 4000))
387
+
388
+ store = ReasoningStore(repo)
389
+ entries = store.query(concept=concept, agent_id=agent_id, scope=scope, k_tokens=k)
390
+
391
+ if not entries:
392
+ return f"No reasoning entries found for concept '{concept}'."
393
+
394
+ lines = [f"Reasoning for '{concept}' ({len(entries)} entries, budget={k} tokens):"]
395
+ for e in entries:
396
+ lines.append(
397
+ f"\n [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]"
398
+ )
399
+ lines.append(f" Reasoning: {e.reasoning_text}")
400
+ if e.decision_text:
401
+ lines.append(f" Decision: {e.decision_text}")
402
+ return "\n".join(lines)
403
+
404
+
405
+ def _tool_devtorch_divergence_detect(repo: Any, args: dict) -> str:
406
+ from devtorch_core.divergence import DivergenceDetector
407
+ concept = args["concept"]
408
+ signals = DivergenceDetector(repo).detect(concept=concept)
409
+ if not signals:
410
+ return f"No divergence detected for concept '{concept}'."
411
+ lines = [f"Divergence signals for '{concept}' ({len(signals)} signals):"]
412
+ for s in signals:
413
+ lines.append(f" [{s.signal_type}/{s.severity}] {s.description}")
414
+ return "\n".join(lines)
415
+
416
+
417
+ def _tool_devtorch_consolidation_status(repo: Any, args: dict) -> str:
418
+ from devtorch_core.consolidation import ConsolidationWorkflow
419
+ concept = args["concept"]
420
+ records = ConsolidationWorkflow(repo).list_for_concept(concept)
421
+ if not records:
422
+ return f"No consolidation records found for concept '{concept}'."
423
+ r = records[0] # most recent
424
+ return (
425
+ f"Consolidation '{r.record_id[:8]}' — concept='{r.concept}' state={r.state}\n"
426
+ f" Decision: {r.decision or '(pending)'}\n"
427
+ f" Trace: {r.decision_trace[:200]}\n"
428
+ f" Agents: {', '.join(r.agents_involved)}"
429
+ )
430
+
431
+
432
+ def _tool_devtorch_hitl_respond(repo: Any, args: dict) -> str:
433
+ from devtorch_core.consolidation import ConsolidationWorkflow
434
+ record_id = args["record_id"]
435
+ author = args["author"]
436
+ comment = args.get("comment", "")
437
+ decision = args["decision"]
438
+ workflow = ConsolidationWorkflow(repo)
439
+ record = workflow.add_human_comment(
440
+ record_id=record_id,
441
+ author=author,
442
+ comment=comment or decision,
443
+ decision=decision,
444
+ )
445
+ return f"Recorded: consolidation '{record_id[:8]}' resolved by '{author}'. State: {record.state}."
446
+
447
+
448
+ def _tool_devtorch_reasoning_plus_config(repo: Any, args: dict) -> str:
449
+ from devtorch_core.reasoning_plus import SettingsStore
450
+ config, source = SettingsStore(repo.gcc_dir).load()
451
+ return json.dumps(
452
+ {
453
+ "source": source,
454
+ "config": config.to_dict(),
455
+ },
456
+ indent=2,
457
+ )
458
+
459
+
460
+ def _tool_devtorch_reasoning_plus_set(repo: Any, args: dict) -> str:
461
+ from devtorch_core.reasoning_plus import ReasoningPlusConfig, SettingsStore
462
+ config, _ = SettingsStore(repo.gcc_dir).load()
463
+ overrides = {k: v for k, v in args.items() if v is not None and k in config.to_dict()}
464
+ config = config.with_values(**overrides)
465
+ SettingsStore(repo.gcc_dir).save_project(config)
466
+ return f"Reasoning Plus settings updated: {json.dumps(config.to_dict(), indent=2)}"
467
+
468
+
469
+ def _tool_devtorch_reasoning_plus_learning_query(repo: Any, args: dict) -> str:
470
+ from devtorch_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
471
+ config, _ = SettingsStore(repo.gcc_dir).load()
472
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
473
+ context = args["context"]
474
+ top_n = args.get("top_n")
475
+ state_hash = args.get("state_hash")
476
+ learnings = drpl.get_relevant_learnings(context, top_n=top_n, state_hash=state_hash)
477
+ session_id = args.get("session_id")
478
+ if session_id:
479
+ learnings = [l for l in learnings if session_id in l.meta.get("session_id", "") or not session_id]
480
+ result = [
481
+ {
482
+ "id": l.id,
483
+ "type": l.type,
484
+ "content": l.short_form(max_chars=300),
485
+ "confidence": l.confidence,
486
+ "validity": l.validity,
487
+ "state_hash": l.state_hash,
488
+ "trigger_concepts": l.trigger_concepts,
489
+ }
490
+ for l in learnings
491
+ ]
492
+ return json.dumps({"count": len(result), "learnings": result}, indent=2)
493
+
494
+
495
+ def _tool_devtorch_reasoning_plus_learning_purge(repo: Any, args: dict) -> str:
496
+ from devtorch_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
497
+ config, _ = SettingsStore(repo.gcc_dir).load()
498
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
499
+ scope = args.get("scope", "stale").lower()
500
+ session_id = args.get("session_id")
501
+ if scope not in ("stale", "deprecated", "all"):
502
+ raise ValueError(f"Invalid purge scope: {scope}")
503
+ target_validity = None if scope == "all" else scope
504
+ learnings = drpl._store.list(validity=target_validity)
505
+ if session_id:
506
+ learnings = [l for l in learnings if any(
507
+ drpl._recorder.get(cid) and drpl._recorder.get(cid).session_id == session_id
508
+ for cid in l.source_reasoning_ids
509
+ )]
510
+ count = 0
511
+ for learning in learnings:
512
+ drpl._store.invalidate(learning.id, reason="purged")
513
+ count += 1
514
+ return f"Purged {count} learning(s) (scope={scope})."
515
+
516
+
517
+ def _tool_devtorch_reasoning_plus_learning_feedback(repo: Any, args: dict) -> str:
518
+ from devtorch_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
519
+ config, _ = SettingsStore(repo.gcc_dir).load()
520
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
521
+ learning_id = args["learning_id"]
522
+ outcome = args["outcome"]
523
+ updated = drpl.apply_feedback(learning_id, outcome)
524
+ if updated is None:
525
+ return f"Learning {learning_id} not found."
526
+ return json.dumps(
527
+ {
528
+ "id": updated.id,
529
+ "validity": updated.validity,
530
+ "confidence": updated.confidence,
531
+ },
532
+ indent=2,
533
+ )
534
+
535
+
536
+ def _tool_devtorch_topics_list(repo: Any, args: dict) -> str:
537
+ from devtorch_core.topics import TopicStore
538
+ show_all = args.get("all", False)
539
+ store = TopicStore(repo.gcc_dir)
540
+ topics = store.list_topics(with_data_only=not show_all)
541
+ if not topics:
542
+ return "No topics found."
543
+ lines = []
544
+ for t in topics:
545
+ tag = f" [{t['source']}]" if t["source"] != "default" else ""
546
+ lines.append(f" {t['name']:<20} ({len(t['concepts'])} concepts: {', '.join(t['concepts'])}){tag}")
547
+ header = "Topics" + (" (full taxonomy)" if show_all else " with captured reasoning") + ":\n"
548
+ return header + "\n".join(lines)
549
+
550
+
551
+ def _tool_devtorch_reasoning_history(repo: Any, args: dict) -> str:
552
+ from devtorch_core.reasoning import ReasoningStore
553
+ from devtorch_core.topics import TopicStore
554
+ topic = args.get("topic")
555
+ concept = args.get("concept")
556
+ max_entries = int(args.get("max_entries", 50))
557
+ agent_id = args.get("agent_id")
558
+
559
+ if not topic and not concept:
560
+ return "Provide either 'topic' or 'concept'."
561
+
562
+ store = ReasoningStore(repo)
563
+
564
+ if concept:
565
+ entries = store.query(concept=concept, agent_id=agent_id, k_tokens=4000)
566
+ return _format_reasoning_entries(concept, entries, max_entries)
567
+
568
+ if topic:
569
+ topic_store = TopicStore(repo.gcc_dir)
570
+ concepts = topic_store.get_concepts(topic)
571
+ if not concepts:
572
+ return f"Topic '{topic}' not found."
573
+ all_lines = [f"Reasoning history for topic '{topic}' ({len(concepts)} concepts):\n"]
574
+ total = 0
575
+ for c in concepts:
576
+ entries = store.query(concept=c, agent_id=agent_id, k_tokens=4000)
577
+ if entries:
578
+ all_lines.append(_format_reasoning_entries(c, entries, max_entries - total))
579
+ total += len(entries)
580
+ if total >= max_entries:
581
+ break
582
+ all_lines.append(f"\nTotal: {total} reasoning entries across {len(concepts)} concepts (capped at {max_entries}).")
583
+ return "\n".join(all_lines)
584
+
585
+ return "No results."
586
+
587
+
588
+ def _format_reasoning_entries(label: str, entries: list, max_entries: int) -> str:
589
+ if not entries:
590
+ return f"\n═══ {label} (0 entries) ═══\n No reasoning found."
591
+ shown = entries[:max_entries] if max_entries > 0 else entries
592
+ lines = [f"\n═══ {label} ({len(entries)} entries, showing {len(shown)}) ═══"]
593
+ for e in shown:
594
+ lines.append(f" [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]")
595
+ lines.append(f" Reasoning: {e.reasoning_text}")
596
+ if e.decision_text:
597
+ lines.append(f" Decision: {e.decision_text}")
598
+ return "\n".join(lines)
599
+
600
+
601
+ def _tool_devtorch_concepts_list(repo: Any, args: dict) -> str:
602
+ from devtorch_core.concept_catalog import ConceptCatalog
603
+ learned_only = args.get("learned_only", False)
604
+ catalog = ConceptCatalog(repo.gcc_dir)
605
+ if learned_only:
606
+ learned = catalog.enumerate_learned()
607
+ if not learned:
608
+ return "No concepts with learning data found."
609
+ lines = ["Concepts learned from interactions:\n"]
610
+ for cname, stats in sorted(learned.items()):
611
+ lines.append(
612
+ f" {cname:<16} {stats['learning_count']} learnings, "
613
+ f"{stats['active']} active, "
614
+ f"{stats['success_rate']:.0%} success, "
615
+ f"conf={stats['avg_confidence']:.2f}"
616
+ )
617
+ return "\n".join(lines)
618
+ else:
619
+ all_concepts = catalog.enumerate_all()
620
+ if not all_concepts:
621
+ return "No concepts found."
622
+ lines = [f"All concepts ({len(all_concepts)} from multiple sources):\n"]
623
+ for cname, sources in all_concepts.items():
624
+ lines.append(f" {cname:<16} {', '.join(sources)}")
625
+ return "\n".join(lines)
626
+
627
+
628
+ def _tool_devtorch_concepts_related(repo: Any, args: dict) -> str:
629
+ from devtorch_core.concept_catalog import ConceptCatalog
630
+ topic = args["topic"]
631
+ catalog = ConceptCatalog(repo.gcc_dir)
632
+ result = catalog.concepts_for_topic(topic, hide_empty=True)
633
+ concepts = result.get("concepts", [])
634
+ taxonomy_only = result.get("taxonomy_only", [])
635
+ if not concepts and not taxonomy_only:
636
+ return f"Topic '{topic}' not found or has no concepts."
637
+ lines = [f"Concepts related to topic '{topic}':\n"]
638
+ for c in concepts:
639
+ parts = [f" {c['name']:<16}"]
640
+ parts.append(f"sources: {', '.join(c['sources'])}")
641
+ if "learning_count" in c:
642
+ parts.append(f"{c['learning_count']} learnings, {c['success_rate']:.0%} success")
643
+ lines.append(" — ".join(parts))
644
+ if taxonomy_only:
645
+ lines.append(f"\n Also in taxonomy (no data): {', '.join(taxonomy_only)}")
646
+ lines.append(f"\n{len(concepts)} concept(s) with data. Use 'devtorch reasoning history --topic {topic}' for full history.")
647
+ return "\n".join(lines)
648
+
649
+
650
+ def _tool_devtorch_sync_pull(repo: Any, args: dict) -> str:
651
+ import json as _json
652
+ from devtorch_core.cloud.sync_bundle import build_remote_delta, build_server_state, compress
653
+ client_state = args.get("sync_state", {})
654
+ delta = build_remote_delta(repo.gcc_dir, client_state)
655
+ server_state = build_server_state(repo.gcc_dir)
656
+ compressed = compress({
657
+ "delta": delta,
658
+ "server_state": server_state,
659
+ "counts": {k: len(v) if isinstance(v, list) else len(v.get("coordination_vector", {})) if isinstance(v, dict) else 0 for k, v in delta.items()},
660
+ })
661
+ return _json.dumps({"bundle": compressed, "server_state": server_state})
662
+
663
+
664
+ def _tool_devtorch_sync_push(repo: Any, args: dict) -> str:
665
+ import json as _json
666
+ from devtorch_core.cloud.sync_bundle import decompress, merge_push, build_server_state
667
+ from devtorch_core.cloud.sync_conflicts import ConflictLog
668
+ encoded = args.get("bundle", "")
669
+ if not encoded:
670
+ return _json.dumps({"error": "No bundle provided"})
671
+ delta = decompress(encoded)
672
+ conflict_log = ConflictLog(repo.gcc_dir)
673
+ result = merge_push(repo.gcc_dir, delta, conflict_log)
674
+ server_state = build_server_state(repo.gcc_dir)
675
+ return _json.dumps({
676
+ "counts": result["counts"],
677
+ "conflicts": result["conflicts"],
678
+ "server_state": server_state,
679
+ })
680
+
681
+
682
+ def _dispatch_tool(repo: Any, name: str, args: dict) -> str:
683
+ if name == "devtorch_commit":
684
+ return _tool_devtorch_commit(repo, args)
685
+ elif name == "devtorch_sensitivity_add":
686
+ return _tool_devtorch_sensitivity_add(repo, args)
687
+ elif name == "devtorch_context":
688
+ return _tool_devtorch_context(repo, args)
689
+ elif name == "devtorch_theta_read":
690
+ return _tool_devtorch_theta_read(repo, args)
691
+ elif name == "devtorch_reasoning_query":
692
+ return _tool_devtorch_reasoning_query(repo, args)
693
+ elif name == "devtorch_divergence_detect":
694
+ return _tool_devtorch_divergence_detect(repo, args)
695
+ elif name == "devtorch_consolidation_status":
696
+ return _tool_devtorch_consolidation_status(repo, args)
697
+ elif name == "devtorch_hitl_respond":
698
+ return _tool_devtorch_hitl_respond(repo, args)
699
+ elif name == "devtorch_reasoning_plus_config":
700
+ return _tool_devtorch_reasoning_plus_config(repo, args)
701
+ elif name == "devtorch_reasoning_plus_set":
702
+ return _tool_devtorch_reasoning_plus_set(repo, args)
703
+ elif name == "devtorch_reasoning_plus_learning_query":
704
+ return _tool_devtorch_reasoning_plus_learning_query(repo, args)
705
+ elif name == "devtorch_reasoning_plus_learning_purge":
706
+ return _tool_devtorch_reasoning_plus_learning_purge(repo, args)
707
+ elif name == "devtorch_reasoning_plus_learning_feedback":
708
+ return _tool_devtorch_reasoning_plus_learning_feedback(repo, args)
709
+ elif name == "devtorch_topics_list":
710
+ return _tool_devtorch_topics_list(repo, args)
711
+ elif name == "devtorch_reasoning_history":
712
+ return _tool_devtorch_reasoning_history(repo, args)
713
+ elif name == "devtorch_concepts_list":
714
+ return _tool_devtorch_concepts_list(repo, args)
715
+ elif name == "devtorch_concepts_related":
716
+ return _tool_devtorch_concepts_related(repo, args)
717
+ elif name == "devtorch_sync_pull":
718
+ return _tool_devtorch_sync_pull(repo, args)
719
+ elif name == "devtorch_sync_push":
720
+ return _tool_devtorch_sync_push(repo, args)
721
+ else:
722
+ raise ValueError(f"Unknown tool: {name}")
723
+
724
+
725
+ # ---------------------------------------------------------------------------
726
+ # MCP SDK path
727
+ # ---------------------------------------------------------------------------
728
+
729
+
730
+ def _run_with_sdk(repo: Any) -> None:
731
+ """Run using the official mcp Python SDK."""
732
+ import asyncio
733
+ from mcp.server import Server
734
+ from mcp.server.stdio import stdio_server
735
+ import mcp.types as types
736
+
737
+ server = Server("devtorch")
738
+
739
+ @server.list_tools() # type: ignore[misc]
740
+ async def list_tools() -> List[types.Tool]:
741
+ result = []
742
+ for t in TOOLS:
743
+ result.append(
744
+ types.Tool(
745
+ name=t["name"],
746
+ description=t["description"],
747
+ inputSchema=t["inputSchema"],
748
+ )
749
+ )
750
+ return result
751
+
752
+ @server.call_tool() # type: ignore[misc]
753
+ async def call_tool(
754
+ name: str, arguments: dict
755
+ ) -> List[types.TextContent]:
756
+ try:
757
+ text = _dispatch_tool(repo, name, arguments or {})
758
+ except Exception as exc:
759
+ text = f"Error: {exc}"
760
+ return [types.TextContent(type="text", text=text)]
761
+
762
+ async def _run() -> None:
763
+ async with stdio_server() as (read_stream, write_stream):
764
+ await server.run(
765
+ read_stream,
766
+ write_stream,
767
+ server.create_initialization_options(),
768
+ )
769
+
770
+ asyncio.run(_run())
771
+
772
+
773
+ # ---------------------------------------------------------------------------
774
+ # Minimal JSON-RPC 2.0 fallback
775
+ # ---------------------------------------------------------------------------
776
+
777
+ _JSONRPC_VERSION = "2.0"
778
+
779
+
780
+ def _make_response(id: Any, result: Any) -> dict:
781
+ return {"jsonrpc": _JSONRPC_VERSION, "id": id, "result": result}
782
+
783
+
784
+ def _make_error(id: Any, code: int, message: str) -> dict:
785
+ return {
786
+ "jsonrpc": _JSONRPC_VERSION,
787
+ "id": id,
788
+ "error": {"code": code, "message": message},
789
+ }
790
+
791
+
792
+ def _handle_request(repo: Any, req: dict) -> Optional[dict]:
793
+ """Dispatch a single JSON-RPC request and return the response dict (or None for notifications)."""
794
+ req_id = req.get("id")
795
+ method = req.get("method", "")
796
+ params = req.get("params") or {}
797
+
798
+ # Notifications (no id) — process but no response
799
+ is_notification = "id" not in req
800
+
801
+ if method == "initialize":
802
+ result = {
803
+ "protocolVersion": "2024-11-05",
804
+ "capabilities": {"tools": {}},
805
+ "serverInfo": {"name": "devtorch", "version": "0.7.0"},
806
+ }
807
+ if is_notification:
808
+ return None
809
+ return _make_response(req_id, result)
810
+
811
+ elif method == "notifications/initialized":
812
+ return None # no response for notifications
813
+
814
+ elif method == "tools/list":
815
+ result = {"tools": TOOLS}
816
+ if is_notification:
817
+ return None
818
+ return _make_response(req_id, result)
819
+
820
+ elif method == "tools/call":
821
+ name = params.get("name", "")
822
+ arguments = params.get("arguments") or {}
823
+ try:
824
+ text = _dispatch_tool(repo, name, arguments)
825
+ result = {
826
+ "content": [{"type": "text", "text": text}],
827
+ "isError": False,
828
+ }
829
+ except Exception as exc:
830
+ result = {
831
+ "content": [{"type": "text", "text": f"Error: {exc}"}],
832
+ "isError": True,
833
+ }
834
+ if is_notification:
835
+ return None
836
+ return _make_response(req_id, result)
837
+
838
+ elif method == "ping":
839
+ if is_notification:
840
+ return None
841
+ return _make_response(req_id, {})
842
+
843
+ else:
844
+ if is_notification:
845
+ return None
846
+ return _make_error(req_id, -32601, f"Method not found: {method}")
847
+
848
+
849
+ def _run_raw_stdio(repo: Any) -> None:
850
+ """Minimal JSON-RPC 2.0 stdio loop (fallback when mcp SDK not installed)."""
851
+ for raw_line in sys.stdin:
852
+ raw_line = raw_line.strip()
853
+ if not raw_line:
854
+ continue
855
+ try:
856
+ req = json.loads(raw_line)
857
+ except json.JSONDecodeError as exc:
858
+ resp = _make_error(None, -32700, f"Parse error: {exc}")
859
+ sys.stdout.write(json.dumps(resp) + "\n")
860
+ sys.stdout.flush()
861
+ continue
862
+
863
+ try:
864
+ resp = _handle_request(repo, req)
865
+ except Exception as exc:
866
+ resp = _make_error(req.get("id"), -32603, f"Internal error: {exc}")
867
+
868
+ if resp is not None:
869
+ sys.stdout.write(json.dumps(resp) + "\n")
870
+ sys.stdout.flush()
871
+
872
+
873
+ # ---------------------------------------------------------------------------
874
+ # Server entry points
875
+ # ---------------------------------------------------------------------------
876
+
877
+
878
+ def run_server(repo: Any) -> None:
879
+ """Run the MCP server using the best available transport."""
880
+ try:
881
+ import mcp # noqa: F401
882
+ _run_with_sdk(repo)
883
+ except ImportError:
884
+ _run_raw_stdio(repo)
885
+
886
+
887
+ def find_gcc_repo() -> Optional[Any]:
888
+ """
889
+ Find an initialised .GCC/ repository.
890
+
891
+ Search order:
892
+ 1. DEVTORCH_GCC_PATH env var — explicit project root (used by IDE MCP configs)
893
+ 2. Walk upward from cwd
894
+ """
895
+ import os
896
+ from devtorch_core import GCCRepository
897
+
898
+ explicit = os.environ.get("DEVTORCH_GCC_PATH", "").strip()
899
+ if explicit:
900
+ r = GCCRepository.at(Path(explicit))
901
+ if r.is_initialized():
902
+ return r
903
+
904
+ for parent in [Path.cwd()] + list(Path.cwd().parents):
905
+ r = GCCRepository.at(parent)
906
+ if r.is_initialized():
907
+ return r
908
+ return None
909
+
910
+
911
+ def main() -> None:
912
+ """Entry point: devtorch mcp-server"""
913
+ gcc = find_gcc_repo()
914
+ if gcc is None:
915
+ print(
916
+ "error: no .GCC/ found in current directory or parents. "
917
+ "Set DEVTORCH_GCC_PATH=/path/to/project to point to your project root.",
918
+ file=sys.stderr,
919
+ )
920
+ sys.exit(1)
921
+ run_server(gcc)
922
+
923
+
924
+ # ---------------------------------------------------------------------------
925
+ # SSE transport (cloud MCP server)
926
+ # ---------------------------------------------------------------------------
927
+
928
+ import asyncio as _asyncio
929
+ import uuid as _uuid_mod
930
+ import json as _json_mod
931
+ from typing import Callable as _Callable
932
+
933
+ # Global session registry: session_id → (asyncio.Queue, GCCRepository)
934
+ _session_queues: dict[str, tuple[_asyncio.Queue, Any]] = {}
935
+
936
+
937
+ def build_sse_app(
938
+ repo_factory: _Callable[[str, str], Any],
939
+ auth_backend: Any = None,
940
+ ) -> Any:
941
+ """Build the FastAPI app for the cloud SSE MCP server."""
942
+ # Use module-level _FastAPI / _Request etc. so FastAPI can resolve the
943
+ # string annotations produced by `from __future__ import annotations`.
944
+ app = _FastAPI(title="DevTorch Cloud MCP Server")
945
+
946
+ if auth_backend is not None:
947
+ from devtorch_core.mcp.auth import APIKeyMiddleware, OAuthMiddleware
948
+ import os
949
+ jwks_urls = [u for u in os.environ.get("DEVTORCH_JWKS_URLS", "").split(",") if u]
950
+ if jwks_urls:
951
+ app.add_middleware(OAuthMiddleware, jwks_urls=jwks_urls)
952
+ app.add_middleware(APIKeyMiddleware, backend=auth_backend)
953
+
954
+ @app.get("/{org_id}/{repo_id}/sse")
955
+ async def sse_endpoint(org_id: str, repo_id: str, request: _Request):
956
+ session_id = str(_uuid_mod.uuid4())
957
+ queue: _asyncio.Queue = _asyncio.Queue()
958
+ repo = repo_factory(org_id, repo_id)
959
+ _session_queues[session_id] = (queue, repo)
960
+
961
+ base_url = str(request.base_url).rstrip("/")
962
+ messages_url = f"{base_url}/{org_id}/{repo_id}/messages?sessionId={session_id}"
963
+
964
+ async def event_stream():
965
+ try:
966
+ yield f"event: endpoint\ndata: {messages_url}\n\n"
967
+ while True:
968
+ try:
969
+ item = await _asyncio.wait_for(queue.get(), timeout=30)
970
+ yield f"event: message\ndata: {_json_mod.dumps(item)}\n\n"
971
+ except _asyncio.TimeoutError:
972
+ yield ": keepalive\n\n"
973
+ finally:
974
+ _session_queues.pop(session_id, None)
975
+
976
+ return _StreamingResponse(
977
+ event_stream(),
978
+ media_type="text/event-stream",
979
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
980
+ )
981
+
982
+ @app.post("/{org_id}/{repo_id}/messages")
983
+ async def messages_endpoint(org_id: str, repo_id: str, sessionId: str, request: _Request):
984
+ entry = _session_queues.get(sessionId)
985
+ if entry is None:
986
+ raise _HTTPException(status_code=404, detail="Session not found")
987
+
988
+ queue, repo = entry
989
+ body = await request.json()
990
+
991
+ if not isinstance(body, dict):
992
+ raise _HTTPException(status_code=400, detail="Request body must be a JSON object")
993
+
994
+ try:
995
+ response = _handle_request(repo, body)
996
+ except Exception as exc:
997
+ response = _make_error(body.get("id"), -32603, str(exc))
998
+
999
+ if response is not None:
1000
+ await queue.put(response)
1001
+
1002
+ return _Response(status_code=202)
1003
+
1004
+ @app.get("/health")
1005
+ async def health():
1006
+ return {"status": "ok"}
1007
+
1008
+ @app.get("/readyz")
1009
+ async def readyz():
1010
+ return {"status": "ready"}
1011
+
1012
+ @app.post("/github/webhook")
1013
+ async def github_webhook(request: _Request) -> _Response:
1014
+ import os
1015
+ secret = os.environ.get("GITHUB_APP_WEBHOOK_SECRET", "").encode()
1016
+ if not secret:
1017
+ return _Response(status_code=501, content=b"GitHub App not configured")
1018
+
1019
+ body = await request.body()
1020
+ signature = request.headers.get("X-Hub-Signature-256", "")
1021
+ event_type = request.headers.get("X-GitHub-Event", "")
1022
+
1023
+ if auth_backend is None:
1024
+ return _Response(status_code=503, content=b"No auth backend configured")
1025
+
1026
+ from devtorch_core.github.app import GitHubAppHandler
1027
+ handler = GitHubAppHandler(backend=auth_backend, webhook_secret=secret)
1028
+ result = handler.handle_webhook(body=body, signature=signature, event_type=event_type)
1029
+
1030
+ if result["status"] == "forbidden":
1031
+ return _Response(status_code=403, content=b"Forbidden")
1032
+ return _Response(
1033
+ status_code=200,
1034
+ content=json.dumps(result).encode(),
1035
+ media_type="application/json",
1036
+ )
1037
+
1038
+ return app
1039
+
1040
+
1041
+ def _run_with_sse(
1042
+ repo_factory: _Callable[[str, str], Any],
1043
+ auth_backend: Any = None,
1044
+ host: str = "0.0.0.0",
1045
+ port: int = 8080,
1046
+ ) -> None:
1047
+ import uvicorn
1048
+ app = build_sse_app(repo_factory=repo_factory, auth_backend=auth_backend)
1049
+ uvicorn.run(app, host=host, port=port)