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/README.md +364 -241
- package/package.json +1 -1
- package/src/commands.json +3584 -2680
- package/src/engine.js +69 -0
- package/src/link.js +8 -0
- package/src/main.js +71 -4
- package/src/mcp.js +378 -53
- package/src/relay.js +2 -1
- package/src/tools.js +18 -0
- package/src/trace.js +55 -0
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
|
+
}
|