sensorium-mcp 3.0.4 → 3.0.5

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 (109) hide show
  1. package/dist/dashboard/routes/data.d.ts.map +1 -1
  2. package/dist/dashboard/routes/data.js +2 -1
  3. package/dist/dashboard/routes/data.js.map +1 -1
  4. package/dist/dashboard/routes/threads.js +1 -1
  5. package/dist/dashboard/routes/threads.js.map +1 -1
  6. package/dist/dashboard/routes.d.ts.map +1 -1
  7. package/dist/dashboard/routes.js +1 -3
  8. package/dist/dashboard/routes.js.map +1 -1
  9. package/dist/data/memory/migration-runner.d.ts +1 -1
  10. package/dist/data/memory/migration-runner.d.ts.map +1 -1
  11. package/dist/data/memory/migration-runner.js +59 -3
  12. package/dist/data/memory/migration-runner.js.map +1 -1
  13. package/dist/data/memory/schema-ddl.d.ts +1 -1
  14. package/dist/data/memory/schema-ddl.d.ts.map +1 -1
  15. package/dist/data/memory/schema-ddl.js +2 -1
  16. package/dist/data/memory/schema-ddl.js.map +1 -1
  17. package/dist/data/memory/thread-registry.js +1 -1
  18. package/dist/data/memory/thread-registry.js.map +1 -1
  19. package/dist/http-server.d.ts.map +1 -1
  20. package/dist/http-server.js +1 -9
  21. package/dist/http-server.js.map +1 -1
  22. package/dist/index.js +3 -6
  23. package/dist/index.js.map +1 -1
  24. package/dist/server/factory.js +1 -1
  25. package/dist/server/factory.js.map +1 -1
  26. package/dist/services/agent-spawn.service.d.ts +7 -1
  27. package/dist/services/agent-spawn.service.d.ts.map +1 -1
  28. package/dist/services/agent-spawn.service.js +69 -45
  29. package/dist/services/agent-spawn.service.js.map +1 -1
  30. package/dist/services/consolidation.service.d.ts.map +1 -1
  31. package/dist/services/consolidation.service.js +49 -35
  32. package/dist/services/consolidation.service.js.map +1 -1
  33. package/dist/services/keeper.service.d.ts +21 -0
  34. package/dist/services/keeper.service.d.ts.map +1 -0
  35. package/dist/services/keeper.service.js +195 -0
  36. package/dist/services/keeper.service.js.map +1 -0
  37. package/dist/services/maintenance-signal.d.ts +2 -0
  38. package/dist/services/maintenance-signal.d.ts.map +1 -1
  39. package/dist/services/maintenance-signal.js +7 -1
  40. package/dist/services/maintenance-signal.js.map +1 -1
  41. package/dist/services/process.service.d.ts +19 -2
  42. package/dist/services/process.service.d.ts.map +1 -1
  43. package/dist/services/process.service.js +104 -10
  44. package/dist/services/process.service.js.map +1 -1
  45. package/dist/services/thread-lifecycle.service.d.ts +5 -0
  46. package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
  47. package/dist/services/thread-lifecycle.service.js +33 -8
  48. package/dist/services/thread-lifecycle.service.js.map +1 -1
  49. package/dist/services/worker-cleanup.service.d.ts +14 -1
  50. package/dist/services/worker-cleanup.service.d.ts.map +1 -1
  51. package/dist/services/worker-cleanup.service.js +36 -38
  52. package/dist/services/worker-cleanup.service.js.map +1 -1
  53. package/dist/sessions.d.ts +0 -5
  54. package/dist/sessions.d.ts.map +1 -1
  55. package/dist/sessions.js +0 -7
  56. package/dist/sessions.js.map +1 -1
  57. package/dist/stdio-server.d.ts.map +1 -1
  58. package/dist/stdio-server.js +1 -7
  59. package/dist/stdio-server.js.map +1 -1
  60. package/dist/tools/delegate-tool.d.ts.map +1 -1
  61. package/dist/tools/delegate-tool.js +2 -2
  62. package/dist/tools/delegate-tool.js.map +1 -1
  63. package/dist/tools/session-tools.js +1 -1
  64. package/dist/tools/session-tools.js.map +1 -1
  65. package/dist/tools/start-session-tool.d.ts.map +1 -1
  66. package/dist/tools/start-session-tool.js +8 -9
  67. package/dist/tools/start-session-tool.js.map +1 -1
  68. package/dist/tools/wait/message-processing.d.ts.map +1 -1
  69. package/dist/tools/wait/message-processing.js +28 -0
  70. package/dist/tools/wait/message-processing.js.map +1 -1
  71. package/dist/tools/wait/poll-loop.js +1 -1
  72. package/dist/tools/wait/poll-loop.js.map +1 -1
  73. package/package.json +1 -1
  74. package/dist/tools/thread-lifecycle.d.ts +0 -6
  75. package/dist/tools/thread-lifecycle.d.ts.map +0 -1
  76. package/dist/tools/thread-lifecycle.js +0 -6
  77. package/dist/tools/thread-lifecycle.js.map +0 -1
  78. package/supervisor/config.go +0 -253
  79. package/supervisor/config_test.go +0 -78
  80. package/supervisor/go.mod +0 -15
  81. package/supervisor/go.sum +0 -20
  82. package/supervisor/health.go +0 -433
  83. package/supervisor/health_test.go +0 -93
  84. package/supervisor/keeper.go +0 -309
  85. package/supervisor/keeper_test.go +0 -27
  86. package/supervisor/lock.go +0 -57
  87. package/supervisor/lock_test.go +0 -54
  88. package/supervisor/log.go +0 -195
  89. package/supervisor/log_test.go +0 -125
  90. package/supervisor/main.go +0 -475
  91. package/supervisor/main_test.go +0 -130
  92. package/supervisor/notify.go +0 -53
  93. package/supervisor/process.go +0 -294
  94. package/supervisor/process_test.go +0 -108
  95. package/supervisor/process_unix.go +0 -14
  96. package/supervisor/process_windows.go +0 -15
  97. package/supervisor/secrets.go +0 -95
  98. package/supervisor/secrets_securevault_test.go +0 -98
  99. package/supervisor/secrets_test.go +0 -119
  100. package/supervisor/self_update.go +0 -282
  101. package/supervisor/self_update_test.go +0 -177
  102. package/supervisor/service_restart_stub.go +0 -9
  103. package/supervisor/service_restart_windows.go +0 -63
  104. package/supervisor/service_stub.go +0 -15
  105. package/supervisor/service_windows.go +0 -194
  106. package/supervisor/update_state.go +0 -264
  107. package/supervisor/update_state_test.go +0 -306
  108. package/supervisor/updater.go +0 -613
  109. package/supervisor/updater_test.go +0 -64
