react-native-debug-toolkit 2.3.0 → 3.1.2
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/README.md +115 -97
- package/README.zh-CN.md +113 -95
- package/bin/debug-toolkit.js +114 -0
- package/lib/commonjs/core/initialize.js +5 -0
- package/lib/commonjs/core/initialize.js.map +1 -1
- package/lib/commonjs/features/network/index.js +28 -2
- package/lib/commonjs/features/network/index.js.map +1 -1
- package/lib/commonjs/features/network/networkInterceptor.js +14 -6
- package/lib/commonjs/features/network/networkInterceptor.js.map +1 -1
- package/lib/commonjs/index.js +56 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ui/panel/DebugPanel.js +25 -0
- package/lib/commonjs/ui/panel/DebugPanel.js.map +1 -1
- package/lib/commonjs/ui/panel/FloatPanelView.js +15 -62
- package/lib/commonjs/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js +495 -0
- package/lib/commonjs/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js +71 -0
- package/lib/commonjs/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/commonjs/utils/DaemonClient.js +721 -0
- package/lib/commonjs/utils/DaemonClient.js.map +1 -0
- package/lib/commonjs/utils/createPersistedObservableStore.js +23 -3
- package/lib/commonjs/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/commonjs/utils/deviceReport.js +132 -0
- package/lib/commonjs/utils/deviceReport.js.map +1 -0
- package/lib/module/core/initialize.js +6 -0
- package/lib/module/core/initialize.js.map +1 -1
- package/lib/module/features/network/index.js +25 -1
- package/lib/module/features/network/index.js.map +1 -1
- package/lib/module/features/network/networkInterceptor.js +14 -6
- package/lib/module/features/network/networkInterceptor.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/ui/panel/DebugPanel.js +26 -1
- package/lib/module/ui/panel/DebugPanel.js.map +1 -1
- package/lib/module/ui/panel/FloatPanelView.js +16 -63
- package/lib/module/ui/panel/FloatPanelView.js.map +1 -1
- package/lib/module/ui/panel/StreamingSettingsModal.js +490 -0
- package/lib/module/ui/panel/StreamingSettingsModal.js.map +1 -0
- package/lib/module/ui/panel/useTabAnimation.js +67 -0
- package/lib/module/ui/panel/useTabAnimation.js.map +1 -0
- package/lib/module/utils/DaemonClient.js +703 -0
- package/lib/module/utils/DaemonClient.js.map +1 -0
- package/lib/module/utils/createPersistedObservableStore.js +23 -3
- package/lib/module/utils/createPersistedObservableStore.js.map +1 -1
- package/lib/module/utils/deviceReport.js +128 -0
- package/lib/module/utils/deviceReport.js.map +1 -0
- package/lib/typescript/src/core/initialize.d.ts.map +1 -1
- package/lib/typescript/src/features/network/index.d.ts +2 -0
- package/lib/typescript/src/features/network/index.d.ts.map +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts +1 -1
- package/lib/typescript/src/features/network/networkInterceptor.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/DebugPanel.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/FloatPanelView.d.ts.map +1 -1
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts +8 -0
- package/lib/typescript/src/ui/panel/StreamingSettingsModal.d.ts.map +1 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts +14 -0
- package/lib/typescript/src/ui/panel/useTabAnimation.d.ts.map +1 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts +141 -0
- package/lib/typescript/src/utils/DaemonClient.d.ts.map +1 -0
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts +2 -1
- package/lib/typescript/src/utils/createPersistedObservableStore.d.ts.map +1 -1
- package/lib/typescript/src/utils/deviceReport.d.ts +18 -0
- package/lib/typescript/src/utils/deviceReport.d.ts.map +1 -0
- package/node/daemon/src/cli.js +82 -0
- package/node/daemon/src/console/console.html +1662 -0
- package/node/daemon/src/console/index.js +47 -0
- package/node/daemon/src/constants.js +38 -0
- package/node/daemon/src/index.js +11 -0
- package/node/daemon/src/server.js +447 -0
- package/node/daemon/src/store.js +187 -0
- package/node/mcp/src/cli.js +31 -0
- package/node/mcp/src/constants.js +13 -0
- package/node/mcp/src/daemonClient.js +132 -0
- package/node/mcp/src/httpClient.js +49 -0
- package/node/mcp/src/index.js +15 -0
- package/node/mcp/src/logs.js +96 -0
- package/node/mcp/src/server.js +144 -0
- package/node/mcp/src/tools.js +84 -0
- package/package.json +8 -3
- package/src/core/initialize.ts +8 -0
- package/src/features/network/index.ts +30 -3
- package/src/features/network/networkInterceptor.ts +19 -6
- package/src/index.ts +22 -0
- package/src/ui/panel/DebugPanel.tsx +23 -1
- package/src/ui/panel/FloatPanelView.tsx +10 -68
- package/src/ui/panel/StreamingSettingsModal.tsx +528 -0
- package/src/ui/panel/useTabAnimation.ts +77 -0
- package/src/utils/DaemonClient.ts +887 -0
- package/src/utils/createPersistedObservableStore.ts +16 -3
- package/src/utils/deviceReport.ts +203 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_DEVICES = 20;
|
|
7
|
+
|
|
8
|
+
function createLogCount(report) {
|
|
9
|
+
const logs = report && typeof report === 'object' ? report.logs : undefined;
|
|
10
|
+
if (!logs || typeof logs !== 'object') {
|
|
11
|
+
return {};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Object.entries(logs).reduce((acc, [type, entries]) => {
|
|
15
|
+
if (Array.isArray(entries)) {
|
|
16
|
+
acc[type] = entries.length;
|
|
17
|
+
}
|
|
18
|
+
return acc;
|
|
19
|
+
}, {});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function slugPart(value) {
|
|
23
|
+
return String(value || 'unknown')
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase()
|
|
26
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
27
|
+
.replace(/^-+|-+$/g, '')
|
|
28
|
+
.slice(0, 80) || 'unknown';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function createDeviceId(report, source) {
|
|
32
|
+
const device = report && typeof report === 'object' && report.device && typeof report.device === 'object'
|
|
33
|
+
? report.device
|
|
34
|
+
: {};
|
|
35
|
+
return [
|
|
36
|
+
slugPart(device.platform),
|
|
37
|
+
slugPart(device.model),
|
|
38
|
+
slugPart(source && source.ip),
|
|
39
|
+
].join('_');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function readPersistedDevices(storagePath, maxDevices) {
|
|
43
|
+
if (!storagePath) {
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const raw = fs.readFileSync(storagePath, 'utf8');
|
|
49
|
+
const parsed = JSON.parse(raw);
|
|
50
|
+
if (!Array.isArray(parsed)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
return parsed
|
|
54
|
+
.filter((deviceLog) => (
|
|
55
|
+
deviceLog &&
|
|
56
|
+
typeof deviceLog === 'object' &&
|
|
57
|
+
typeof deviceLog.deviceId === 'string' &&
|
|
58
|
+
deviceLog.report &&
|
|
59
|
+
typeof deviceLog.report === 'object'
|
|
60
|
+
))
|
|
61
|
+
.slice(-maxDevices);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createMemoryStore(options = {}) {
|
|
68
|
+
const maxDevices = options.maxDevices || DEFAULT_MAX_DEVICES;
|
|
69
|
+
const onUpdate = options.onUpdate || null;
|
|
70
|
+
const storagePath = options.storagePath || null;
|
|
71
|
+
const devices = readPersistedDevices(storagePath, maxDevices);
|
|
72
|
+
|
|
73
|
+
function persist() {
|
|
74
|
+
if (!storagePath) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
fs.mkdirSync(path.dirname(storagePath), { recursive: true });
|
|
80
|
+
const tmpPath = `${storagePath}.tmp`;
|
|
81
|
+
fs.writeFileSync(tmpPath, JSON.stringify(devices, null, 2));
|
|
82
|
+
fs.renameSync(tmpPath, storagePath);
|
|
83
|
+
} catch {
|
|
84
|
+
// Persistence is best-effort; daemon HTTP API should keep working.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function saveReport(report, metadata = {}) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const receivedAt = new Date(now).toISOString();
|
|
91
|
+
const source = metadata.source || null;
|
|
92
|
+
const deviceId = createDeviceId(report, source);
|
|
93
|
+
const existingIndex = devices.findIndex((item) => item.deviceId === deviceId);
|
|
94
|
+
const existing = existingIndex >= 0 ? devices[existingIndex] : null;
|
|
95
|
+
const deviceLog = {
|
|
96
|
+
deviceId,
|
|
97
|
+
firstSeenAt: existing ? existing.firstSeenAt : receivedAt,
|
|
98
|
+
lastSeenAt: receivedAt,
|
|
99
|
+
receivedAt,
|
|
100
|
+
source,
|
|
101
|
+
device: report.device || null,
|
|
102
|
+
report,
|
|
103
|
+
logCount: createLogCount(report),
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (existingIndex >= 0) {
|
|
107
|
+
devices.splice(existingIndex, 1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
devices.push(deviceLog);
|
|
111
|
+
while (devices.length > maxDevices) {
|
|
112
|
+
devices.shift();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
persist();
|
|
116
|
+
if (onUpdate) onUpdate(deviceLog, 'full');
|
|
117
|
+
return deviceLog;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function appendLogs(deviceId, delta) {
|
|
121
|
+
const deviceLog = devices.find((item) => item.deviceId === deviceId);
|
|
122
|
+
if (!deviceLog) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const deltaLogs = (delta && delta.logs) || {};
|
|
127
|
+
Object.entries(deltaLogs).forEach(([type, entries]) => {
|
|
128
|
+
if (!Array.isArray(entries)) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (!deviceLog.report.logs[type]) {
|
|
132
|
+
deviceLog.report.logs[type] = [];
|
|
133
|
+
}
|
|
134
|
+
deviceLog.report.logs[type].push(...entries);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
deviceLog.lastSeenAt = new Date(Date.now()).toISOString();
|
|
138
|
+
deviceLog.receivedAt = deviceLog.lastSeenAt;
|
|
139
|
+
deviceLog.logCount = createLogCount(deviceLog.report);
|
|
140
|
+
persist();
|
|
141
|
+
if (onUpdate) onUpdate(deviceLog, 'delta', delta);
|
|
142
|
+
return deviceLog;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function listDevices() {
|
|
146
|
+
return devices
|
|
147
|
+
.slice()
|
|
148
|
+
.reverse()
|
|
149
|
+
.map((deviceLog) => ({
|
|
150
|
+
deviceId: deviceLog.deviceId,
|
|
151
|
+
firstSeenAt: deviceLog.firstSeenAt,
|
|
152
|
+
lastSeenAt: deviceLog.lastSeenAt,
|
|
153
|
+
receivedAt: deviceLog.receivedAt,
|
|
154
|
+
device: deviceLog.device || null,
|
|
155
|
+
source: deviceLog.source || null,
|
|
156
|
+
logCount: deviceLog.logCount,
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function getLatestDevice() {
|
|
161
|
+
return devices[devices.length - 1] || null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function getDevice(deviceId) {
|
|
165
|
+
return devices.find((deviceLog) => deviceLog.deviceId === deviceId) || null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function clear() {
|
|
169
|
+
devices.splice(0, devices.length);
|
|
170
|
+
persist();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
appendLogs,
|
|
175
|
+
clear,
|
|
176
|
+
getDevice,
|
|
177
|
+
getLatestDevice,
|
|
178
|
+
listDevices,
|
|
179
|
+
saveReport,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
createDeviceId,
|
|
185
|
+
createLogCount,
|
|
186
|
+
createMemoryStore,
|
|
187
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ensureDaemon } = require('./daemonClient');
|
|
4
|
+
const { startStdioServer } = require('./server');
|
|
5
|
+
|
|
6
|
+
function startMcpFromCli() {
|
|
7
|
+
let daemonPromise;
|
|
8
|
+
|
|
9
|
+
const ensureDaemonOnce = () => {
|
|
10
|
+
if (!daemonPromise) {
|
|
11
|
+
daemonPromise = ensureDaemon();
|
|
12
|
+
daemonPromise.then((result) => {
|
|
13
|
+
if (!result.ok) {
|
|
14
|
+
process.stderr.write(`Debug toolkit daemon unavailable: ${result.error}\n`);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return daemonPromise;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
ensureDaemonOnce();
|
|
22
|
+
startStdioServer({
|
|
23
|
+
context: {
|
|
24
|
+
ensureDaemon: ensureDaemonOnce,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
startMcpFromCli,
|
|
31
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const MCP_SERVER_NAME = 'react-native-debug-toolkit-mcp';
|
|
4
|
+
const MCP_SERVER_VERSION = '0.1.0';
|
|
5
|
+
const DEFAULT_DAEMON_ORIGIN = 'http://127.0.0.1:3799';
|
|
6
|
+
const EXPECTED_PROTOCOL_VERSION = 2;
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
DEFAULT_DAEMON_ORIGIN,
|
|
10
|
+
EXPECTED_PROTOCOL_VERSION,
|
|
11
|
+
MCP_SERVER_NAME,
|
|
12
|
+
MCP_SERVER_VERSION,
|
|
13
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
DEFAULT_DAEMON_ORIGIN,
|
|
9
|
+
EXPECTED_PROTOCOL_VERSION,
|
|
10
|
+
} = require('./constants');
|
|
11
|
+
const { requestJson } = require('./httpClient');
|
|
12
|
+
|
|
13
|
+
function getDaemonOrigin() {
|
|
14
|
+
return process.env.DEBUG_TOOLKIT_DAEMON_ORIGIN || DEFAULT_DAEMON_ORIGIN;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function readHealth(origin = getDaemonOrigin()) {
|
|
18
|
+
const response = await requestJson(origin, '/health', { timeoutMs: 800 });
|
|
19
|
+
if (response.status !== 200 || !response.body?.ok) {
|
|
20
|
+
throw new Error(`Daemon health check failed with status ${response.status}`);
|
|
21
|
+
}
|
|
22
|
+
if (response.body.protocolVersion !== EXPECTED_PROTOCOL_VERSION) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Daemon protocol mismatch: expected ${EXPECTED_PROTOCOL_VERSION}, got ${response.body.protocolVersion}`,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return response.body;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findDaemonBin() {
|
|
31
|
+
if (process.env.DEBUG_TOOLKIT_DAEMON_BIN) {
|
|
32
|
+
return process.env.DEBUG_TOOLKIT_DAEMON_BIN;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
return require.resolve('react-native-debug-toolkit/bin/debug-toolkit.js');
|
|
37
|
+
} catch {
|
|
38
|
+
const localPath = path.resolve(__dirname, '../../../bin/debug-toolkit.js');
|
|
39
|
+
return fs.existsSync(localPath) ? localPath : null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getDaemonBindHost() {
|
|
44
|
+
return process.env.DEBUG_TOOLKIT_DAEMON_HOST || '0.0.0.0';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function spawnDaemon(origin = getDaemonOrigin()) {
|
|
48
|
+
const daemonBin = findDaemonBin();
|
|
49
|
+
if (!daemonBin) {
|
|
50
|
+
throw new Error('Cannot find react-native-debug-toolkit debug-toolkit bin');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const url = new URL(origin);
|
|
54
|
+
const child = spawn(process.execPath, [
|
|
55
|
+
daemonBin,
|
|
56
|
+
'--daemon-only',
|
|
57
|
+
'--host',
|
|
58
|
+
getDaemonBindHost(),
|
|
59
|
+
'--port',
|
|
60
|
+
url.port || '3799',
|
|
61
|
+
], {
|
|
62
|
+
detached: true,
|
|
63
|
+
stdio: 'ignore',
|
|
64
|
+
});
|
|
65
|
+
child.unref();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function sleep(ms) {
|
|
69
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function ensureDaemon(options = {}) {
|
|
73
|
+
const origin = options.origin || getDaemonOrigin();
|
|
74
|
+
const timeoutMs = options.timeoutMs || 3000;
|
|
75
|
+
const pollMs = options.pollMs || 150;
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
return { ok: true, health: await readHealth(origin), origin, spawned: false };
|
|
80
|
+
} catch (initialError) {
|
|
81
|
+
try {
|
|
82
|
+
spawnDaemon(origin);
|
|
83
|
+
} catch (spawnError) {
|
|
84
|
+
return {
|
|
85
|
+
ok: false,
|
|
86
|
+
origin,
|
|
87
|
+
error: `${initialError.message}; ${spawnError.message}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
93
|
+
await sleep(pollMs);
|
|
94
|
+
try {
|
|
95
|
+
return { ok: true, health: await readHealth(origin), origin, spawned: true };
|
|
96
|
+
} catch {
|
|
97
|
+
// Keep polling until timeout; the detached daemon may still be binding.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
origin,
|
|
104
|
+
error: `Daemon did not become healthy within ${timeoutMs}ms at ${origin}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function readDevice(origin, deviceId) {
|
|
109
|
+
const requestPath = deviceId ? `/devices/${encodeURIComponent(deviceId)}` : '/devices/latest';
|
|
110
|
+
const response = await requestJson(origin, requestPath, { timeoutMs: 3000 });
|
|
111
|
+
if (response.status !== 200 || !response.body?.ok) {
|
|
112
|
+
throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
|
|
113
|
+
}
|
|
114
|
+
return response.body;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function readDevices(origin) {
|
|
118
|
+
const response = await requestJson(origin, '/devices', { timeoutMs: 3000 });
|
|
119
|
+
if (response.status !== 200 || !response.body?.ok) {
|
|
120
|
+
throw new Error(response.body?.error || `Daemon request failed with status ${response.status}`);
|
|
121
|
+
}
|
|
122
|
+
return response.body;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
module.exports = {
|
|
126
|
+
ensureDaemon,
|
|
127
|
+
findDaemonBin,
|
|
128
|
+
getDaemonOrigin,
|
|
129
|
+
readHealth,
|
|
130
|
+
readDevice,
|
|
131
|
+
readDevices,
|
|
132
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const http = require('http');
|
|
4
|
+
const https = require('https');
|
|
5
|
+
|
|
6
|
+
function requestJson(origin, path, options = {}) {
|
|
7
|
+
const url = new URL(path, origin);
|
|
8
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
9
|
+
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
const req = transport.request(url, {
|
|
12
|
+
method: options.method || 'GET',
|
|
13
|
+
headers: options.headers || {},
|
|
14
|
+
timeout: options.timeoutMs || 3000,
|
|
15
|
+
}, (res) => {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
18
|
+
res.on('end', () => {
|
|
19
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
20
|
+
let body = null;
|
|
21
|
+
try {
|
|
22
|
+
body = raw ? JSON.parse(raw) : null;
|
|
23
|
+
} catch {
|
|
24
|
+
reject(new Error(`Daemon returned non-JSON response from ${path}`));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resolve({
|
|
29
|
+
status: res.statusCode || 0,
|
|
30
|
+
body,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
req.on('timeout', () => {
|
|
36
|
+
req.destroy(new Error(`Daemon request timed out: ${path}`));
|
|
37
|
+
});
|
|
38
|
+
req.on('error', reject);
|
|
39
|
+
|
|
40
|
+
if (options.body) {
|
|
41
|
+
req.write(JSON.stringify(options.body));
|
|
42
|
+
}
|
|
43
|
+
req.end();
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
requestJson,
|
|
49
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ensureDaemon } = require('./daemonClient');
|
|
4
|
+
const { handleMessage, startStdioServer } = require('./server');
|
|
5
|
+
const { callTool, getAppLogsTool, listAppDevicesTool, tools } = require('./tools');
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
callTool,
|
|
9
|
+
ensureDaemon,
|
|
10
|
+
getAppLogsTool,
|
|
11
|
+
handleMessage,
|
|
12
|
+
listAppDevicesTool,
|
|
13
|
+
startStdioServer,
|
|
14
|
+
tools,
|
|
15
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const KNOWN_LOG_TYPES = ['network', 'console', 'navigation', 'track', 'zustand'];
|
|
4
|
+
|
|
5
|
+
function isFailedLog(entry) {
|
|
6
|
+
return Boolean(
|
|
7
|
+
entry &&
|
|
8
|
+
typeof entry === 'object' &&
|
|
9
|
+
(
|
|
10
|
+
entry.error ||
|
|
11
|
+
entry.level === 'error' ||
|
|
12
|
+
entry.response?.success === false ||
|
|
13
|
+
entry.response?.status >= 400
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function stripBodies(value, parentKey) {
|
|
19
|
+
if (Array.isArray(value)) {
|
|
20
|
+
return value.map((item) => stripBodies(item, parentKey));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!value || typeof value !== 'object') {
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return Object.entries(value).reduce((acc, [key, child]) => {
|
|
28
|
+
const normalizedKey = key.toLowerCase();
|
|
29
|
+
if (normalizedKey === 'body' || (parentKey === 'response' && normalizedKey === 'data')) {
|
|
30
|
+
return acc;
|
|
31
|
+
}
|
|
32
|
+
acc[key] = stripBodies(child, normalizedKey);
|
|
33
|
+
return acc;
|
|
34
|
+
}, {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function selectLogs(report, options = {}) {
|
|
38
|
+
const logs = report?.logs && typeof report.logs === 'object' ? report.logs : {};
|
|
39
|
+
const logType = options.logType;
|
|
40
|
+
const limit = Number.isFinite(options.limit) && options.limit > 0
|
|
41
|
+
? Math.min(Math.floor(options.limit), 200)
|
|
42
|
+
: 50;
|
|
43
|
+
const includeBodies = options.includeBodies !== false;
|
|
44
|
+
const failedOnly = options.failedOnly === true;
|
|
45
|
+
|
|
46
|
+
let entries;
|
|
47
|
+
if (logType) {
|
|
48
|
+
entries = Array.isArray(logs[logType]) ? logs[logType] : [];
|
|
49
|
+
} else {
|
|
50
|
+
entries = Object.entries(logs).flatMap(([type, typeEntries]) => (
|
|
51
|
+
Array.isArray(typeEntries)
|
|
52
|
+
? typeEntries.map((entry) => ({ type, entry }))
|
|
53
|
+
: []
|
|
54
|
+
));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (failedOnly) {
|
|
58
|
+
entries = entries.filter((item) => isFailedLog(item.entry || item));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
entries = entries.slice(-limit);
|
|
62
|
+
|
|
63
|
+
if (logType) {
|
|
64
|
+
return includeBodies ? entries : entries.map(stripBodies);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return entries.map((item) => ({
|
|
68
|
+
type: item.type,
|
|
69
|
+
entry: includeBodies ? item.entry : stripBodies(item.entry),
|
|
70
|
+
}));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function createToolPayload(device, options = {}) {
|
|
74
|
+
const report = device.report || { version: 2, logs: {} };
|
|
75
|
+
const logs = selectLogs(report, options);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
ok: true,
|
|
79
|
+
deviceId: device.deviceId,
|
|
80
|
+
receivedAt: device.receivedAt,
|
|
81
|
+
lastSeenAt: device.lastSeenAt,
|
|
82
|
+
logType: options.logType || 'all',
|
|
83
|
+
failedOnly: options.failedOnly === true,
|
|
84
|
+
includeBodies: options.includeBodies !== false,
|
|
85
|
+
count: logs.length,
|
|
86
|
+
logs,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = {
|
|
91
|
+
KNOWN_LOG_TYPES,
|
|
92
|
+
createToolPayload,
|
|
93
|
+
isFailedLog,
|
|
94
|
+
selectLogs,
|
|
95
|
+
stripBodies,
|
|
96
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
MCP_SERVER_NAME,
|
|
5
|
+
MCP_SERVER_VERSION,
|
|
6
|
+
} = require('./constants');
|
|
7
|
+
const { callTool, tools } = require('./tools');
|
|
8
|
+
|
|
9
|
+
function writeMessage(output, message) {
|
|
10
|
+
output.write(`${JSON.stringify(message)}\n`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createError(id, code, message) {
|
|
14
|
+
return {
|
|
15
|
+
jsonrpc: '2.0',
|
|
16
|
+
id,
|
|
17
|
+
error: { code, message },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function handleMessage(message, context) {
|
|
22
|
+
const id = message.id;
|
|
23
|
+
const method = message.method;
|
|
24
|
+
|
|
25
|
+
if (!method) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (method === 'initialize') {
|
|
30
|
+
return {
|
|
31
|
+
jsonrpc: '2.0',
|
|
32
|
+
id,
|
|
33
|
+
result: {
|
|
34
|
+
protocolVersion: message.params?.protocolVersion || '2024-11-05',
|
|
35
|
+
capabilities: { tools: {} },
|
|
36
|
+
serverInfo: {
|
|
37
|
+
name: MCP_SERVER_NAME,
|
|
38
|
+
version: MCP_SERVER_VERSION,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (method === 'notifications/initialized') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (method === 'ping') {
|
|
49
|
+
return {
|
|
50
|
+
jsonrpc: '2.0',
|
|
51
|
+
id,
|
|
52
|
+
result: {},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (method === 'tools/list') {
|
|
57
|
+
return {
|
|
58
|
+
jsonrpc: '2.0',
|
|
59
|
+
id,
|
|
60
|
+
result: {
|
|
61
|
+
tools,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (method === 'tools/call') {
|
|
67
|
+
try {
|
|
68
|
+
const result = await callTool(
|
|
69
|
+
message.params?.name,
|
|
70
|
+
message.params?.arguments || {},
|
|
71
|
+
context,
|
|
72
|
+
);
|
|
73
|
+
return {
|
|
74
|
+
jsonrpc: '2.0',
|
|
75
|
+
id,
|
|
76
|
+
result: {
|
|
77
|
+
content: [
|
|
78
|
+
{
|
|
79
|
+
type: 'text',
|
|
80
|
+
text: JSON.stringify(result, null, 2),
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return createError(id, -32000, error.message || 'Tool call failed');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (id === undefined) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return createError(id, -32601, `Method not found: ${method}`);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function startStdioServer(options = {}) {
|
|
98
|
+
const input = options.input || process.stdin;
|
|
99
|
+
const output = options.output || process.stdout;
|
|
100
|
+
const errorOutput = options.errorOutput || process.stderr;
|
|
101
|
+
const context = options.context || {};
|
|
102
|
+
let buffer = '';
|
|
103
|
+
|
|
104
|
+
input.setEncoding('utf8');
|
|
105
|
+
input.on('data', (chunk) => {
|
|
106
|
+
buffer += chunk;
|
|
107
|
+
const lines = buffer.split(/\r?\n/);
|
|
108
|
+
buffer = lines.pop() || '';
|
|
109
|
+
|
|
110
|
+
lines.forEach((line) => {
|
|
111
|
+
const trimmed = line.trim();
|
|
112
|
+
if (!trimmed) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let message;
|
|
117
|
+
try {
|
|
118
|
+
message = JSON.parse(trimmed);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
errorOutput.write(`Invalid MCP JSON message: ${error.message}\n`);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
handleMessage(message, context)
|
|
125
|
+
.then((response) => {
|
|
126
|
+
if (response) {
|
|
127
|
+
writeMessage(output, response);
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
.catch((error) => {
|
|
131
|
+
if (message.id !== undefined) {
|
|
132
|
+
writeMessage(output, createError(message.id, -32000, error.message || 'MCP error'));
|
|
133
|
+
} else {
|
|
134
|
+
errorOutput.write(`MCP notification failed: ${error.message}\n`);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = {
|
|
142
|
+
handleMessage,
|
|
143
|
+
startStdioServer,
|
|
144
|
+
};
|