decibel-tools-mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/dist/agentic/compiler.d.ts +21 -0
  4. package/dist/agentic/compiler.d.ts.map +1 -0
  5. package/dist/agentic/compiler.js +267 -0
  6. package/dist/agentic/compiler.js.map +1 -0
  7. package/dist/agentic/golden.d.ts +25 -0
  8. package/dist/agentic/golden.d.ts.map +1 -0
  9. package/dist/agentic/golden.js +255 -0
  10. package/dist/agentic/golden.js.map +1 -0
  11. package/dist/agentic/index.d.ts +17 -0
  12. package/dist/agentic/index.d.ts.map +1 -0
  13. package/dist/agentic/index.js +153 -0
  14. package/dist/agentic/index.js.map +1 -0
  15. package/dist/agentic/linter.d.ts +20 -0
  16. package/dist/agentic/linter.d.ts.map +1 -0
  17. package/dist/agentic/linter.js +340 -0
  18. package/dist/agentic/linter.js.map +1 -0
  19. package/dist/agentic/renderer.d.ts +17 -0
  20. package/dist/agentic/renderer.d.ts.map +1 -0
  21. package/dist/agentic/renderer.js +277 -0
  22. package/dist/agentic/renderer.js.map +1 -0
  23. package/dist/agentic/types.d.ts +199 -0
  24. package/dist/agentic/types.d.ts.map +1 -0
  25. package/dist/agentic/types.js +8 -0
  26. package/dist/agentic/types.js.map +1 -0
  27. package/dist/architectAdrs.d.ts +19 -0
  28. package/dist/architectAdrs.d.ts.map +1 -0
  29. package/dist/architectAdrs.js +123 -0
  30. package/dist/architectAdrs.js.map +1 -0
  31. package/dist/config.d.ts +8 -0
  32. package/dist/config.d.ts.map +1 -0
  33. package/dist/config.js +19 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/dataRoot.d.ts +45 -0
  36. package/dist/dataRoot.d.ts.map +1 -0
  37. package/dist/dataRoot.js +201 -0
  38. package/dist/dataRoot.js.map +1 -0
  39. package/dist/decibelPaths.d.ts +42 -0
  40. package/dist/decibelPaths.d.ts.map +1 -0
  41. package/dist/decibelPaths.js +150 -0
  42. package/dist/decibelPaths.js.map +1 -0
  43. package/dist/httpServer.d.ts +49 -0
  44. package/dist/httpServer.d.ts.map +1 -0
  45. package/dist/httpServer.js +1472 -0
  46. package/dist/httpServer.js.map +1 -0
  47. package/dist/lib/benchmark.d.ts +110 -0
  48. package/dist/lib/benchmark.d.ts.map +1 -0
  49. package/dist/lib/benchmark.js +338 -0
  50. package/dist/lib/benchmark.js.map +1 -0
  51. package/dist/lib/supabase.d.ts +123 -0
  52. package/dist/lib/supabase.d.ts.map +1 -0
  53. package/dist/lib/supabase.js +91 -0
  54. package/dist/lib/supabase.js.map +1 -0
  55. package/dist/projectPaths.d.ts +27 -0
  56. package/dist/projectPaths.d.ts.map +1 -0
  57. package/dist/projectPaths.js +86 -0
  58. package/dist/projectPaths.js.map +1 -0
  59. package/dist/projectRegistry.d.ts +97 -0
  60. package/dist/projectRegistry.d.ts.map +1 -0
  61. package/dist/projectRegistry.js +362 -0
  62. package/dist/projectRegistry.js.map +1 -0
  63. package/dist/sentinelIssues.d.ts +44 -0
  64. package/dist/sentinelIssues.d.ts.map +1 -0
  65. package/dist/sentinelIssues.js +213 -0
  66. package/dist/sentinelIssues.js.map +1 -0
  67. package/dist/server.d.ts +3 -0
  68. package/dist/server.d.ts.map +1 -0
  69. package/dist/server.js +93 -0
  70. package/dist/server.js.map +1 -0
  71. package/dist/test.d.ts +7 -0
  72. package/dist/test.d.ts.map +1 -0
  73. package/dist/test.js +77 -0
  74. package/dist/test.js.map +1 -0
  75. package/dist/tools/agentic/index.d.ts +7 -0
  76. package/dist/tools/agentic/index.d.ts.map +1 -0
  77. package/dist/tools/agentic/index.js +180 -0
  78. package/dist/tools/agentic/index.js.map +1 -0
  79. package/dist/tools/architect/index.d.ts +9 -0
  80. package/dist/tools/architect/index.d.ts.map +1 -0
  81. package/dist/tools/architect/index.js +383 -0
  82. package/dist/tools/architect/index.js.map +1 -0
  83. package/dist/tools/architect.d.ts +19 -0
  84. package/dist/tools/architect.d.ts.map +1 -0
  85. package/dist/tools/architect.js +88 -0
  86. package/dist/tools/architect.js.map +1 -0
  87. package/dist/tools/bench.d.ts +89 -0
  88. package/dist/tools/bench.d.ts.map +1 -0
  89. package/dist/tools/bench.js +826 -0
  90. package/dist/tools/bench.js.map +1 -0
  91. package/dist/tools/context/index.d.ts +11 -0
  92. package/dist/tools/context/index.d.ts.map +1 -0
  93. package/dist/tools/context/index.js +439 -0
  94. package/dist/tools/context/index.js.map +1 -0
  95. package/dist/tools/context.d.ts +146 -0
  96. package/dist/tools/context.d.ts.map +1 -0
  97. package/dist/tools/context.js +481 -0
  98. package/dist/tools/context.js.map +1 -0
  99. package/dist/tools/crit.d.ts +63 -0
  100. package/dist/tools/crit.d.ts.map +1 -0
  101. package/dist/tools/crit.js +159 -0
  102. package/dist/tools/crit.js.map +1 -0
  103. package/dist/tools/data-inspector.d.ts +188 -0
  104. package/dist/tools/data-inspector.d.ts.map +1 -0
  105. package/dist/tools/data-inspector.js +650 -0
  106. package/dist/tools/data-inspector.js.map +1 -0
  107. package/dist/tools/deck.d.ts +11 -0
  108. package/dist/tools/deck.d.ts.map +1 -0
  109. package/dist/tools/deck.js +170 -0
  110. package/dist/tools/deck.js.map +1 -0
  111. package/dist/tools/designer/index.d.ts +6 -0
  112. package/dist/tools/designer/index.d.ts.map +1 -0
  113. package/dist/tools/designer/index.js +177 -0
  114. package/dist/tools/designer/index.js.map +1 -0
  115. package/dist/tools/designer.d.ts +20 -0
  116. package/dist/tools/designer.d.ts.map +1 -0
  117. package/dist/tools/designer.js +75 -0
  118. package/dist/tools/designer.js.map +1 -0
  119. package/dist/tools/dojo/index.d.ts +13 -0
  120. package/dist/tools/dojo/index.d.ts.map +1 -0
  121. package/dist/tools/dojo/index.js +547 -0
  122. package/dist/tools/dojo/index.js.map +1 -0
  123. package/dist/tools/dojo.d.ts +254 -0
  124. package/dist/tools/dojo.d.ts.map +1 -0
  125. package/dist/tools/dojo.js +933 -0
  126. package/dist/tools/dojo.js.map +1 -0
  127. package/dist/tools/dojoBench.d.ts +49 -0
  128. package/dist/tools/dojoBench.d.ts.map +1 -0
  129. package/dist/tools/dojoBench.js +205 -0
  130. package/dist/tools/dojoBench.js.map +1 -0
  131. package/dist/tools/dojoGraduated.d.ts +50 -0
  132. package/dist/tools/dojoGraduated.d.ts.map +1 -0
  133. package/dist/tools/dojoGraduated.js +174 -0
  134. package/dist/tools/dojoGraduated.js.map +1 -0
  135. package/dist/tools/dojoPolicy.d.ts +65 -0
  136. package/dist/tools/dojoPolicy.d.ts.map +1 -0
  137. package/dist/tools/dojoPolicy.js +263 -0
  138. package/dist/tools/dojoPolicy.js.map +1 -0
  139. package/dist/tools/friction/index.d.ts +7 -0
  140. package/dist/tools/friction/index.d.ts.map +1 -0
  141. package/dist/tools/friction/index.js +239 -0
  142. package/dist/tools/friction/index.js.map +1 -0
  143. package/dist/tools/friction.d.ts +82 -0
  144. package/dist/tools/friction.d.ts.map +1 -0
  145. package/dist/tools/friction.js +331 -0
  146. package/dist/tools/friction.js.map +1 -0
  147. package/dist/tools/index.d.ts +5 -0
  148. package/dist/tools/index.d.ts.map +1 -0
  149. package/dist/tools/index.js +52 -0
  150. package/dist/tools/index.js.map +1 -0
  151. package/dist/tools/learnings/index.d.ts +5 -0
  152. package/dist/tools/learnings/index.d.ts.map +1 -0
  153. package/dist/tools/learnings/index.js +123 -0
  154. package/dist/tools/learnings/index.js.map +1 -0
  155. package/dist/tools/learnings.d.ts +41 -0
  156. package/dist/tools/learnings.d.ts.map +1 -0
  157. package/dist/tools/learnings.js +149 -0
  158. package/dist/tools/learnings.js.map +1 -0
  159. package/dist/tools/oracle/index.d.ts +5 -0
  160. package/dist/tools/oracle/index.d.ts.map +1 -0
  161. package/dist/tools/oracle/index.js +97 -0
  162. package/dist/tools/oracle/index.js.map +1 -0
  163. package/dist/tools/oracle.d.ts +90 -0
  164. package/dist/tools/oracle.d.ts.map +1 -0
  165. package/dist/tools/oracle.js +529 -0
  166. package/dist/tools/oracle.js.map +1 -0
  167. package/dist/tools/policy.d.ts +119 -0
  168. package/dist/tools/policy.d.ts.map +1 -0
  169. package/dist/tools/policy.js +406 -0
  170. package/dist/tools/policy.js.map +1 -0
  171. package/dist/tools/provenance/index.d.ts +4 -0
  172. package/dist/tools/provenance/index.d.ts.map +1 -0
  173. package/dist/tools/provenance/index.js +58 -0
  174. package/dist/tools/provenance/index.js.map +1 -0
  175. package/dist/tools/provenance.d.ts +75 -0
  176. package/dist/tools/provenance.d.ts.map +1 -0
  177. package/dist/tools/provenance.js +197 -0
  178. package/dist/tools/provenance.js.map +1 -0
  179. package/dist/tools/rateLimiter.d.ts +45 -0
  180. package/dist/tools/rateLimiter.d.ts.map +1 -0
  181. package/dist/tools/rateLimiter.js +91 -0
  182. package/dist/tools/rateLimiter.js.map +1 -0
  183. package/dist/tools/registry/index.d.ts +10 -0
  184. package/dist/tools/registry/index.d.ts.map +1 -0
  185. package/dist/tools/registry/index.js +467 -0
  186. package/dist/tools/registry/index.js.map +1 -0
  187. package/dist/tools/registry.d.ts +3 -0
  188. package/dist/tools/registry.d.ts.map +1 -0
  189. package/dist/tools/registry.js +189 -0
  190. package/dist/tools/registry.js.map +1 -0
  191. package/dist/tools/roadmap/index.d.ts +9 -0
  192. package/dist/tools/roadmap/index.d.ts.map +1 -0
  193. package/dist/tools/roadmap/index.js +250 -0
  194. package/dist/tools/roadmap/index.js.map +1 -0
  195. package/dist/tools/roadmap.d.ts +88 -0
  196. package/dist/tools/roadmap.d.ts.map +1 -0
  197. package/dist/tools/roadmap.js +365 -0
  198. package/dist/tools/roadmap.js.map +1 -0
  199. package/dist/tools/sentinel/index.d.ts +19 -0
  200. package/dist/tools/sentinel/index.d.ts.map +1 -0
  201. package/dist/tools/sentinel/index.js +832 -0
  202. package/dist/tools/sentinel/index.js.map +1 -0
  203. package/dist/tools/sentinel-scan-data.d.ts +90 -0
  204. package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
  205. package/dist/tools/sentinel-scan-data.js +122 -0
  206. package/dist/tools/sentinel-scan-data.js.map +1 -0
  207. package/dist/tools/sentinel.d.ts +156 -0
  208. package/dist/tools/sentinel.d.ts.map +1 -0
  209. package/dist/tools/sentinel.js +603 -0
  210. package/dist/tools/sentinel.js.map +1 -0
  211. package/dist/tools/shared/index.d.ts +4 -0
  212. package/dist/tools/shared/index.d.ts.map +1 -0
  213. package/dist/tools/shared/index.js +7 -0
  214. package/dist/tools/shared/index.js.map +1 -0
  215. package/dist/tools/shared/project.d.ts +17 -0
  216. package/dist/tools/shared/project.d.ts.map +1 -0
  217. package/dist/tools/shared/project.js +36 -0
  218. package/dist/tools/shared/project.js.map +1 -0
  219. package/dist/tools/shared/response.d.ts +10 -0
  220. package/dist/tools/shared/response.d.ts.map +1 -0
  221. package/dist/tools/shared/response.js +36 -0
  222. package/dist/tools/shared/response.js.map +1 -0
  223. package/dist/tools/shared/validation.d.ts +10 -0
  224. package/dist/tools/shared/validation.d.ts.map +1 -0
  225. package/dist/tools/shared/validation.js +26 -0
  226. package/dist/tools/shared/validation.js.map +1 -0
  227. package/dist/tools/studio/cloud-spine.d.ts +27 -0
  228. package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
  229. package/dist/tools/studio/cloud-spine.js +773 -0
  230. package/dist/tools/studio/cloud-spine.js.map +1 -0
  231. package/dist/tools/studio/index.d.ts +154 -0
  232. package/dist/tools/studio/index.d.ts.map +1 -0
  233. package/dist/tools/studio/index.js +525 -0
  234. package/dist/tools/studio/index.js.map +1 -0
  235. package/dist/tools/testSpec.d.ts +122 -0
  236. package/dist/tools/testSpec.d.ts.map +1 -0
  237. package/dist/tools/testSpec.js +525 -0
  238. package/dist/tools/testSpec.js.map +1 -0
  239. package/dist/tools/toolsIndex.d.ts +5 -0
  240. package/dist/tools/toolsIndex.d.ts.map +1 -0
  241. package/dist/tools/toolsIndex.js +37 -0
  242. package/dist/tools/toolsIndex.js.map +1 -0
  243. package/dist/tools/types.d.ts +30 -0
  244. package/dist/tools/types.d.ts.map +1 -0
  245. package/dist/tools/types.js +7 -0
  246. package/dist/tools/types.js.map +1 -0
  247. package/dist/tools/voice/index.d.ts +8 -0
  248. package/dist/tools/voice/index.d.ts.map +1 -0
  249. package/dist/tools/voice/index.js +176 -0
  250. package/dist/tools/voice/index.js.map +1 -0
  251. package/dist/tools/voice.d.ts +291 -0
  252. package/dist/tools/voice.d.ts.map +1 -0
  253. package/dist/tools/voice.js +734 -0
  254. package/dist/tools/voice.js.map +1 -0
  255. package/package.json +54 -0
