Files
low-code-react-app-b/scripts/mcp/codeql-mcp.cjs
2026-01-19 10:10:37 +00:00

313 lines
8.4 KiB
JavaScript

#!/usr/bin/env node
const { spawn } = require('node:child_process')
const fs = require('node:fs')
function send(message) {
process.stdout.write(`${JSON.stringify(message)}\n`)
}
function respond(id, result) {
send({ jsonrpc: '2.0', id, result })
}
function respondError(id, code, message) {
send({ jsonrpc: '2.0', id, error: { code, message } })
}
function toolList() {
return [
{
name: 'codeql_analyze',
description: 'Run CodeQL database analyze with a query pack.',
inputSchema: {
type: 'object',
properties: {
dbPath: { type: 'string' },
queryPack: { type: 'string' },
output: { type: 'string' },
format: { type: 'string', default: 'sarifv2.1.0' },
rerun: { type: 'boolean', default: false },
extraArgs: { type: 'array', items: { type: 'string' } },
},
required: ['dbPath', 'queryPack'],
},
},
{
name: 'codeql_database_create',
description: 'Create a CodeQL database.',
inputSchema: {
type: 'object',
properties: {
dbPath: { type: 'string' },
language: { type: 'string', default: 'javascript' },
sourceRoot: { type: 'string', default: '.' },
buildMode: { type: 'string', default: 'none' },
extraArgs: { type: 'array', items: { type: 'string' } },
},
required: ['dbPath'],
},
},
{
name: 'codeql_pack_install',
description: 'Install dependencies for a local CodeQL pack.',
inputSchema: {
type: 'object',
properties: {
packDir: { type: 'string' },
extraArgs: { type: 'array', items: { type: 'string' } },
},
required: ['packDir'],
},
},
{
name: 'codeql_pack_download',
description: 'Download a CodeQL pack by name.',
inputSchema: {
type: 'object',
properties: {
pack: { type: 'string' },
extraArgs: { type: 'array', items: { type: 'string' } },
},
required: ['pack'],
},
},
{
name: 'codeql_resolve_packs',
description: 'List available CodeQL packs.',
inputSchema: {
type: 'object',
properties: {
extraArgs: { type: 'array', items: { type: 'string' } },
},
},
},
{
name: 'codeql_resolve_languages',
description: 'List available CodeQL languages.',
inputSchema: {
type: 'object',
properties: {
extraArgs: { type: 'array', items: { type: 'string' } },
},
},
},
{
name: 'codeql_sarif_summary',
description: 'Summarize a SARIF file by rule and total count.',
inputSchema: {
type: 'object',
properties: {
sarifPath: { type: 'string' },
maxPerRule: { type: 'number', default: 5 },
},
required: ['sarifPath'],
},
},
]
}
function runCodeql(args, options = {}) {
return new Promise((resolve, reject) => {
const proc = spawn('codeql', args, { stdio: ['ignore', 'pipe', 'pipe'], ...options })
let stdout = ''
let stderr = ''
proc.stdout.on('data', (chunk) => {
stdout += chunk.toString()
})
proc.stderr.on('data', (chunk) => {
stderr += chunk.toString()
})
proc.on('error', (err) => reject(err))
proc.on('close', (code) => {
resolve({ code, stdout, stderr, args })
})
})
}
function runCodeqlAnalyze(input) {
const {
dbPath,
queryPack,
output,
format = 'sarifv2.1.0',
rerun = false,
extraArgs = [],
} = input || {}
if (!dbPath || !queryPack) {
return Promise.reject(new Error('dbPath and queryPack are required'))
}
const args = ['database', 'analyze', dbPath, queryPack, `--format=${format}`]
if (output) args.push(`--output=${output}`)
if (rerun) args.push('--rerun')
if (Array.isArray(extraArgs)) args.push(...extraArgs)
return runCodeql(args)
}
function runCodeqlDatabaseCreate(input) {
const {
dbPath,
language = 'javascript',
sourceRoot = '.',
buildMode = 'none',
extraArgs = [],
} = input || {}
if (!dbPath) {
return Promise.reject(new Error('dbPath is required'))
}
const args = [
'database',
'create',
dbPath,
`--language=${language}`,
`--source-root=${sourceRoot}`,
`--build-mode`,
buildMode,
]
if (Array.isArray(extraArgs)) args.push(...extraArgs)
return runCodeql(args)
}
function runCodeqlPackInstall(input) {
const { packDir, extraArgs = [] } = input || {}
if (!packDir) return Promise.reject(new Error('packDir is required'))
const args = ['pack', 'install']
if (Array.isArray(extraArgs)) args.push(...extraArgs)
return runCodeql(args, { cwd: packDir })
}
function runCodeqlPackDownload(input) {
const { pack, extraArgs = [] } = input || {}
if (!pack) return Promise.reject(new Error('pack is required'))
const args = ['pack', 'download', pack]
if (Array.isArray(extraArgs)) args.push(...extraArgs)
return runCodeql(args)
}
function runCodeqlResolvePacks(input) {
const { extraArgs = [] } = input || {}
const args = ['resolve', 'packs']
if (Array.isArray(extraArgs)) args.push(...extraArgs)
return runCodeql(args)
}
function runCodeqlResolveLanguages(input) {
const { extraArgs = [] } = input || {}
const args = ['resolve', 'languages']
if (Array.isArray(extraArgs)) args.push(...extraArgs)
return runCodeql(args)
}
function summarizeSarif(input) {
const { sarifPath, maxPerRule = 5 } = input || {}
if (!sarifPath) return Promise.reject(new Error('sarifPath is required'))
const raw = fs.readFileSync(sarifPath, 'utf8')
const sarif = JSON.parse(raw)
const run = sarif.runs?.[0]
const results = run?.results || []
const byRule = new Map()
for (const r of results) {
const rule = r.ruleId || 'unknown'
if (!byRule.has(rule)) byRule.set(rule, [])
byRule.get(rule).push(r)
}
const summary = []
for (const [rule, items] of byRule) {
const sample = items.slice(0, maxPerRule).map((r) => {
const loc = r.locations?.[0]?.physicalLocation
return {
file: loc?.artifactLocation?.uri || 'unknown',
line: loc?.region?.startLine || 0,
message: r.message?.text || '',
}
})
summary.push({ ruleId: rule, count: items.length, sample })
}
summary.sort((a, b) => b.count - a.count)
return Promise.resolve({
total: results.length,
rules: summary,
})
}
async function handleRequest(message) {
const { id, method, params } = message
if (method === 'initialize') {
return respond(id, {
capabilities: { tools: { list: true, call: true } },
serverInfo: { name: 'codeql-mcp', version: '0.1.0' },
})
}
if (method === 'tools/list') {
return respond(id, { tools: toolList() })
}
if (method === 'tools/call') {
const { name, arguments: args } = params || {}
try {
let result
if (name === 'codeql_analyze') {
result = await runCodeqlAnalyze(args)
} else if (name === 'codeql_database_create') {
result = await runCodeqlDatabaseCreate(args)
} else if (name === 'codeql_pack_install') {
result = await runCodeqlPackInstall(args)
} else if (name === 'codeql_pack_download') {
result = await runCodeqlPackDownload(args)
} else if (name === 'codeql_resolve_packs') {
result = await runCodeqlResolvePacks(args)
} else if (name === 'codeql_resolve_languages') {
result = await runCodeqlResolveLanguages(args)
} else if (name === 'codeql_sarif_summary') {
result = await summarizeSarif(args)
} else {
return respondError(id, -32601, `Unknown tool: ${name}`)
}
return respond(id, {
content: [
{
type: 'text',
text: result.code !== undefined
? `exit: ${result.code}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`
: JSON.stringify(result, null, 2),
},
],
})
} catch (err) {
return respondError(id, -32603, err.message)
}
}
return respondError(id, -32601, `Unknown method: ${method}`)
}
let buffer = ''
process.stdin.setEncoding('utf8')
process.stdin.on('data', (chunk) => {
buffer += chunk
let idx
while ((idx = buffer.indexOf('\n')) >= 0) {
const line = buffer.slice(0, idx).trim()
buffer = buffer.slice(idx + 1)
if (!line) continue
try {
const message = JSON.parse(line)
handleRequest(message)
} catch (err) {
respondError(null, -32700, 'Invalid JSON')
}
}
})