MCP Server
by mokemoke0821
Verified
/**
* MCPサーバー改善第3フェーズ
* 問題点:日本語文字化け、特殊文字処理、ファイル検索機能
*/
// ===== 1. PowerShell文字化け問題解決 =====
/**
* PowerShellプロセスを改善して起動する関数
* UTF-8エンコーディングを適切に設定
*/
function startPowerShellProcess(command, options = {}) {
const { spawn } = require('child_process');
// エンコーディング設定用のコマンドプレフィックス
const encodingPrefix = '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; ';
// BOMの自動検出と処理用の設定
const bomDetectionPrefix = '$OutputEncoding = New-Object -typename System.Text.UTF8Encoding -argumentlist $false; ';
// コマンドの前にエンコーディング設定を追加
const fullCommand = encodingPrefix + bomDetectionPrefix + command;
// PowerShellの起動パラメータ
const args = [
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-Command', fullCommand
];
// 環境変数設定
const env = {
...process.env,
PYTHONIOENCODING: 'utf-8',
LANG: 'ja_JP.UTF-8',
LC_ALL: 'ja_JP.UTF-8',
...options.env
};
// バッファサイズ設定(既に20MBに増量されている前提)
const maxBuffer = 20 * 1024 * 1024;
// PowerShellプロセスを起動
const ps = spawn('powershell.exe', args, {
env,
shell: true,
windowsHide: true,
maxBuffer,
...options,
stdio: ['pipe', 'pipe', 'pipe']
});
// 標準出力と標準エラー出力をUTF-8でデコード
ps.stdout.setEncoding('utf-8');
ps.stderr.setEncoding('utf-8');
return ps;
}
/**
* CMDプロセスを改善して起動する関数
*/
function startCmdProcess(command, options = {}) {
const { spawn } = require('child_process');
// エンコーディング設定用のコマンドプレフィックス
const encodingPrefix = 'chcp 65001 > nul && ';
// コマンドの前にエンコーディング設定を追加
const fullCommand = encodingPrefix + command;
// 環境変数設定
const env = {
...process.env,
PYTHONIOENCODING: 'utf-8',
LANG: 'ja_JP.UTF-8',
LC_ALL: 'ja_JP.UTF-8',
...options.env
};
// バッファサイズ設定
const maxBuffer = 20 * 1024 * 1024;
// CMDプロセスを起動
const cmd = spawn('cmd.exe', ['/c', fullCommand], {
env,
shell: true,
windowsHide: true,
maxBuffer,
...options,
stdio: ['pipe', 'pipe', 'pipe']
});
// 標準出力と標準エラー出力をUTF-8でデコード
cmd.stdout.setEncoding('utf-8');
cmd.stderr.setEncoding('utf-8');
return cmd;
}
/**
* GitBashプロセスを改善して起動する関数
*/
function startGitBashProcess(command, options = {}) {
const { spawn } = require('child_process');
const path = require('path');
// GitBashの実行ファイルパス
const gitBashPath = process.env.GIT_BASH_PATH || 'C:\\Program Files\\Git\\bin\\bash.exe';
// 環境変数設定
const env = {
...process.env,
PYTHONIOENCODING: 'utf-8',
LANG: 'ja_JP.UTF-8',
LC_ALL: 'ja_JP.UTF-8',
TERM: 'xterm-256color',
...options.env
};
// バッファサイズ設定
const maxBuffer = 20 * 1024 * 1024;
// GitBashプロセスを起動
const bash = spawn(gitBashPath, ['-c', command], {
env,
shell: true,
windowsHide: true,
maxBuffer,
...options,
stdio: ['pipe', 'pipe', 'pipe']
});
// 標準出力と標準エラー出力をUTF-8でデコード
bash.stdout.setEncoding('utf-8');
bash.stderr.setEncoding('utf-8');
return bash;
}
// ===== 2. 特殊文字を含むコマンドの実行サポート =====
/**
* 特殊文字を含むコマンドを安全に処理する関数
* パイプライン(|)、セミコロン(;)などの特殊文字を適切に処理
*/
function safeExecuteCommand(command, shellType, options = {}) {
// shellTypeに応じたプロセス起動関数を選択
let startProcess;
let processOptions = { ...options };
switch (shellType.toLowerCase()) {
case 'powershell':
startProcess = startPowerShellProcess;
break;
case 'cmd':
startProcess = startCmdProcess;
break;
case 'gitbash':
startProcess = startGitBashProcess;
break;
default:
throw new Error(`不明なシェルタイプ: ${shellType}`);
}
// コマンドをそのまま実行(特殊文字のエスケープはshellに任せる)
return startProcess(command, processOptions);
}
/**
* 特殊文字をエスケープする関数
* 各シェルタイプに応じたエスケープ処理を行う
*/
function escapeSpecialChars(command, shellType) {
if (!command) return command;
switch (shellType.toLowerCase()) {
case 'powershell':
// PowerShellは基本的にそのまま実行可能だが、一部の特殊ケースのみエスケープ
return command
.replace(/`/g, '``') // バッククォート
.replace(/(\$(?![\w{]))/g, '`$1'); // 変数名でないドル記号
case 'cmd':
// CMDでの特殊文字エスケープ(複雑なパイプライン処理を可能に)
// パイプやセミコロンは基本的にエスケープせず、処理を可能にする
return command;
case 'gitbash':
// GitBashでの処理(基本的にはそのまま実行可能)
return command;
default:
// 不明なシェルタイプの場合はそのまま返す
return command;
}
}
// ===== 3. ファイル検索機能の改善 =====
/**
* 改善されたファイル検索関数(searchGlob)
* 日本語ファイル名を適切に処理し、結果を正しく返す
*/
async function enhancedSearchGlob(basePath, pattern, options = {}) {
const fs = require('fs').promises;
const path = require('path');
const util = require('util');
const glob = util.promisify(require('glob'));
const micromatch = require('micromatch');
try {
// パスの正規化
const normalizedPath = path.normalize(basePath || '.');
// 絶対パスへの変換
const absolutePath = path.isAbsolute(normalizedPath)
? normalizedPath
: path.resolve(process.cwd(), normalizedPath);
// パスが存在するか確認
try {
await fs.access(absolutePath);
} catch (err) {
throw new Error(`指定されたパス "${absolutePath}" にアクセスできません: ${err.message}`);
}
// glob検索オプション
const globOptions = {
cwd: absolutePath,
nodir: false, // ディレクトリも含める
dot: true, // ドットファイルも含める
matchBase: true, // ベース名のみでマッチングを許可
windowsPathsNoEscape: true, // Windows パスでエスケープを無効化
absolute: true, // 絶対パスで結果を返す
...options
};
// globを使用してファイル検索を実行
let results = await glob(pattern, globOptions);
// 結果が空の場合はmicromatchを試す
if (results.length === 0) {
// ディレクトリ内の全ファイルを取得
const allFiles = await getAllFilesRecursive(absolutePath);
// micromatchを使用してフィルタリング
results = micromatch(allFiles, pattern, {
basename: true,
dot: true,
windows: process.platform === 'win32'
});
}
// 結果のファイルパスを正規化
return results.map(filePath => path.normalize(filePath));
} catch (err) {
console.error(`searchGlob エラー: ${err.message}`);
throw err;
}
}
/**
* 指定ディレクトリ内の全ファイルを再帰的に取得
*/
async function getAllFilesRecursive(dirPath) {
const fs = require('fs').promises;
const path = require('path');
try {
const entries = await fs.readdir(dirPath, { withFileTypes: true });
const files = await Promise.all(entries.map(async (entry) => {
const fullPath = path.join(dirPath, entry.name);
return entry.isDirectory()
? getAllFilesRecursive(fullPath)
: fullPath;
}));
return files.flat();
} catch (err) {
console.error(`getAllFilesRecursive エラー: ${err.message}`);
return [];
}
}
/**
* 改善されたディレクトリリスト関数(listFiles)
* 日本語パスやファイル名を適切に処理
*/
async function enhancedListFiles(dirPath) {
const fs = require('fs').promises;
const path = require('path');
try {
// パスの正規化
const normalizedPath = path.normalize(dirPath || '.');
// 絶対パスへの変換
const absolutePath = path.isAbsolute(normalizedPath)
? normalizedPath
: path.resolve(process.cwd(), normalizedPath);
// ディレクトリが存在するか確認
try {
const stats = await fs.stat(absolutePath);
if (!stats.isDirectory()) {
throw new Error(`"${absolutePath}" はディレクトリではありません。`);
}
} catch (err) {
throw new Error(`指定されたパス "${absolutePath}" にアクセスできません: ${err.message}`);
}
// ディレクトリ内のエントリを取得
const entries = await fs.readdir(absolutePath, { withFileTypes: true });
// ファイルとディレクトリの情報を構築
const result = {
directory: absolutePath,
files: [],
directories: []
};
// 各エントリの情報を取得
for (const entry of entries) {
const entryPath = path.join(absolutePath, entry.name);
const stats = await fs.stat(entryPath);
const info = {
name: entry.name,
path: entryPath,
size: stats.size,
modified: stats.mtime.toISOString()
};
if (entry.isDirectory()) {
result.directories.push(info);
} else {
result.files.push(info);
}
}
return result;
} catch (err) {
console.error(`listFiles エラー: ${err.message}`);
throw err;
}
}
// ===== 4. エラー処理とログ機能 =====
/**
* エラーメッセージを適切に処理して返す関数
* 日本語エラーメッセージが文字化けしないよう処理
*/
function formatErrorMessage(error, shellType) {
if (!error) return '';
let errorMessage = error.message || error.toString();
// シェルタイプに応じたエラーメッセージの処理
switch (shellType.toLowerCase()) {
case 'powershell':
// PowerShellのエラーメッセージからノイズを除去
errorMessage = errorMessage
.replace(/At line:(\d+) char:(\d+)\s*\+/g, '')
.replace(/\s+\+ CategoryInfo\s+:.+/gs, '')
.replace(/\s+\+ FullyQualifiedErrorId\s+:.+/gs, '')
.trim();
break;
case 'cmd':
// CMDのエラーメッセージを整形
errorMessage = errorMessage.trim();
break;
case 'gitbash':
// GitBashのエラーメッセージを整形
errorMessage = errorMessage.trim();
break;
}
return errorMessage;
}
/**
* 詳細なログ出力機能
*/
function enhancedLogger(level, message, data = null) {
const timestamp = new Date().toISOString();
const logPrefix = `[${timestamp}] [${level.toUpperCase()}]`;
let logMessage = `${logPrefix} ${message}`;
if (data) {
// データがオブジェクトの場合は整形して出力
if (typeof data === 'object') {
try {
logMessage += `\n${JSON.stringify(data, null, 2)}`;
} catch (err) {
logMessage += `\n[オブジェクトをJSON化できません: ${err.message}]`;
}
} else {
// それ以外の場合はそのまま出力
logMessage += ` ${data}`;
}
}
// ログレベルに応じた出力先を選択
switch (level.toLowerCase()) {
case 'error':
console.error(logMessage);
break;
case 'warn':
console.warn(logMessage);
break;
case 'info':
console.info(logMessage);
break;
case 'debug':
console.debug(logMessage);
break;
default:
console.log(logMessage);
}
return logMessage;
}
// ===== 5. 統合テスト機能 =====
/**
* 各機能のテストを実行する関数
*/
async function runIntegrationTests() {
const results = {
powershell: {
encoding: false,
specialChars: false,
japanese: false
},
cmd: {
encoding: false,
specialChars: false,
japanese: false
},
gitbash: {
encoding: false,
specialChars: false,
japanese: false
},
fileSystem: {
searchGlob: false,
listFiles: false,
japaneseFiles: false
}
};
try {
// PowerShellテスト
console.log("===== PowerShell テスト開始 =====");
// 日本語エンコーディングテスト
try {
const ps = startPowerShellProcess('Write-Output "こんにちは、世界!"');
let output = '';
ps.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise((resolve) => {
ps.on('close', (code) => {
results.powershell.encoding = output.includes('こんにちは、世界!');
console.log(`PowerShell エンコーディングテスト: ${results.powershell.encoding ? '成功' : '失敗'}`);
resolve();
});
});
} catch (err) {
console.error(`PowerShell エンコーディングテスト エラー: ${err.message}`);
}
// 特殊文字テスト
try {
const ps = startPowerShellProcess('Get-Process | Select-Object -First 5');
let output = '';
ps.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise((resolve) => {
ps.on('close', (code) => {
results.powershell.specialChars = output.length > 0 && code === 0;
console.log(`PowerShell 特殊文字テスト: ${results.powershell.specialChars ? '成功' : '失敗'}`);
resolve();
});
});
} catch (err) {
console.error(`PowerShell 特殊文字テスト エラー: ${err.message}`);
}
// CMD テスト
console.log("\n===== CMD テスト開始 =====");
// 日本語エンコーディングテスト
try {
const cmd = startCmdProcess('echo こんにちは、世界!');
let output = '';
cmd.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise((resolve) => {
cmd.on('close', (code) => {
results.cmd.encoding = output.includes('こんにちは、世界!');
console.log(`CMD エンコーディングテスト: ${results.cmd.encoding ? '成功' : '失敗'}`);
resolve();
});
});
} catch (err) {
console.error(`CMD エンコーディングテスト エラー: ${err.message}`);
}
// 特殊文字テスト
try {
const cmd = startCmdProcess('dir /b & echo "テスト"');
let output = '';
cmd.stdout.on('data', (data) => {
output += data.toString();
});
await new Promise((resolve) => {
cmd.on('close', (code) => {
results.cmd.specialChars = output.length > 0 && output.includes('テスト');
console.log(`CMD 特殊文字テスト: ${results.cmd.specialChars ? '成功' : '失敗'}`);
resolve();
});
});
} catch (err) {
console.error(`CMD 特殊文字テスト エラー: ${err.message}`);
}
// ファイル検索テスト
console.log("\n===== ファイル検索テスト開始 =====");
try {
const searchResult = await enhancedSearchGlob('.', '*.js');
results.fileSystem.searchGlob = searchResult.length >= 0; // 結果があるかどうかに関わらず、正常に実行されればOK
console.log(`searchGlob テスト: ${results.fileSystem.searchGlob ? '成功' : '失敗'}`);
} catch (err) {
console.error(`searchGlob テスト エラー: ${err.message}`);
}
try {
const listResult = await enhancedListFiles('.');
results.fileSystem.listFiles = listResult && listResult.directory;
console.log(`listFiles テスト: ${results.fileSystem.listFiles ? '成功' : '失敗'}`);
} catch (err) {
console.error(`listFiles テスト エラー: ${err.message}`);
}
// 最終結果の集計
const totalTests = Object.keys(results).reduce((total, category) => {
return total + Object.keys(results[category]).length;
}, 0);
const passedTests = Object.keys(results).reduce((total, category) => {
return total + Object.values(results[category]).filter(result => result).length;
}, 0);
console.log(`\n===== テスト結果概要 =====`);
console.log(`合計テスト数: ${totalTests}`);
console.log(`成功テスト数: ${passedTests}`);
console.log(`成功率: ${Math.round((passedTests / totalTests) * 100)}%`);
return results;
} catch (err) {
console.error(`統合テスト実行エラー: ${err.message}`);
throw err;
}
}
// ===== 6. メイン統合機能 =====
/**
* 改善されたMCPサーバーの初期化関数
* 各修正を適用するエントリーポイント
*/
function initEnhancedMCPServer() {
const originalExecuteCommand = global.executeCommand || null;
const originalSearchGlob = global.searchGlob || null;
const originalListFiles = global.listFiles || null;
// 既存の関数をバックアップしてから上書き
// 1. executeCommand 関数の拡張
global.executeCommand = async function(options) {
const { shell, command, workingDir } = options;
if (!shell || !command) {
throw new Error("シェルタイプとコマンドは必須です");
}
// 特殊文字を含むコマンドの安全な実行
try {
enhancedLogger('info', `コマンド実行開始`, { shell, command });
// 作業ディレクトリの設定
const processOptions = {};
if (workingDir) {
processOptions.cwd = workingDir;
}
// 適切なシェルでコマンドを実行
const process = safeExecuteCommand(command, shell, processOptions);
// 標準出力と標準エラー出力を収集
let stdout = '';
let stderr = '';
process.stdout.on('data', (data) => {
stdout += data.toString();
});
process.stderr.on('data', (data) => {
stderr += data.toString();
});
// プロセスの終了を待機
const exitCode = await new Promise((resolve) => {
process.on('close', (code) => {
resolve(code);
});
});
// 実行結果をログに記録
enhancedLogger('info', `コマンド実行完了`, { exitCode });
// 結果を返す
return {
stdout,
stderr: formatErrorMessage(stderr, shell),
exitCode
};
} catch (err) {
enhancedLogger('error', `コマンド実行エラー`, err);
return {
stdout: '',
stderr: formatErrorMessage(err, shell),
exitCode: 1
};
}
};
// 2. searchGlob 関数の拡張
global.searchGlob = async function(path, pattern) {
try {
enhancedLogger('info', `ファイル検索開始`, { path, pattern });
const results = await enhancedSearchGlob(path, pattern);
enhancedLogger('info', `ファイル検索完了`, { count: results.length });
return results;
} catch (err) {
enhancedLogger('error', `ファイル検索エラー`, err);
throw err;
}
};
// 3. listFiles 関数の拡張
global.listFiles = async function(path) {
try {
enhancedLogger('info', `ディレクトリリスト開始`, { path });
const results = await enhancedListFiles(path);
enhancedLogger('info', `ディレクトリリスト完了`, {
directory: results.directory,
filesCount: results.files.length,
directoriesCount: results.directories.length
});
return results;
} catch (err) {
enhancedLogger('error', `ディレクトリリストエラー`, err);
throw err;
}
};
// 初期化完了のログ
enhancedLogger('info', `拡張MCPサーバー初期化完了`);
// 自動テスト実行(オプション)
if (process.env.MCP_AUTO_TEST === 'true') {
runIntegrationTests()
.then(results => {
enhancedLogger('info', `自動テスト完了`, results);
})
.catch(err => {
enhancedLogger('error', `自動テストエラー`, err);
});
}
return {
originalExecuteCommand,
originalSearchGlob,
originalListFiles
};
}
// エクスポート
module.exports = {
startPowerShellProcess,
startCmdProcess,
startGitBashProcess,
safeExecuteCommand,
escapeSpecialChars,
enhancedSearchGlob,
enhancedListFiles,
formatErrorMessage,
enhancedLogger,
runIntegrationTests,
initEnhancedMCPServer
};