@@ -0,0 +1,1472 @@
1
+ /**
2
+ * HTTP Server Mode for Decibel MCP
3
+ *
4
+ * Exposes the MCP server over HTTP for remote access (e.g., ChatGPT, Mother).
5
+ *
6
+ * Usage:
7
+ * node dist/server.js --http --port 8787
8
+ * node dist/server.js --http --port 8787 --auth-token YOUR_SECRET
9
+ *
10
+ * Endpoints:
11
+ * GET /health - Health check
12
+ * GET /tools - List available tools
13
+ * POST /call - Execute any tool: { tool: string, arguments: object }
14
+ * POST /dojo/wish - Shorthand for dojo_add_wish
15
+ * POST /dojo/propose - Shorthand for dojo_create_proposal
16
+ * POST /dojo/scaffold - Shorthand for dojo_scaffold_experiment
17
+ * POST /dojo/run - Shorthand for dojo_run_experiment
18
+ * POST /dojo/results - Shorthand for dojo_get_results
19
+ * POST /dojo/artifact - Shorthand for dojo_read_artifact
20
+ * GET /dojo/list - Shorthand for dojo_list
21
+ * POST /mcp - Full MCP protocol endpoint
22
+ *
23
+ * All responses use status envelope:
24
+ * { "status": "executed", ...data }
25
+ * { "status": "error", "error": "...", "code": "..." }
26
+ */
27
+ import { createServer } from 'http';
28
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
29
+ import { readFileSync } from 'fs';
30
+ import { fileURLToPath } from 'url';
31
+ import { dirname, join } from 'path';
32
+ import { log } from './config.js';
33
+ import { isSupabaseConfigured } from './lib/supabase.js';
34
+ import { modularToolMap, modularTools } from './tools/index.js';
35
+ import { createProposal, scaffoldExperiment, listDojo, runExperiment, getExperimentResults, addWish, listWishes, canGraduate, readArtifact, isDojoError, } from './tools/dojo.js';
36
+ import { dojoBench, isDojoBenchError, } from './tools/dojoBench.js';
37
+ import { decibelBench, decibelBenchCompare, isDecibelBenchError, isBenchCompareError, } from './tools/bench.js';
38
+ import { contextRefresh, contextPin, contextUnpin, contextList, eventAppend, eventSearch, artifactList, artifactRead, isContextError, } from './tools/context.js';
39
+ import { createPolicy, listPolicies, getPolicy, compileOversight, isPolicyError, } from './tools/policy.js';
40
+ import { createTestSpec, listTestSpecs, compileTests, auditPolicies, isTestSpecError, } from './tools/testSpec.js';
41
+ import { voiceInboxAdd, } from './tools/voice.js';
42
+ import {
43
+ // Image
44
+ generateImage, getImageStatus,
45
+ // Meshy 3D
46
+ meshyGenerate, getMeshyStatus, meshyDownload,
47
+ // Tripo 3D
48
+ tripoGenerate, getTripoStatus, tripoDownload,
49
+ // Kling Video
50
+ klingGenerateVideo, klingGenerateTextVideo, klingGenerateAvatar, getKlingStatus,
51
+ // Utility
52
+ listTasks, } from './tools/studio/index.js';
53
+ import { listProjects } from './projectRegistry.js';
54
+ import { listEpics, listRepoIssues, isProjectResolutionError, } from './tools/sentinel.js';
55
+ // ============================================================================
56
+ // Version Info
57
+ // ============================================================================
58
+ const __filename = fileURLToPath(import.meta.url);
59
+ const __dirname = dirname(__filename);
60
+ function getVersion() {
61
+ try {
62
+ // Try to read from package.json (works in both dev and prod)
63
+ const pkgPath = join(__dirname, '..', 'package.json');
64
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
65
+ return { version: pkg.version || '0.0.0', name: pkg.name || 'decibel-tools-mcp' };
66
+ }
67
+ catch {
68
+ return { version: '0.3.0', name: 'decibel-tools-mcp' };
69
+ }
70
+ }
71
+ const PKG = getVersion();
72
+ // ============================================================================
73
+ // Landing Page HTML
74
+ // ============================================================================
75
+ const LANDING_PAGE_HTML = `<!DOCTYPE html>
76
+ <html lang="en">
77
+ <head>
78
+ <meta charset="UTF-8">
79
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
80
+ <title>Decibel Tools - Project Intelligence for AI Agents</title>
81
+ <style>
82
+ * { box-sizing: border-box; margin: 0; padding: 0; }
83
+ body {
84
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
85
+ background: #0a0a0a;
86
+ color: #e5e5e5;
87
+ min-height: 100vh;
88
+ display: flex;
89
+ flex-direction: column;
90
+ align-items: center;
91
+ padding: 2rem;
92
+ }
93
+ .container { max-width: 720px; width: 100%; }
94
+ h1 {
95
+ font-size: 2.5rem;
96
+ font-weight: 700;
97
+ margin-bottom: 0.5rem;
98
+ background: linear-gradient(135deg, #fff 0%, #888 100%);
99
+ -webkit-background-clip: text;
100
+ -webkit-text-fill-color: transparent;
101
+ }
102
+ .tagline { font-size: 1.25rem; color: #888; margin-bottom: 2rem; }
103
+ .cta {
104
+ background: #1a1a1a;
105
+ border: 1px solid #333;
106
+ border-radius: 12px;
107
+ padding: 2rem;
108
+ margin-bottom: 2rem;
109
+ text-align: center;
110
+ }
111
+ .cta h2 { font-size: 1.1rem; font-weight: 500; margin-bottom: 1rem; color: #ccc; }
112
+ .manual { font-size: 0.85rem; color: #666; margin-top: 1rem; }
113
+ .manual code { background: #222; padding: 0.2rem 0.4rem; border-radius: 4px; font-family: 'SF Mono', Monaco, monospace; }
114
+ .features { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 2rem; }
115
+ .feature { background: #111; border: 1px solid #222; border-radius: 8px; padding: 1.25rem; }
116
+ .feature h3 { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.5rem; color: #fff; }
117
+ .feature p { font-size: 0.85rem; color: #888; line-height: 1.5; }
118
+ .tools { background: #111; border: 1px solid #222; border-radius: 8px; padding: 1.5rem; margin-bottom: 2rem; }
119
+ .tools h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; }
120
+ .tool-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.75rem; }
121
+ .tool { font-size: 0.8rem; color: #888; padding: 0.5rem; background: #1a1a1a; border-radius: 4px; text-align: center; }
122
+ .tool strong { color: #ccc; display: block; margin-bottom: 0.25rem; }
123
+ footer { margin-top: auto; padding-top: 2rem; font-size: 0.8rem; color: #444; }
124
+ footer a { color: #666; text-decoration: none; }
125
+ footer a:hover { color: #888; }
126
+ @media (max-width: 600px) { .features, .tool-grid { grid-template-columns: 1fr; } }
127
+ pre { background: #0d0d0d; padding: 1rem; border-radius: 6px; overflow-x: auto; text-align: left; font-size: 0.85rem; color: #888; }
128
+ </style>
129
+ </head>
130
+ <body>
131
+ <div class="container">
132
+ <h1>Decibel Tools</h1>
133
+ <p class="tagline">Project intelligence for AI coding agents</p>
134
+ <div class="cta">
135
+ <h2>Get Started</h2>
136
+ <p class="manual" style="margin-bottom: 1rem; color: #aaa;">1. Clone the beta repo</p>
137
+ <pre><code>git clone https://github.com/decibelsystems/decibel-tools-beta.git</code></pre>
138
+ <p class="manual" style="margin-top: 1rem; margin-bottom: 1rem; color: #aaa;">2. Add to your MCP config</p>
139
+ <pre><code>{
140
+ "mcpServers": {
141
+ "decibel-tools": {
142
+ "command": "node",
143
+ "args": ["/path/to/decibel-tools-beta/server.js"]
144
+ }
145
+ }
146
+ }</code></pre>
147
+ <p class="manual" style="margin-top: 1rem;">
148
+ Works with <strong style="color: #ccc;">Cursor</strong>, <strong style="color: #ccc;">Claude Desktop</strong>, and any MCP client
149
+ </p>
150
+ </div>
151
+ <div class="features">
152
+ <div class="feature"><h3>Sentinel</h3><p>Track epics, issues, and incidents. Your agent knows what's in flight.</p></div>
153
+ <div class="feature"><h3>Architect</h3><p>Record ADRs and decisions. Context persists across sessions.</p></div>
154
+ <div class="feature"><h3>Dojo</h3><p>Incubate ideas with wishes, proposals, and experiments.</p></div>
155
+ <div class="feature"><h3>Oracle</h3><p>Get AI-powered recommendations on what to work on next.</p></div>
156
+ </div>
157
+ <div class="tools">
158
+ <h2>50+ MCP Tools</h2>
159
+ <div class="tool-grid">
160
+ <div class="tool"><strong>sentinel_log_epic</strong>Create epics</div>
161
+ <div class="tool"><strong>sentinel_createIssue</strong>Track issues</div>
162
+ <div class="tool"><strong>architect_createAdr</strong>Record decisions</div>
163
+ <div class="tool"><strong>dojo_add_wish</strong>Capture ideas</div>
164
+ <div class="tool"><strong>oracle_next_actions</strong>Get guidance</div>
165
+ <div class="tool"><strong>friction_log</strong>Track pain points</div>
166
+ </div>
167
+ </div>
168
+ <footer>
169
+ <a href="https://github.com/decibelsystems/decibel-tools-beta">GitHub</a> ·
170
+ <a href="https://modelcontextprotocol.io">MCP Protocol</a>
171
+ </footer>
172
+ </div>
173
+ </body>
174
+ </html>`;
175
+ // ============================================================================
176
+ // Response Helpers
177
+ // ============================================================================
178
+ /**
179
+ * Wrap a successful result in status envelope
180
+ */
181
+ function wrapSuccess(data) {
182
+ return { status: 'executed', ...data };
183
+ }
184
+ /**
185
+ * Wrap an error in status envelope
186
+ */
187
+ function wrapError(error, code) {
188
+ return { status: 'error', error, ...(code && { code }) };
189
+ }
190
+ /**
191
+ * Send JSON response with status envelope
192
+ */
193
+ function sendJson(res, statusCode, data) {
194
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
195
+ res.end(JSON.stringify(data));
196
+ }
197
+ /**
198
+ * Parse JSON body from request
199
+ */
200
+ async function parseBody(req) {
201
+ return new Promise((resolve, reject) => {
202
+ let body = '';
203
+ req.on('data', (chunk) => { body += chunk; });
204
+ req.on('end', () => {
205
+ try {
206
+ resolve(body ? JSON.parse(body) : {});
207
+ }
208
+ catch {
209
+ reject(new Error('Invalid JSON'));
210
+ }
211
+ });
212
+ req.on('error', reject);
213
+ });
214
+ }
215
+ // ============================================================================
216
+ // Tool Executor
217
+ // ============================================================================
218
+ /**
219
+ * Execute a Dojo tool and return normalized result
220
+ */
221
+ async function executeDojoTool(tool, args) {
222
+ try {
223
+ let result;
224
+ switch (tool) {
225
+ case 'dojo_add_wish':
226
+ result = await addWish(args);
227
+ break;
228
+ case 'dojo_create_proposal':
229
+ result = await createProposal(args);
230
+ break;
231
+ case 'dojo_scaffold_experiment':
232
+ result = await scaffoldExperiment(args);
233
+ break;
234
+ case 'dojo_run_experiment':
235
+ result = await runExperiment(args);
236
+ break;
237
+ case 'dojo_get_results':
238
+ result = await getExperimentResults(args);
239
+ break;
240
+ case 'dojo_list':
241
+ result = await listDojo(args);
242
+ break;
243
+ case 'dojo_list_wishes':
244
+ result = await listWishes(args);
245
+ break;
246
+ case 'dojo_can_graduate':
247
+ result = await canGraduate(args);
248
+ break;
249
+ case 'dojo_read_artifact':
250
+ result = await readArtifact(args);
251
+ break;
252
+ case 'dojo_bench':
253
+ result = await dojoBench(args);
254
+ break;
255
+ // Benchmark tools (ISS-0014)
256
+ case 'decibel_bench':
257
+ result = await decibelBench(args);
258
+ break;
259
+ case 'decibel_bench_compare':
260
+ result = await decibelBenchCompare(args);
261
+ break;
262
+ // Context Pack tools
263
+ case 'decibel_context_refresh':
264
+ result = await contextRefresh(args);
265
+ break;
266
+ case 'decibel_context_pin':
267
+ result = await contextPin(args);
268
+ break;
269
+ case 'decibel_context_unpin':
270
+ result = await contextUnpin(args);
271
+ break;
272
+ case 'decibel_context_list':
273
+ result = await contextList(args);
274
+ break;
275
+ case 'decibel_event_append':
276
+ result = await eventAppend(args);
277
+ break;
278
+ case 'decibel_event_search':
279
+ result = await eventSearch(args);
280
+ break;
281
+ case 'decibel_artifact_list':
282
+ result = await artifactList(args);
283
+ break;
284
+ case 'decibel_artifact_read':
285
+ result = await artifactRead(args);
286
+ break;
287
+ // Architect Policy tools (ADR-004 Oversight Pack)
288
+ case 'architect_createPolicy':
289
+ result = await createPolicy(args);
290
+ break;
291
+ case 'architect_listPolicies':
292
+ result = await listPolicies(args);
293
+ break;
294
+ case 'architect_getPolicy':
295
+ result = await getPolicy(args);
296
+ break;
297
+ case 'architect_compileOversight':
298
+ result = await compileOversight(args);
299
+ break;
300
+ // Sentinel Test tools (ADR-004 Oversight Pack)
301
+ case 'sentinel_createTestSpec':
302
+ result = await createTestSpec(args);
303
+ break;
304
+ case 'sentinel_listTestSpecs':
305
+ result = await listTestSpecs(args);
306
+ break;
307
+ case 'sentinel_compileTests':
308
+ result = await compileTests(args);
309
+ break;
310
+ case 'sentinel_auditPolicies':
311
+ result = await auditPolicies(args);
312
+ break;
313
+ default: {
314
+ // Fallback to modular tools (deck, studio, etc.)
315
+ const modularTool = modularToolMap.get(tool);
316
+ if (modularTool) {
317
+ const modularResult = await modularTool.handler(args);
318
+ // Parse the JSON from the text result
319
+ const text = modularResult.content[0]?.text;
320
+ if (text) {
321
+ try {
322
+ result = JSON.parse(text);
323
+ }
324
+ catch {
325
+ result = { message: text };
326
+ }
327
+ }
328
+ else {
329
+ result = { success: true };
330
+ }
331
+ break;
332
+ }
333
+ return wrapError(`Unknown tool: ${tool}`, 'UNKNOWN_TOOL');
334
+ }
335
+ }
336
+ // Check for Dojo error response
337
+ if (isDojoError(result)) {
338
+ return wrapError(result.error, `EXIT_${result.exitCode}`);
339
+ }
340
+ // Check for Bench error response
341
+ if (isDojoBenchError(result)) {
342
+ return wrapError(result.error, `EXIT_${result.exitCode}`);
343
+ }
344
+ // Check for Decibel Bench error response
345
+ if (isDecibelBenchError(result)) {
346
+ return wrapError(result.error, `EXIT_${result.exitCode}`);
347
+ }
348
+ // Check for Bench Compare error response
349
+ if (isBenchCompareError(result)) {
350
+ return wrapError(result.error, `EXIT_${result.exitCode}`);
351
+ }
352
+ // Check for Context error response
353
+ if (isContextError(result)) {
354
+ return wrapError(result.error, 'CONTEXT_ERROR');
355
+ }
356
+ // Check for Policy error response
357
+ if (isPolicyError(result)) {
358
+ return wrapError(result.message, result.error);
359
+ }
360
+ // Check for TestSpec error response
361
+ if (isTestSpecError(result)) {
362
+ return wrapError(result.message, result.error);
363
+ }
364
+ return wrapSuccess(result);
365
+ }
366
+ catch (error) {
367
+ const message = error instanceof Error ? error.message : String(error);
368
+ // Check for specific error types
369
+ if (message.includes('Rate limit')) {
370
+ return wrapError(message, 'RATE_LIMITED');
371
+ }
372
+ if (message.includes('Access denied')) {
373
+ return wrapError(message, 'ACCESS_DENIED');
374
+ }
375
+ if (message.includes('not found')) {
376
+ return wrapError(message, 'NOT_FOUND');
377
+ }
378
+ return wrapError(message, 'EXECUTION_ERROR');
379
+ }
380
+ }
381
+ /**
382
+ * Get list of available tools
383
+ */
384
+ function getAvailableTools() {
385
+ return [
386
+ { name: 'dojo_add_wish', description: 'Log a capability wish with optional context' },
387
+ { name: 'dojo_create_proposal', description: 'Create a proposal (can link to wish_id)' },
388
+ { name: 'dojo_scaffold_experiment', description: 'Create experiment from proposal' },
389
+ { name: 'dojo_run_experiment', description: 'Run experiment in sandbox mode' },
390
+ { name: 'dojo_get_results', description: 'Get experiment results' },
391
+ { name: 'dojo_list', description: 'List proposals, experiments, wishes' },
392
+ { name: 'dojo_list_wishes', description: 'List wishes' },
393
+ { name: 'dojo_can_graduate', description: 'Check graduation eligibility' },
394
+ { name: 'dojo_read_artifact', description: 'Read artifact from experiment results' },
395
+ { name: 'dojo_bench', description: 'Run benchmark on a Dojo experiment' },
396
+ // Context Pack tools (ADR-002)
397
+ { name: 'decibel_context_refresh', description: 'Compile full context pack' },
398
+ { name: 'decibel_context_pin', description: 'Pin a fact to persistent memory' },
399
+ { name: 'decibel_context_unpin', description: 'Remove a pinned fact' },
400
+ { name: 'decibel_context_list', description: 'List pinned facts' },
401
+ { name: 'decibel_event_append', description: 'Append event to journal' },
402
+ { name: 'decibel_event_search', description: 'Search events' },
403
+ { name: 'decibel_artifact_list', description: 'List artifacts for a run' },
404
+ { name: 'decibel_artifact_read', description: 'Read artifact by run_id and name' },
405
+ // Architect Policy tools (ADR-004 Oversight Pack)
406
+ { name: 'architect_createPolicy', description: 'Create a policy atom' },
407
+ { name: 'architect_listPolicies', description: 'List policies (filter by severity/tags)' },
408
+ { name: 'architect_getPolicy', description: 'Get a specific policy by ID' },
409
+ { name: 'architect_compileOversight', description: 'Compile policies into documentation' },
410
+ // Sentinel Test tools (ADR-004 Oversight Pack)
411
+ { name: 'sentinel_createTestSpec', description: 'Create a test specification' },
412
+ { name: 'sentinel_listTestSpecs', description: 'List test specifications' },
413
+ { name: 'sentinel_compileTests', description: 'Compile test manifest' },
414
+ { name: 'sentinel_auditPolicies', description: 'Audit policy compliance' },
415
+ // Include modular tools (deck, studio, etc.)
416
+ ...modularTools.map(t => ({
417
+ name: t.definition.name,
418
+ description: t.definition.description,
419
+ })),
420
+ ];
421
+ }
422
+ /**
423
+ * Start an HTTP server that handles MCP requests
424
+ *
425
+ * Note: This creates a single stateless transport. Each request is handled
426
+ * independently. For full session support, this would need to be expanded.
427
+ */
428
+ export async function startHttpServer(server, options) {
429
+ const { port, authToken, host = '0.0.0.0' } = options;
430
+ // Create transport in STATELESS mode (better for ChatGPT compatibility)
431
+ // Setting sessionIdGenerator to undefined disables session tracking
432
+ const transport = new StreamableHTTPServerTransport({
433
+ sessionIdGenerator: undefined, // Stateless mode
434
+ });
435
+ // Connect the MCP server to the transport
436
+ await server.connect(transport);
437
+ const httpServer = createServer(async (req, res) => {
438
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
439
+ const path = url.pathname;
440
+ log(`HTTP: ${req.method} ${path}`);
441
+ // CORS headers for browser access (required for ChatGPT connector)
442
+ res.setHeader('Access-Control-Allow-Origin', '*');
443
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
444
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Accept');
445
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
446
+ // (a) Handle preflight OPTIONS requests
447
+ if (req.method === 'OPTIONS') {
448
+ res.writeHead(204);
449
+ res.end();
450
+ return;
451
+ }
452
+ // (c) Root health check - GET / returns 200
453
+ if (path === '/' && req.method === 'GET') {
454
+ res.writeHead(200, { 'Content-Type': 'application/json' });
455
+ res.end(JSON.stringify({
456
+ status: 'ok',
457
+ name: PKG.name,
458
+ version: PKG.version,
459
+ api_version: 'v1',
460
+ }));
461
+ return;
462
+ }
463
+ // Health check at /health too
464
+ if (path === '/health') {
465
+ res.writeHead(200, { 'Content-Type': 'application/json' });
466
+ res.end(JSON.stringify({
467
+ status: 'ok',
468
+ version: PKG.version,
469
+ api_version: 'v1',
470
+ supabase_configured: isSupabaseConfigured(),
471
+ env_check: {
472
+ has_supabase_url: !!process.env.SUPABASE_URL,
473
+ has_supabase_service_key: !!process.env.SUPABASE_SERVICE_KEY,
474
+ },
475
+ }));
476
+ return;
477
+ }
478
+ // Landing page at /tools
479
+ if (path === '/tools' && req.method === 'GET') {
480
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
481
+ res.end(LANDING_PAGE_HTML);
482
+ return;
483
+ }
484
+ // Serve OpenAPI spec for ChatGPT Actions (handle GET and POST)
485
+ if ((path === '/openapi.yaml' || path === '/openapi.json') && (req.method === 'GET' || req.method === 'POST')) {
486
+ try {
487
+ const specPath = join(__dirname, '..', 'openapi.yaml');
488
+ const spec = readFileSync(specPath, 'utf-8');
489
+ if (path === '/openapi.json') {
490
+ // Convert YAML to JSON if requested
491
+ const yaml = await import('yaml');
492
+ const parsed = yaml.parse(spec);
493
+ res.writeHead(200, { 'Content-Type': 'application/json' });
494
+ res.end(JSON.stringify(parsed, null, 2));
495
+ }
496
+ else {
497
+ res.writeHead(200, { 'Content-Type': 'text/yaml' });
498
+ res.end(spec);
499
+ }
500
+ }
501
+ catch (error) {
502
+ res.writeHead(404, { 'Content-Type': 'application/json' });
503
+ res.end(JSON.stringify({ error: 'OpenAPI spec not found' }));
504
+ }
505
+ return;
506
+ }
507
+ // (d) OAuth discovery routes - return 404 (not 400) to keep connector wizard happy
508
+ if (path === '/.well-known/oauth-authorization-server' ||
509
+ path === '/.well-known/openid-configuration' ||
510
+ path === '/oauth/authorize' ||
511
+ path === '/oauth/token' ||
512
+ path === '/oauth/register') {
513
+ res.writeHead(404, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({ error: 'Not found' }));
515
+ return;
516
+ }
517
+ // Auth check (if token provided)
518
+ if (authToken) {
519
+ const authHeader = req.headers.authorization;
520
+ if (!authHeader || authHeader !== `Bearer ${authToken}`) {
521
+ log('HTTP: Unauthorized request');
522
+ sendJson(res, 401, wrapError('Unauthorized', 'UNAUTHORIZED'));
523
+ return;
524
+ }
525
+ }
526
+ // ========================================================================
527
+ // Simple REST Endpoints (for Mother and other AI agents)
528
+ // ========================================================================
529
+ // GET /tools - List available tools
530
+ if (path === '/tools' && req.method === 'GET') {
531
+ sendJson(res, 200, wrapSuccess({
532
+ version: PKG.version,
533
+ api_version: 'v1',
534
+ tools: getAvailableTools(),
535
+ }));
536
+ return;
537
+ }
538
+ // POST /call - Execute any tool
539
+ if (path === '/call' && req.method === 'POST') {
540
+ try {
541
+ const body = await parseBody(req);
542
+ const tool = body.tool;
543
+ const args = (body.arguments || {});
544
+ if (!tool) {
545
+ sendJson(res, 400, wrapError('Missing "tool" field', 'MISSING_TOOL'));
546
+ return;
547
+ }
548
+ log(`HTTP: /call tool=${tool}`);
549
+ const result = await executeDojoTool(tool, args);
550
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
551
+ }
552
+ catch (error) {
553
+ const message = error instanceof Error ? error.message : String(error);
554
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
555
+ }
556
+ return;
557
+ }
558
+ // ========================================================================
559
+ // Dojo Convenience Endpoints
560
+ // ========================================================================
561
+ // POST /dojo/wish - Add a wish
562
+ if (path === '/dojo/wish' && req.method === 'POST') {
563
+ try {
564
+ const body = await parseBody(req);
565
+ log('HTTP: /dojo/wish');
566
+ const result = await executeDojoTool('dojo_add_wish', body);
567
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
568
+ }
569
+ catch (error) {
570
+ const message = error instanceof Error ? error.message : String(error);
571
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
572
+ }
573
+ return;
574
+ }
575
+ // POST /dojo/propose - Create a proposal
576
+ if (path === '/dojo/propose' && req.method === 'POST') {
577
+ try {
578
+ const body = await parseBody(req);
579
+ log('HTTP: /dojo/propose');
580
+ const result = await executeDojoTool('dojo_create_proposal', body);
581
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
582
+ }
583
+ catch (error) {
584
+ const message = error instanceof Error ? error.message : String(error);
585
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
586
+ }
587
+ return;
588
+ }
589
+ // POST /dojo/scaffold - Scaffold experiment
590
+ if (path === '/dojo/scaffold' && req.method === 'POST') {
591
+ try {
592
+ const body = await parseBody(req);
593
+ log('HTTP: /dojo/scaffold');
594
+ const result = await executeDojoTool('dojo_scaffold_experiment', body);
595
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
596
+ }
597
+ catch (error) {
598
+ const message = error instanceof Error ? error.message : String(error);
599
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
600
+ }
601
+ return;
602
+ }
603
+ // POST /dojo/run - Run experiment
604
+ if (path === '/dojo/run' && req.method === 'POST') {
605
+ try {
606
+ const body = await parseBody(req);
607
+ log('HTTP: /dojo/run');
608
+ const result = await executeDojoTool('dojo_run_experiment', body);
609
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
610
+ }
611
+ catch (error) {
612
+ const message = error instanceof Error ? error.message : String(error);
613
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
614
+ }
615
+ return;
616
+ }
617
+ // POST /dojo/results - Get experiment results
618
+ if (path === '/dojo/results' && req.method === 'POST') {
619
+ try {
620
+ const body = await parseBody(req);
621
+ log('HTTP: /dojo/results');
622
+ const result = await executeDojoTool('dojo_get_results', body);
623
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
624
+ }
625
+ catch (error) {
626
+ const message = error instanceof Error ? error.message : String(error);
627
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
628
+ }
629
+ return;
630
+ }
631
+ // GET /dojo/list - List all (or POST with filter)
632
+ if (path === '/dojo/list') {
633
+ try {
634
+ const body = req.method === 'POST' ? await parseBody(req) : {};
635
+ // For GET, try to get project_id from query params
636
+ if (req.method === 'GET') {
637
+ const projectId = url.searchParams.get('project_id');
638
+ if (projectId) {
639
+ body.project_id = projectId;
640
+ }
641
+ }
642
+ log('HTTP: /dojo/list');
643
+ const result = await executeDojoTool('dojo_list', body);
644
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
645
+ }
646
+ catch (error) {
647
+ const message = error instanceof Error ? error.message : String(error);
648
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
649
+ }
650
+ return;
651
+ }
652
+ // GET /dojo/wishes - List wishes
653
+ if (path === '/dojo/wishes') {
654
+ try {
655
+ const body = req.method === 'POST' ? await parseBody(req) : {};
656
+ if (req.method === 'GET') {
657
+ const projectId = url.searchParams.get('project_id');
658
+ if (projectId) {
659
+ body.project_id = projectId;
660
+ }
661
+ }
662
+ log('HTTP: /dojo/wishes');
663
+ const result = await executeDojoTool('dojo_list_wishes', body);
664
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
665
+ }
666
+ catch (error) {
667
+ const message = error instanceof Error ? error.message : String(error);
668
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
669
+ }
670
+ return;
671
+ }
672
+ // POST /dojo/can-graduate - Check graduation eligibility
673
+ if (path === '/dojo/can-graduate' && req.method === 'POST') {
674
+ try {
675
+ const body = await parseBody(req);
676
+ log('HTTP: /dojo/can-graduate');
677
+ const result = await executeDojoTool('dojo_can_graduate', body);
678
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
679
+ }
680
+ catch (error) {
681
+ const message = error instanceof Error ? error.message : String(error);
682
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
683
+ }
684
+ return;
685
+ }
686
+ // POST /dojo/artifact - Read artifact from experiment results
687
+ if (path === '/dojo/artifact' && req.method === 'POST') {
688
+ try {
689
+ const body = await parseBody(req);
690
+ log('HTTP: /dojo/artifact');
691
+ const result = await executeDojoTool('dojo_read_artifact', body);
692
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
693
+ }
694
+ catch (error) {
695
+ const message = error instanceof Error ? error.message : String(error);
696
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
697
+ }
698
+ return;
699
+ }
700
+ // POST /dojo/bench - Run benchmark on experiment
701
+ if (path === '/dojo/bench' && req.method === 'POST') {
702
+ try {
703
+ const body = await parseBody(req);
704
+ log('HTTP: /dojo/bench');
705
+ const result = await executeDojoTool('dojo_bench', body);
706
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
707
+ }
708
+ catch (error) {
709
+ const message = error instanceof Error ? error.message : String(error);
710
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
711
+ }
712
+ return;
713
+ }
714
+ // ========================================================================
715
+ // Benchmark Endpoints (ISS-0014)
716
+ // ========================================================================
717
+ // POST /bench/run - Run a benchmark suite
718
+ if (path === '/bench/run' && req.method === 'POST') {
719
+ try {
720
+ const body = await parseBody(req);
721
+ log('HTTP: /bench/run');
722
+ const result = await executeDojoTool('decibel_bench', body);
723
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
724
+ }
725
+ catch (error) {
726
+ const message = error instanceof Error ? error.message : String(error);
727
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
728
+ }
729
+ return;
730
+ }
731
+ // POST /bench/compare - Compare two baselines
732
+ if (path === '/bench/compare' && req.method === 'POST') {
733
+ try {
734
+ const body = await parseBody(req);
735
+ log('HTTP: /bench/compare');
736
+ const result = await executeDojoTool('decibel_bench_compare', body);
737
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
738
+ }
739
+ catch (error) {
740
+ const message = error instanceof Error ? error.message : String(error);
741
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
742
+ }
743
+ return;
744
+ }
745
+ // ========================================================================
746
+ // Context Pack Endpoints (ADR-002)
747
+ // ========================================================================
748
+ // POST /context/refresh - Compile full context pack
749
+ if (path === '/context/refresh' && req.method === 'POST') {
750
+ try {
751
+ const body = await parseBody(req);
752
+ log('HTTP: /context/refresh');
753
+ const result = await executeDojoTool('decibel_context_refresh', body);
754
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
755
+ }
756
+ catch (error) {
757
+ const message = error instanceof Error ? error.message : String(error);
758
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
759
+ }
760
+ return;
761
+ }
762
+ // POST /context/pin - Pin a fact
763
+ if (path === '/context/pin' && req.method === 'POST') {
764
+ try {
765
+ const body = await parseBody(req);
766
+ log('HTTP: /context/pin');
767
+ const result = await executeDojoTool('decibel_context_pin', body);
768
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
769
+ }
770
+ catch (error) {
771
+ const message = error instanceof Error ? error.message : String(error);
772
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
773
+ }
774
+ return;
775
+ }
776
+ // POST /context/unpin - Unpin a fact
777
+ if (path === '/context/unpin' && req.method === 'POST') {
778
+ try {
779
+ const body = await parseBody(req);
780
+ log('HTTP: /context/unpin');
781
+ const result = await executeDojoTool('decibel_context_unpin', body);
782
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
783
+ }
784
+ catch (error) {
785
+ const message = error instanceof Error ? error.message : String(error);
786
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
787
+ }
788
+ return;
789
+ }
790
+ // GET/POST /context/list - List pinned facts
791
+ if (path === '/context/list' && (req.method === 'GET' || req.method === 'POST')) {
792
+ try {
793
+ const body = req.method === 'POST' ? await parseBody(req) : {};
794
+ if (req.method === 'GET') {
795
+ const projectId = url.searchParams.get('project_id');
796
+ if (projectId) {
797
+ body.project_id = projectId;
798
+ }
799
+ }
800
+ log('HTTP: /context/list');
801
+ const result = await executeDojoTool('decibel_context_list', body);
802
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
803
+ }
804
+ catch (error) {
805
+ const message = error instanceof Error ? error.message : String(error);
806
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
807
+ }
808
+ return;
809
+ }
810
+ // POST /event/append - Append event to journal
811
+ if (path === '/event/append' && req.method === 'POST') {
812
+ try {
813
+ const body = await parseBody(req);
814
+ log('HTTP: /event/append');
815
+ const result = await executeDojoTool('decibel_event_append', body);
816
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
817
+ }
818
+ catch (error) {
819
+ const message = error instanceof Error ? error.message : String(error);
820
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
821
+ }
822
+ return;
823
+ }
824
+ // GET/POST /event/search - Search events
825
+ if (path === '/event/search' && (req.method === 'GET' || req.method === 'POST')) {
826
+ try {
827
+ const body = req.method === 'POST' ? await parseBody(req) : {};
828
+ if (req.method === 'GET') {
829
+ const projectId = url.searchParams.get('project_id');
830
+ const query = url.searchParams.get('query');
831
+ const limit = url.searchParams.get('limit');
832
+ if (projectId) {
833
+ body.project_id = projectId;
834
+ }
835
+ if (query) {
836
+ body.query = query;
837
+ }
838
+ if (limit) {
839
+ body.limit = parseInt(limit, 10);
840
+ }
841
+ }
842
+ log('HTTP: /event/search');
843
+ const result = await executeDojoTool('decibel_event_search', body);
844
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
845
+ }
846
+ catch (error) {
847
+ const message = error instanceof Error ? error.message : String(error);
848
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
849
+ }
850
+ return;
851
+ }
852
+ // POST /artifact/list - List artifacts for a run
853
+ if (path === '/artifact/list' && req.method === 'POST') {
854
+ try {
855
+ const body = await parseBody(req);
856
+ log('HTTP: /artifact/list');
857
+ const result = await executeDojoTool('decibel_artifact_list', body);
858
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
859
+ }
860
+ catch (error) {
861
+ const message = error instanceof Error ? error.message : String(error);
862
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
863
+ }
864
+ return;
865
+ }
866
+ // POST /artifact/read - Read artifact by run_id and name
867
+ if (path === '/artifact/read' && req.method === 'POST') {
868
+ try {
869
+ const body = await parseBody(req);
870
+ log('HTTP: /artifact/read');
871
+ const result = await executeDojoTool('decibel_artifact_read', body);
872
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
873
+ }
874
+ catch (error) {
875
+ const message = error instanceof Error ? error.message : String(error);
876
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
877
+ }
878
+ return;
879
+ }
880
+ // ========================================================================
881
+ // iOS Mobile App Endpoint
882
+ // ========================================================================
883
+ // Helper: Call ML classifier sidecar (optional, graceful fallback)
884
+ async function classifyWithML(transcript) {
885
+ try {
886
+ const controller = new AbortController();
887
+ const timeout = setTimeout(() => controller.abort(), 1000); // 1s timeout
888
+ const resp = await fetch('http://127.0.0.1:8790/classify', {
889
+ method: 'POST',
890
+ headers: { 'Content-Type': 'application/json' },
891
+ body: JSON.stringify({ transcript }),
892
+ signal: controller.signal,
893
+ });
894
+ clearTimeout(timeout);
895
+ if (resp.ok) {
896
+ return await resp.json();
897
+ }
898
+ }
899
+ catch {
900
+ // Classifier not running or timed out - that's fine
901
+ }
902
+ return null;
903
+ }
904
+ // Helper: Log training sample to ML classifier
905
+ async function logTrainingSample(data) {
906
+ try {
907
+ await fetch('http://127.0.0.1:8790/log', {
908
+ method: 'POST',
909
+ headers: { 'Content-Type': 'application/json' },
910
+ body: JSON.stringify(data),
911
+ });
912
+ }
913
+ catch {
914
+ // Best effort logging
915
+ }
916
+ }
917
+ // POST /api/inbox - Receive voice transcript from iOS app
918
+ if (path === '/api/inbox' && req.method === 'POST') {
919
+ try {
920
+ const body = await parseBody(req);
921
+ log('HTTP: /api/inbox (iOS)');
922
+ // Validate required field
923
+ const transcript = body.transcript;
924
+ if (!transcript) {
925
+ sendJson(res, 400, wrapError('Missing "transcript" field', 'MISSING_TRANSCRIPT'));
926
+ return;
927
+ }
928
+ // Build tags array
929
+ const tags = [];
930
+ if (body.device)
931
+ tags.push(`device:${body.device}`);
932
+ // User's explicit intent (from button tap)
933
+ // iOS sends as "event_type", also accept "intent" for compatibility
934
+ const userIntent = (body.event_type || body.intent);
935
+ // ML classification (optional - graceful fallback if not running)
936
+ const mlResult = await classifyWithML(transcript);
937
+ let finalIntent = userIntent;
938
+ let wasOverridden = false;
939
+ let mlConfidence = 0;
940
+ if (mlResult) {
941
+ mlConfidence = mlResult.confidence;
942
+ log(`HTTP: ML classified as "${mlResult.intent}" (${(mlResult.confidence * 100).toFixed(0)}%)`);
943
+ if (userIntent) {
944
+ // User provided intent - ML can override if confident and disagrees
945
+ if (mlResult.intent !== userIntent && mlResult.confidence > 0.75) {
946
+ finalIntent = mlResult.intent;
947
+ wasOverridden = true;
948
+ tags.push('ml:overridden');
949
+ log(`HTTP: ML overriding user intent "${userIntent}" → "${mlResult.intent}"`);
950
+ }
951
+ // Log training sample (user label = ground truth)
952
+ logTrainingSample({
953
+ transcript,
954
+ user_label: userIntent,
955
+ predicted: mlResult.intent,
956
+ confidence: mlResult.confidence,
957
+ was_overridden: wasOverridden,
958
+ });
959
+ }
960
+ else {
961
+ // No user intent - use ML prediction
962
+ finalIntent = mlResult.intent;
963
+ tags.push('ml:predicted');
964
+ }
965
+ }
966
+ // Mark as human-labeled if user provided intent
967
+ if (userIntent) {
968
+ tags.push('labeled:human');
969
+ tags.push(`user_intent:${userIntent}`);
970
+ }
971
+ // Map iOS payload to VoiceInboxAddInput
972
+ const voiceInput = {
973
+ transcript,
974
+ source: 'mobile_app',
975
+ project_id: body.project_id,
976
+ process_immediately: true, // Process on receipt
977
+ tags: tags.length > 0 ? tags : undefined,
978
+ // Pass final intent (may be ML-overridden)
979
+ explicit_intent: finalIntent,
980
+ };
981
+ const result = await voiceInboxAdd(voiceInput);
982
+ sendJson(res, 200, wrapSuccess({
983
+ inbox_id: result.inbox_id,
984
+ transcript: result.transcript,
985
+ intent: result.intent,
986
+ intent_confidence: result.intent_confidence,
987
+ inbox_status: result.status,
988
+ immediate_result: result.immediate_result,
989
+ // ML metadata
990
+ labeled: !!userIntent,
991
+ user_intent: userIntent || null,
992
+ ml_intent: mlResult?.intent || null,
993
+ ml_confidence: mlResult ? Math.round(mlResult.confidence * 100) / 100 : null,
994
+ was_overridden: wasOverridden,
995
+ }));
996
+ }
997
+ catch (error) {
998
+ const message = error instanceof Error ? error.message : String(error);
999
+ log(`HTTP: /api/inbox error: ${message}`);
1000
+ sendJson(res, 400, wrapError(message, 'VOICE_INBOX_ERROR'));
1001
+ }
1002
+ return;
1003
+ }
1004
+ // ========================================================================
1005
+ // iOS App API Endpoints (StatusSnapshot compatible)
1006
+ // ========================================================================
1007
+ // GET /api/projects - List registered projects for iOS project picker
1008
+ if (path === '/api/projects' && req.method === 'GET') {
1009
+ try {
1010
+ log('HTTP: /api/projects');
1011
+ const projects = listProjects();
1012
+ sendJson(res, 200, wrapSuccess({
1013
+ projects: projects.map(p => ({
1014
+ id: p.id,
1015
+ name: p.name || p.id,
1016
+ aliases: p.aliases || [],
1017
+ })),
1018
+ }));
1019
+ }
1020
+ catch (error) {
1021
+ const message = error instanceof Error ? error.message : String(error);
1022
+ log(`HTTP: /api/projects error: ${message}`);
1023
+ sendJson(res, 500, wrapError(message, 'PROJECTS_ERROR'));
1024
+ }
1025
+ return;
1026
+ }
1027
+ // GET /api/status - StatusSnapshot for iOS StatusView
1028
+ if (path === '/api/status' && req.method === 'GET') {
1029
+ try {
1030
+ log('HTTP: /api/status');
1031
+ const projects = listProjects();
1032
+ // Check system health by listing each project's data
1033
+ const systemsHealth = {
1034
+ sentinel: { status: 'healthy', message: null },
1035
+ oracle: { status: 'healthy', message: null },
1036
+ dojo: { status: 'healthy', message: null },
1037
+ architect: { status: 'healthy', message: null },
1038
+ };
1039
+ // Build project summaries
1040
+ const projectSummaries = [];
1041
+ for (const project of projects) {
1042
+ try {
1043
+ // Get epic count
1044
+ const epicsResult = await listEpics({ projectId: project.id });
1045
+ const epicCount = isProjectResolutionError(epicsResult)
1046
+ ? 0
1047
+ : epicsResult.epics?.length || 0;
1048
+ // Get open issues count
1049
+ const issuesResult = await listRepoIssues({ projectId: project.id, status: 'open' });
1050
+ const openIssueCount = isProjectResolutionError(issuesResult)
1051
+ ? 0
1052
+ : issuesResult.issues?.length || 0;
1053
+ projectSummaries.push({
1054
+ project_id: project.id,
1055
+ name: project.name || project.id,
1056
+ active_epics: epicCount,
1057
+ open_issues: openIssueCount,
1058
+ last_activity: null, // Would need to scan files for timestamps
1059
+ });
1060
+ }
1061
+ catch {
1062
+ // If we can't get data for a project, still include it with zeros
1063
+ projectSummaries.push({
1064
+ project_id: project.id,
1065
+ name: project.name || project.id,
1066
+ active_epics: 0,
1067
+ open_issues: 0,
1068
+ last_activity: null,
1069
+ });
1070
+ }
1071
+ }
1072
+ const snapshot = {
1073
+ snapshot_id: crypto.randomUUID(),
1074
+ generated_at: new Date().toISOString(),
1075
+ source: {
1076
+ generator: 'mcp-server',
1077
+ version: PKG.version,
1078
+ },
1079
+ systems: systemsHealth,
1080
+ projects: projectSummaries,
1081
+ builds: [],
1082
+ alerts: [],
1083
+ };
1084
+ sendJson(res, 200, wrapSuccess(snapshot));
1085
+ }
1086
+ catch (error) {
1087
+ const message = error instanceof Error ? error.message : String(error);
1088
+ log(`HTTP: /api/status error: ${message}`);
1089
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1090
+ }
1091
+ return;
1092
+ }
1093
+ // ========================================================================
1094
+ // Studio API Endpoints (frontend_v0.2 compatible)
1095
+ // ========================================================================
1096
+ // POST /api/generate-flux-kontext-image - Start image generation
1097
+ if (path === '/api/generate-flux-kontext-image' && req.method === 'POST') {
1098
+ try {
1099
+ const body = await parseBody(req);
1100
+ log('HTTP: /api/generate-flux-kontext-image');
1101
+ // Validate required fields
1102
+ if (!body.prompt) {
1103
+ sendJson(res, 400, wrapError('Missing "prompt" field', 'MISSING_PROMPT'));
1104
+ return;
1105
+ }
1106
+ const input = {
1107
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1108
+ user_id: body.user_id || 'anonymous',
1109
+ prompt: body.prompt,
1110
+ input_image: body.input_image,
1111
+ aspect_ratio: body.aspect_ratio || '16:9',
1112
+ model: body.model || 'flux-kontext-pro',
1113
+ };
1114
+ const result = await generateImage(input);
1115
+ sendJson(res, 200, wrapSuccess(result));
1116
+ }
1117
+ catch (error) {
1118
+ const message = error instanceof Error ? error.message : String(error);
1119
+ log(`HTTP: /api/generate-flux-kontext-image error: ${message}`);
1120
+ sendJson(res, 500, wrapError(message, 'GENERATION_ERROR'));
1121
+ }
1122
+ return;
1123
+ }
1124
+ // GET /api/flux-kontext-status/:taskId - Check image generation status
1125
+ if (path.startsWith('/api/flux-kontext-status/') && req.method === 'GET') {
1126
+ try {
1127
+ const taskId = path.replace('/api/flux-kontext-status/', '');
1128
+ log(`HTTP: /api/flux-kontext-status/${taskId}`);
1129
+ if (!taskId) {
1130
+ sendJson(res, 400, wrapError('Missing task ID', 'MISSING_TASK_ID'));
1131
+ return;
1132
+ }
1133
+ const status = getImageStatus(taskId);
1134
+ if (!status) {
1135
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1136
+ return;
1137
+ }
1138
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1139
+ res.end(JSON.stringify(status));
1140
+ }
1141
+ catch (error) {
1142
+ const message = error instanceof Error ? error.message : String(error);
1143
+ log(`HTTP: /api/flux-kontext-status error: ${message}`);
1144
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1145
+ }
1146
+ return;
1147
+ }
1148
+ // ========================================================================
1149
+ // Meshy 3D Generation Endpoints
1150
+ // ========================================================================
1151
+ // POST /api/meshy/generate - Start 3D generation
1152
+ if (path === '/api/meshy/generate' && req.method === 'POST') {
1153
+ try {
1154
+ const body = await parseBody(req);
1155
+ log('HTTP: /api/meshy/generate');
1156
+ if (!body.mode) {
1157
+ sendJson(res, 400, wrapError('Missing "mode" field', 'MISSING_MODE'));
1158
+ return;
1159
+ }
1160
+ const input = {
1161
+ mode: body.mode,
1162
+ prompt: body.prompt,
1163
+ image_url: body.image_url,
1164
+ image_urls: body.image_urls,
1165
+ preview_task_id: body.preview_task_id,
1166
+ model_input: body.model_input,
1167
+ parameters: body.parameters,
1168
+ asset_id: body.asset_id,
1169
+ user_id: body.user_id,
1170
+ };
1171
+ const result = await meshyGenerate(input);
1172
+ sendJson(res, 200, wrapSuccess(result));
1173
+ }
1174
+ catch (error) {
1175
+ const message = error instanceof Error ? error.message : String(error);
1176
+ log(`HTTP: /api/meshy/generate error: ${message}`);
1177
+ sendJson(res, 500, wrapError(message, 'MESHY_ERROR'));
1178
+ }
1179
+ return;
1180
+ }
1181
+ // GET /api/meshy/status/:taskId - Check 3D generation status
1182
+ if (path.startsWith('/api/meshy/status/') && req.method === 'GET') {
1183
+ try {
1184
+ const taskId = path.replace('/api/meshy/status/', '').split('?')[0];
1185
+ log(`HTTP: /api/meshy/status/${taskId}`);
1186
+ const status = getMeshyStatus(taskId);
1187
+ if (!status) {
1188
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1189
+ return;
1190
+ }
1191
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1192
+ res.end(JSON.stringify(status));
1193
+ }
1194
+ catch (error) {
1195
+ const message = error instanceof Error ? error.message : String(error);
1196
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1197
+ }
1198
+ return;
1199
+ }
1200
+ // POST /api/meshy/download - Download completed model
1201
+ if (path === '/api/meshy/download' && req.method === 'POST') {
1202
+ try {
1203
+ const body = await parseBody(req);
1204
+ log('HTTP: /api/meshy/download');
1205
+ if (!body.task_id) {
1206
+ sendJson(res, 400, wrapError('Missing "task_id" field', 'MISSING_TASK_ID'));
1207
+ return;
1208
+ }
1209
+ const result = await meshyDownload(body.task_id, body.asset_id || `asset_${Date.now()}`, body.user_id || 'anonymous');
1210
+ sendJson(res, 200, wrapSuccess(result));
1211
+ }
1212
+ catch (error) {
1213
+ const message = error instanceof Error ? error.message : String(error);
1214
+ sendJson(res, 500, wrapError(message, 'DOWNLOAD_ERROR'));
1215
+ }
1216
+ return;
1217
+ }
1218
+ // ========================================================================
1219
+ // Tripo 3D Generation Endpoints
1220
+ // ========================================================================
1221
+ // POST /api/tripo/generate - Start Tripo 3D generation
1222
+ if (path === '/api/tripo/generate' && req.method === 'POST') {
1223
+ try {
1224
+ const body = await parseBody(req);
1225
+ log('HTTP: /api/tripo/generate');
1226
+ if (!body.type) {
1227
+ sendJson(res, 400, wrapError('Missing "type" field', 'MISSING_TYPE'));
1228
+ return;
1229
+ }
1230
+ const input = {
1231
+ type: body.type,
1232
+ prompt: body.prompt,
1233
+ image_url: body.image_url,
1234
+ image_urls: body.image_urls,
1235
+ parameters: body.parameters,
1236
+ asset_id: body.asset_id,
1237
+ user_id: body.user_id,
1238
+ };
1239
+ const result = await tripoGenerate(input);
1240
+ sendJson(res, 200, wrapSuccess(result));
1241
+ }
1242
+ catch (error) {
1243
+ const message = error instanceof Error ? error.message : String(error);
1244
+ log(`HTTP: /api/tripo/generate error: ${message}`);
1245
+ sendJson(res, 500, wrapError(message, 'TRIPO_ERROR'));
1246
+ }
1247
+ return;
1248
+ }
1249
+ // GET /api/tripo/task/:taskId - Check Tripo task status
1250
+ if (path.startsWith('/api/tripo/task/') && req.method === 'GET') {
1251
+ try {
1252
+ const taskId = path.replace('/api/tripo/task/', '');
1253
+ log(`HTTP: /api/tripo/task/${taskId}`);
1254
+ const status = getTripoStatus(taskId);
1255
+ if (!status) {
1256
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1257
+ return;
1258
+ }
1259
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1260
+ res.end(JSON.stringify(status));
1261
+ }
1262
+ catch (error) {
1263
+ const message = error instanceof Error ? error.message : String(error);
1264
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1265
+ }
1266
+ return;
1267
+ }
1268
+ // POST /api/tripo/download/:taskId - Download Tripo model
1269
+ if (path.startsWith('/api/tripo/download/') && req.method === 'POST') {
1270
+ try {
1271
+ const taskId = path.replace('/api/tripo/download/', '');
1272
+ const body = await parseBody(req);
1273
+ log(`HTTP: /api/tripo/download/${taskId}`);
1274
+ const result = await tripoDownload(taskId, body.asset_id || `asset_${Date.now()}`, body.user_id || 'anonymous');
1275
+ sendJson(res, 200, wrapSuccess(result));
1276
+ }
1277
+ catch (error) {
1278
+ const message = error instanceof Error ? error.message : String(error);
1279
+ sendJson(res, 500, wrapError(message, 'DOWNLOAD_ERROR'));
1280
+ }
1281
+ return;
1282
+ }
1283
+ // ========================================================================
1284
+ // Kling Video Generation Endpoints
1285
+ // ========================================================================
1286
+ // POST /api/generate-kling-video - Image to video
1287
+ if (path === '/api/generate-kling-video' && req.method === 'POST') {
1288
+ try {
1289
+ const body = await parseBody(req);
1290
+ log('HTTP: /api/generate-kling-video');
1291
+ if (!body.image_url || !body.prompt) {
1292
+ sendJson(res, 400, wrapError('Missing "image_url" or "prompt" field', 'MISSING_FIELDS'));
1293
+ return;
1294
+ }
1295
+ const input = {
1296
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1297
+ image_url: body.image_url,
1298
+ prompt: body.prompt,
1299
+ negative_prompt: body.negative_prompt,
1300
+ duration: body.duration || 5,
1301
+ aspect_ratio: body.aspect_ratio || '16:9',
1302
+ cfg_scale: body.cfg_scale,
1303
+ seed: body.seed,
1304
+ user_id: body.user_id,
1305
+ model: body.model,
1306
+ sound: body.sound,
1307
+ };
1308
+ const result = await klingGenerateVideo(input);
1309
+ sendJson(res, 200, wrapSuccess(result));
1310
+ }
1311
+ catch (error) {
1312
+ const message = error instanceof Error ? error.message : String(error);
1313
+ log(`HTTP: /api/generate-kling-video error: ${message}`);
1314
+ sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
1315
+ }
1316
+ return;
1317
+ }
1318
+ // POST /api/generate-kling-text-video - Text to video
1319
+ if (path === '/api/generate-kling-text-video' && req.method === 'POST') {
1320
+ try {
1321
+ const body = await parseBody(req);
1322
+ log('HTTP: /api/generate-kling-text-video');
1323
+ if (!body.prompt) {
1324
+ sendJson(res, 400, wrapError('Missing "prompt" field', 'MISSING_PROMPT'));
1325
+ return;
1326
+ }
1327
+ const input = {
1328
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1329
+ prompt: body.prompt,
1330
+ negative_prompt: body.negative_prompt,
1331
+ duration: body.duration || 5,
1332
+ aspect_ratio: body.aspect_ratio || '16:9',
1333
+ cfg_scale: body.cfg_scale,
1334
+ user_id: body.user_id,
1335
+ model: body.model,
1336
+ sound: body.sound,
1337
+ };
1338
+ const result = await klingGenerateTextVideo(input);
1339
+ sendJson(res, 200, wrapSuccess(result));
1340
+ }
1341
+ catch (error) {
1342
+ const message = error instanceof Error ? error.message : String(error);
1343
+ log(`HTTP: /api/generate-kling-text-video error: ${message}`);
1344
+ sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
1345
+ }
1346
+ return;
1347
+ }
1348
+ // POST /api/generate-kling-avatar - Avatar/lip-sync video
1349
+ if (path === '/api/generate-kling-avatar' && req.method === 'POST') {
1350
+ try {
1351
+ const body = await parseBody(req);
1352
+ log('HTTP: /api/generate-kling-avatar');
1353
+ if (!body.image_url || !body.audio_url) {
1354
+ sendJson(res, 400, wrapError('Missing "image_url" or "audio_url" field', 'MISSING_FIELDS'));
1355
+ return;
1356
+ }
1357
+ const input = {
1358
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1359
+ image_url: body.image_url,
1360
+ audio_url: body.audio_url,
1361
+ prompt: body.prompt,
1362
+ user_id: body.user_id,
1363
+ model: body.model,
1364
+ };
1365
+ const result = await klingGenerateAvatar(input);
1366
+ sendJson(res, 200, wrapSuccess(result));
1367
+ }
1368
+ catch (error) {
1369
+ const message = error instanceof Error ? error.message : String(error);
1370
+ log(`HTTP: /api/generate-kling-avatar error: ${message}`);
1371
+ sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
1372
+ }
1373
+ return;
1374
+ }
1375
+ // GET /api/kling-video-status/:taskId - Check video generation status
1376
+ if (path.startsWith('/api/kling-video-status/') && req.method === 'GET') {
1377
+ try {
1378
+ const taskId = path.replace('/api/kling-video-status/', '');
1379
+ log(`HTTP: /api/kling-video-status/${taskId}`);
1380
+ const status = getKlingStatus(taskId);
1381
+ if (!status) {
1382
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1383
+ return;
1384
+ }
1385
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1386
+ res.end(JSON.stringify(status));
1387
+ }
1388
+ catch (error) {
1389
+ const message = error instanceof Error ? error.message : String(error);
1390
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1391
+ }
1392
+ return;
1393
+ }
1394
+ // GET /api/studio/tasks - List all tasks (debug endpoint)
1395
+ if (path === '/api/studio/tasks' && req.method === 'GET') {
1396
+ try {
1397
+ log('HTTP: /api/studio/tasks');
1398
+ const tasks = listTasks();
1399
+ sendJson(res, 200, wrapSuccess({ tasks, count: tasks.length }));
1400
+ }
1401
+ catch (error) {
1402
+ const message = error instanceof Error ? error.message : String(error);
1403
+ sendJson(res, 500, wrapError(message, 'LIST_ERROR'));
1404
+ }
1405
+ return;
1406
+ }
1407
+ // ========================================================================
1408
+ // Full MCP Protocol Endpoint
1409
+ // ========================================================================
1410
+ // (b) MCP endpoint - supports GET, POST, DELETE via StreamableHTTPServerTransport
1411
+ // Handle at /mcp, /sse, /sse/ (ChatGPT uses trailing slash), and root / for compatibility
1412
+ if (path === '/mcp' || path === '/sse' || path === '/sse/' || (path === '/' && (req.method === 'POST' || req.method === 'DELETE'))) {
1413
+ try {
1414
+ await transport.handleRequest(req, res);
1415
+ }
1416
+ catch (error) {
1417
+ const message = error instanceof Error ? error.message : String(error);
1418
+ log(`HTTP: Error handling MCP request: ${message}`);
1419
+ if (!res.writableEnded) {
1420
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1421
+ res.end(JSON.stringify({ error: message }));
1422
+ }
1423
+ }
1424
+ return;
1425
+ }
1426
+ // 404 for all other paths
1427
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1428
+ res.end(JSON.stringify({ error: 'Not found' }));
1429
+ });
1430
+ httpServer.listen(port, host, () => {
1431
+ log(`HTTP Server listening on http://${host}:${port}`);
1432
+ console.log(`
1433
+ ╔══════════════════════════════════════════════════════════════╗
1434
+ ║ Decibel MCP Server - HTTP Mode v${PKG.version}${' '.repeat(Math.max(0, 24 - PKG.version.length))}║
1435
+ ╠══════════════════════════════════════════════════════════════╣
1436
+ ║ Endpoints: ║
1437
+ ║ GET /health Health check ║
1438
+ ║ GET /tools List tools ║
1439
+ ║ POST /call Execute tool (generic) ║
1440
+ ║ POST /dojo/wish Add wish ║
1441
+ ║ POST /dojo/propose Create proposal ║
1442
+ ║ POST /dojo/scaffold Scaffold experiment ║
1443
+ ║ POST /dojo/run Run experiment ║
1444
+ ║ POST /dojo/results Get results ║
1445
+ ║ POST /dojo/artifact Read artifact file ║
1446
+ ║ GET /dojo/list List all ║
1447
+ ║ POST /mcp Full MCP protocol ║
1448
+ ╠══════════════════════════════════════════════════════════════╣
1449
+ ║ Base URL: http://${host}:${port}${' '.repeat(Math.max(0, 40 - port.toString().length - host.length))}║
1450
+ ${authToken ? '║ Auth: Bearer token required ║' : '║ Auth: None (use --auth-token for security) ║'}
1451
+ ╠══════════════════════════════════════════════════════════════╣
1452
+ ║ Response format: {"status": "executed"|"error", ...} ║
1453
+ ╚══════════════════════════════════════════════════════════════╝
1454
+ `);
1455
+ });
1456
+ }
1457
+ /**
1458
+ * Parse command line arguments for HTTP mode
1459
+ */
1460
+ export function parseHttpArgs(args) {
1461
+ const httpMode = args.includes('--http');
1462
+ const portIndex = args.indexOf('--port');
1463
+ // Render sets PORT env var - use it if available
1464
+ const defaultPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 8787;
1465
+ const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : defaultPort;
1466
+ const authIndex = args.indexOf('--auth-token');
1467
+ const authToken = authIndex !== -1 ? args[authIndex + 1] : undefined;
1468
+ const hostIndex = args.indexOf('--host');
1469
+ const host = hostIndex !== -1 ? args[hostIndex + 1] : '0.0.0.0';
1470
+ return { httpMode, port, authToken, host };
1471
+ }
1472
+ //# sourceMappingURL=httpServer.js.map