recall-mcp-v3 3.9.3 → 3.9.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/__tests__/first-tool-call-beacon.test.d.ts +16 -0
- package/dist/__tests__/first-tool-call-beacon.test.d.ts.map +1 -0
- package/dist/__tests__/first-tool-call-beacon.test.js +198 -0
- package/dist/__tests__/first-tool-call-beacon.test.js.map +1 -0
- package/dist/__tests__/started-beacon.test.d.ts +14 -0
- package/dist/__tests__/started-beacon.test.d.ts.map +1 -0
- package/dist/__tests__/started-beacon.test.js +200 -0
- package/dist/__tests__/started-beacon.test.js.map +1 -0
- package/dist/first-tool-call-beacon.d.ts +44 -0
- package/dist/first-tool-call-beacon.d.ts.map +1 -0
- package/dist/first-tool-call-beacon.js +87 -0
- package/dist/first-tool-call-beacon.js.map +1 -0
- package/dist/index.js +21 -0
- package/dist/index.js.map +1 -1
- package/dist/started-beacon.d.ts +37 -0
- package/dist/started-beacon.d.ts.map +1 -0
- package/dist/started-beacon.js +80 -0
- package/dist/started-beacon.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the MCP first-tool-call beacon factory (issue #66).
|
|
3
|
+
*
|
|
4
|
+
* Pinned contracts:
|
|
5
|
+
* - First invocation calls heartbeat() with state='first_tool_call',
|
|
6
|
+
* mcp_client from detection, clientVersion from factory opts,
|
|
7
|
+
* transport='stdio'.
|
|
8
|
+
* - Subsequent invocations are no-ops (process-local guard).
|
|
9
|
+
* - Heartbeat failures are caught + logged; beacon function never throws.
|
|
10
|
+
* - Detection failures unlock the guard for retry, never propagate.
|
|
11
|
+
* - Permanent failure ("No auth token persisted") locks the guard;
|
|
12
|
+
* transient failures unlock it.
|
|
13
|
+
* - Two factories from one process each have their own guard.
|
|
14
|
+
*/
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=first-tool-call-beacon.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-tool-call-beacon.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/first-tool-call-beacon.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG"}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the MCP first-tool-call beacon factory (issue #66).
|
|
3
|
+
*
|
|
4
|
+
* Pinned contracts:
|
|
5
|
+
* - First invocation calls heartbeat() with state='first_tool_call',
|
|
6
|
+
* mcp_client from detection, clientVersion from factory opts,
|
|
7
|
+
* transport='stdio'.
|
|
8
|
+
* - Subsequent invocations are no-ops (process-local guard).
|
|
9
|
+
* - Heartbeat failures are caught + logged; beacon function never throws.
|
|
10
|
+
* - Detection failures unlock the guard for retry, never propagate.
|
|
11
|
+
* - Permanent failure ("No auth token persisted") locks the guard;
|
|
12
|
+
* transient failures unlock it.
|
|
13
|
+
* - Two factories from one process each have their own guard.
|
|
14
|
+
*/
|
|
15
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
16
|
+
import { createFirstToolCallBeacon } from '../first-tool-call-beacon.js';
|
|
17
|
+
const FAKE_DETECTION = {
|
|
18
|
+
mcp_client: 'claude-code',
|
|
19
|
+
ai_provider: 'anthropic',
|
|
20
|
+
environment: 'terminal',
|
|
21
|
+
os: 'darwin',
|
|
22
|
+
transport: 'stdio',
|
|
23
|
+
confidence: 'high',
|
|
24
|
+
raw: {},
|
|
25
|
+
};
|
|
26
|
+
describe('createFirstToolCallBeacon', () => {
|
|
27
|
+
it('first call fires heartbeat with first_tool_call + injected detection + version', async () => {
|
|
28
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
29
|
+
const beacon = createFirstToolCallBeacon({
|
|
30
|
+
version: '3.9.5',
|
|
31
|
+
heartbeat: heartbeatMock,
|
|
32
|
+
getDetection: () => FAKE_DETECTION,
|
|
33
|
+
});
|
|
34
|
+
await beacon();
|
|
35
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
36
|
+
expect(heartbeatMock).toHaveBeenCalledWith({
|
|
37
|
+
state: 'first_tool_call',
|
|
38
|
+
mcpClient: 'claude-code',
|
|
39
|
+
clientVersion: '3.9.5',
|
|
40
|
+
transport: 'stdio',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
it('second call is a no-op (process-local guard)', async () => {
|
|
44
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
45
|
+
const beacon = createFirstToolCallBeacon({
|
|
46
|
+
version: '3.9.5',
|
|
47
|
+
heartbeat: heartbeatMock,
|
|
48
|
+
getDetection: () => FAKE_DETECTION,
|
|
49
|
+
});
|
|
50
|
+
await beacon();
|
|
51
|
+
await beacon();
|
|
52
|
+
await beacon();
|
|
53
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
it('does NOT fire detection more than once when called twice', async () => {
|
|
56
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
57
|
+
const detectionMock = vi.fn(() => FAKE_DETECTION);
|
|
58
|
+
const beacon = createFirstToolCallBeacon({
|
|
59
|
+
version: '3.9.5',
|
|
60
|
+
heartbeat: heartbeatMock,
|
|
61
|
+
getDetection: detectionMock,
|
|
62
|
+
});
|
|
63
|
+
await beacon();
|
|
64
|
+
await beacon();
|
|
65
|
+
expect(detectionMock).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
it('catches and logs heartbeat failures (does not throw to caller)', async () => {
|
|
68
|
+
const heartbeatMock = vi.fn(async () => {
|
|
69
|
+
throw new Error('No auth token persisted');
|
|
70
|
+
});
|
|
71
|
+
const logMock = vi.fn();
|
|
72
|
+
const beacon = createFirstToolCallBeacon({
|
|
73
|
+
version: '3.9.5',
|
|
74
|
+
heartbeat: heartbeatMock,
|
|
75
|
+
getDetection: () => FAKE_DETECTION,
|
|
76
|
+
log: logMock,
|
|
77
|
+
});
|
|
78
|
+
// Must not reject — beacon is fire-and-forget at every call site.
|
|
79
|
+
await expect(beacon()).resolves.toBeUndefined();
|
|
80
|
+
expect(logMock).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(logMock.mock.calls[0][0]).toMatch(/first_tool_call beacon.*No auth token persisted/);
|
|
82
|
+
});
|
|
83
|
+
it('locks the guard on permanent failures (no auth token) — avoids retry storm', async () => {
|
|
84
|
+
const heartbeatMock = vi.fn(async (_input) => {
|
|
85
|
+
throw new Error('No auth token persisted; run `recall-mcp-v3 auth` before sending heartbeats');
|
|
86
|
+
});
|
|
87
|
+
const beacon = createFirstToolCallBeacon({
|
|
88
|
+
version: '3.9.5',
|
|
89
|
+
heartbeat: heartbeatMock,
|
|
90
|
+
getDetection: () => FAKE_DETECTION,
|
|
91
|
+
log: () => undefined,
|
|
92
|
+
});
|
|
93
|
+
await beacon();
|
|
94
|
+
await beacon();
|
|
95
|
+
await beacon();
|
|
96
|
+
// No-token-persisted is stable for the life of the process; retrying
|
|
97
|
+
// would hammer the API on every CallTool without changing the answer.
|
|
98
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
99
|
+
});
|
|
100
|
+
it('allows retry on transient failures (network error / timeout / 5xx)', async () => {
|
|
101
|
+
let attempt = 0;
|
|
102
|
+
const heartbeatMock = vi.fn(async (_input) => {
|
|
103
|
+
attempt += 1;
|
|
104
|
+
if (attempt === 1) {
|
|
105
|
+
throw new Error('Heartbeat failed: HTTP 503 Service Unavailable — transient');
|
|
106
|
+
}
|
|
107
|
+
// Subsequent attempts succeed.
|
|
108
|
+
});
|
|
109
|
+
const beacon = createFirstToolCallBeacon({
|
|
110
|
+
version: '3.9.5',
|
|
111
|
+
heartbeat: heartbeatMock,
|
|
112
|
+
getDetection: () => FAKE_DETECTION,
|
|
113
|
+
log: () => undefined,
|
|
114
|
+
});
|
|
115
|
+
await beacon();
|
|
116
|
+
await beacon();
|
|
117
|
+
// Transient failure unlocks the guard so the second CallTool retries.
|
|
118
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(2);
|
|
119
|
+
});
|
|
120
|
+
it('locks the guard once a transient failure eventually succeeds', async () => {
|
|
121
|
+
let attempt = 0;
|
|
122
|
+
const heartbeatMock = vi.fn(async (_input) => {
|
|
123
|
+
attempt += 1;
|
|
124
|
+
if (attempt === 1) {
|
|
125
|
+
throw new Error('Heartbeat failed: AbortError');
|
|
126
|
+
}
|
|
127
|
+
// Second call succeeds.
|
|
128
|
+
});
|
|
129
|
+
const beacon = createFirstToolCallBeacon({
|
|
130
|
+
version: '3.9.5',
|
|
131
|
+
heartbeat: heartbeatMock,
|
|
132
|
+
getDetection: () => FAKE_DETECTION,
|
|
133
|
+
log: () => undefined,
|
|
134
|
+
});
|
|
135
|
+
await beacon();
|
|
136
|
+
await beacon();
|
|
137
|
+
// Third call after success should NOT re-fire.
|
|
138
|
+
await beacon();
|
|
139
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
it('catches detection failures + leaves guard open for retry', async () => {
|
|
142
|
+
let detectionAttempt = 0;
|
|
143
|
+
const detectionMock = vi.fn(() => {
|
|
144
|
+
detectionAttempt += 1;
|
|
145
|
+
if (detectionAttempt === 1) {
|
|
146
|
+
throw new Error('detect failed: ENOENT');
|
|
147
|
+
}
|
|
148
|
+
return FAKE_DETECTION;
|
|
149
|
+
});
|
|
150
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
151
|
+
const logMock = vi.fn();
|
|
152
|
+
const beacon = createFirstToolCallBeacon({
|
|
153
|
+
version: '3.9.5',
|
|
154
|
+
heartbeat: heartbeatMock,
|
|
155
|
+
getDetection: detectionMock,
|
|
156
|
+
log: logMock,
|
|
157
|
+
});
|
|
158
|
+
// First call: detection throws. Must NOT reject, must NOT call heartbeat,
|
|
159
|
+
// must log + leave the guard open so a future CallTool retries.
|
|
160
|
+
await expect(beacon()).resolves.toBeUndefined();
|
|
161
|
+
expect(heartbeatMock).not.toHaveBeenCalled();
|
|
162
|
+
expect(logMock).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(logMock.mock.calls[0][0]).toMatch(/first_tool_call beacon.*detection failed.*ENOENT/);
|
|
164
|
+
// Second call: detection succeeds, heartbeat fires.
|
|
165
|
+
await beacon();
|
|
166
|
+
expect(detectionMock).toHaveBeenCalledTimes(2);
|
|
167
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
168
|
+
});
|
|
169
|
+
it('two factories from one process have independent guards', async () => {
|
|
170
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
171
|
+
const detectionMock = () => FAKE_DETECTION;
|
|
172
|
+
const beaconA = createFirstToolCallBeacon({
|
|
173
|
+
version: '3.9.5',
|
|
174
|
+
heartbeat: heartbeatMock,
|
|
175
|
+
getDetection: detectionMock,
|
|
176
|
+
});
|
|
177
|
+
const beaconB = createFirstToolCallBeacon({
|
|
178
|
+
version: '3.9.5',
|
|
179
|
+
heartbeat: heartbeatMock,
|
|
180
|
+
getDetection: detectionMock,
|
|
181
|
+
});
|
|
182
|
+
await beaconA();
|
|
183
|
+
await beaconB();
|
|
184
|
+
// Each beacon fires once (independent state).
|
|
185
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(2);
|
|
186
|
+
});
|
|
187
|
+
it('forwards different mcp_client values from detection', async () => {
|
|
188
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
189
|
+
const beacon = createFirstToolCallBeacon({
|
|
190
|
+
version: '3.9.5',
|
|
191
|
+
heartbeat: heartbeatMock,
|
|
192
|
+
getDetection: () => ({ ...FAKE_DETECTION, mcp_client: 'cursor' }),
|
|
193
|
+
});
|
|
194
|
+
await beacon();
|
|
195
|
+
expect(heartbeatMock.mock.calls[0][0]).toMatchObject({ mcpClient: 'cursor' });
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
//# sourceMappingURL=first-tool-call-beacon.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-tool-call-beacon.test.js","sourceRoot":"","sources":["../../src/__tests__/first-tool-call-beacon.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the MCP startup beacon factory (PR 7).
|
|
3
|
+
*
|
|
4
|
+
* Pinned contracts:
|
|
5
|
+
* - First invocation calls heartbeat() with state='mcp_started',
|
|
6
|
+
* mcp_client from detection, clientVersion from factory opts,
|
|
7
|
+
* transport='stdio'.
|
|
8
|
+
* - Subsequent invocations are no-ops (process-local guard).
|
|
9
|
+
* - Heartbeat failures are caught + logged; beacon function never throws.
|
|
10
|
+
* - The factory is independent — two factories from one process each
|
|
11
|
+
* have their own guard.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=started-beacon.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"started-beacon.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/started-beacon.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the MCP startup beacon factory (PR 7).
|
|
3
|
+
*
|
|
4
|
+
* Pinned contracts:
|
|
5
|
+
* - First invocation calls heartbeat() with state='mcp_started',
|
|
6
|
+
* mcp_client from detection, clientVersion from factory opts,
|
|
7
|
+
* transport='stdio'.
|
|
8
|
+
* - Subsequent invocations are no-ops (process-local guard).
|
|
9
|
+
* - Heartbeat failures are caught + logged; beacon function never throws.
|
|
10
|
+
* - The factory is independent — two factories from one process each
|
|
11
|
+
* have their own guard.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
14
|
+
import { createStartedBeacon } from '../started-beacon.js';
|
|
15
|
+
const FAKE_DETECTION = {
|
|
16
|
+
mcp_client: 'claude-code',
|
|
17
|
+
ai_provider: 'anthropic',
|
|
18
|
+
environment: 'terminal',
|
|
19
|
+
os: 'darwin',
|
|
20
|
+
transport: 'stdio',
|
|
21
|
+
confidence: 'high',
|
|
22
|
+
raw: {},
|
|
23
|
+
};
|
|
24
|
+
describe('createStartedBeacon', () => {
|
|
25
|
+
it('first call fires heartbeat with mcp_started + injected detection + version', async () => {
|
|
26
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
27
|
+
const beacon = createStartedBeacon({
|
|
28
|
+
version: '3.9.4',
|
|
29
|
+
heartbeat: heartbeatMock,
|
|
30
|
+
getDetection: () => FAKE_DETECTION,
|
|
31
|
+
});
|
|
32
|
+
await beacon();
|
|
33
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(heartbeatMock).toHaveBeenCalledWith({
|
|
35
|
+
state: 'mcp_started',
|
|
36
|
+
mcpClient: 'claude-code',
|
|
37
|
+
clientVersion: '3.9.4',
|
|
38
|
+
transport: 'stdio',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
it('second call is a no-op (process-local guard)', async () => {
|
|
42
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
43
|
+
const beacon = createStartedBeacon({
|
|
44
|
+
version: '3.9.4',
|
|
45
|
+
heartbeat: heartbeatMock,
|
|
46
|
+
getDetection: () => FAKE_DETECTION,
|
|
47
|
+
});
|
|
48
|
+
await beacon();
|
|
49
|
+
await beacon();
|
|
50
|
+
await beacon();
|
|
51
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
52
|
+
});
|
|
53
|
+
it('does NOT fire detection more than once when called twice', async () => {
|
|
54
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
55
|
+
const detectionMock = vi.fn(() => FAKE_DETECTION);
|
|
56
|
+
const beacon = createStartedBeacon({
|
|
57
|
+
version: '3.9.4',
|
|
58
|
+
heartbeat: heartbeatMock,
|
|
59
|
+
getDetection: detectionMock,
|
|
60
|
+
});
|
|
61
|
+
await beacon();
|
|
62
|
+
await beacon();
|
|
63
|
+
expect(detectionMock).toHaveBeenCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
it('catches and logs heartbeat failures (does not throw to caller)', async () => {
|
|
66
|
+
const heartbeatMock = vi.fn(async () => {
|
|
67
|
+
throw new Error('No auth token persisted');
|
|
68
|
+
});
|
|
69
|
+
const logMock = vi.fn();
|
|
70
|
+
const beacon = createStartedBeacon({
|
|
71
|
+
version: '3.9.4',
|
|
72
|
+
heartbeat: heartbeatMock,
|
|
73
|
+
getDetection: () => FAKE_DETECTION,
|
|
74
|
+
log: logMock,
|
|
75
|
+
});
|
|
76
|
+
// Must not reject — beacon is fire-and-forget at every call site.
|
|
77
|
+
await expect(beacon()).resolves.toBeUndefined();
|
|
78
|
+
expect(logMock).toHaveBeenCalledTimes(1);
|
|
79
|
+
expect(logMock.mock.calls[0][0]).toMatch(/mcp_started beacon.*No auth token persisted/);
|
|
80
|
+
});
|
|
81
|
+
it('locks the guard on permanent failures (no auth token) — avoids retry storm', async () => {
|
|
82
|
+
const heartbeatMock = vi.fn(async (_input) => {
|
|
83
|
+
throw new Error('No auth token persisted; run `recall-mcp-v3 auth` before sending heartbeats');
|
|
84
|
+
});
|
|
85
|
+
const beacon = createStartedBeacon({
|
|
86
|
+
version: '3.9.4',
|
|
87
|
+
heartbeat: heartbeatMock,
|
|
88
|
+
getDetection: () => FAKE_DETECTION,
|
|
89
|
+
log: () => undefined,
|
|
90
|
+
});
|
|
91
|
+
await beacon();
|
|
92
|
+
await beacon();
|
|
93
|
+
await beacon();
|
|
94
|
+
// No-token-persisted is stable for the life of the process; retrying
|
|
95
|
+
// would hammer the API on every request without changing the answer.
|
|
96
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
97
|
+
});
|
|
98
|
+
it('allows retry on transient failures (network error / timeout / 5xx)', async () => {
|
|
99
|
+
let attempt = 0;
|
|
100
|
+
const heartbeatMock = vi.fn(async (_input) => {
|
|
101
|
+
attempt += 1;
|
|
102
|
+
if (attempt === 1) {
|
|
103
|
+
throw new Error('Heartbeat failed: HTTP 503 Service Unavailable — transient');
|
|
104
|
+
}
|
|
105
|
+
// Subsequent attempts succeed.
|
|
106
|
+
});
|
|
107
|
+
const beacon = createStartedBeacon({
|
|
108
|
+
version: '3.9.4',
|
|
109
|
+
heartbeat: heartbeatMock,
|
|
110
|
+
getDetection: () => FAKE_DETECTION,
|
|
111
|
+
log: () => undefined,
|
|
112
|
+
});
|
|
113
|
+
await beacon();
|
|
114
|
+
await beacon();
|
|
115
|
+
// Transient failure unlocks the guard so the second request retries.
|
|
116
|
+
// A flaky moment must not permanently drop the funnel signal for the
|
|
117
|
+
// life of a long-running session.
|
|
118
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(2);
|
|
119
|
+
});
|
|
120
|
+
it('locks the guard once a transient failure eventually succeeds', async () => {
|
|
121
|
+
let attempt = 0;
|
|
122
|
+
const heartbeatMock = vi.fn(async (_input) => {
|
|
123
|
+
attempt += 1;
|
|
124
|
+
if (attempt === 1) {
|
|
125
|
+
throw new Error('Heartbeat failed: AbortError');
|
|
126
|
+
}
|
|
127
|
+
// Second call succeeds.
|
|
128
|
+
});
|
|
129
|
+
const beacon = createStartedBeacon({
|
|
130
|
+
version: '3.9.4',
|
|
131
|
+
heartbeat: heartbeatMock,
|
|
132
|
+
getDetection: () => FAKE_DETECTION,
|
|
133
|
+
log: () => undefined,
|
|
134
|
+
});
|
|
135
|
+
await beacon();
|
|
136
|
+
await beacon();
|
|
137
|
+
// Third call after success should NOT re-fire.
|
|
138
|
+
await beacon();
|
|
139
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(2);
|
|
140
|
+
});
|
|
141
|
+
it('catches detection failures + leaves guard open for retry', async () => {
|
|
142
|
+
let detectionAttempt = 0;
|
|
143
|
+
const detectionMock = vi.fn(() => {
|
|
144
|
+
detectionAttempt += 1;
|
|
145
|
+
if (detectionAttempt === 1) {
|
|
146
|
+
throw new Error('detect failed: ENOENT');
|
|
147
|
+
}
|
|
148
|
+
return FAKE_DETECTION;
|
|
149
|
+
});
|
|
150
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
151
|
+
const logMock = vi.fn();
|
|
152
|
+
const beacon = createStartedBeacon({
|
|
153
|
+
version: '3.9.4',
|
|
154
|
+
heartbeat: heartbeatMock,
|
|
155
|
+
getDetection: detectionMock,
|
|
156
|
+
log: logMock,
|
|
157
|
+
});
|
|
158
|
+
// First call: detection throws. Must NOT reject, must NOT call heartbeat,
|
|
159
|
+
// must log + leave the guard open so a future call retries. Without this,
|
|
160
|
+
// a single transient detection error permanently drops mcp_started for
|
|
161
|
+
// the life of the process.
|
|
162
|
+
await expect(beacon()).resolves.toBeUndefined();
|
|
163
|
+
expect(heartbeatMock).not.toHaveBeenCalled();
|
|
164
|
+
expect(logMock).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(logMock.mock.calls[0][0]).toMatch(/mcp_started beacon.*detection failed.*ENOENT/);
|
|
166
|
+
// Second call: detection succeeds, heartbeat fires.
|
|
167
|
+
await beacon();
|
|
168
|
+
expect(detectionMock).toHaveBeenCalledTimes(2);
|
|
169
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(1);
|
|
170
|
+
});
|
|
171
|
+
it('two factories from one process have independent guards', async () => {
|
|
172
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
173
|
+
const detectionMock = () => FAKE_DETECTION;
|
|
174
|
+
const beaconA = createStartedBeacon({
|
|
175
|
+
version: '3.9.4',
|
|
176
|
+
heartbeat: heartbeatMock,
|
|
177
|
+
getDetection: detectionMock,
|
|
178
|
+
});
|
|
179
|
+
const beaconB = createStartedBeacon({
|
|
180
|
+
version: '3.9.4',
|
|
181
|
+
heartbeat: heartbeatMock,
|
|
182
|
+
getDetection: detectionMock,
|
|
183
|
+
});
|
|
184
|
+
await beaconA();
|
|
185
|
+
await beaconB();
|
|
186
|
+
// Each beacon fires once (independent state).
|
|
187
|
+
expect(heartbeatMock).toHaveBeenCalledTimes(2);
|
|
188
|
+
});
|
|
189
|
+
it('forwards different mcp_client values from detection', async () => {
|
|
190
|
+
const heartbeatMock = vi.fn(async (_input) => undefined);
|
|
191
|
+
const beacon = createStartedBeacon({
|
|
192
|
+
version: '3.9.4',
|
|
193
|
+
heartbeat: heartbeatMock,
|
|
194
|
+
getDetection: () => ({ ...FAKE_DETECTION, mcp_client: 'cursor' }),
|
|
195
|
+
});
|
|
196
|
+
await beacon();
|
|
197
|
+
expect(heartbeatMock.mock.calls[0][0]).toMatchObject({ mcpClient: 'cursor' });
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
//# sourceMappingURL=started-beacon.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"started-beacon.test.js","sourceRoot":"","sources":["../../src/__tests__/started-beacon.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP first-tool-call beacon factory — install-health funnel state
|
|
3
|
+
* `first_tool_call`, the missing piece after PR #64.
|
|
4
|
+
*
|
|
5
|
+
* PR #64 wired only `mcp_started` into ListTools/CallTool. The funnel
|
|
6
|
+
* also expects a `first_tool_call` event the first time the user (or
|
|
7
|
+
* their AI tool) actually invokes a Recall MCP tool — that's the
|
|
8
|
+
* signal that goes from "MCP is loaded" to "MCP is being used."
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own module (separate from index.ts) for the same
|
|
11
|
+
* reason `started-beacon.ts` does: the MCP entry binary's bottom-of-
|
|
12
|
+
* file bootstrap (`new RecallServer(); server.run()`) runs at module-
|
|
13
|
+
* load time, so routing tests through this file keeps that side
|
|
14
|
+
* effect away from vitest workers.
|
|
15
|
+
*/
|
|
16
|
+
import { heartbeat } from './auth/heartbeat.js';
|
|
17
|
+
import { getCachedDetection } from './detect.js';
|
|
18
|
+
/**
|
|
19
|
+
* Build a once-only "first tool call" beacon callable.
|
|
20
|
+
*
|
|
21
|
+
* Calling the returned function fires `heartbeat({ state:
|
|
22
|
+
* 'first_tool_call' })` exactly once per invocation of this factory,
|
|
23
|
+
* against a permanent failure mode. Transient failures (network blip,
|
|
24
|
+
* 5xx, AbortSignal timeout, or a detection probe error) leave the
|
|
25
|
+
* guard open so the next CallTool retries.
|
|
26
|
+
*
|
|
27
|
+
* Designed to be called only from the CallTool request handler —
|
|
28
|
+
* unlike the startup beacon, ListTools enumeration alone does not
|
|
29
|
+
* count as "the user used a tool." Firing on ListTools would inflate
|
|
30
|
+
* the metric with `claude mcp list`-style enumeration commands.
|
|
31
|
+
*
|
|
32
|
+
* Failures are caught and logged to stderr; a beacon failure must
|
|
33
|
+
* never break the MCP request the user is making.
|
|
34
|
+
*
|
|
35
|
+
* Deps are injectable so tests can mock heartbeat + detection without
|
|
36
|
+
* patching modules.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createFirstToolCallBeacon(opts: {
|
|
39
|
+
version: string;
|
|
40
|
+
heartbeat?: typeof heartbeat;
|
|
41
|
+
getDetection?: typeof getCachedDetection;
|
|
42
|
+
log?: (msg: string) => void;
|
|
43
|
+
}): () => Promise<void>;
|
|
44
|
+
//# sourceMappingURL=first-tool-call-beacon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-tool-call-beacon.d.ts","sourceRoot":"","sources":["../src/first-tool-call-beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE;IAC9C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACzC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAgDtB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP first-tool-call beacon factory — install-health funnel state
|
|
3
|
+
* `first_tool_call`, the missing piece after PR #64.
|
|
4
|
+
*
|
|
5
|
+
* PR #64 wired only `mcp_started` into ListTools/CallTool. The funnel
|
|
6
|
+
* also expects a `first_tool_call` event the first time the user (or
|
|
7
|
+
* their AI tool) actually invokes a Recall MCP tool — that's the
|
|
8
|
+
* signal that goes from "MCP is loaded" to "MCP is being used."
|
|
9
|
+
*
|
|
10
|
+
* Lives in its own module (separate from index.ts) for the same
|
|
11
|
+
* reason `started-beacon.ts` does: the MCP entry binary's bottom-of-
|
|
12
|
+
* file bootstrap (`new RecallServer(); server.run()`) runs at module-
|
|
13
|
+
* load time, so routing tests through this file keeps that side
|
|
14
|
+
* effect away from vitest workers.
|
|
15
|
+
*/
|
|
16
|
+
import { heartbeat } from './auth/heartbeat.js';
|
|
17
|
+
import { getCachedDetection } from './detect.js';
|
|
18
|
+
/**
|
|
19
|
+
* Build a once-only "first tool call" beacon callable.
|
|
20
|
+
*
|
|
21
|
+
* Calling the returned function fires `heartbeat({ state:
|
|
22
|
+
* 'first_tool_call' })` exactly once per invocation of this factory,
|
|
23
|
+
* against a permanent failure mode. Transient failures (network blip,
|
|
24
|
+
* 5xx, AbortSignal timeout, or a detection probe error) leave the
|
|
25
|
+
* guard open so the next CallTool retries.
|
|
26
|
+
*
|
|
27
|
+
* Designed to be called only from the CallTool request handler —
|
|
28
|
+
* unlike the startup beacon, ListTools enumeration alone does not
|
|
29
|
+
* count as "the user used a tool." Firing on ListTools would inflate
|
|
30
|
+
* the metric with `claude mcp list`-style enumeration commands.
|
|
31
|
+
*
|
|
32
|
+
* Failures are caught and logged to stderr; a beacon failure must
|
|
33
|
+
* never break the MCP request the user is making.
|
|
34
|
+
*
|
|
35
|
+
* Deps are injectable so tests can mock heartbeat + detection without
|
|
36
|
+
* patching modules.
|
|
37
|
+
*/
|
|
38
|
+
export function createFirstToolCallBeacon(opts) {
|
|
39
|
+
let fired = false;
|
|
40
|
+
const heartbeatImpl = opts.heartbeat ?? heartbeat;
|
|
41
|
+
const detectionImpl = opts.getDetection ?? getCachedDetection;
|
|
42
|
+
const logImpl = opts.log ?? ((msg) => console.error(msg));
|
|
43
|
+
return async () => {
|
|
44
|
+
if (fired)
|
|
45
|
+
return;
|
|
46
|
+
fired = true;
|
|
47
|
+
let detection;
|
|
48
|
+
try {
|
|
49
|
+
detection = detectionImpl();
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
// Detection errors are always transient — env probing can blip
|
|
53
|
+
// on a transient FS/process read. Unlock the guard so the next
|
|
54
|
+
// CallTool retries; never let detection failure silently drop
|
|
55
|
+
// the funnel signal for the life of a long-running session.
|
|
56
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
57
|
+
fired = false;
|
|
58
|
+
logImpl(`[first_tool_call beacon] detection failed: ${msg}`);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
await heartbeatImpl({
|
|
63
|
+
state: 'first_tool_call',
|
|
64
|
+
mcpClient: detection.mcp_client,
|
|
65
|
+
clientVersion: opts.version,
|
|
66
|
+
transport: 'stdio',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
71
|
+
// Differentiate permanent from transient failures:
|
|
72
|
+
// - "No auth token persisted" is stable for the life of this
|
|
73
|
+
// process (user hasn't run `recall-mcp-v3 auth` yet). Keep
|
|
74
|
+
// fired=true so we don't hammer the API on every CallTool.
|
|
75
|
+
// - Network blips, 5xx, AbortSignal timeout are transient. Let
|
|
76
|
+
// the next CallTool retry — a flaky moment shouldn't
|
|
77
|
+
// permanently drop the funnel signal for a long-running
|
|
78
|
+
// Claude Code session.
|
|
79
|
+
const isPermanent = msg.includes('No auth token persisted');
|
|
80
|
+
if (!isPermanent) {
|
|
81
|
+
fired = false;
|
|
82
|
+
}
|
|
83
|
+
logImpl(`[first_tool_call beacon] heartbeat failed: ${msg}`);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=first-tool-call-beacon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"first-tool-call-beacon.js","sourceRoot":"","sources":["../src/first-tool-call-beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,yBAAyB,CAAC,IAKzC;IACC,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC;IAClD,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,IAAI,kBAAkB,CAAC;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAElE,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,KAAK;YAAE,OAAO;QAClB,KAAK,GAAG,IAAI,CAAC;QACb,IAAI,SAAS,CAAC;QACd,IAAI,CAAC;YACH,SAAS,GAAG,aAAa,EAAE,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,+DAA+D;YAC/D,+DAA+D;YAC/D,8DAA8D;YAC9D,4DAA4D;YAC5D,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,KAAK,GAAG,KAAK,CAAC;YACd,OAAO,CAAC,8CAA8C,GAAG,EAAE,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,aAAa,CAAC;gBAClB,KAAK,EAAE,iBAAiB;gBACxB,SAAS,EAAE,SAAS,CAAC,UAAU;gBAC/B,aAAa,EAAE,IAAI,CAAC,OAAO;gBAC3B,SAAS,EAAE,OAAO;aACnB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,mDAAmD;YACnD,8DAA8D;YAC9D,8DAA8D;YAC9D,8DAA8D;YAC9D,gEAAgE;YAChE,wDAAwD;YACxD,2DAA2D;YAC3D,0BAA0B;YAC1B,MAAM,WAAW,GAAG,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;YAC5D,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;YACD,OAAO,CACL,8CAA8C,GAAG,EAAE,CACpD,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,8 @@ const { version: MCP_VERSION } = require('../package.json');
|
|
|
17
17
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
18
18
|
import { getContext, getOrigin, getUserSessions, getHistory, getTranscripts, saveSession, logDecision, getTeamActivity, } from './tools.js';
|
|
19
19
|
import { runCli } from './cli.js';
|
|
20
|
+
import { createStartedBeacon } from './started-beacon.js';
|
|
21
|
+
import { createFirstToolCallBeacon } from './first-tool-call-beacon.js';
|
|
20
22
|
// Tool definitions
|
|
21
23
|
const TOOLS = [
|
|
22
24
|
{
|
|
@@ -200,6 +202,8 @@ const TOOLS = [
|
|
|
200
202
|
];
|
|
201
203
|
class RecallServer {
|
|
202
204
|
server;
|
|
205
|
+
fireStartedBeacon;
|
|
206
|
+
fireFirstToolCallBeacon;
|
|
203
207
|
constructor() {
|
|
204
208
|
this.server = new Server({
|
|
205
209
|
name: 'recall-mcp',
|
|
@@ -209,15 +213,32 @@ class RecallServer {
|
|
|
209
213
|
tools: {},
|
|
210
214
|
},
|
|
211
215
|
});
|
|
216
|
+
this.fireStartedBeacon = createStartedBeacon({ version: MCP_VERSION });
|
|
217
|
+
this.fireFirstToolCallBeacon = createFirstToolCallBeacon({ version: MCP_VERSION });
|
|
212
218
|
this.setupHandlers();
|
|
213
219
|
}
|
|
214
220
|
setupHandlers() {
|
|
215
221
|
// List available tools
|
|
216
222
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
223
|
+
// Fire mcp_started exactly once per process. ListTools is the first
|
|
224
|
+
// method any AI tool calls after MCP initialize; firing here proves
|
|
225
|
+
// the tool actually loaded the server (not just enumerated the
|
|
226
|
+
// binary). Fire-and-forget so the response is never blocked.
|
|
227
|
+
void this.fireStartedBeacon();
|
|
217
228
|
return { tools: TOOLS };
|
|
218
229
|
});
|
|
219
230
|
// Handle tool calls
|
|
220
231
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
232
|
+
// Belt-and-suspenders: some clients call CallTool without a
|
|
233
|
+
// prior ListTools (e.g., after a reconnection that skipped
|
|
234
|
+
// capability negotiation). The beacon's internal guard makes
|
|
235
|
+
// duplicate firings a cheap no-op.
|
|
236
|
+
void this.fireStartedBeacon();
|
|
237
|
+
// Fire `first_tool_call` once per process. Unlike mcp_started,
|
|
238
|
+
// this only fires from CallTool — ListTools enumeration alone
|
|
239
|
+
// is not "the user used a tool." Fire-and-forget so the
|
|
240
|
+
// response is never blocked.
|
|
241
|
+
void this.fireFirstToolCallBeacon();
|
|
221
242
|
const { name, arguments: args = {} } = request.params;
|
|
222
243
|
try {
|
|
223
244
|
let result;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;GAQG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,MAAM,QAAQ,CAAC;AAEvC,gCAAgC;AAChC,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC/C,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;AAC5D,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GAEvB,MAAM,oCAAoC,CAAC;AAE5C,OAAO,EACL,UAAU,EACV,SAAS,EACT,eAAe,EACf,UAAU,EACV,cAAc,EACd,WAAW,EACX,WAAW,EACX,eAAe,GAShB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP startup beacon factory — install-health PR 7 / P1.E.2.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own module (separate from index.ts) so tests can import
|
|
5
|
+
* it without booting a real `RecallServer` over stdio. The MCP entry
|
|
6
|
+
* binary's bottom-of-file bootstrap (`new RecallServer(); server.run()`)
|
|
7
|
+
* runs at module-load time; routing tests through this file keeps that
|
|
8
|
+
* side effect away from vitest workers.
|
|
9
|
+
*/
|
|
10
|
+
import { heartbeat } from './auth/heartbeat.js';
|
|
11
|
+
import { getCachedDetection } from './detect.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build a once-only "MCP started" beacon callable.
|
|
14
|
+
*
|
|
15
|
+
* Calling the returned function fires `heartbeat({ state: 'mcp_started' })`
|
|
16
|
+
* exactly once per invocation of this factory, against a permanent
|
|
17
|
+
* failure mode. Transient failures (network blip, 5xx, AbortSignal
|
|
18
|
+
* timeout, or a detection probe error) leave the guard open so the
|
|
19
|
+
* next request retries.
|
|
20
|
+
*
|
|
21
|
+
* Designed to be called from the first incoming MCP request handler
|
|
22
|
+
* (ListTools or CallTool) — that's what proves the AI tool actually
|
|
23
|
+
* loaded the server, not just enumerated the binary.
|
|
24
|
+
*
|
|
25
|
+
* Failures are caught and logged to stderr; a beacon failure must
|
|
26
|
+
* never break the MCP request the user is making.
|
|
27
|
+
*
|
|
28
|
+
* Deps are injectable so tests can mock heartbeat + detection without
|
|
29
|
+
* patching modules.
|
|
30
|
+
*/
|
|
31
|
+
export declare function createStartedBeacon(opts: {
|
|
32
|
+
version: string;
|
|
33
|
+
heartbeat?: typeof heartbeat;
|
|
34
|
+
getDetection?: typeof getCachedDetection;
|
|
35
|
+
log?: (msg: string) => void;
|
|
36
|
+
}): () => Promise<void>;
|
|
37
|
+
//# sourceMappingURL=started-beacon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"started-beacon.d.ts","sourceRoot":"","sources":["../src/started-beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,OAAO,SAAS,CAAC;IAC7B,YAAY,CAAC,EAAE,OAAO,kBAAkB,CAAC;IACzC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAgDtB"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP startup beacon factory — install-health PR 7 / P1.E.2.
|
|
3
|
+
*
|
|
4
|
+
* Lives in its own module (separate from index.ts) so tests can import
|
|
5
|
+
* it without booting a real `RecallServer` over stdio. The MCP entry
|
|
6
|
+
* binary's bottom-of-file bootstrap (`new RecallServer(); server.run()`)
|
|
7
|
+
* runs at module-load time; routing tests through this file keeps that
|
|
8
|
+
* side effect away from vitest workers.
|
|
9
|
+
*/
|
|
10
|
+
import { heartbeat } from './auth/heartbeat.js';
|
|
11
|
+
import { getCachedDetection } from './detect.js';
|
|
12
|
+
/**
|
|
13
|
+
* Build a once-only "MCP started" beacon callable.
|
|
14
|
+
*
|
|
15
|
+
* Calling the returned function fires `heartbeat({ state: 'mcp_started' })`
|
|
16
|
+
* exactly once per invocation of this factory, against a permanent
|
|
17
|
+
* failure mode. Transient failures (network blip, 5xx, AbortSignal
|
|
18
|
+
* timeout, or a detection probe error) leave the guard open so the
|
|
19
|
+
* next request retries.
|
|
20
|
+
*
|
|
21
|
+
* Designed to be called from the first incoming MCP request handler
|
|
22
|
+
* (ListTools or CallTool) — that's what proves the AI tool actually
|
|
23
|
+
* loaded the server, not just enumerated the binary.
|
|
24
|
+
*
|
|
25
|
+
* Failures are caught and logged to stderr; a beacon failure must
|
|
26
|
+
* never break the MCP request the user is making.
|
|
27
|
+
*
|
|
28
|
+
* Deps are injectable so tests can mock heartbeat + detection without
|
|
29
|
+
* patching modules.
|
|
30
|
+
*/
|
|
31
|
+
export function createStartedBeacon(opts) {
|
|
32
|
+
let fired = false;
|
|
33
|
+
const heartbeatImpl = opts.heartbeat ?? heartbeat;
|
|
34
|
+
const detectionImpl = opts.getDetection ?? getCachedDetection;
|
|
35
|
+
const logImpl = opts.log ?? ((msg) => console.error(msg));
|
|
36
|
+
return async () => {
|
|
37
|
+
if (fired)
|
|
38
|
+
return;
|
|
39
|
+
fired = true;
|
|
40
|
+
let detection;
|
|
41
|
+
try {
|
|
42
|
+
detection = detectionImpl();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// Detection errors are always transient — env probing can blip on
|
|
46
|
+
// a transient FS/process read. Unlock the guard so the next request
|
|
47
|
+
// retries; never let detection failure silently drop the funnel
|
|
48
|
+
// signal for the life of a long-running session.
|
|
49
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
50
|
+
fired = false;
|
|
51
|
+
logImpl(`[mcp_started beacon] detection failed: ${msg}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
await heartbeatImpl({
|
|
56
|
+
state: 'mcp_started',
|
|
57
|
+
mcpClient: detection.mcp_client,
|
|
58
|
+
clientVersion: opts.version,
|
|
59
|
+
transport: 'stdio',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
64
|
+
// Differentiate permanent from transient failures:
|
|
65
|
+
// - "No auth token persisted" is stable for the life of this
|
|
66
|
+
// process (user hasn't run `recall-mcp-v3 auth` yet). Keep
|
|
67
|
+
// fired=true so we don't hammer the API on every request.
|
|
68
|
+
// - Network blips, 5xx, AbortSignal timeout are transient. Let
|
|
69
|
+
// the next request retry — a flaky moment shouldn't
|
|
70
|
+
// permanently drop the funnel signal for a long-running
|
|
71
|
+
// Claude Code session.
|
|
72
|
+
const isPermanent = msg.includes('No auth token persisted');
|
|
73
|
+
if (!isPermanent) {
|
|
74
|
+
fired = false;
|
|
75
|
+
}
|
|
76
|
+
logImpl(`[mcp_started beacon] heartbeat failed: ${msg}`);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=started-beacon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"started-beacon.js","sourceRoot":"","sources":["../src/started-beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAKnC;IACC,IAAI,KAAK,GAAG,KAAK,CAAC;IAClB,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC;IAClD,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,IAAI,kBAAkB,CAAC;IAC9D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAElE,OAAO,KAAK,IAAI,EAAE;QAChB,IAAI,KAAK;YAAE,OAAO;QAClB,KAAK,GAAG,IAAI,CAAC;QACb,IAAI,SAAS,CAAC;QACd,IAAI,CAAC;YACH,SAAS,GAAG,aAAa,EAAE,CAAC;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,kEAAkE;YAClE,oEAAoE;YACpE,gEAAgE;YAChE,iDAAiD;YACjD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,KAAK,GAAG,KAAK,CAAC;YACd,OAAO,CAAC,0CAA0C,GAAG,EAAE,CAAC,CAAC;YACzD,OAAO;QACT,CAAC;QACD,IAAI,CAAC;YACH,MAAM,aAAa,CAAC;gBAClB,KAAK,EAAE,aAAa;gBACpB,SAAS,EAAE,SAAS,CAAC,UAAU;gBAC/B,aAAa,EAAE,IAAI,CAAC,OAAO;gBAC3B,SAAS,EAAE,OAAO;aACnB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,mDAAmD;YACnD,8DAA8D;YAC9D,8DAA8D;YAC9D,6DAA6D;YAC7D,gEAAgE;YAChE,uDAAuD;YACvD,2DAA2D;YAC3D,0BAA0B;YAC1B,MAAM,WAAW,GAAG,GAAG,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC;YAC5D,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,KAAK,GAAG,KAAK,CAAC;YAChB,CAAC;YACD,OAAO,CACL,0CAA0C,GAAG,EAAE,CAChD,CAAC;QACJ,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|