scanruler-mcp 0.2.0 → 0.4.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 +431 -241
- package/bin/scanruler-mcp.js +15 -8
- package/package.json +1 -1
- package/src/commands.json +4435 -2504
- package/src/config.js +38 -6
- package/src/engine.js +69 -0
- package/src/link.js +304 -285
- package/src/main.js +105 -5
- package/src/mcp.js +1155 -290
- package/src/relay.js +218 -217
- package/src/tools.js +207 -14
- package/src/trace.js +55 -0
- package/src/update.js +380 -0
package/src/config.js
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// has to present, made on the first run and kept in the user's config
|
|
4
4
|
// folder so it is pasted into ScanRuler's Settings once, not every time —
|
|
5
5
|
// and the commands the last page that connected had, so an agent that lists
|
|
6
|
-
// the tools once, before a page is back, still sees all of them
|
|
6
|
+
// the tools once, before a page is back, still sees all of them — and the
|
|
7
|
+
// folders the scans were opened from, where its files go. The command list
|
|
8
|
+
// carries the page's version: a server shipped with a newer list of its own
|
|
9
|
+
// lists that one (mcp.js).
|
|
7
10
|
|
|
8
11
|
import { randomBytes } from 'node:crypto'
|
|
9
12
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
@@ -22,20 +25,49 @@ export function configDir(env = process.env, platform = process.platform) {
|
|
|
22
25
|
/** A new token: 24 random bytes, URL-safe — it travels in a link. */
|
|
23
26
|
const newToken = () => randomBytes(24).toString('base64url')
|
|
24
27
|
|
|
25
|
-
/** The command list the last connected page sent,
|
|
28
|
+
/** The command list the last connected page sent, with the page's version
|
|
29
|
+
* and plugins and when it was kept — `{ commands, version, plugins, saved }`
|
|
30
|
+
* — or null. A list kept by a server before 0.4.0 has no version. */
|
|
26
31
|
export function loadRemembered(dir = configDir()) {
|
|
27
32
|
try {
|
|
28
33
|
const kept = JSON.parse(readFileSync(join(dir, 'tools.json'), 'utf8'))
|
|
29
|
-
|
|
34
|
+
if (!Array.isArray(kept.commands) || kept.commands.length === 0) return null
|
|
35
|
+
return {
|
|
36
|
+
commands: kept.commands,
|
|
37
|
+
version: typeof kept.version === 'string' ? kept.version : null,
|
|
38
|
+
plugins: Array.isArray(kept.plugins) ? kept.plugins.map(String) : null,
|
|
39
|
+
saved: typeof kept.saved === 'string' ? kept.saved : null,
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Keep a page's command list for the next run: `{ commands, version,
|
|
47
|
+
* plugins }` — the page's version, for the next run to tell whether the
|
|
48
|
+
* list it was shipped with is newer — or the list alone. */
|
|
49
|
+
export function saveRemembered(kept, dir = configDir()) {
|
|
50
|
+
const { commands, version = null, plugins = null } = Array.isArray(kept) ? { commands: kept } : kept
|
|
51
|
+
mkdirSync(dir, { recursive: true })
|
|
52
|
+
writeFileSync(join(dir, 'tools.json'), JSON.stringify({ saved: new Date().toISOString(), version, plugins, commands }) + '\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The folders scans and projects were opened from or saved into, the
|
|
56
|
+
* latest first, or null — kept beside the command list, so a copy started
|
|
57
|
+
* afresh still writes and lists where the last one did. */
|
|
58
|
+
export function loadFolders(dir = configDir()) {
|
|
59
|
+
try {
|
|
60
|
+
const kept = JSON.parse(readFileSync(join(dir, 'folders.json'), 'utf8'))
|
|
61
|
+
return Array.isArray(kept.folders) && kept.folders.every((f) => typeof f === 'string') ? kept.folders : null
|
|
30
62
|
} catch {
|
|
31
63
|
return null
|
|
32
64
|
}
|
|
33
65
|
}
|
|
34
66
|
|
|
35
|
-
/** Keep
|
|
36
|
-
export function
|
|
67
|
+
/** Keep the folders for the next run, and for the other copies. */
|
|
68
|
+
export function saveFolders(folders, dir = configDir()) {
|
|
37
69
|
mkdirSync(dir, { recursive: true })
|
|
38
|
-
writeFileSync(join(dir, '
|
|
70
|
+
writeFileSync(join(dir, 'folders.json'), JSON.stringify({ saved: new Date().toISOString(), folders }) + '\n')
|
|
39
71
|
}
|
|
40
72
|
|
|
41
73
|
/** The stored config, with a token made and saved if there was none. */
|
package/src/engine.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// The headless backend: ScanRuler itself run in this process, with no
|
|
3
|
+
// browser — no scan, no viewport, nobody watching — in place of the page.
|
|
4
|
+
// The engine is a build of the app for Node (npm run build:engine in a
|
|
5
|
+
// ScanRuler checkout writes dist-engine/engine.mjs), found by its path and
|
|
6
|
+
// loaded as it is: this package names nothing in it and imports nothing from
|
|
7
|
+
// the app. It offers the commands that run without the page, with the same
|
|
8
|
+
// names, inputs and answers as the page's; the rest answer `unavailable`,
|
|
9
|
+
// naming the page mode. One engine to a process — its state is the app's
|
|
10
|
+
// own — so two servers started headless share nothing.
|
|
11
|
+
|
|
12
|
+
import { EventEmitter } from 'node:events'
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
15
|
+
import { toolName } from './tools.js'
|
|
16
|
+
|
|
17
|
+
/** Where --headless looks: the engine built in the ScanRuler checkout this
|
|
18
|
+
* package is part of. */
|
|
19
|
+
export const CHECKOUT_ENGINE = fileURLToPath(new URL('../../dist-engine/engine.mjs', import.meta.url))
|
|
20
|
+
|
|
21
|
+
/** Load and start the engine at `file`. Throws, saying what to do, when
|
|
22
|
+
* there is none there or it is no engine. */
|
|
23
|
+
export async function loadEngine(file) {
|
|
24
|
+
if (!existsSync(file)) throw new Error(`There is no ScanRuler engine at ${file}. Build one with "npm run build:engine" in the ScanRuler checkout, or give --engine the path of one.`)
|
|
25
|
+
const mod = await import(pathToFileURL(file).href)
|
|
26
|
+
if (typeof mod.startEngine !== 'function') throw new Error(`${file} is not a ScanRuler engine: it has no startEngine().`)
|
|
27
|
+
return mod.startEngine()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The engine, as the page link is to the server (link.js): always
|
|
31
|
+
* connected, its commands what a page would say it has. */
|
|
32
|
+
export class EngineLink extends EventEmitter {
|
|
33
|
+
/** @param {{ app: string, version: string, plugins: string[], listCommands(): object[], pageCommands(): string[], run(name: string, input: unknown, onProgress?: (text: string) => void): Promise<unknown>, stop(): Promise<void> }} engine */
|
|
34
|
+
constructor(engine, file) {
|
|
35
|
+
super()
|
|
36
|
+
this.engine = engine
|
|
37
|
+
this.file = file
|
|
38
|
+
this.backend = 'headless'
|
|
39
|
+
this.role = 'headless'
|
|
40
|
+
this.error = null
|
|
41
|
+
this.page = { app: engine.app, version: engine.version, origin: null, plugins: engine.plugins, viewport: false, commands: engine.listCommands() }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get connected() {
|
|
45
|
+
return true
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** A command run in the engine; refused with { code, message } as the
|
|
49
|
+
* page refuses one. */
|
|
50
|
+
run(name, input, _binaryKey, onProgress) {
|
|
51
|
+
return this.engine.run(name, input, onProgress).catch((e) => Promise.reject({ code: e?.code ?? 'failed', message: e?.message ?? String(e) }))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The command behind a tool that needs the page, by the tool's name —
|
|
55
|
+
* null for one the engine has not got at all. */
|
|
56
|
+
pageCommand(tool) {
|
|
57
|
+
return this.engine.pageCommands().find((name) => toolName({ name }) === tool) ?? null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
waitForPage() {
|
|
61
|
+
return Promise.resolve(true)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
start() {}
|
|
65
|
+
|
|
66
|
+
close() {
|
|
67
|
+
void this.engine.stop()
|
|
68
|
+
}
|
|
69
|
+
}
|