@@ -1,433 +0,0 @@
1
- package main
2
-
3
- import (
4
- "bytes"
5
- "context"
6
- "encoding/json"
7
- "fmt"
8
- "io"
9
- "net/http"
10
- "strings"
11
- "time"
12
- )
13
-
14
- // MCPClient wraps HTTP interactions with the MCP server.
15
- type MCPClient struct {
16
- BaseURL string
17
- Secret string
18
- Client *http.Client
19
- Log *Logger
20
- headers map[string]string
21
- }
22
-
23
- func NewMCPClient(port int, secret string) *MCPClient {
24
- h := map[string]string{
25
- "Content-Type": "application/json",
26
- "Accept": "application/json",
27
- }
28
- if secret != "" {
29
- h["Authorization"] = "Bearer " + secret
30
- }
31
- return &MCPClient{
32
- BaseURL: fmt.Sprintf("http://127.0.0.1:%d", port),
33
- Secret: secret,
34
- Client: &http.Client{Timeout: 10 * time.Second},
35
- headers: h,
36
- }
37
- }
38
-
39
- func (m *MCPClient) authHeaders() map[string]string {
40
- return m.headers
41
- }
42
-
43
- func (m *MCPClient) doReq(ctx context.Context, method, path string, body any) (*http.Response, error) {
44
- var bodyReader io.Reader
45
- if body != nil {
46
- data, err := json.Marshal(body)
47
- if err != nil {
48
- return nil, err
49
- }
50
- bodyReader = bytes.NewReader(data)
51
- }
52
-
53
- req, err := http.NewRequestWithContext(ctx, method, m.BaseURL+path, bodyReader)
54
- if err != nil {
55
- return nil, err
56
- }
57
- for k, v := range m.authHeaders() {
58
- req.Header.Set(k, v)
59
- }
60
- return m.Client.Do(req)
61
- }
62
-
63
- // IsServerReady checks if the MCP server is responding (OPTIONS /mcp).
64
- func (m *MCPClient) IsServerReady(ctx context.Context) bool {
65
- ctx2, cancel := context.WithTimeout(ctx, 3*time.Second)
66
- defer cancel()
67
- resp, err := m.doReq(ctx2, "OPTIONS", "/mcp", nil)
68
- if err != nil {
69
- if m.Log != nil {
70
- m.Log.Debug("IsServerReady: OPTIONS /mcp failed: %v", err)
71
- }
72
- return false
73
- }
74
- defer resp.Body.Close()
75
- ready := resp.StatusCode < 500
76
- if m.Log != nil {
77
- m.Log.Debug("IsServerReady: OPTIONS /mcp => %d (ready=%v)", resp.StatusCode, ready)
78
- }
79
- return ready
80
- }
81
-
82
- // WaitForReady polls the MCP server until it's ready, or timeout expires.
83
- func (m *MCPClient) WaitForReady(ctx context.Context, pollInterval, timeout time.Duration) bool {
84
- ctx2, cancel := context.WithTimeout(ctx, timeout)
85
- defer cancel()
86
- ticker := time.NewTicker(pollInterval)
87
- defer ticker.Stop()
88
-
89
- for {
90
- if m.IsServerReady(ctx2) {
91
- return true
92
- }
93
- select {
94
- case <-ctx2.Done():
95
- return false
96
- case <-ticker.C:
97
- // try again
98
- }
99
- }
100
- }
101
-
102
- // PrepareShutdown asks the MCP server to write a reconnect snapshot before being killed.
103
- // Best-effort: returns false on any error so the caller can proceed with the kill.
104
- func (m *MCPClient) PrepareShutdown(ctx context.Context) bool {
105
- ctx2, cancel := context.WithTimeout(ctx, 3*time.Second)
106
- defer cancel()
107
- resp, err := m.doReq(ctx2, "POST", "/api/prepare-shutdown", nil)
108
- if err != nil {
109
- if m.Log != nil {
110
- m.Log.Debug("PrepareShutdown: POST /api/prepare-shutdown failed: %v", err)
111
- }
112
- return false
113
- }
114
- defer resp.Body.Close()
115
- ok := resp.StatusCode == 200
116
- if m.Log != nil {
117
- m.Log.Info("PrepareShutdown: POST /api/prepare-shutdown => %d", resp.StatusCode)
118
- }
119
- return ok
120
- }
121
-
122
- // GetRootThreads fetches the list of root threads from the server.
123
- func (m *MCPClient) GetRootThreads(ctx context.Context) ([]map[string]any, error) {
124
- return m.fetchThreadList(ctx, "/api/threads/roots")
125
- }
126
-
127
- // GetKeepAliveThreads fetches all threads with keepAlive=true (excluding worker threads).
128
- func (m *MCPClient) GetKeepAliveThreads(ctx context.Context) ([]map[string]any, error) {
129
- return m.fetchThreadList(ctx, "/api/threads/keepalive")
130
- }
131
-
132
- // fetchThreadList is a shared helper for thread-list endpoints.
133
- func (m *MCPClient) fetchThreadList(ctx context.Context, path string) ([]map[string]any, error) {
134
- ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
135
- defer cancel()
136
- resp, err := m.doReq(ctx2, "GET", path, nil)
137
- if err != nil {
138
- return nil, err
139
- }
140
- defer resp.Body.Close()
141
- if resp.StatusCode != 200 {
142
- return nil, fmt.Errorf("GET %s: %d", path, resp.StatusCode)
143
- }
144
- body, err := io.ReadAll(resp.Body)
145
- if err != nil {
146
- return nil, err
147
- }
148
-
149
- // Try bare array first, then wrapped {"threads": [...]}
150
- var result []map[string]any
151
- if err := json.Unmarshal(body, &result); err == nil {
152
- return result, nil
153
- }
154
- var wrapped struct {
155
- Threads []map[string]any `json:"threads"`
156
- }
157
- if err := json.Unmarshal(body, &wrapped); err != nil {
158
- return nil, fmt.Errorf("cannot parse %s response: %w", path, err)
159
- }
160
- return wrapped.Threads, nil
161
- }
162
-
163
- // IsThreadRunning checks if a specific thread is running on the MCP server.
164
- func (m *MCPClient) IsThreadRunning(ctx context.Context, threadID int) bool {
165
- ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
166
- defer cancel()
167
- resp, err := m.doReq(ctx2, "GET", fmt.Sprintf("/api/threads/%d/running", threadID), nil)
168
- if err != nil {
169
- if m.Log != nil {
170
- m.Log.Debug("IsThreadRunning(%d): request failed: %v", threadID, err)
171
- }
172
- return false
173
- }
174
- defer resp.Body.Close()
175
- if resp.StatusCode != 200 {
176
- if m.Log != nil {
177
- m.Log.Debug("IsThreadRunning(%d): HTTP %d", threadID, resp.StatusCode)
178
- }
179
- return false
180
- }
181
- var result struct {
182
- Running bool `json:"running"`
183
- }
184
- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
185
- if m.Log != nil {
186
- m.Log.Debug("IsThreadRunning(%d): decode error: %v", threadID, err)
187
- }
188
- return false
189
- }
190
- if m.Log != nil {
191
- m.Log.Debug("IsThreadRunning(%d): running=%v", threadID, result.Running)
192
- }
193
- return result.Running
194
- }
195
-
196
- // IsThreadStuck checks the per-thread heartbeat to detect stuck threads.
197
- func (m *MCPClient) IsThreadStuck(ctx context.Context, threadID int, threshold time.Duration) bool {
198
- ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
199
- defer cancel()
200
- resp, err := m.doReq(ctx2, "GET", fmt.Sprintf("/api/threads/%d/heartbeat", threadID), nil)
201
- if err != nil {
202
- if m.Log != nil {
203
- m.Log.Debug("IsThreadStuck(%d): heartbeat request failed: %v", threadID, err)
204
- }
205
- return false // can't determine — default to not stuck
206
- }
207
- defer resp.Body.Close()
208
- if resp.StatusCode != 200 {
209
- if m.Log != nil {
210
- m.Log.Debug("IsThreadStuck(%d): heartbeat HTTP %d", threadID, resp.StatusCode)
211
- }
212
- return false
213
- }
214
- var result struct {
215
- LastActivityMs int64 `json:"lastActivityMs"`
216
- }
217
- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
218
- if m.Log != nil {
219
- m.Log.Debug("IsThreadStuck(%d): heartbeat decode error: %v", threadID, err)
220
- }
221
- return false
222
- }
223
- if result.LastActivityMs <= 0 {
224
- if m.Log != nil {
225
- m.Log.Debug("IsThreadStuck(%d): no lastActivityMs data", threadID)
226
- }
227
- return false
228
- }
229
- age := time.Duration(time.Now().UnixMilli()-result.LastActivityMs) * time.Millisecond
230
- stuck := age > threshold
231
- if m.Log != nil {
232
- m.Log.Debug("IsThreadStuck(%d): age=%v threshold=%v stuck=%v", threadID, age.Round(time.Second), threshold, stuck)
233
- }
234
- return stuck
235
- }
236
-
237
- // parseJsonOrSse extracts JSON from either a plain JSON response or an SSE stream.
238
- // The MCP SDK may return SSE format when Accept includes text/event-stream.
239
- func parseJsonOrSse(body []byte, contentType string) (map[string]any, error) {
240
- if strings.Contains(contentType, "text/event-stream") {
241
- // Extract JSON from SSE data: lines
242
- for _, line := range strings.Split(string(body), "\n") {
243
- line = strings.TrimSpace(line)
244
- if strings.HasPrefix(line, "data:") {
245
- jsonStr := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
246
- if jsonStr == "" {
247
- continue
248
- }
249
- var result map[string]any
250
- if err := json.Unmarshal([]byte(jsonStr), &result); err == nil {
251
- return result, nil
252
- }
253
- }
254
- }
255
- return nil, fmt.Errorf("no valid JSON found in SSE stream")
256
- }
257
- var result map[string]any
258
- if err := json.Unmarshal(body, &result); err != nil {
259
- return nil, err
260
- }
261
- return result, nil
262
- }
263
-
264
- // OpenMCPSession creates an MCP session via the initialize handshake.
265
- // Returns the session ID or empty string on failure.
266
- func (m *MCPClient) OpenMCPSession(ctx context.Context) (string, error) {
267
- if m.Log != nil {
268
- m.Log.Debug("OpenMCPSession: initiating handshake")
269
- }
270
- ctx2, cancel := context.WithTimeout(ctx, 30*time.Second)
271
- defer cancel()
272
-
273
- initPayload := map[string]any{
274
- "jsonrpc": "2.0",
275
- "id": 1,
276
- "method": "initialize",
277
- "params": map[string]any{
278
- "protocolVersion": "2025-03-26",
279
- "capabilities": map[string]any{},
280
- "clientInfo": map[string]any{
281
- "name": "sensorium-supervisor",
282
- "version": "1.0.0",
283
- },
284
- },
285
- }
286
-
287
- resp, err := m.doReq(ctx2, "POST", "/mcp", initPayload)
288
- if err != nil {
289
- return "", fmt.Errorf("initialize: %w", err)
290
- }
291
- defer resp.Body.Close()
292
-
293
- if resp.StatusCode >= 400 {
294
- return "", fmt.Errorf("initialize HTTP %d", resp.StatusCode)
295
- }
296
-
297
- sessionID := resp.Header.Get("Mcp-Session-Id")
298
- if sessionID == "" {
299
- if m.Log != nil {
300
- m.Log.Debug("OpenMCPSession: no Mcp-Session-Id in response headers")
301
- }
302
- return "", fmt.Errorf("initialize succeeded but no session ID returned")
303
- }
304
-
305
- // Send initialized notification
306
- notifPayload := map[string]any{
307
- "jsonrpc": "2.0",
308
- "method": "notifications/initialized",
309
- }
310
- notifReq, err := http.NewRequestWithContext(ctx2, "POST", m.BaseURL+"/mcp", nil)
311
- if err != nil {
312
- return sessionID, nil // session created, notification failed — non-fatal
313
- }
314
- data, err := json.Marshal(notifPayload)
315
- if err != nil {
316
- return sessionID, nil // session created, notification failed — non-fatal
317
- }
318
- notifReq.Body = io.NopCloser(bytes.NewReader(data))
319
- for k, v := range m.authHeaders() {
320
- notifReq.Header.Set(k, v)
321
- }
322
- notifReq.Header.Set("Mcp-Session-Id", sessionID)
323
- nResp, err := m.Client.Do(notifReq)
324
- if err == nil {
325
- nResp.Body.Close()
326
- }
327
-
328
- return sessionID, nil
329
- }
330
-
331
- // CloseMCPSession closes a session via HTTP DELETE.
332
- func (m *MCPClient) CloseMCPSession(ctx context.Context, sessionID string) {
333
- if m.Log != nil {
334
- m.Log.Debug("CloseMCPSession: closing session %s", sessionID)
335
- }
336
- ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
337
- defer cancel()
338
- req, err := http.NewRequestWithContext(ctx2, "DELETE", m.BaseURL+"/mcp", nil)
339
- if err != nil {
340
- return
341
- }
342
- for k, v := range m.authHeaders() {
343
- req.Header.Set(k, v)
344
- }
345
- req.Header.Set("Mcp-Session-Id", sessionID)
346
- resp, err := m.Client.Do(req)
347
- if err == nil {
348
- resp.Body.Close()
349
- }
350
- }
351
-
352
- // CallStartThread invokes the start_thread MCP tool via JSON-RPC.
353
- func (m *MCPClient) CallStartThread(ctx context.Context, sessionID string, threadID int, sessionName, client, workingDir string) (string, error) {
354
- if m.Log != nil {
355
- m.Log.Debug("CallStartThread: threadID=%d session=%q client=%q workDir=%q", threadID, sessionName, client, workingDir)
356
- }
357
- ctx2, cancel := context.WithTimeout(ctx, 30*time.Second)
358
- defer cancel()
359
-
360
- args := map[string]any{
361
- "threadId": threadID,
362
- "name": sessionName,
363
- "agentType": client,
364
- }
365
- if workingDir != "" {
366
- args["workingDirectory"] = workingDir
367
- }
368
-
369
- payload := map[string]any{
370
- "jsonrpc": "2.0",
371
- "id": 2,
372
- "method": "tools/call",
373
- "params": map[string]any{
374
- "name": "start_thread",
375
- "arguments": args,
376
- },
377
- }
378
-
379
- req, err := http.NewRequestWithContext(ctx2, "POST", m.BaseURL+"/mcp", nil)
380
- if err != nil {
381
- return "", err
382
- }
383
- data, err := json.Marshal(payload)
384
- if err != nil {
385
- return "", err
386
- }
387
- req.Body = io.NopCloser(bytes.NewReader(data))
388
- for k, v := range m.authHeaders() {
389
- req.Header.Set(k, v)
390
- }
391
- req.Header.Set("Mcp-Session-Id", sessionID)
392
-
393
- resp, err := m.Client.Do(req)
394
- if err != nil {
395
- return "", fmt.Errorf("start_thread: %w", err)
396
- }
397
- defer resp.Body.Close()
398
-
399
- if resp.StatusCode >= 400 {
400
- return "", fmt.Errorf("start_thread HTTP %d", resp.StatusCode)
401
- }
402
-
403
- body, err := io.ReadAll(resp.Body)
404
- if err != nil {
405
- return "", err
406
- }
407
-
408
- result, err := parseJsonOrSse(body, resp.Header.Get("Content-Type"))
409
- if err != nil {
410
- return "", fmt.Errorf("start_thread parse: %w", err)
411
- }
412
-
413
- // Check for JSON-RPC error
414
- if errObj, ok := result["error"]; ok {
415
- if errMap, ok := errObj.(map[string]any); ok {
416
- return "", fmt.Errorf("start_thread RPC error: %v", errMap["message"])
417
- }
418
- return "", fmt.Errorf("start_thread RPC error: %v", errObj)
419
- }
420
-
421
- // Extract text from result.content[0].text
422
- if res, ok := result["result"].(map[string]any); ok {
423
- if content, ok := res["content"].([]any); ok && len(content) > 0 {
424
- if item, ok := content[0].(map[string]any); ok {
425
- if text, ok := item["text"].(string); ok {
426
- return text, nil
427
- }
428
- }
429
- }
430
- }
431
-
432
- return "", nil
433
- }
@@ -1,93 +0,0 @@
1
- package main
2
-
3
- import (
4
- "context"
5
- "net/http"
6
- "net/http/httptest"
7
- "testing"
8
- )
9
-
10
- func TestGetRootThreads_WrappedJSON(t *testing.T) {
11
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12
- w.Header().Set("Content-Type", "application/json")
13
- w.Write([]byte(`{"threads":[{"threadId":1327,"name":"Sensorium 1","keepAlive":true}]}`))
14
- }))
15
- defer srv.Close()
16
-
17
- mcp := &MCPClient{BaseURL: srv.URL, Client: srv.Client()}
18
- threads, err := mcp.GetRootThreads(context.Background())
19
- if err != nil {
20
- t.Fatalf("unexpected error: %v", err)
21
- }
22
- if len(threads) != 1 {
23
- t.Fatalf("got %d threads, want 1", len(threads))
24
- }
25
- if threads[0]["name"] != "Sensorium 1" {
26
- t.Errorf("name = %v, want Sensorium 1", threads[0]["name"])
27
- }
28
- }
29
-
30
- func TestGetRootThreads_BareArray(t *testing.T) {
31
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
32
- w.Header().Set("Content-Type", "application/json")
33
- w.Write([]byte(`[{"threadId":7526,"name":"Thread A"},{"threadId":8888,"name":"Thread B"}]`))
34
- }))
35
- defer srv.Close()
36
-
37
- mcp := &MCPClient{BaseURL: srv.URL, Client: srv.Client()}
38
- threads, err := mcp.GetRootThreads(context.Background())
39
- if err != nil {
40
- t.Fatalf("unexpected error: %v", err)
41
- }
42
- if len(threads) != 2 {
43
- t.Fatalf("got %d threads, want 2", len(threads))
44
- }
45
- }
46
-
47
- func TestIsServerReady_Healthy(t *testing.T) {
48
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
49
- if r.Method == "OPTIONS" {
50
- w.WriteHeader(204)
51
- return
52
- }
53
- }))
54
- defer srv.Close()
55
-
56
- mcp := &MCPClient{BaseURL: srv.URL, Client: srv.Client()}
57
- if !mcp.IsServerReady(context.Background()) {
58
- t.Error("expected server to be ready")
59
- }
60
- }
61
-
62
- func TestIsServerReady_Down(t *testing.T) {
63
- mcp := &MCPClient{BaseURL: "http://127.0.0.1:1", Client: http.DefaultClient}
64
- if mcp.IsServerReady(context.Background()) {
65
- t.Error("expected server to be unreachable")
66
- }
67
- }
68
-
69
- func TestIsThreadRunning_True(t *testing.T) {
70
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
71
- w.Header().Set("Content-Type", "application/json")
72
- w.Write([]byte(`{"running":true}`))
73
- }))
74
- defer srv.Close()
75
-
76
- mcp := &MCPClient{BaseURL: srv.URL, Client: srv.Client()}
77
- if !mcp.IsThreadRunning(context.Background(), 1234) {
78
- t.Error("expected thread to be running")
79
- }
80
- }
81
-
82
- func TestIsThreadRunning_False(t *testing.T) {
83
- srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84
- w.Header().Set("Content-Type", "application/json")
85
- w.Write([]byte(`{"running":false}`))
86
- }))
87
- defer srv.Close()
88
-
89
- mcp := &MCPClient{BaseURL: srv.URL, Client: srv.Client()}
90
- if mcp.IsThreadRunning(context.Background(), 1234) {
91
- t.Error("expected thread to not be running")
92
- }
93
- }