thinkstack-core 4.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. thinkstack_core/__init__.py +158 -0
  2. thinkstack_core/aggphi_textual.py +275 -0
  3. thinkstack_core/alerts/__init__.py +23 -0
  4. thinkstack_core/alerts/base.py +46 -0
  5. thinkstack_core/alerts/config.py +60 -0
  6. thinkstack_core/alerts/dispatcher.py +110 -0
  7. thinkstack_core/alerts/jira.py +96 -0
  8. thinkstack_core/alerts/linear.py +72 -0
  9. thinkstack_core/alerts/pagerduty.py +66 -0
  10. thinkstack_core/alerts/slack.py +81 -0
  11. thinkstack_core/alerts/teams.py +70 -0
  12. thinkstack_core/audit/__init__.py +43 -0
  13. thinkstack_core/audit/exporter.py +297 -0
  14. thinkstack_core/audit/privacy.py +101 -0
  15. thinkstack_core/audit/scrubber.py +149 -0
  16. thinkstack_core/audit/service.py +67 -0
  17. thinkstack_core/audit/signing.py +127 -0
  18. thinkstack_core/broadcast/__init__.py +4 -0
  19. thinkstack_core/broadcast/broadcaster.py +100 -0
  20. thinkstack_core/broadcast/watcher.py +71 -0
  21. thinkstack_core/capability.py +639 -0
  22. thinkstack_core/cloud/__init__.py +1 -0
  23. thinkstack_core/cloud/client_config.py +472 -0
  24. thinkstack_core/cloud/client_configs/.claude-opencode-fallback.json +8 -0
  25. thinkstack_core/cloud/client_configs/.claude-stdio.json +13 -0
  26. thinkstack_core/cloud/client_configs/.cursor-mcp.json +13 -0
  27. thinkstack_core/cloud/client_configs/.opencode-bridge.json +13 -0
  28. thinkstack_core/cloud/client_configs/.opencode.json +15 -0
  29. thinkstack_core/cloud/client_configs/.vscode-mcp.json +13 -0
  30. thinkstack_core/cloud/mcp_client.py +229 -0
  31. thinkstack_core/cloud/setup.py +144 -0
  32. thinkstack_core/cloud/sync.py +143 -0
  33. thinkstack_core/cloud/sync_bundle.py +639 -0
  34. thinkstack_core/cloud/sync_conflicts.py +183 -0
  35. thinkstack_core/cloud/sync_state.py +159 -0
  36. thinkstack_core/cloud/team_sync.py +337 -0
  37. thinkstack_core/cloud/thinkstack-mcp-bridge.js +357 -0
  38. thinkstack_core/codex/__init__.py +9 -0
  39. thinkstack_core/codex/__main__.py +97 -0
  40. thinkstack_core/codex/capture.py +208 -0
  41. thinkstack_core/codex/proxy.py +412 -0
  42. thinkstack_core/compat.py +103 -0
  43. thinkstack_core/concept_catalog.py +209 -0
  44. thinkstack_core/consolidation/__init__.py +3 -0
  45. thinkstack_core/consolidation/synthesizer.py +87 -0
  46. thinkstack_core/consolidation/workflow.py +175 -0
  47. thinkstack_core/daemon/__init__.py +27 -0
  48. thinkstack_core/daemon/supervisor.py +293 -0
  49. thinkstack_core/daemon/watcher.py +244 -0
  50. thinkstack_core/dashboard_api.py +2012 -0
  51. thinkstack_core/deltaf.py +97 -0
  52. thinkstack_core/disclosure.py +50 -0
  53. thinkstack_core/divergence/__init__.py +3 -0
  54. thinkstack_core/divergence/detector.py +166 -0
  55. thinkstack_core/gateway/__init__.py +32 -0
  56. thinkstack_core/gateway/key_manager.py +124 -0
  57. thinkstack_core/gateway/metrics_webhook.py +252 -0
  58. thinkstack_core/gateway/policy.py +262 -0
  59. thinkstack_core/gateway/server.py +727 -0
  60. thinkstack_core/gateway/sso.py +233 -0
  61. thinkstack_core/gcc.py +1246 -0
  62. thinkstack_core/github/__init__.py +35 -0
  63. thinkstack_core/github/app.py +240 -0
  64. thinkstack_core/github/comment_builder.py +113 -0
  65. thinkstack_core/github/pat.py +76 -0
  66. thinkstack_core/github/pr_parser.py +82 -0
  67. thinkstack_core/github/pr_reporter.py +555 -0
  68. thinkstack_core/gitlab/__init__.py +177 -0
  69. thinkstack_core/hitl/__init__.py +4 -0
  70. thinkstack_core/hitl/channels.py +129 -0
  71. thinkstack_core/hitl/orchestrator.py +95 -0
  72. thinkstack_core/hooks/__init__.py +17 -0
  73. thinkstack_core/hooks/claude_code.py +228 -0
  74. thinkstack_core/hooks/git_capture.py +341 -0
  75. thinkstack_core/hooks/git_commit.py +182 -0
  76. thinkstack_core/hooks/installer.py +850 -0
  77. thinkstack_core/hooks/pre_commit.py +157 -0
  78. thinkstack_core/hooks/runner.py +386 -0
  79. thinkstack_core/identity/__init__.py +4 -0
  80. thinkstack_core/identity/agent.py +86 -0
  81. thinkstack_core/identity/providers.py +85 -0
  82. thinkstack_core/invariants.py +182 -0
  83. thinkstack_core/mcp/__init__.py +10 -0
  84. thinkstack_core/mcp/auth.py +177 -0
  85. thinkstack_core/mcp/server.py +1215 -0
  86. thinkstack_core/metrics/__init__.py +35 -0
  87. thinkstack_core/metrics/aggregate.py +215 -0
  88. thinkstack_core/metrics/calibrate.py +198 -0
  89. thinkstack_core/metrics/calibration.py +125 -0
  90. thinkstack_core/metrics/credibility.py +288 -0
  91. thinkstack_core/metrics/delivery_time.py +70 -0
  92. thinkstack_core/metrics/dhs.py +126 -0
  93. thinkstack_core/metrics/mcs.py +96 -0
  94. thinkstack_core/metrics/roi.py +88 -0
  95. thinkstack_core/metrics/session_writer.py +81 -0
  96. thinkstack_core/metrics/shadow_ai.py +117 -0
  97. thinkstack_core/metrics/sprint_writer.py +243 -0
  98. thinkstack_core/observability/__init__.py +78 -0
  99. thinkstack_core/observability/datadog.py +157 -0
  100. thinkstack_core/observability/formatter.py +119 -0
  101. thinkstack_core/observability/report.py +264 -0
  102. thinkstack_core/observability/servicenow.py +147 -0
  103. thinkstack_core/observability/splunk.py +218 -0
  104. thinkstack_core/observability/webhook.py +227 -0
  105. thinkstack_core/parser/__init__.py +30 -0
  106. thinkstack_core/parser/blocks.py +216 -0
  107. thinkstack_core/parser/inference.py +159 -0
  108. thinkstack_core/parser/thinking.py +112 -0
  109. thinkstack_core/projects.py +169 -0
  110. thinkstack_core/prompt_artifact.py +76 -0
  111. thinkstack_core/proxy/__init__.py +9 -0
  112. thinkstack_core/proxy/routes/__init__.py +1 -0
  113. thinkstack_core/proxy/routes/anthropic.py +264 -0
  114. thinkstack_core/proxy/routes/azure_openai.py +336 -0
  115. thinkstack_core/proxy/routes/gemini.py +331 -0
  116. thinkstack_core/proxy/routes/groq.py +284 -0
  117. thinkstack_core/proxy/routes/ollama.py +279 -0
  118. thinkstack_core/proxy/routes/openai.py +287 -0
  119. thinkstack_core/proxy/server.py +356 -0
  120. thinkstack_core/query/__init__.py +15 -0
  121. thinkstack_core/query/grep.py +181 -0
  122. thinkstack_core/query/hybrid.py +86 -0
  123. thinkstack_core/query/semantic.py +157 -0
  124. thinkstack_core/rdp.py +105 -0
  125. thinkstack_core/reasoning/__init__.py +4 -0
  126. thinkstack_core/reasoning/entry.py +31 -0
  127. thinkstack_core/reasoning/store.py +122 -0
  128. thinkstack_core/reasoning_plus/__init__.py +70 -0
  129. thinkstack_core/reasoning_plus/augmenter.py +337 -0
  130. thinkstack_core/reasoning_plus/capture.py +51 -0
  131. thinkstack_core/reasoning_plus/config.py +313 -0
  132. thinkstack_core/reasoning_plus/context.py +262 -0
  133. thinkstack_core/reasoning_plus/learning/__init__.py +125 -0
  134. thinkstack_core/reasoning_plus/learning/analytics.py +141 -0
  135. thinkstack_core/reasoning_plus/learning/api.py +784 -0
  136. thinkstack_core/reasoning_plus/learning/chain.py +285 -0
  137. thinkstack_core/reasoning_plus/learning/composer.py +141 -0
  138. thinkstack_core/reasoning_plus/learning/conflicts.py +184 -0
  139. thinkstack_core/reasoning_plus/learning/context_collector.py +194 -0
  140. thinkstack_core/reasoning_plus/learning/cross_project.py +234 -0
  141. thinkstack_core/reasoning_plus/learning/deny_list.py +108 -0
  142. thinkstack_core/reasoning_plus/learning/embeddings.py +209 -0
  143. thinkstack_core/reasoning_plus/learning/evolution.py +119 -0
  144. thinkstack_core/reasoning_plus/learning/extractor.py +271 -0
  145. thinkstack_core/reasoning_plus/learning/filter_five_layer.py +95 -0
  146. thinkstack_core/reasoning_plus/learning/models.py +149 -0
  147. thinkstack_core/reasoning_plus/learning/org_store.py +156 -0
  148. thinkstack_core/reasoning_plus/learning/pii.py +142 -0
  149. thinkstack_core/reasoning_plus/learning/promotion.py +58 -0
  150. thinkstack_core/reasoning_plus/learning/provenance.py +126 -0
  151. thinkstack_core/reasoning_plus/learning/recorder.py +81 -0
  152. thinkstack_core/reasoning_plus/learning/relevance.py +122 -0
  153. thinkstack_core/reasoning_plus/learning/state.py +86 -0
  154. thinkstack_core/reasoning_plus/learning/store.py +178 -0
  155. thinkstack_core/reasoning_plus/learning/theta_learning_bridge.py +94 -0
  156. thinkstack_core/reasoning_plus/prompt.py +90 -0
  157. thinkstack_core/rep.py +134 -0
  158. thinkstack_core/rep_network/__init__.py +25 -0
  159. thinkstack_core/rep_network/merge.py +70 -0
  160. thinkstack_core/rep_network/node.py +137 -0
  161. thinkstack_core/rep_network/server.py +140 -0
  162. thinkstack_core/rep_network/sync.py +207 -0
  163. thinkstack_core/sensitivity.py +182 -0
  164. thinkstack_core/serve.py +258 -0
  165. thinkstack_core/session/__init__.py +39 -0
  166. thinkstack_core/session/disagreement.py +188 -0
  167. thinkstack_core/session/models.py +114 -0
  168. thinkstack_core/session/orchestrator.py +182 -0
  169. thinkstack_core/session/planner.py +169 -0
  170. thinkstack_core/session/simulator.py +132 -0
  171. thinkstack_core/signing.py +290 -0
  172. thinkstack_core/sis.py +197 -0
  173. thinkstack_core/skills/pr-reviewer/SKILL.md +204 -0
  174. thinkstack_core/skills/thinkstack-auto-sync/SKILL.md +175 -0
  175. thinkstack_core/skills/thinkstack-session-start/SKILL.md +136 -0
  176. thinkstack_core/storage.py +308 -0
  177. thinkstack_core/templates/__init__.py +6 -0
  178. thinkstack_core/templates/engine.py +122 -0
  179. thinkstack_core/templates/go.py +18 -0
  180. thinkstack_core/templates/infra.py +19 -0
  181. thinkstack_core/templates/library/__init__.py +18 -0
  182. thinkstack_core/templates/library/api_design.md +27 -0
  183. thinkstack_core/templates/library/bug_fix.md +27 -0
  184. thinkstack_core/templates/library/decision_record.md +27 -0
  185. thinkstack_core/templates/library/engine.py +228 -0
  186. thinkstack_core/templates/library/security_review.md +30 -0
  187. thinkstack_core/templates/python.py +19 -0
  188. thinkstack_core/templates/react.py +18 -0
  189. thinkstack_core/templates/typescript.py +18 -0
  190. thinkstack_core/theta.py +221 -0
  191. thinkstack_core/theta_synthesis.py +268 -0
  192. thinkstack_core/topics.py +320 -0
  193. thinkstack_core/variance.py +219 -0
  194. thinkstack_core/wrapper/__init__.py +52 -0
  195. thinkstack_core/wrapper/anthropic.py +487 -0
  196. thinkstack_core/wrapper/base.py +562 -0
  197. thinkstack_core/wrapper/bedrock.py +342 -0
  198. thinkstack_core/wrapper/gemini.py +422 -0
  199. thinkstack_core/wrapper/ollama.py +527 -0
  200. thinkstack_core/wrapper/openai.py +461 -0
  201. thinkstack_core-4.0.0.dist-info/METADATA +868 -0
  202. thinkstack_core-4.0.0.dist-info/RECORD +205 -0
  203. thinkstack_core-4.0.0.dist-info/WHEEL +5 -0
  204. thinkstack_core-4.0.0.dist-info/entry_points.txt +2 -0
  205. thinkstack_core-4.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1215 @@
