scanruler-mcp 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/trace.js ADDED
@@ -0,0 +1,55 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // The call trace: one JSON line per tool call into a file of the session's
3
+ // own, for measuring a run afterwards — how long each call took, how much
4
+ // went each way, what was refused, and whether the tab was hidden. Off
5
+ // unless asked for (--trace <dir>, SCANRULER_MCP_TRACE); local only; an
6
+ // argument's contents are written only with --trace-args, its size always.
7
+ // scripts/trace-summary.mjs reads the files.
8
+
9
+ import { appendFileSync, mkdirSync } from 'node:fs'
10
+ import { join } from 'node:path'
11
+
12
+ /** Bytes of a value as JSON, UTF-8. */
13
+ export const jsonBytes = (value) => (value === undefined ? 0 : Buffer.byteLength(JSON.stringify(value), 'utf8'))
14
+
15
+ /** What an MCP reply weighs: its text in UTF-8 bytes, a picture by its
16
+ * base64 — what the client is handed — and the error code of a refusal. */
17
+ export function replyMeasure(reply) {
18
+ let out = 0
19
+ let images = 0
20
+ for (const c of reply?.content ?? []) {
21
+ if (c.type === 'text') out += Buffer.byteLength(c.text ?? '', 'utf8')
22
+ else if (c.type === 'image') {
23
+ out += (c.data ?? '').length
24
+ images++
25
+ }
26
+ }
27
+ const text = reply?.content?.find((c) => c.type === 'text')?.text ?? ''
28
+ const error = reply?.isError ? (/^([a-z_]+): /.exec(text)?.[1] ?? 'failed') : null
29
+ return { out, error, ...(images ? { images } : {}) }
30
+ }
31
+
32
+ /** A trace into a new file in `dir`, or null when `dir` is empty. Each
33
+ * line is a JSON object with `ev`: "start" (the server), "list" (the tool
34
+ * list handed out), "call" (a tool call answered), "entry" (one call of a
35
+ * batch), "job" (a change started with wait: false, finished) and
36
+ * "visibility" (the tab hidden or shown). Times are ms since the epoch
37
+ * (`t`, the start) and durations ms. */
38
+ export function createTrace(dir, { args = false, now = () => Date.now(), pid = process.pid } = {}) {
39
+ if (!dir) return null
40
+ mkdirSync(dir, { recursive: true })
41
+ const stamp = new Date(now()).toISOString().replace(/[:.]/g, '-').replace(/Z$/, '')
42
+ const file = join(dir, `scanruler-mcp-${stamp}-${pid}.jsonl`)
43
+ let broken = false
44
+ const write = (line) => {
45
+ if (broken) return
46
+ try {
47
+ appendFileSync(file, `${JSON.stringify(line)}\n`)
48
+ } catch (e) {
49
+ // A trace that cannot be written is not a reason to fail the call.
50
+ broken = true
51
+ process.stderr.write(`[scanruler-mcp] The trace stopped: ${e?.message ?? e}\n`)
52
+ }
53
+ }
54
+ return { file, args, now, write }
55
+ }