scanruler-mcp 0.3.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 +107 -40
- package/bin/scanruler-mcp.js +15 -8
- package/package.json +1 -1
- package/src/commands.json +4611 -3584
- package/src/config.js +38 -6
- package/src/link.js +304 -293
- package/src/main.js +44 -11
- package/src/mcp.js +1155 -615
- package/src/relay.js +218 -218
- package/src/tools.js +192 -17
- package/src/update.js +380 -0
package/src/main.js
CHANGED
|
@@ -10,13 +10,14 @@ import { resolve } from 'node:path'
|
|
|
10
10
|
import { format, parseArgs } from 'node:util'
|
|
11
11
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
12
12
|
import { openInBrowser } from './browser.js'
|
|
13
|
-
import { configDir, loadConfig, loadRemembered, saveRemembered } from './config.js'
|
|
13
|
+
import { configDir, loadConfig, loadFolders, loadRemembered, saveFolders, saveRemembered } from './config.js'
|
|
14
14
|
import { CHECKOUT_ENGINE, EngineLink, loadEngine } from './engine.js'
|
|
15
15
|
import { originAllowed } from './link.js'
|
|
16
|
-
import { createMcpServer, TOOL_MODES } from './mcp.js'
|
|
16
|
+
import { ANSWER_WITHIN_MS, createMcpServer, TOOL_MODES } from './mcp.js'
|
|
17
17
|
import { SharedLink } from './relay.js'
|
|
18
18
|
import { createTrace } from './trace.js'
|
|
19
19
|
import { httpServer } from './serve.js'
|
|
20
|
+
import { NO_UPDATE_ENV, startUpdates, watchParent } from './update.js'
|
|
20
21
|
|
|
21
22
|
const DEFAULT_PORT = 7317
|
|
22
23
|
const HOSTED_APP = 'https://scanruler.com/'
|
|
@@ -39,21 +40,34 @@ Usage: scanruler-mcp [options]
|
|
|
39
40
|
--headless the same, with the engine built in the ScanRuler checkout
|
|
40
41
|
this server is part of
|
|
41
42
|
--tools <mode> full (the default) lists each of ScanRuler's commands as a tool;
|
|
42
|
-
compact lists
|
|
43
|
-
rest read through scanruler_tools and run through
|
|
44
|
-
— for a small context window
|
|
43
|
+
compact lists the server's own and the commands that return a
|
|
44
|
+
picture, the rest read through scanruler_tools and run through
|
|
45
|
+
scanruler_batch — for a small context window
|
|
45
46
|
--trace <dir> write a line per tool call — tool, ms, bytes each way, error,
|
|
46
47
|
whether the tab was hidden — to a new file in <dir>, for
|
|
47
48
|
measuring a run (scripts/trace-summary.mjs in the ScanRuler
|
|
48
49
|
repository sums it up); off by default
|
|
49
50
|
--trace-args with --trace, also write each call's arguments (otherwise
|
|
50
51
|
only their size)
|
|
52
|
+
--answer-within <s> hold a call at most this many seconds (default ${ANSWER_WITHIN_MS / 1000}): a change,
|
|
53
|
+
a batch or a long read still running then answers with its job
|
|
54
|
+
and runs on, and scanruler_wait waits no longer — under the
|
|
55
|
+
minute a client or a bridge may cut a call at; 0 holds a call
|
|
56
|
+
to its end
|
|
57
|
+
--no-update do not keep this server current (${NO_UPDATE_ENV}=1)
|
|
51
58
|
--pair print the pairing token and link, and exit
|
|
59
|
+
--version print this server's version, and exit
|
|
52
60
|
--help this
|
|
53
61
|
|
|
62
|
+
Once a day, in the background, the server asks npm's registry for its latest
|
|
63
|
+
version; when there is a newer one, npm fetches it, and the next start runs it
|
|
64
|
+
in this one's place. Not for a copy run from a git checkout, a version pinned
|
|
65
|
+
in the agent's config (scanruler-mcp@0.4.0), or with --no-update.
|
|
66
|
+
|
|
54
67
|
The pairing token is made on the first run and kept in ${join2(configDir(), 'config.json')};
|
|
55
|
-
SCANRULER_MCP_TOKEN overrides it. SCANRULER_MCP_PORT, SCANRULER_MCP_TOOLS
|
|
56
|
-
SCANRULER_MCP_TRACE stand in for --port, --tools
|
|
68
|
+
SCANRULER_MCP_TOKEN overrides it. SCANRULER_MCP_PORT, SCANRULER_MCP_TOOLS,
|
|
69
|
+
SCANRULER_MCP_TRACE and SCANRULER_MCP_ANSWER_WITHIN stand in for --port, --tools,
|
|
70
|
+
--trace and --answer-within. The agent's scanruler_open tool opens ScanRuler
|
|
57
71
|
in the default browser, paired. By hand: open the pairing link once, or in
|
|
58
72
|
ScanRuler go to Settings → Agents → Local agent connection and enter the port
|
|
59
73
|
and the token.
|
|
@@ -61,7 +75,7 @@ and the token.
|
|
|
61
75
|
|
|
62
76
|
/** ScanRuler run in this process from the engine at `file`, in place of
|
|
63
77
|
* the page — no port, no pairing. */
|
|
64
|
-
async function headless(file, { cwd, outDir, tools, trace }) {
|
|
78
|
+
async function headless(file, { cwd, outDir, tools, trace, answerWithinMs, updates }) {
|
|
65
79
|
// stdout is the MCP channel, the transport's alone: whatever else writes
|
|
66
80
|
// to it — the app's logging, the kernel's own reports — goes to stderr.
|
|
67
81
|
const stdout = process.stdout
|
|
@@ -78,10 +92,11 @@ async function headless(file, { cwd, outDir, tools, trace }) {
|
|
|
78
92
|
cwd,
|
|
79
93
|
outDir,
|
|
80
94
|
connectHelp: () => 'This server runs ScanRuler headless, in its own process; no page connects to it.',
|
|
81
|
-
serverInfo: () => ({ name: 'scanruler-mcp', version: VERSION, backend: 'headless', engine: file, tools, outDir, ...(trace ? { trace: trace.file } : {}) }),
|
|
95
|
+
serverInfo: () => ({ name: 'scanruler-mcp', version: VERSION, ...updates.info(), backend: 'headless', engine: file, tools, outDir, ...(trace ? { trace: trace.file } : {}) }),
|
|
82
96
|
remembered: null,
|
|
83
97
|
tools,
|
|
84
98
|
trace,
|
|
99
|
+
answerWithinMs,
|
|
85
100
|
})
|
|
86
101
|
server.onclose = () => {
|
|
87
102
|
link.close()
|
|
@@ -111,7 +126,10 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
111
126
|
tools: { type: 'string' },
|
|
112
127
|
trace: { type: 'string' },
|
|
113
128
|
'trace-args': { type: 'boolean' },
|
|
129
|
+
'answer-within': { type: 'string' },
|
|
130
|
+
'no-update': { type: 'boolean' },
|
|
114
131
|
pair: { type: 'boolean' },
|
|
132
|
+
version: { type: 'boolean' },
|
|
115
133
|
help: { type: 'boolean' },
|
|
116
134
|
},
|
|
117
135
|
strict: true,
|
|
@@ -120,12 +138,21 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
120
138
|
process.stderr.write(HELP)
|
|
121
139
|
return
|
|
122
140
|
}
|
|
141
|
+
if (values.version) {
|
|
142
|
+
process.stdout.write(`${VERSION}\n`)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
123
145
|
const port = Number(values.port ?? process.env.SCANRULER_MCP_PORT ?? DEFAULT_PORT)
|
|
124
146
|
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`--port ${values.port} is not a port.`)
|
|
125
147
|
const tools = values.tools ?? (process.env.SCANRULER_MCP_TOOLS || 'full')
|
|
126
148
|
if (!TOOL_MODES.includes(tools)) {
|
|
127
149
|
throw new Error(`${values.tools === undefined ? 'SCANRULER_MCP_TOOLS' : '--tools'} ${tools} is not one of ${TOOL_MODES.join(', ')}.`)
|
|
128
150
|
}
|
|
151
|
+
const within = values['answer-within'] ?? process.env.SCANRULER_MCP_ANSWER_WITHIN
|
|
152
|
+
const answerWithinMs = within === undefined || within === '' ? ANSWER_WITHIN_MS : Number(within) * 1000
|
|
153
|
+
if (!Number.isFinite(answerWithinMs) || answerWithinMs < 0) {
|
|
154
|
+
throw new Error(`${values['answer-within'] === undefined ? 'SCANRULER_MCP_ANSWER_WITHIN' : '--answer-within'} ${within} is not a number of seconds.`)
|
|
155
|
+
}
|
|
129
156
|
const config = loadConfig()
|
|
130
157
|
const token = process.env.SCANRULER_MCP_TOKEN || config.token
|
|
131
158
|
const app = values.app ?? (values.serve ? `http://127.0.0.1:${port}/` : HOSTED_APP)
|
|
@@ -137,13 +164,17 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
137
164
|
return
|
|
138
165
|
}
|
|
139
166
|
|
|
167
|
+
// Kept current in the background (update.js); a copy handed to stops
|
|
168
|
+
// with the launcher that started it.
|
|
169
|
+
const updates = startUpdates({ version: VERSION, argv })
|
|
170
|
+
watchParent()
|
|
140
171
|
const cwd = process.cwd()
|
|
141
172
|
const outDir = values.out ? (values.out.match(/^([A-Za-z]:)?[\\/]/) ? values.out : `${cwd}/${values.out}`) : cwd
|
|
142
173
|
const traceDir = values.trace ?? process.env.SCANRULER_MCP_TRACE
|
|
143
174
|
const trace = traceDir ? createTrace(resolve(cwd, traceDir), { args: values['trace-args'] === true }) : null
|
|
144
175
|
if (trace) log(`Tracing every call to ${trace.file}${trace.args ? ', arguments too' : ''}.`)
|
|
145
176
|
const engineFile = values.engine ?? process.env.SCANRULER_ENGINE ?? (values.headless ? CHECKOUT_ENGINE : null)
|
|
146
|
-
if (engineFile) return headless(resolve(cwd, engineFile), { cwd, outDir, tools, trace })
|
|
177
|
+
if (engineFile) return headless(resolve(cwd, engineFile), { cwd, outDir, tools, trace, answerWithinMs, updates })
|
|
147
178
|
const origins = values['allow-origin'] ?? []
|
|
148
179
|
const http = httpServer({ serve: values.serve, allowOrigin: (origin) => originAllowed(origin, origins), version: VERSION })
|
|
149
180
|
// The port, or — when another copy holds it — the relay through that one.
|
|
@@ -161,11 +192,13 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
161
192
|
cwd,
|
|
162
193
|
outDir,
|
|
163
194
|
connectHelp: () => (link.error ? `${link.error} ` : '') + connectHelp(),
|
|
164
|
-
serverInfo: () => ({ name: 'scanruler-mcp', version: VERSION, port, role: link.role, tools, outDir, config: config.file, ...(trace ? { trace: trace.file } : {}), ...(link.error ? { error: link.error } : {}) }),
|
|
195
|
+
serverInfo: () => ({ name: 'scanruler-mcp', version: VERSION, ...updates.info(), port, role: link.role, tools, outDir, config: config.file, ...(trace ? { trace: trace.file } : {}), ...(link.error ? { error: link.error } : {}) }),
|
|
165
196
|
remembered: { load: () => loadRemembered(), save: (commands) => saveRemembered(commands) },
|
|
197
|
+
folderStore: { load: () => loadFolders(), save: (folders) => saveFolders(folders) },
|
|
166
198
|
openPage: () => openInBrowser(pairing),
|
|
167
199
|
tools,
|
|
168
200
|
trace,
|
|
201
|
+
answerWithinMs,
|
|
169
202
|
})
|
|
170
203
|
const transport = new StdioServerTransport()
|
|
171
204
|
server.onclose = () => {
|