1
+ """
2
+ S7 MCP Server — ThinkStack 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
+ thinkstack_commit — Commit a reasoning milestone to .GCC/
10
+ thinkstack_sensitivity_add — Record a sensitivity signal
11
+ thinkstack_context — Retrieve bounded context bundle
12
+ thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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": "thinkstack_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
+ "name": "thinkstack_sync_status",
313
+ "description": "Report syncable item counts (commits, learnings, theta concepts) for sync status",
314
+ "inputSchema": {
315
+ "type": "object",
316
+ "properties": {},
317
+ },
318
+ },
319
+ {
320
+ "name": "thinkstack_learning_search",
321
+ "description": "Search learnings across project-local and org stores by concept, type, scope, or text",
322
+ "inputSchema": {
323
+ "type": "object",
324
+ "properties": {
325
+ "query": {
326
+ "type": "string",
327
+ "description": "Search query (matches content, trigger concepts)",
328
+ },
329
+ "type": {
330
+ "type": "string",
331
+ "description": "Filter by learning type (insight/correction/pattern/prescription/canon)",
332
+ },
333
+ "scope": {
334
+ "type": "string",
335
+ "description": "Filter by scope (branch/project/org)",
336
+ },
337
+ "top_n": {
338
+ "type": "integer",
339
+ "description": "Max results (default 10)",
340
+ },
341
+ },
342
+ "required": ["query"],
343
+ },
344
+ },
345
+ {
346
+ "name": "thinkstack_learning_promote",
347
+ "description": "Promote a learning to org scope so it becomes available cross-project",
348
+ "inputSchema": {
349
+ "type": "object",
350
+ "properties": {
351
+ "learning_id": {
352
+ "type": "string",
353
+ "description": "Learning ID to promote",
354
+ },
355
+ "to_scope": {
356
+ "type": "string",
357
+ "description": "Target scope (default: org)",
358
+ },
359
+ },
360
+ "required": ["learning_id"],
361
+ },
362
+ },
363
+ {
364
+ "name": "thinkstack_learning_deny_list",
365
+ "description": "Read or update the per-project learning deny list (concepts and IDs to exclude from cross-project)",
366
+ "inputSchema": {
367
+ "type": "object",
368
+ "properties": {
369
+ "action": {
370
+ "type": "string",
371
+ "description": "show / add-concept / remove-concept / add-learning / remove-learning",
372
+ },
373
+ "value": {
374
+ "type": "string",
375
+ "description": "Concept name or learning ID for the action",
376
+ },
377
+ },
378
+ "required": ["action"],
379
+ },
380
+ },
381
+ ]
382
+
383
+ # ---------------------------------------------------------------------------
384
+ # Tool implementations
385
+ # ---------------------------------------------------------------------------
386
+
387
+
388
+ def _tool_thinkstack_commit(repo: Any, args: dict) -> str:
389
+ message = args.get("message", "")
390
+ concepts_raw = args.get("concepts", "")
391
+ concepts = [c.strip() for c in concepts_raw.split(",") if c.strip()] if concepts_raw else None
392
+ repo.commit(message=message, concepts=concepts)
393
+ parts = [f"Committed to .GCC/"]
394
+ if concepts:
395
+ parts.append(f"[concepts: {', '.join(concepts)}]")
396
+ return " ".join(parts)
397
+
398
+
399
+ def _tool_thinkstack_sensitivity_add(repo: Any, args: dict) -> str:
400
+ from thinkstack_core.sensitivity import SensitivityEvent
401
+
402
+ concept = args["concept"]
403
+ signal = args["signal"]
404
+ confidence = float(args["confidence"])
405
+ disclosure = args.get("disclosure", "PROTECTED").upper()
406
+ if disclosure not in ("PUBLIC", "PROTECTED", "PRIVATE"):
407
+ disclosure = "PROTECTED"
408
+
409
+ ev = SensitivityEvent(
410
+ source_node="mcp-client",
411
+ target_concept=concept,
412
+ counterfactuals=signal,
413
+ confidence=confidence,
414
+ disclosure_level=disclosure,
415
+ message=signal,
416
+ )
417
+ repo.add_sensitivity(ev)
418
+ return f"Sensitivity recorded: {concept} @ {confidence}"
419
+
420
+
421
+ def _tool_thinkstack_context(repo: Any, args: dict) -> str:
422
+ k = int(args.get("k", 8000))
423
+ bundle = repo.context_bundle(
424
+ k_tokens=k, policy={"sensitivity_policy": "default"}
425
+ )
426
+ # bundle is a dict; format it as a readable string
427
+ artifacts = bundle.get("artifacts", [])
428
+ if not artifacts:
429
+ return f"Context bundle (budget={k} tokens): no artifacts found."
430
+
431
+ lines = [f"Context bundle (budget={k} tokens, {len(artifacts)} artifacts):"]
432
+ for art in artifacts:
433
+ # Support both "name" (test mock) and "path" (real GCCRepository output)
434
+ name = art.get("name", art.get("path", art.get("id", "unknown")))
435
+ reason = art.get("reason", "")
436
+ # Support both "tokens" (test mock) and "approx_tokens" (real GCCRepository output)
437
+ tokens = art.get("tokens", art.get("approx_tokens", "?"))
438
+ lines.append(f" - {name} ({tokens} tokens): {reason}")
439
+ return "\n".join(lines)
440
+
441
+
442
+ def _tool_thinkstack_theta_read(repo: Any, args: dict) -> str:
443
+ theta = repo.get_theta().get("coordination_vector", {})
444
+ concepts = args.get("concepts")
445
+ if concepts:
446
+ theta = {k: v for k, v in theta.items() if k in concepts}
447
+ return json.dumps(theta, indent=2)
448
+
449
+
450
+ def _tool_thinkstack_reasoning_query(repo: Any, args: dict) -> str:
451
+ from thinkstack_core.reasoning import ReasoningStore
452
+
453
+ concept = args["concept"]
454
+ agent_id = args.get("agent_id")
455
+ scope = args.get("scope", "branch")
456
+ k = int(args.get("k", 4000))
457
+
458
+ store = ReasoningStore(repo)
459
+ entries = store.query(concept=concept, agent_id=agent_id, scope=scope, k_tokens=k)
460
+
461
+ if not entries:
462
+ return f"No reasoning entries found for concept '{concept}'."
463
+
464
+ lines = [f"Reasoning for '{concept}' ({len(entries)} entries, budget={k} tokens):"]
465
+ for e in entries:
466
+ lines.append(
467
+ f"\n [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]"
468
+ )
469
+ lines.append(f" Reasoning: {e.reasoning_text}")
470
+ if e.decision_text:
471
+ lines.append(f" Decision: {e.decision_text}")
472
+ return "\n".join(lines)
473
+
474
+
475
+ def _tool_thinkstack_divergence_detect(repo: Any, args: dict) -> str:
476
+ from thinkstack_core.divergence import DivergenceDetector
477
+ concept = args["concept"]
478
+ signals = DivergenceDetector(repo).detect(concept=concept)
479
+ if not signals:
480
+ return f"No divergence detected for concept '{concept}'."
481
+ lines = [f"Divergence signals for '{concept}' ({len(signals)} signals):"]
482
+ for s in signals:
483
+ lines.append(f" [{s.signal_type}/{s.severity}] {s.description}")
484
+ return "\n".join(lines)
485
+
486
+
487
+ def _tool_thinkstack_consolidation_status(repo: Any, args: dict) -> str:
488
+ from thinkstack_core.consolidation import ConsolidationWorkflow
489
+ concept = args["concept"]
490
+ records = ConsolidationWorkflow(repo).list_for_concept(concept)
491
+ if not records:
492
+ return f"No consolidation records found for concept '{concept}'."
493
+ r = records[0] # most recent
494
+ return (
495
+ f"Consolidation '{r.record_id[:8]}' — concept='{r.concept}' state={r.state}\n"
496
+ f" Decision: {r.decision or '(pending)'}\n"
497
+ f" Trace: {r.decision_trace[:200]}\n"
498
+ f" Agents: {', '.join(r.agents_involved)}"
499
+ )
500
+
501
+
502
+ def _tool_thinkstack_hitl_respond(repo: Any, args: dict) -> str:
503
+ from thinkstack_core.consolidation import ConsolidationWorkflow
504
+ record_id = args["record_id"]
505
+ author = args["author"]
506
+ comment = args.get("comment", "")
507
+ decision = args["decision"]
508
+ workflow = ConsolidationWorkflow(repo)
509
+ record = workflow.add_human_comment(
510
+ record_id=record_id,
511
+ author=author,
512
+ comment=comment or decision,
513
+ decision=decision,
514
+ )
515
+ return f"Recorded: consolidation '{record_id[:8]}' resolved by '{author}'. State: {record.state}."
516
+
517
+
518
+ def _tool_thinkstack_reasoning_plus_config(repo: Any, args: dict) -> str:
519
+ from thinkstack_core.reasoning_plus import SettingsStore
520
+ config, source = SettingsStore(repo.gcc_dir).load()
521
+ return json.dumps(
522
+ {
523
+ "source": source,
524
+ "config": config.to_dict(),
525
+ },
526
+ indent=2,
527
+ )
528
+
529
+
530
+ def _tool_thinkstack_reasoning_plus_set(repo: Any, args: dict) -> str:
531
+ from thinkstack_core.reasoning_plus import ReasoningPlusConfig, SettingsStore
532
+ config, _ = SettingsStore(repo.gcc_dir).load()
533
+ overrides = {k: v for k, v in args.items() if v is not None and k in config.to_dict()}
534
+ config = config.with_values(**overrides)
535
+ SettingsStore(repo.gcc_dir).save_project(config)
536
+ return f"Reasoning Plus settings updated: {json.dumps(config.to_dict(), indent=2)}"
537
+
538
+
539
+ def _tool_thinkstack_reasoning_plus_learning_query(repo: Any, args: dict) -> str:
540
+ from thinkstack_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
541
+ config, _ = SettingsStore(repo.gcc_dir).load()
542
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
543
+ context = args["context"]
544
+ top_n = args.get("top_n")
545
+ state_hash = args.get("state_hash")
546
+ learnings = drpl.get_relevant_learnings(context, top_n=top_n, state_hash=state_hash)
547
+ session_id = args.get("session_id")
548
+ if session_id:
549
+ learnings = [l for l in learnings if session_id in l.meta.get("session_id", "") or not session_id]
550
+ result = [
551
+ {
552
+ "id": l.id,
553
+ "type": l.type,
554
+ "content": l.short_form(max_chars=300),
555
+ "confidence": l.confidence,
556
+ "validity": l.validity,
557
+ "state_hash": l.state_hash,
558
+ "trigger_concepts": l.trigger_concepts,
559
+ }
560
+ for l in learnings
561
+ ]
562
+ return json.dumps({"count": len(result), "learnings": result}, indent=2)
563
+
564
+
565
+ def _tool_thinkstack_reasoning_plus_learning_purge(repo: Any, args: dict) -> str:
566
+ from thinkstack_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
567
+ config, _ = SettingsStore(repo.gcc_dir).load()
568
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
569
+ scope = args.get("scope", "stale").lower()
570
+ session_id = args.get("session_id")
571
+ if scope not in ("stale", "deprecated", "all"):
572
+ raise ValueError(f"Invalid purge scope: {scope}")
573
+ target_validity = None if scope == "all" else scope
574
+ learnings = drpl._store.list(validity=target_validity)
575
+ if session_id:
576
+ learnings = [l for l in learnings if any(
577
+ drpl._recorder.get(cid) and drpl._recorder.get(cid).session_id == session_id
578
+ for cid in l.source_reasoning_ids
579
+ )]
580
+ count = 0
581
+ for learning in learnings:
582
+ drpl._store.invalidate(learning.id, reason="purged")
583
+ count += 1
584
+ return f"Purged {count} learning(s) (scope={scope})."
585
+
586
+
587
+ def _tool_thinkstack_reasoning_plus_learning_feedback(repo: Any, args: dict) -> str:
588
+ from thinkstack_core.reasoning_plus import ReasoningPlusConfig, ReasoningPlusLearning, SettingsStore
589
+ config, _ = SettingsStore(repo.gcc_dir).load()
590
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
591
+ learning_id = args["learning_id"]
592
+ outcome = args["outcome"]
593
+ updated = drpl.apply_feedback(learning_id, outcome)
594
+ if updated is None:
595
+ return f"Learning {learning_id} not found."
596
+ return json.dumps(
597
+ {
598
+ "id": updated.id,
599
+ "validity": updated.validity,
600
+ "confidence": updated.confidence,
601
+ },
602
+ indent=2,
603
+ )
604
+
605
+
606
+ def _tool_thinkstack_topics_list(repo: Any, args: dict) -> str:
607
+ from thinkstack_core.topics import TopicStore
608
+ show_all = args.get("all", False)
609
+ store = TopicStore(repo.gcc_dir)
610
+ topics = store.list_topics(with_data_only=not show_all)
611
+ if not topics:
612
+ return "No topics found."
613
+ lines = []
614
+ for t in topics:
615
+ tag = f" [{t['source']}]" if t["source"] != "default" else ""
616
+ lines.append(f" {t['name']:<20} ({len(t['concepts'])} concepts: {', '.join(t['concepts'])}){tag}")
617
+ header = "Topics" + (" (full taxonomy)" if show_all else " with captured reasoning") + ":\n"
618
+ return header + "\n".join(lines)
619
+
620
+
621
+ def _tool_thinkstack_reasoning_history(repo: Any, args: dict) -> str:
622
+ from thinkstack_core.reasoning import ReasoningStore
623
+ from thinkstack_core.topics import TopicStore
624
+ topic = args.get("topic")
625
+ concept = args.get("concept")
626
+ max_entries = int(args.get("max_entries", 50))
627
+ agent_id = args.get("agent_id")
628
+
629
+ if not topic and not concept:
630
+ return "Provide either 'topic' or 'concept'."
631
+
632
+ store = ReasoningStore(repo)
633
+
634
+ if concept:
635
+ entries = store.query(concept=concept, agent_id=agent_id, k_tokens=4000)
636
+ return _format_reasoning_entries(concept, entries, max_entries)
637
+
638
+ if topic:
639
+ topic_store = TopicStore(repo.gcc_dir)
640
+ concepts = topic_store.get_concepts(topic)
641
+ if not concepts:
642
+ return f"Topic '{topic}' not found."
643
+ all_lines = [f"Reasoning history for topic '{topic}' ({len(concepts)} concepts):\n"]
644
+ total = 0
645
+ for c in concepts:
646
+ entries = store.query(concept=c, agent_id=agent_id, k_tokens=4000)
647
+ if entries:
648
+ all_lines.append(_format_reasoning_entries(c, entries, max_entries - total))
649
+ total += len(entries)
650
+ if total >= max_entries:
651
+ break
652
+ all_lines.append(f"\nTotal: {total} reasoning entries across {len(concepts)} concepts (capped at {max_entries}).")
653
+ return "\n".join(all_lines)
654
+
655
+ return "No results."
656
+
657
+
658
+ def _format_reasoning_entries(label: str, entries: list, max_entries: int) -> str:
659
+ if not entries:
660
+ return f"\n═══ {label} (0 entries) ═══\n No reasoning found."
661
+ shown = entries[:max_entries] if max_entries > 0 else entries
662
+ lines = [f"\n═══ {label} ({len(entries)} entries, showing {len(shown)}) ═══"]
663
+ for e in shown:
664
+ lines.append(f" [{e.timestamp[:19]}] agent={e.agent_id} conf={e.confidence:.2f} [{e.disclosure_level}]")
665
+ lines.append(f" Reasoning: {e.reasoning_text}")
666
+ if e.decision_text:
667
+ lines.append(f" Decision: {e.decision_text}")
668
+ return "\n".join(lines)
669
+
670
+
671
+ def _tool_thinkstack_concepts_list(repo: Any, args: dict) -> str:
672
+ from thinkstack_core.concept_catalog import ConceptCatalog
673
+ learned_only = args.get("learned_only", False)
674
+ catalog = ConceptCatalog(repo.gcc_dir)
675
+ if learned_only:
676
+ learned = catalog.enumerate_learned()
677
+ if not learned:
678
+ return "No concepts with learning data found."
679
+ lines = ["Concepts learned from interactions:\n"]
680
+ for cname, stats in sorted(learned.items()):
681
+ lines.append(
682
+ f" {cname:<16} {stats['learning_count']} learnings, "
683
+ f"{stats['active']} active, "
684
+ f"{stats['success_rate']:.0%} success, "
685
+ f"conf={stats['avg_confidence']:.2f}"
686
+ )
687
+ return "\n".join(lines)
688
+ else:
689
+ all_concepts = catalog.enumerate_all()
690
+ if not all_concepts:
691
+ return "No concepts found."
692
+ lines = [f"All concepts ({len(all_concepts)} from multiple sources):\n"]
693
+ for cname, sources in all_concepts.items():
694
+ lines.append(f" {cname:<16} {', '.join(sources)}")
695
+ return "\n".join(lines)
696
+
697
+
698
+ def _tool_thinkstack_concepts_related(repo: Any, args: dict) -> str:
699
+ from thinkstack_core.concept_catalog import ConceptCatalog
700
+ topic = args["topic"]
701
+ catalog = ConceptCatalog(repo.gcc_dir)
702
+ result = catalog.concepts_for_topic(topic, hide_empty=True)
703
+ concepts = result.get("concepts", [])
704
+ taxonomy_only = result.get("taxonomy_only", [])
705
+ if not concepts and not taxonomy_only:
706
+ return f"Topic '{topic}' not found or has no concepts."
707
+ lines = [f"Concepts related to topic '{topic}':\n"]
708
+ for c in concepts:
709
+ parts = [f" {c['name']:<16}"]
710
+ parts.append(f"sources: {', '.join(c['sources'])}")
711
+ if "learning_count" in c:
712
+ parts.append(f"{c['learning_count']} learnings, {c['success_rate']:.0%} success")
713
+ lines.append(" — ".join(parts))
714
+ if taxonomy_only:
715
+ lines.append(f"\n Also in taxonomy (no data): {', '.join(taxonomy_only)}")
716
+ lines.append(f"\n{len(concepts)} concept(s) with data. Use 'thinkstack reasoning history --topic {topic}' for full history.")
717
+ return "\n".join(lines)
718
+
719
+
720
+ def _tool_thinkstack_sync_pull(repo: Any, args: dict) -> str:
721
+ import json as _json
722
+ from thinkstack_core.cloud.sync_bundle import build_remote_delta, build_server_state, compress
723
+ client_state = args.get("sync_state", {})
724
+ delta = build_remote_delta(repo.gcc_dir, client_state)
725
+ server_state = build_server_state(repo.gcc_dir)
726
+ compressed = compress({
727
+ "delta": delta,
728
+ "server_state": server_state,
729
+ "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()},
730
+ })
731
+ return _json.dumps({"bundle": compressed, "server_state": server_state})
732
+
733
+
734
+ def _tool_thinkstack_sync_push(repo: Any, args: dict) -> str:
735
+ import json as _json
736
+ from thinkstack_core.cloud.sync_bundle import decompress, merge_push, build_server_state
737
+ from thinkstack_core.cloud.sync_conflicts import ConflictLog
738
+ encoded = args.get("bundle", "")
739
+ if not encoded:
740
+ return _json.dumps({"error": "No bundle provided"})
741
+ delta = decompress(encoded)
742
+ conflict_log = ConflictLog(repo.gcc_dir)
743
+ result = merge_push(repo.gcc_dir, delta, conflict_log)
744
+ server_state = build_server_state(repo.gcc_dir)
745
+ return _json.dumps({
746
+ "counts": result["counts"],
747
+ "conflicts": result["conflicts"],
748
+ "server_state": server_state,
749
+ })
750
+
751
+
752
+ def _tool_thinkstack_sync_status(repo: Any, args: dict) -> str:
753
+ import json as _json
754
+ from thinkstack_core.cloud.sync_bundle import build_sync_status
755
+ return _json.dumps(build_sync_status(repo.gcc_dir))
756
+
757
+
758
+ def _tool_thinkstack_learning_search(repo: Any, args: dict) -> str:
759
+ import json as _json
760
+ from thinkstack_core.reasoning_plus.learning.api import ReasoningPlusLearning
761
+ from thinkstack_core.reasoning_plus.config import ReasoningPlusConfig
762
+
763
+ config = ReasoningPlusConfig()
764
+ drpl = ReasoningPlusLearning(gcc_dir=repo.gcc_dir, repo_path=repo.root, config=config)
765
+ results = drpl.learning_search(
766
+ query=args.get("query", ""),
767
+ search_type=args.get("type"),
768
+ scope=args.get("scope"),
769
+ top_n=args.get("top_n", 10),
770
+ )
771
+ return _json.dumps({
772
+ "count": len(results),
773
+ "results": [
774
+ {
775
+ "id": l.id,
776
+ "content": l.short_form(),
777
+ "type": l.type,
778
+ "scope": l.scope,
779
+ "confidence": l.confidence,
780
+ "concepts": l.trigger_concepts,
781
+ }
782
+ for l in results
783
+ ],
784
+ })
785
+
786
+
787
+ def _tool_thinkstack_learning_promote(repo: Any, args: dict) -> str:
788
+ import json as _json
789
+ from thinkstack_core.reasoning_plus.learning.promotion import promote_learning
790
+
791
+ result = promote_learning(
792
+ learning_id=args.get("learning_id", ""),
793
+ gcc_dir=repo.gcc_dir,
794
+ to_scope=args.get("to_scope", "org"),
795
+ promoted_by="mcp",
796
+ )
797
+ if result is None:
798
+ return _json.dumps({"error": "Learning not found or cannot be promoted"})
799
+ return _json.dumps({
800
+ "id": result.id,
801
+ "scope": result.scope,
802
+ "type": result.type,
803
+ "confidence": result.confidence,
804
+ "promoted_at": result.promoted_at,
805
+ })
806
+
807
+
808
+ def _tool_thinkstack_learning_deny_list(repo: Any, args: dict) -> str:
809
+ import json as _json
810
+ from thinkstack_core.reasoning_plus.learning.deny_list import (
811
+ DenyList, load_deny_list, save_deny_list,
812
+ )
813
+
814
+ action = args.get("action", "show")
815
+ value = args.get("value", "")
816
+
817
+ dl = load_deny_list(repo.gcc_dir)
818
+ if action == "show":
819
+ return _json.dumps(dl.to_dict())
820
+ elif action == "add-concept":
821
+ dl.add_concept(value)
822
+ save_deny_list(dl, repo.gcc_dir)
823
+ return _json.dumps({"status": "added", "excluded_concepts": dl.excluded_concepts})
824
+ elif action == "remove-concept":
825
+ ok = dl.remove_concept(value)
826
+ save_deny_list(dl, repo.gcc_dir)
827
+ return _json.dumps({"status": "removed" if ok else "not_found", "excluded_concepts": dl.excluded_concepts})
828
+ elif action == "add-learning":
829
+ dl.add_learning(value)
830
+ save_deny_list(dl, repo.gcc_dir)
831
+ return _json.dumps({"status": "added", "excluded_learnings": dl.excluded_learnings})
832
+ elif action == "remove-learning":
833
+ ok = dl.remove_learning(value)
834
+ save_deny_list(dl, repo.gcc_dir)
835
+ return _json.dumps({"status": "removed" if ok else "not_found", "excluded_learnings": dl.excluded_learnings})
836
+ else:
837
+ return _json.dumps({"error": f"Unknown action: {action}"})
838
+
839
+
840
+ def _dispatch_tool(repo: Any, name: str, args: dict) -> str:
841
+ if name == "thinkstack_commit":
842
+ return _tool_thinkstack_commit(repo, args)
843
+ elif name == "thinkstack_sensitivity_add":
844
+ return _tool_thinkstack_sensitivity_add(repo, args)
845
+ elif name == "thinkstack_context":
846
+ return _tool_thinkstack_context(repo, args)
847
+ elif name == "thinkstack_theta_read":
848
+ return _tool_thinkstack_theta_read(repo, args)
849
+ elif name == "thinkstack_reasoning_query":
850
+ return _tool_thinkstack_reasoning_query(repo, args)
851
+ elif name == "thinkstack_divergence_detect":
852
+ return _tool_thinkstack_divergence_detect(repo, args)
853
+ elif name == "thinkstack_consolidation_status":
854
+ return _tool_thinkstack_consolidation_status(repo, args)
855
+ elif name == "thinkstack_hitl_respond":
856
+ return _tool_thinkstack_hitl_respond(repo, args)
857
+ elif name == "thinkstack_reasoning_plus_config":
858
+ return _tool_thinkstack_reasoning_plus_config(repo, args)
859
+ elif name == "thinkstack_reasoning_plus_set":
860
+ return _tool_thinkstack_reasoning_plus_set(repo, args)
861
+ elif name == "thinkstack_reasoning_plus_learning_query":
862
+ return _tool_thinkstack_reasoning_plus_learning_query(repo, args)
863
+ elif name == "thinkstack_reasoning_plus_learning_purge":
864
+ return _tool_thinkstack_reasoning_plus_learning_purge(repo, args)
865
+ elif name == "thinkstack_reasoning_plus_learning_feedback":
866
+ return _tool_thinkstack_reasoning_plus_learning_feedback(repo, args)
867
+ elif name == "thinkstack_topics_list":
868
+ return _tool_thinkstack_topics_list(repo, args)
869
+ elif name == "thinkstack_reasoning_history":
870
+ return _tool_thinkstack_reasoning_history(repo, args)
871
+ elif name == "thinkstack_concepts_list":
872
+ return _tool_thinkstack_concepts_list(repo, args)
873
+ elif name == "thinkstack_concepts_related":
874
+ return _tool_thinkstack_concepts_related(repo, args)
875
+ elif name == "thinkstack_sync_pull":
876
+ return _tool_thinkstack_sync_pull(repo, args)
877
+ elif name == "thinkstack_sync_push":
878
+ return _tool_thinkstack_sync_push(repo, args)
879
+ elif name == "thinkstack_sync_status":
880
+ return _tool_thinkstack_sync_status(repo, args)
881
+ elif name == "thinkstack_learning_search":
882
+ return _tool_thinkstack_learning_search(repo, args)
883
+ elif name == "thinkstack_learning_promote":
884
+ return _tool_thinkstack_learning_promote(repo, args)
885
+ elif name == "thinkstack_learning_deny_list":
886
+ return _tool_thinkstack_learning_deny_list(repo, args)
887
+ else:
888
+ raise ValueError(f"Unknown tool: {name}")
889
+
890
+
891
+ # ---------------------------------------------------------------------------
892
+ # MCP SDK path
893
+ # ---------------------------------------------------------------------------
894
+
895
+
896
+ def _run_with_sdk(repo: Any) -> None:
897
+ """Run using the official mcp Python SDK."""
898
+ import asyncio
899
+ from mcp.server import Server
900
+ from mcp.server.stdio import stdio_server
901
+ import mcp.types as types
902
+
903
+ server = Server("thinkstack")
904
+
905
+ @server.list_tools() # type: ignore[misc]
906
+ async def list_tools() -> List[types.Tool]:
907
+ result = []
908
+ for t in TOOLS:
909
+ result.append(
910
+ types.Tool(
911
+ name=t["name"],
912
+ description=t["description"],
913
+ inputSchema=t["inputSchema"],
914
+ )
915
+ )
916
+ return result
917
+
918
+ @server.call_tool() # type: ignore[misc]
919
+ async def call_tool(
920
+ name: str, arguments: dict
921
+ ) -> List[types.TextContent]:
922
+ try:
923
+ text = _dispatch_tool(repo, name, arguments or {})
924
+ except Exception as exc:
925
+ text = f"Error: {exc}"
926
+ return [types.TextContent(type="text", text=text)]
927
+
928
+ async def _run() -> None:
929
+ async with stdio_server() as (read_stream, write_stream):
930
+ await server.run(
931
+ read_stream,
932
+ write_stream,
933
+ server.create_initialization_options(),
934
+ )
935
+
936
+ asyncio.run(_run())
937
+
938
+
939
+ # ---------------------------------------------------------------------------
940
+ # Minimal JSON-RPC 2.0 fallback
941
+ # ---------------------------------------------------------------------------
942
+
943
+ _JSONRPC_VERSION = "2.0"
944
+
945
+
946
+ def _make_response(id: Any, result: Any) -> dict:
947
+ return {"jsonrpc": _JSONRPC_VERSION, "id": id, "result": result}
948
+
949
+
950
+ def _make_error(id: Any, code: int, message: str) -> dict:
951
+ return {
952
+ "jsonrpc": _JSONRPC_VERSION,
953
+ "id": id,
954
+ "error": {"code": code, "message": message},
955
+ }
956
+
957
+
958
+ def _handle_request(repo: Any, req: dict) -> Optional[dict]:
959
+ """Dispatch a single JSON-RPC request and return the response dict (or None for notifications)."""
960
+ req_id = req.get("id")
961
+ method = req.get("method", "")
962
+ params = req.get("params") or {}
963
+
964
+ # Notifications (no id) — process but no response
965
+ is_notification = "id" not in req
966
+
967
+ if method == "initialize":
968
+ result = {
969
+ "protocolVersion": "2024-11-05",
970
+ "capabilities": {"tools": {}},
971
+ "serverInfo": {"name": "thinkstack", "version": "0.7.0"},
972
+ }
973
+ if is_notification:
974
+ return None
975
+ return _make_response(req_id, result)
976
+
977
+ elif method == "notifications/initialized":
978
+ return None # no response for notifications
979
+
980
+ elif method == "tools/list":
981
+ result = {"tools": TOOLS}
982
+ if is_notification:
983
+ return None
984
+ return _make_response(req_id, result)
985
+
986
+ elif method == "tools/call":
987
+ name = params.get("name", "")
988
+ arguments = params.get("arguments") or {}
989
+ try:
990
+ text = _dispatch_tool(repo, name, arguments)
991
+ result = {
992
+ "content": [{"type": "text", "text": text}],
993
+ "isError": False,
994
+ }
995
+ except Exception as exc:
996
+ result = {
997
+ "content": [{"type": "text", "text": f"Error: {exc}"}],
998
+ "isError": True,
999
+ }
1000
+ if is_notification:
1001
+ return None
1002
+ return _make_response(req_id, result)
1003
+
1004
+ elif method == "ping":
1005
+ if is_notification:
1006
+ return None
1007
+ return _make_response(req_id, {})
1008
+
1009
+ else:
1010
+ if is_notification:
1011
+ return None
1012
+ return _make_error(req_id, -32601, f"Method not found: {method}")
1013
+
1014
+
1015
+ def _run_raw_stdio(repo: Any) -> None:
1016
+ """Minimal JSON-RPC 2.0 stdio loop (fallback when mcp SDK not installed)."""
1017
+ for raw_line in sys.stdin:
1018
+ raw_line = raw_line.strip()
1019
+ if not raw_line:
1020
+ continue
1021
+ try:
1022
+ req = json.loads(raw_line)
1023
+ except json.JSONDecodeError as exc:
1024
+ resp = _make_error(None, -32700, f"Parse error: {exc}")
1025
+ sys.stdout.write(json.dumps(resp) + "\n")
1026
+ sys.stdout.flush()
1027
+ continue
1028
+
1029
+ try:
1030
+ resp = _handle_request(repo, req)
1031
+ except Exception as exc:
1032
+ resp = _make_error(req.get("id"), -32603, f"Internal error: {exc}")
1033
+
1034
+ if resp is not None:
1035
+ sys.stdout.write(json.dumps(resp) + "\n")
1036
+ sys.stdout.flush()
1037
+
1038
+
1039
+ # ---------------------------------------------------------------------------
1040
+ # Server entry points
1041
+ # ---------------------------------------------------------------------------
1042
+
1043
+
1044
+ def run_server(repo: Any) -> None:
1045
+ """Run the MCP server using the best available transport."""
1046
+ try:
1047
+ import mcp # noqa: F401
1048
+ _run_with_sdk(repo)
1049
+ except ImportError:
1050
+ _run_raw_stdio(repo)
1051
+
1052
+
1053
+ def find_gcc_repo() -> Optional[Any]:
1054
+ """
1055
+ Find an initialised .GCC/ repository.
1056
+
1057
+ Search order:
1058
+ 1. THINKSTACK_GCC_PATH env var — explicit project root (used by IDE MCP configs)
1059
+ 2. Walk upward from cwd
1060
+ """
1061
+ import os
1062
+ from thinkstack_core import GCCRepository
1063
+
1064
+ explicit = os.environ.get("THINKSTACK_GCC_PATH", "").strip()
1065
+ if explicit:
1066
+ r = GCCRepository.at(Path(explicit))
1067
+ if r.is_initialized():
1068
+ return r
1069
+
1070
+ for parent in [Path.cwd()] + list(Path.cwd().parents):
1071
+ r = GCCRepository.at(parent)
1072
+ if r.is_initialized():
1073
+ return r
1074
+ return None
1075
+
1076
+
1077
+ def main() -> None:
1078
+ """Entry point: thinkstack mcp-server"""
1079
+ gcc = find_gcc_repo()
1080
+ if gcc is None:
1081
+ print(
1082
+ "error: no .GCC/ found in current directory or parents. "
1083
+ "Set THINKSTACK_GCC_PATH=/path/to/project to point to your project root.",
1084
+ file=sys.stderr,
1085
+ )
1086
+ sys.exit(1)
1087
+ run_server(gcc)
1088
+
1089
+
1090
+ # ---------------------------------------------------------------------------
1091
+ # SSE transport (cloud MCP server)
1092
+ # ---------------------------------------------------------------------------
1093
+
1094
+ import asyncio as _asyncio
1095
+ import uuid as _uuid_mod
1096
+ import json as _json_mod
1097
+ from typing import Callable as _Callable
1098
+
1099
+ # Global session registry: session_id → (asyncio.Queue, GCCRepository)
1100
+ _session_queues: dict[str, tuple[_asyncio.Queue, Any]] = {}
1101
+
1102
+
1103
+ def build_sse_app(
1104
+ repo_factory: _Callable[[str, str], Any],
1105
+ auth_backend: Any = None,
1106
+ ) -> Any:
1107
+ """Build the FastAPI app for the cloud SSE MCP server."""
1108
+ # Use module-level _FastAPI / _Request etc. so FastAPI can resolve the
1109
+ # string annotations produced by `from __future__ import annotations`.
1110
+ app = _FastAPI(title="ThinkStack Cloud MCP Server")
1111
+
1112
+ if auth_backend is not None:
1113
+ from thinkstack_core.mcp.auth import APIKeyMiddleware, OAuthMiddleware
1114
+ import os
1115
+ jwks_urls = [u for u in os.environ.get("THINKSTACK_JWKS_URLS", "").split(",") if u]
1116
+ if jwks_urls:
1117
+ app.add_middleware(OAuthMiddleware, jwks_urls=jwks_urls)
1118
+ app.add_middleware(APIKeyMiddleware, backend=auth_backend)
1119
+
1120
+ @app.get("/{org_id}/{repo_id}/sse")
1121
+ async def sse_endpoint(org_id: str, repo_id: str, request: _Request):
1122
+ session_id = str(_uuid_mod.uuid4())
1123
+ queue: _asyncio.Queue = _asyncio.Queue()
1124
+ repo = repo_factory(org_id, repo_id)
1125
+ _session_queues[session_id] = (queue, repo)
1126
+
1127
+ base_url = str(request.base_url).rstrip("/")
1128
+ messages_url = f"{base_url}/{org_id}/{repo_id}/messages?sessionId={session_id}"
1129
+
1130
+ async def event_stream():
1131
+ try:
1132
+ yield f"event: endpoint\ndata: {messages_url}\n\n"
1133
+ while True:
1134
+ try:
1135
+ item = await _asyncio.wait_for(queue.get(), timeout=30)
1136
+ yield f"event: message\ndata: {_json_mod.dumps(item)}\n\n"
1137
+ except _asyncio.TimeoutError:
1138
+ yield ": keepalive\n\n"
1139
+ finally:
1140
+ _session_queues.pop(session_id, None)
1141
+
1142
+ return _StreamingResponse(
1143
+ event_stream(),
1144
+ media_type="text/event-stream",
1145
+ headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"},
1146
+ )
1147
+
1148
+ @app.post("/{org_id}/{repo_id}/messages")
1149
+ async def messages_endpoint(org_id: str, repo_id: str, sessionId: str, request: _Request):
1150
+ entry = _session_queues.get(sessionId)
1151
+ if entry is None:
1152
+ raise _HTTPException(status_code=404, detail="Session not found")
1153
+
1154
+ queue, repo = entry
1155
+ body = await request.json()
1156
+
1157
+ if not isinstance(body, dict):
1158
+ raise _HTTPException(status_code=400, detail="Request body must be a JSON object")
1159
+
1160
+ try:
1161
+ response = _handle_request(repo, body)
1162
+ except Exception as exc:
1163
+ response = _make_error(body.get("id"), -32603, str(exc))
1164
+
1165
+ if response is not None:
1166
+ await queue.put(response)
1167
+
1168
+ return _Response(status_code=202)
1169
+
1170
+ @app.get("/health")
1171
+ async def health():
1172
+ return {"status": "ok"}
1173
+
1174
+ @app.get("/readyz")
1175
+ async def readyz():
1176
+ return {"status": "ready"}
1177
+
1178
+ @app.post("/github/webhook")
1179
+ async def github_webhook(request: _Request) -> _Response:
1180
+ import os
1181
+ secret = os.environ.get("GITHUB_APP_WEBHOOK_SECRET", "").encode()
1182
+ if not secret:
1183
+ return _Response(status_code=501, content=b"GitHub App not configured")
1184
+
1185
+ body = await request.body()
1186
+ signature = request.headers.get("X-Hub-Signature-256", "")
1187
+ event_type = request.headers.get("X-GitHub-Event", "")
1188
+
1189
+ if auth_backend is None:
1190
+ return _Response(status_code=503, content=b"No auth backend configured")
1191
+
1192
+ from thinkstack_core.github.app import GitHubAppHandler
1193
+ handler = GitHubAppHandler(backend=auth_backend, webhook_secret=secret)
1194
+ result = handler.handle_webhook(body=body, signature=signature, event_type=event_type)
1195
+
1196
+ if result["status"] == "forbidden":
1197
+ return _Response(status_code=403, content=b"Forbidden")
1198
+ return _Response(
1199
+ status_code=200,
1200
+ content=json.dumps(result).encode(),
1201
+ media_type="application/json",
1202
+ )
1203
+
1204
+ return app
1205
+
1206
+
1207
+ def _run_with_sse(
1208
+ repo_factory: _Callable[[str, str], Any],
1209
+ auth_backend: Any = None,
1210
+ host: str = "0.0.0.0",
1211
+ port: int = 8080,
1212
+ ) -> None:
1213
+ import uvicorn
1214
+ app = build_sse_app(repo_factory=repo_factory, auth_backend=auth_backend)
1215
+ uvicorn.run(app, host=host, port=port)