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/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 five and the commands that return a picture, the
43
- rest read through scanruler_tools and run through scanruler_batch
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 and
56
- SCANRULER_MCP_TRACE stand in for --port, --tools and --trace. The agent's scanruler_open tool opens ScanRuler
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 = () => {