opencode-skills-collection 4.0.74 → 4.0.75

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.
Files changed (50) hide show
  1. package/bundled-skills/.antigravity-install-manifest.json +14 -1
  2. package/bundled-skills/claude-api/SKILL.md +4 -4
  3. package/bundled-skills/claude-api/curl/examples.md +5 -5
  4. package/bundled-skills/claude-api/java/claude-api.md +1 -1
  5. package/bundled-skills/claude-api/php/claude-api.md +2 -2
  6. package/bundled-skills/claude-api/python/claude-api/README.md +13 -13
  7. package/bundled-skills/claude-api/python/claude-api/batches.md +3 -3
  8. package/bundled-skills/claude-api/python/claude-api/files-api.md +3 -3
  9. package/bundled-skills/claude-api/python/claude-api/streaming.md +6 -6
  10. package/bundled-skills/claude-api/python/claude-api/tool-use.md +19 -19
  11. package/bundled-skills/claude-api/ruby/claude-api.md +3 -3
  12. package/bundled-skills/claude-api/shared/error-codes.md +2 -2
  13. package/bundled-skills/claude-api/shared/live-sources.md +1 -1
  14. package/bundled-skills/claude-api/shared/models.md +4 -4
  15. package/bundled-skills/claude-api/shared/tool-use-concepts.md +2 -2
  16. package/bundled-skills/claude-api/typescript/claude-api/README.md +12 -12
  17. package/bundled-skills/claude-api/typescript/claude-api/batches.md +2 -2
  18. package/bundled-skills/claude-api/typescript/claude-api/files-api.md +1 -1
  19. package/bundled-skills/claude-api/typescript/claude-api/streaming.md +4 -4
  20. package/bundled-skills/claude-api/typescript/claude-api/tool-use.md +14 -14
  21. package/bundled-skills/electron-drive-skill/LICENSE +21 -0
  22. package/bundled-skills/electron-drive-skill/README.md +172 -0
  23. package/bundled-skills/electron-drive-skill/SKILL.md +237 -0
  24. package/bundled-skills/electron-drive-skill/drive.config.json.example +10 -0
  25. package/bundled-skills/electron-drive-skill/package.json +28 -0
  26. package/bundled-skills/electron-drive-skill/scripts/daemon.mjs +352 -0
  27. package/bundled-skills/electron-drive-skill/scripts/drive.mjs +250 -0
  28. package/bundled-skills/electron-drive-skill/scripts/lib.mjs +165 -0
  29. package/bundled-skills/electron-drive-skill/scripts/lib.test.mjs +56 -0
  30. package/bundled-skills/mcp-dependency-drift-audit/SKILL.md +169 -0
  31. package/bundled-skills/nexrad-mosaic-access/SKILL.md +297 -0
  32. package/bundled-skills/nexrad-mosaic-construction/SKILL.md +277 -0
  33. package/bundled-skills/nexrad-product-access/SKILL.md +258 -0
  34. package/bundled-skills/nexrad-radar-visualization/SKILL.md +318 -0
  35. package/bundled-skills/radar-satellite-analysis/SKILL.md +314 -0
  36. package/bundled-skills/traderspy-market-briefing/SKILL.md +132 -0
  37. package/bundled-skills/traderspy-market-screener/SKILL.md +182 -0
  38. package/bundled-skills/traderspy-market-screener/references/condition-cookbook.md +70 -0
  39. package/bundled-skills/traderspy-position-check/SKILL.md +130 -0
  40. package/bundled-skills/traderspy-position-check/references/risk-math.md +60 -0
  41. package/bundled-skills/traderspy-smart-money/SKILL.md +151 -0
  42. package/bundled-skills/traderspy-smart-money/references/exchange-notes.md +40 -0
  43. package/bundled-skills/traderspy-technical-analysis/SKILL.md +162 -0
  44. package/bundled-skills/traderspy-technical-analysis/references/derivatives-guide.md +57 -0
  45. package/bundled-skills/traderspy-technical-analysis/references/indicator-guide.md +47 -0
  46. package/bundled-skills/traderspy-trading-signals/SKILL.md +156 -0
  47. package/bundled-skills/traderspy-trading-signals/references/signal-fields.md +64 -0
  48. package/bundled-skills/unified-ai-gateway/SKILL.md +3 -3
  49. package/package.json +1 -1
  50. package/skills_index.json +235 -1
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Start an Electron app under Playwright and operate it from the command line,
4
+ * one command per call. Written for coding agents, usable by anyone.
5
+ *
6
+ * Run it from anywhere inside the project; see SKILL.md and README.md.
7
+ */
8
+ import { spawn, spawnSync } from 'node:child_process'
9
+ import { existsSync, mkdirSync, openSync, readFileSync, rmSync, statSync } from 'node:fs'
10
+ import http from 'node:http'
11
+ import path from 'node:path'
12
+ import { fileURLToPath } from 'node:url'
13
+
14
+ import {
15
+ daemonLogPath,
16
+ driveRoot,
17
+ isAlive,
18
+ loadConfig,
19
+ parseArgs,
20
+ profileDir,
21
+ PROJECT_ROOT,
22
+ readState,
23
+ startOptions,
24
+ statePath
25
+ } from './lib.mjs'
26
+
27
+ const here = path.dirname(fileURLToPath(import.meta.url))
28
+
29
+ const USAGE = `drive <command>
30
+
31
+ start [--dev] [--build] [--profile <name>] [--fresh]
32
+ Launch the app on a scratch profile. Default: the last build
33
+ ('build' in drive.config.json runs first with --build). --dev: the
34
+ dev launch, against a renderer dev server you started yourself.
35
+ --fresh wipes the profile first (first-run state).
36
+ status whether the app is up, its profile and window
37
+ snapshot accessibility tree of the window
38
+ screenshot [--out <png>] [--selector <css>]
39
+ click <target> Playwright selector: 'text=Settings', 'role=button[name="Save"]', css
40
+ fill <target> <text>
41
+ select <target> <value|label> a <select>, by option value or visible label
42
+ press <key> e.g. Enter, Escape, Control+K
43
+ wait <target> [--timeout <ms>]
44
+ eval <js | -> in the renderer; an expression, or a body with 'return'
45
+ main <js | -> in the main process; 'electron' and 'process' are in scope
46
+ '-' reads the code from stdin
47
+ logs [--lines <n>] recent console output, plus 'logFile' from the config
48
+ stop quit the app and the daemon
49
+ `
50
+
51
+ const fail = (message) => {
52
+ console.error(message)
53
+ process.exit(1)
54
+ }
55
+
56
+ const send = (command, args = {}) => {
57
+ const state = readState()
58
+ if (!state || !isAlive(state.pid)) {
59
+ if (state) rmSync(statePath(), { force: true })
60
+ return fail("Nothing is running. Start it with 'drive start'.")
61
+ }
62
+
63
+ const body = JSON.stringify({ token: state.token, command, args })
64
+ return new Promise((resolve, reject) => {
65
+ const request = http.request(
66
+ { host: '127.0.0.1', port: state.port, method: 'POST', headers: { 'content-type': 'application/json' } },
67
+ (response) => {
68
+ let data = ''
69
+ response.on('data', (chunk) => {
70
+ data += chunk
71
+ })
72
+ response.on('end', () => {
73
+ try {
74
+ const parsed = JSON.parse(data)
75
+ if (parsed.ok) resolve(parsed.result)
76
+ else reject(new Error(parsed.error))
77
+ } catch (e) {
78
+ reject(e)
79
+ }
80
+ })
81
+ }
82
+ )
83
+ request.on('error', reject)
84
+ request.end(body)
85
+ })
86
+ }
87
+
88
+ const print = (result) => {
89
+ if (typeof result === 'string') console.log(result)
90
+ else console.log(JSON.stringify(result, null, 2))
91
+ }
92
+
93
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
94
+
95
+ const urlUp = (url) =>
96
+ new Promise((resolve) => {
97
+ const request = http.get(url, { timeout: 1000 }, (response) => {
98
+ response.resume()
99
+ resolve(true)
100
+ })
101
+ request.on('error', () => resolve(false))
102
+ request.on('timeout', () => {
103
+ request.destroy()
104
+ resolve(false)
105
+ })
106
+ })
107
+
108
+ /** The file Electron will run for `args[0]`: the file itself, or a package's `main`. */
109
+ const entryFile = (target) => {
110
+ const resolved = path.resolve(PROJECT_ROOT, target)
111
+ if (!existsSync(resolved) || !statSync(resolved).isDirectory()) return resolved
112
+ const pkg = JSON.parse(readFileSync(path.join(resolved, 'package.json'), 'utf8'))
113
+ return path.join(resolved, pkg.main ?? 'index.js')
114
+ }
115
+
116
+ const start = async (flags) => {
117
+ const options = startOptions(flags)
118
+ const config = loadConfig()
119
+ const existing = readState()
120
+ if (existing && isAlive(existing.pid)) {
121
+ fail(`Already driving the app (profile '${existing.profile}', ${existing.mode}). Run 'drive stop' first.`)
122
+ }
123
+ if (existing) rmSync(statePath(), { force: true })
124
+
125
+ if (options.mode === 'dev') {
126
+ if (options.build) fail("--build applies to the built app only. With --dev, the dev server rebuilds the renderer.")
127
+ if (config.dev.url && !(await urlUp(config.dev.url))) {
128
+ fail(`The renderer dev server is not up at ${config.dev.url}. Start it first, without letting it launch its own Electron.`)
129
+ }
130
+ } else {
131
+ if (options.build) {
132
+ if (!config.build) fail("--build needs a 'build' command in drive.config.json.")
133
+ console.log(`Building (${config.build})...`)
134
+ const built = spawnSync(config.build, { cwd: PROJECT_ROOT, stdio: 'inherit', shell: true })
135
+ if (built.status !== 0) fail(`'${config.build}' failed.`)
136
+ }
137
+ const entry = entryFile(config.args[0] ?? '.')
138
+ if (!existsSync(entry)) fail(`No built app: ${entry} does not exist. Run 'drive start --build'.`)
139
+ const ageMinutes = Math.round((Date.now() - statSync(entry).mtimeMs) / 60000)
140
+ console.log(`Using the build from ${ageMinutes} minute(s) ago. Pass --build to rebuild.`)
141
+ }
142
+
143
+ const dataDir = profileDir(options.profile)
144
+ if (options.fresh) rmSync(dataDir, { recursive: true, force: true })
145
+ mkdirSync(dataDir, { recursive: true })
146
+ mkdirSync(driveRoot(), { recursive: true })
147
+
148
+ const logFd = openSync(daemonLogPath(), 'w')
149
+ const daemon = spawn(
150
+ process.execPath,
151
+ [path.join(here, 'daemon.mjs'), options.mode, options.profile, dataDir],
152
+ {
153
+ cwd: PROJECT_ROOT,
154
+ detached: true,
155
+ stdio: ['ignore', logFd, logFd],
156
+ env: { ...process.env, DRIVE_PROJECT_ROOT: PROJECT_ROOT }
157
+ }
158
+ )
159
+ daemon.unref()
160
+
161
+ console.log(`Starting (${options.mode}, profile '${options.profile}')...`)
162
+ const deadline = Date.now() + 120_000
163
+ while (Date.now() < deadline) {
164
+ if (readState()) break
165
+ if (!isAlive(daemon.pid)) {
166
+ console.error(readFileSync(daemonLogPath(), 'utf8'))
167
+ fail('The driver failed to start the app (log above).')
168
+ }
169
+ await sleep(500)
170
+ }
171
+ if (!readState()) fail(`Timed out waiting for the app. See ${daemonLogPath()}.`)
172
+ print(await send('status'))
173
+ }
174
+
175
+ /** The code argument, or stdin when it is '-'. */
176
+ const codeFrom = (positional) =>
177
+ positional.length === 1 && positional[0] === '-' ? readFileSync(0, 'utf8') : positional.join(' ')
178
+
179
+ const run = async () => {
180
+ const { command, positional, flags } = parseArgs(process.argv.slice(2))
181
+ const need = (count, what) => {
182
+ if (positional.length < count) fail(`Usage: drive ${command} ${what}`)
183
+ }
184
+
185
+ switch (command) {
186
+ case 'start':
187
+ return start(flags)
188
+ case 'status': {
189
+ const state = readState()
190
+ if (!state || !isAlive(state.pid)) return print('Not running.')
191
+ return print(await send('status'))
192
+ }
193
+ case 'stop': {
194
+ const state = readState()
195
+ if (!state || !isAlive(state.pid)) {
196
+ rmSync(statePath(), { force: true })
197
+ if (state?.electronPid && isAlive(state.electronPid)) {
198
+ process.kill(state.electronPid, 'SIGKILL')
199
+ return print('The daemon was gone but the app was not; killed it.')
200
+ }
201
+ return print('Not running.')
202
+ }
203
+ return print(await send('stop'))
204
+ }
205
+ case 'snapshot':
206
+ return print(await send('snapshot'))
207
+ case 'screenshot':
208
+ return print(
209
+ await send('screenshot', {
210
+ out: typeof flags.out === 'string' ? path.resolve(flags.out) : undefined,
211
+ selector: typeof flags.selector === 'string' ? flags.selector : undefined
212
+ })
213
+ )
214
+ case 'click':
215
+ need(1, '<target>')
216
+ return print(await send('click', { target: positional[0] }))
217
+ case 'fill':
218
+ need(2, '<target> <text>')
219
+ return print(await send('fill', { target: positional[0], text: positional.slice(1).join(' ') }))
220
+ case 'select':
221
+ need(2, '<target> <value|label>')
222
+ return print(await send('select', { target: positional[0], value: positional.slice(1).join(' ') }))
223
+ case 'press':
224
+ need(1, '<key>')
225
+ return print(await send('press', { key: positional[0] }))
226
+ case 'wait':
227
+ need(1, '<target>')
228
+ return print(
229
+ await send('wait', {
230
+ target: positional[0],
231
+ timeout: typeof flags.timeout === 'string' ? Number(flags.timeout) : undefined
232
+ })
233
+ )
234
+ case 'eval':
235
+ need(1, '<js>')
236
+ return print(await send('eval', { code: codeFrom(positional) }))
237
+ case 'main':
238
+ need(1, '<js>')
239
+ return print(await send('main', { code: codeFrom(positional) }))
240
+ case 'logs':
241
+ return print(await send('logs', { lines: typeof flags.lines === 'string' ? Number(flags.lines) : undefined }))
242
+ case 'help':
243
+ case '--help':
244
+ return print(USAGE)
245
+ default:
246
+ return fail(`Unknown command '${command}'.\n\n${USAGE}`)
247
+ }
248
+ }
249
+
250
+ run().catch((e) => fail(e.message))
@@ -0,0 +1,165 @@
1
+ /**
2
+ * The pure half of the driver: project discovery, config, argument parsing,
3
+ * paths and the state file. No Playwright and no Electron here, so it can be
4
+ * tested without launching anything (`node --test scripts/`).
5
+ */
6
+ import { createHash } from 'node:crypto'
7
+ import { existsSync, readFileSync } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import path from 'node:path'
10
+
11
+ /** The nearest directory at or above `from` with a package.json. */
12
+ export const findProjectRoot = (from = process.cwd()) => {
13
+ let dir = path.resolve(from)
14
+ for (;;) {
15
+ if (existsSync(path.join(dir, 'package.json'))) return dir
16
+ const parent = path.dirname(dir)
17
+ if (parent === dir) throw new Error(`No package.json at or above ${from}. Run the driver from inside your Electron project.`)
18
+ dir = parent
19
+ }
20
+ }
21
+
22
+ export const PROJECT_ROOT = process.env.DRIVE_PROJECT_ROOT
23
+ ? path.resolve(process.env.DRIVE_PROJECT_ROOT)
24
+ : findProjectRoot()
25
+
26
+ /**
27
+ * `drive.config.json` at the project root, over the defaults. Every field is
28
+ * optional; with no file at all, `start` runs `electron .` on the last build.
29
+ */
30
+ export const loadConfig = (root = PROJECT_ROOT) => {
31
+ const file = path.join(root, 'drive.config.json')
32
+ let config = {}
33
+ if (existsSync(file)) {
34
+ try {
35
+ config = JSON.parse(readFileSync(file, 'utf8'))
36
+ } catch (e) {
37
+ throw new Error(`${file} is not valid JSON: ${e.message}`)
38
+ }
39
+ }
40
+ return {
41
+ build: config.build ?? null,
42
+ args: config.args ?? ['.'],
43
+ env: config.env ?? {},
44
+ logFile: config.logFile ?? null,
45
+ dev: {
46
+ args: config.dev?.args ?? config.args ?? ['.'],
47
+ env: config.dev?.env ?? {},
48
+ url: config.dev?.url ?? null
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Everything the driver owns lives under here, never in the real profile. One
55
+ * directory per project, so two apps never share a scratch profile.
56
+ */
57
+ export const driveRoot = (root = PROJECT_ROOT) => {
58
+ if (process.env.DRIVE_ROOT) return process.env.DRIVE_ROOT
59
+ const hash = createHash('sha256').update(root).digest('hex').slice(0, 8)
60
+ return path.join(tmpdir(), 'electron-drive', `${path.basename(root)}-${hash}`)
61
+ }
62
+
63
+ export const statePath = () => path.join(driveRoot(), 'daemon.json')
64
+ export const daemonLogPath = () => path.join(driveRoot(), 'daemon.log')
65
+ export const screenshotDir = () => path.join(driveRoot(), 'screenshots')
66
+
67
+ /**
68
+ * A profile name becomes a directory name, so it is restricted to something
69
+ * that cannot climb out of `profiles/`.
70
+ */
71
+ export const profileDir = (profile) => {
72
+ if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(profile)) {
73
+ throw new Error(`Invalid profile name '${profile}': letters, digits, '-' and '_' only.`)
74
+ }
75
+ return path.join(driveRoot(), 'profiles', profile)
76
+ }
77
+
78
+ export const readState = () => {
79
+ const file = statePath()
80
+ if (!existsSync(file)) return null
81
+ try {
82
+ return JSON.parse(readFileSync(file, 'utf8'))
83
+ } catch {
84
+ return null
85
+ }
86
+ }
87
+
88
+ export const isAlive = (pid) => {
89
+ if (!pid) return false
90
+ try {
91
+ process.kill(pid, 0)
92
+ return true
93
+ } catch (e) {
94
+ return e?.code === 'EPERM'
95
+ }
96
+ }
97
+
98
+ const BOOLEAN_FLAGS = new Set(['dev', 'build', 'fresh'])
99
+
100
+ /**
101
+ * `drive <command> [positional...] [--flag [value]]`.
102
+ *
103
+ * Flags known to be boolean take no value, so `start --dev --fresh` is two
104
+ * flags rather than `dev = '--fresh'`. Everything after a bare `--` is
105
+ * positional, for JavaScript that itself starts with dashes.
106
+ */
107
+ export const parseArgs = (argv) => {
108
+ const [command = 'help', ...rest] = argv
109
+ const positional = []
110
+ const flags = {}
111
+
112
+ for (let i = 0; i < rest.length; i += 1) {
113
+ const arg = rest[i]
114
+ if (arg === '--') {
115
+ positional.push(...rest.slice(i + 1))
116
+ break
117
+ }
118
+ if (arg.startsWith('--')) {
119
+ const name = arg.slice(2)
120
+ const next = rest[i + 1]
121
+ if (BOOLEAN_FLAGS.has(name) || next === undefined || next.startsWith('--')) {
122
+ flags[name] = true
123
+ } else {
124
+ flags[name] = next
125
+ i += 1
126
+ }
127
+ } else {
128
+ positional.push(arg)
129
+ }
130
+ }
131
+
132
+ return { command, positional, flags }
133
+ }
134
+
135
+ export const startOptions = (flags) => ({
136
+ mode: flags.dev ? 'dev' : 'built',
137
+ build: flags.build === true,
138
+ profile: typeof flags.profile === 'string' ? flags.profile : 'default',
139
+ fresh: flags.fresh === true
140
+ })
141
+
142
+ /**
143
+ * The child's environment: the caller's, minus the variables that break a
144
+ * launch, plus the config's, where `{profile}` becomes the scratch profile
145
+ * directory.
146
+ *
147
+ * `ELECTRON_RUN_AS_NODE=1` makes Electron start as plain Node, where `app` is
148
+ * undefined and nothing works, and some shells set it.
149
+ */
150
+ export const childEnv = (base, dataDir, mode, extra) => {
151
+ const env = { ...base }
152
+ delete env.ELECTRON_RUN_AS_NODE
153
+ if (mode === 'dev') env.NODE_ENV = 'development'
154
+ else delete env.NODE_ENV
155
+ for (const [key, value] of Object.entries(extra)) {
156
+ env[key] = String(value).replaceAll('{profile}', dataDir)
157
+ }
158
+ return env
159
+ }
160
+
161
+ /**
162
+ * JavaScript from the command line, made into an async function body: a bare
163
+ * expression is returned, anything with its own `return` is used as written.
164
+ */
165
+ export const asFunctionBody = (code) => (/\breturn\b/.test(code) ? code : `return (${code})`)
@@ -0,0 +1,56 @@
1
+ // node --test scripts/
2
+ import assert from 'node:assert/strict'
3
+ import { mkdtempSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
5
+ import path from 'node:path'
6
+ import { test } from 'node:test'
7
+
8
+ const project = mkdtempSync(path.join(tmpdir(), 'drive-test-'))
9
+ writeFileSync(path.join(project, 'package.json'), '{}')
10
+ process.env.DRIVE_PROJECT_ROOT = project
11
+ delete process.env.DRIVE_ROOT
12
+ const lib = await import('./lib.mjs')
13
+
14
+ test('parses commands, valued and boolean flags, and --', () => {
15
+ assert.deepEqual(lib.parseArgs(['fill', 'role=textbox', 'hello', 'world']), {
16
+ command: 'fill',
17
+ positional: ['role=textbox', 'hello', 'world'],
18
+ flags: {}
19
+ })
20
+ assert.deepEqual(lib.parseArgs(['start', '--dev', '--profile', 'x', '--fresh']).flags, {
21
+ dev: true,
22
+ profile: 'x',
23
+ fresh: true
24
+ })
25
+ assert.deepEqual(lib.parseArgs(['eval', '--', '--x', '1']).positional, ['--x', '1'])
26
+ assert.equal(lib.parseArgs([]).command, 'help')
27
+ })
28
+
29
+ test('keeps profiles inside the driver root', () => {
30
+ assert.ok(lib.profileDir('default').startsWith(lib.driveRoot()))
31
+ for (const bad of ['../../Library', 'a/b', '']) assert.throws(() => lib.profileDir(bad), /Invalid profile/)
32
+ })
33
+
34
+ test('child env drops ELECTRON_RUN_AS_NODE and fills {profile}', () => {
35
+ const env = lib.childEnv({ ELECTRON_RUN_AS_NODE: '1', NODE_ENV: 'test', KEEP: 'me' }, '/scratch', 'built', {
36
+ APP_DATA: '{profile}/data'
37
+ })
38
+ assert.deepEqual(env, { KEEP: 'me', APP_DATA: '/scratch/data' })
39
+ assert.equal(lib.childEnv({}, '/s', 'dev', {}).NODE_ENV, 'development')
40
+ })
41
+
42
+ test('config defaults to electron . with no file', () => {
43
+ assert.deepEqual(lib.loadConfig(project).args, ['.'])
44
+ assert.deepEqual(lib.loadConfig(project).dev.args, ['.'])
45
+ })
46
+
47
+ test('a malformed config names the file', () => {
48
+ const broken = mkdtempSync(path.join(tmpdir(), 'drive-test-'))
49
+ writeFileSync(path.join(broken, 'drive.config.json'), '{ "args": [".",] }')
50
+ assert.throws(() => lib.loadConfig(broken), /drive\.config\.json is not valid JSON/)
51
+ })
52
+
53
+ test('wraps bare expressions, keeps bodies with return', () => {
54
+ assert.equal(lib.asFunctionBody('1 + 1'), 'return (1 + 1)')
55
+ assert.equal(lib.asFunctionBody('return 2'), 'return 2')
56
+ })
@@ -0,0 +1,169 @@
1
+ ---
2
+ name: mcp-dependency-drift-audit
3
+ description: "Statically audit MCP configs for mutable npm/npx package references before approval or CI, without executing discovered MCP servers."
4
+ category: security
5
+ risk: safe
6
+ source: "https://github.com/tomelias10/mcp-drift-check"
7
+ source_repo: tomelias10/mcp-drift-check
8
+ source_type: community
9
+ date_added: "2026-09-25"
10
+ author: tomelias10
11
+ tags: [mcp, ai-security, supply-chain, devsecops, sarif]
12
+ tools: [claude, cursor, gemini, codex, windsurf]
13
+ license: MIT
14
+ license_source: "https://github.com/tomelias10/mcp-drift-check/blob/v0/LICENSE"
15
+ ---
16
+
17
+ # MCP Dependency Drift Audit
18
+
19
+ ## Overview
20
+
21
+ Audit Model Context Protocol configuration files for npm/npx package references that can resolve to different code after the configuration was reviewed. The workflow is deliberately static: it reads config text, classifies version selectors, and recommends reproducibility fixes without starting discovered MCP servers.
22
+
23
+ Dependency mutability is a review-boundary signal, not proof that a package is malicious, vulnerable, or compromised.
24
+
25
+ ## When to Use This Skill
26
+
27
+ - Before approving `.mcp.json`, Cursor, VS Code, GitHub Copilot, Claude, or Windsurf MCP configuration.
28
+ - When adding an MCP configuration gate to pull requests or CI.
29
+ - During an AI-agent security review where npm/npx-backed MCP servers are configured.
30
+ - When checking whether a previously reviewed MCP config can silently resolve to newer package code.
31
+
32
+ ## How It Works
33
+
34
+ ### Step 1: Locate and read MCP configs without executing them
35
+
36
+ Inspect known project paths such as `.mcp.json`, `.github/mcp.json`, `.cursor/mcp.json`, `.vscode/mcp.json`, and `.windsurf/mcp.json`, plus any MCP config explicitly supplied by the user.
37
+
38
+ Read them as text/JSON only. Never run a discovered `command` or `args` value as part of this audit. Stay inside the requested workspace by default; only include user-level/global MCP configuration if the user explicitly asks for machine-wide scope.
39
+
40
+ ### Step 2: Use MCP Drift Check when already available
41
+
42
+ If `mcp-drift-check` is already installed, use the workspace-scoped mode from the intended repo root:
43
+
44
+ ```bash
45
+ mcp-drift-check scan-workspace --markdown
46
+ ```
47
+
48
+ For CI evidence:
49
+
50
+ ```bash
51
+ mcp-drift-check scan-workspace --sarif mcp-drift.sarif --markdown
52
+ ```
53
+
54
+ If the scanner is not installed, explain that the following command fetches the open-source scanner from GitHub and get user approval before running it:
55
+
56
+ ```bash
57
+ uvx --from git+https://github.com/tomelias10/mcp-drift-check mcp-drift-check scan-workspace --markdown
58
+ ```
59
+
60
+ If the user declines network access or installation, continue with the manual rules below.
61
+
62
+ ### Step 3: Apply the manual static rules
63
+
64
+ For npm/npx-style package selectors:
65
+
66
+ | Selector | Classification | Reason |
67
+ | --- | --- | --- |
68
+ | `package@1.2.3` | SAFE | Exact package version is reproducible. |
69
+ | bare `package` or `@scope/package` | HIGH | A later resolution can select different package code. |
70
+ | `package@latest` | HIGH | The selector is explicitly mutable. |
71
+ | `package@^1.2.0`, `~1.2.0`, `>=1.2.0`, wildcard | MEDIUM | Resolution can change within the allowed range. |
72
+ | local path, script, binary, unknown executable | REVIEW | npm version-drift rules do not establish update behavior. |
73
+
74
+ Treat `-y` / `--yes` as context only. It suppresses interactive confirmation but is not a vulnerability by itself.
75
+
76
+ ### Step 4: Recommend remediation without inventing versions
77
+
78
+ For mutable npm/npx references, recommend pinning to an exact version that the team has actually reviewed and then updating that pin deliberately.
79
+
80
+ Do not guess the version to pin. If registry lookup is needed, explain that it requires network access and ask before querying it.
81
+
82
+ Pinning improves reproducibility. It does not establish package provenance, vulnerability status, authorization safety, prompt-injection resistance, or runtime isolation.
83
+
84
+ ### Step 5: Produce a bounded report
85
+
86
+ Return:
87
+
88
+ | Config | MCP server | Package/reference | Classification | Why | Recommended next step |
89
+ | --- | --- | --- | --- | --- | --- |
90
+
91
+ Then state:
92
+
93
+ - No MCP servers were executed during the audit.
94
+ - Mutable dependency references are review/reproducibility signals, not breach claims.
95
+ - A clean dependency-drift result is not a complete MCP security assessment.
96
+
97
+ ## Examples
98
+
99
+ ### Example 1: Audit before approval
100
+
101
+ **User:** `Audit this repo's MCP config before we approve it. Do not run any MCP servers.`
102
+
103
+ **Expected shape:**
104
+
105
+ ```text
106
+ .mcp.json | browser | @example/browser-mcp@latest | HIGH
107
+ Reason: @latest can resolve to different package code later.
108
+ Next step: pin the exact version your team reviews and update it deliberately.
109
+
110
+ No MCP servers were executed.
111
+ ```
112
+
113
+ ### Example 2: Add a CI gate
114
+
115
+ For GitHub Actions:
116
+
117
+ ```yaml
118
+ - uses: actions/checkout@v4
119
+ - uses: tomelias10/mcp-drift-check@v0
120
+ ```
121
+
122
+ MCP Drift Check can also emit SARIF 2.1.0 for GitHub Code Scanning. The action defaults to workspace-only discovery; machine-wide user-level scanning remains an explicit `scan-all` choice.
123
+
124
+ ## Best Practices
125
+
126
+ - ✅ Read config text only during discovery and classification.
127
+ - ✅ Keep the default scope to the requested workspace/repository.
128
+ - ✅ Separate reproducibility findings from vulnerability or compromise claims.
129
+ - ✅ Pin only a version the team has actually reviewed.
130
+ - ✅ Keep sensitive config values out of reports and public issues.
131
+ - ❌ Do not execute server commands found in a config to "verify" a static finding.
132
+ - ❌ Do not treat a clean result as a general MCP security guarantee.
133
+
134
+ ## Limitations
135
+
136
+ - This workflow only covers dependency mutability/review drift for package-backed MCP configuration.
137
+ - It does not assess prompt injection, tool poisoning, OAuth/authorization, server implementation flaws, malware, provenance, or runtime sandboxing.
138
+ - Local executables and non-npm launch paths require separate review.
139
+ - A static config can be reproducible and still unsafe for other reasons.
140
+
141
+ ## Security & Safety Notes
142
+
143
+ - The default workflow is read-only, local, and workspace-scoped.
144
+ - Never execute discovered MCP server commands during this audit.
145
+ - Never include credentials, tokens, private headers, or secret values in the output.
146
+ - The optional `uvx` path makes a network fetch from the declared GitHub source; obtain user approval before running it.
147
+ - If deeper active testing is requested, stop and establish explicit authorization and scope before any intrusive action.
148
+
149
+ ## Common Pitfalls
150
+
151
+ - **Problem:** Calling an unpinned dependency a vulnerability.
152
+ **Solution:** Describe it as a reproducibility/review-drift signal unless separate evidence establishes security impact.
153
+ - **Problem:** Pinning to today's registry version without knowing what was reviewed.
154
+ **Solution:** Ask for the reviewed version or, with approval, reconstruct relevant package history; do not guess.
155
+ - **Problem:** Treating `-y` as the core finding.
156
+ **Solution:** Report it only as context; the version selector determines the dependency-drift classification.
157
+ - **Problem:** Scanning user-level MCP configs when the user only asked about one repo.
158
+ **Solution:** Use `scan-workspace` by default; use `scan-all` only when machine-wide scope is explicit.
159
+
160
+ ## Related Skills
161
+
162
+ - `@mcp-server-security` - Broader MCP server hardening, authentication, authorization, transport, and runtime controls.
163
+ - `@supply-chain-security` - Broader software supply-chain assessment beyond MCP configuration.
164
+ - `@sast-scanning` - Static application-security analysis for source code rather than MCP launch configuration.
165
+
166
+ ## Additional Resources
167
+
168
+ - [MCP Drift Check stable v0](https://github.com/tomelias10/mcp-drift-check/tree/v0) - MIT zero-execution scanner, GitHub Action, workspace-scoped mode, and SARIF output.
169
+ - [Public MCP dependency drift examples](https://site-creator-vinext-starter.surfaceproof.workers.dev/research/mcp-dependency-drift) - Commit-specific public configuration examples and methodology limitations.