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.
- package/dist/dashboard/routes/data.d.ts.map +1 -1
- package/dist/dashboard/routes/data.js +2 -1
- package/dist/dashboard/routes/data.js.map +1 -1
- package/dist/dashboard/routes/threads.js +1 -1
- package/dist/dashboard/routes/threads.js.map +1 -1
- package/dist/dashboard/routes.d.ts.map +1 -1
- package/dist/dashboard/routes.js +1 -3
- package/dist/dashboard/routes.js.map +1 -1
- package/dist/data/memory/migration-runner.d.ts +1 -1
- package/dist/data/memory/migration-runner.d.ts.map +1 -1
- package/dist/data/memory/migration-runner.js +59 -3
- package/dist/data/memory/migration-runner.js.map +1 -1
- package/dist/data/memory/schema-ddl.d.ts +1 -1
- package/dist/data/memory/schema-ddl.d.ts.map +1 -1
- package/dist/data/memory/schema-ddl.js +2 -1
- package/dist/data/memory/schema-ddl.js.map +1 -1
- package/dist/data/memory/thread-registry.js +1 -1
- package/dist/data/memory/thread-registry.js.map +1 -1
- package/dist/http-server.d.ts.map +1 -1
- package/dist/http-server.js +1 -9
- package/dist/http-server.js.map +1 -1
- package/dist/index.js +3 -6
- package/dist/index.js.map +1 -1
- package/dist/server/factory.js +1 -1
- package/dist/server/factory.js.map +1 -1
- package/dist/services/agent-spawn.service.d.ts +7 -1
- package/dist/services/agent-spawn.service.d.ts.map +1 -1
- package/dist/services/agent-spawn.service.js +69 -45
- package/dist/services/agent-spawn.service.js.map +1 -1
- package/dist/services/consolidation.service.d.ts.map +1 -1
- package/dist/services/consolidation.service.js +49 -35
- package/dist/services/consolidation.service.js.map +1 -1
- package/dist/services/keeper.service.d.ts +21 -0
- package/dist/services/keeper.service.d.ts.map +1 -0
- package/dist/services/keeper.service.js +195 -0
- package/dist/services/keeper.service.js.map +1 -0
- package/dist/services/maintenance-signal.d.ts +2 -0
- package/dist/services/maintenance-signal.d.ts.map +1 -1
- package/dist/services/maintenance-signal.js +7 -1
- package/dist/services/maintenance-signal.js.map +1 -1
- package/dist/services/process.service.d.ts +19 -2
- package/dist/services/process.service.d.ts.map +1 -1
- package/dist/services/process.service.js +104 -10
- package/dist/services/process.service.js.map +1 -1
- package/dist/services/thread-lifecycle.service.d.ts +5 -0
- package/dist/services/thread-lifecycle.service.d.ts.map +1 -1
- package/dist/services/thread-lifecycle.service.js +33 -8
- package/dist/services/thread-lifecycle.service.js.map +1 -1
- package/dist/services/worker-cleanup.service.d.ts +14 -1
- package/dist/services/worker-cleanup.service.d.ts.map +1 -1
- package/dist/services/worker-cleanup.service.js +36 -38
- package/dist/services/worker-cleanup.service.js.map +1 -1
- package/dist/sessions.d.ts +0 -5
- package/dist/sessions.d.ts.map +1 -1
- package/dist/sessions.js +0 -7
- package/dist/sessions.js.map +1 -1
- package/dist/stdio-server.d.ts.map +1 -1
- package/dist/stdio-server.js +1 -7
- package/dist/stdio-server.js.map +1 -1
- package/dist/tools/delegate-tool.d.ts.map +1 -1
- package/dist/tools/delegate-tool.js +2 -2
- package/dist/tools/delegate-tool.js.map +1 -1
- package/dist/tools/session-tools.js +1 -1
- package/dist/tools/session-tools.js.map +1 -1
- package/dist/tools/start-session-tool.d.ts.map +1 -1
- package/dist/tools/start-session-tool.js +8 -9
- package/dist/tools/start-session-tool.js.map +1 -1
- package/dist/tools/wait/message-processing.d.ts.map +1 -1
- package/dist/tools/wait/message-processing.js +28 -0
- package/dist/tools/wait/message-processing.js.map +1 -1
- package/dist/tools/wait/poll-loop.js +1 -1
- package/dist/tools/wait/poll-loop.js.map +1 -1
- package/package.json +1 -1
- package/dist/tools/thread-lifecycle.d.ts +0 -6
- package/dist/tools/thread-lifecycle.d.ts.map +0 -1
- package/dist/tools/thread-lifecycle.js +0 -6
- package/dist/tools/thread-lifecycle.js.map +0 -1
- package/supervisor/config.go +0 -253
- package/supervisor/config_test.go +0 -78
- package/supervisor/go.mod +0 -15
- package/supervisor/go.sum +0 -20
- package/supervisor/health.go +0 -433
- package/supervisor/health_test.go +0 -93
- package/supervisor/keeper.go +0 -309
- package/supervisor/keeper_test.go +0 -27
- package/supervisor/lock.go +0 -57
- package/supervisor/lock_test.go +0 -54
- package/supervisor/log.go +0 -195
- package/supervisor/log_test.go +0 -125
- package/supervisor/main.go +0 -475
- package/supervisor/main_test.go +0 -130
- package/supervisor/notify.go +0 -53
- package/supervisor/process.go +0 -294
- package/supervisor/process_test.go +0 -108
- package/supervisor/process_unix.go +0 -14
- package/supervisor/process_windows.go +0 -15
- package/supervisor/secrets.go +0 -95
- package/supervisor/secrets_securevault_test.go +0 -98
- package/supervisor/secrets_test.go +0 -119
- package/supervisor/self_update.go +0 -282
- package/supervisor/self_update_test.go +0 -177
- package/supervisor/service_restart_stub.go +0 -9
- package/supervisor/service_restart_windows.go +0 -63
- package/supervisor/service_stub.go +0 -15
- package/supervisor/service_windows.go +0 -194
- package/supervisor/update_state.go +0 -264
- package/supervisor/update_state_test.go +0 -306
- package/supervisor/updater.go +0 -613
- package/supervisor/updater_test.go +0 -64
package/supervisor/health.go
DELETED
|
@@ -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
|
-
}
|