はじめに
皆さんこんにちは。muguです。
この記事は…
- Node.js で WebSocket サーバーを自作してみたい方
- ファイル操作やチャットコマンドができるサーバーを体験したい方
- 「MCP (Model Context Protocol) って何?」という初学者の方
に向けて ダミーのサンプルプロジェクトを使い、
ゼロからセットアップ → 体験できる手順 をまとめています。
それでは早速学んでいきましょう!!
MCP とは何か?
MCPは、Anthropic 社が提唱・オープンソース化した「AI(主に LLM)と外部データソースやツールを標準化された方法で接続するためのプロトコル」です。
- 例えるなら「AI アプリのための USB-C」
→ どんな AI アプリでも、どんなデータソースやツールでも、MCP 準拠なら"つなぐだけ"で連携できる - 目的:
- LLM(Claude 等)が「社内データ」「クラウドサービス」「ローカルファイル」「各種ツール」などに安全・双方向でアクセスできるようにする
- これまでの"個別実装"による分断・非効率を解消し、AI エコシステムの標準化・拡張性・セキュリティを実現
目次
前提条件
ソフトウェア | バージョン目安 |
---|---|
Node.js | v14 以上(LTS 推奨) |
npm | Node.js に付属 |
ターミナル | Bash / PowerShell / CMD |
プロジェクト構成
sample-app/
├── package.json
├── .env # ※git 管理しない
├── workspace/ # ファイル操作対象
└── src/
├── main-server.js
├── main-client.js
└── file-utils.js
セットアップ手順
1. package.json と依存パッケージ
package.json
{
"name": "sample-app",
"version": "1.0.0",
"main": "src/main-server.js",
"scripts": {
"start": "node src/main-server.js"
},
"dependencies": {
"dotenv": "^16.0.3",
"express": "^4.18.2",
"node-fetch": "^2.6.9",
"ws": "^8.13.0"
}
}
npm install
2. .env の作成
.env
PORT=4000 # HTTP サーバー
WS_PORT=9000 # WebSocket サーバー
LLM_ENDPOINT=https://api.example.com/v1/generate-text
.env
は必ず.gitignore
へ。公開リポジトリにアクセストークンを晒さないよう注意してください。
3. workspace ディレクトリ
mkdir workspace
主要ソースコード
main-server.js
src/main-server.js
const path = require('path');
const express = require('express');
const WebSocket = require('ws');
const fetch = (...a) => import('node-fetch').then(({ default: f }) => f(...a));
const FileUtils = require('./file-utils');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 4000;
const wsPort = process.env.WS_PORT || 9000;
const files = new FileUtils();
/* =================================================
* LLM モック呼び出し
* ================================================= */
async function callLLM(prompt) {
// 本番では fetch(process.env.LLM_ENDPOINT, { ... }) で呼び出す
return `【AI 生成文】「${prompt}」に関する営業メール案です。\n– ここに本文 –`;
}
/* =================================================
* コマンドハンドラ
* ================================================= */
async function handleCommand({ content }) {
const [cmd, ...rest] = content.trim().split(/\s+/);
switch (cmd) {
case '/help':
return {
type: 'help',
content: `利用可能コマンド:\n/help - このヘルプを表示\n/ls - ファイル一覧\n/create <path> <text> - ファイル作成\n/read <path> - ファイル表示\n/edit <path> <text> - ファイル更新\n/delete <path> - ファイル削除\n/chat <prompt> - AI に依頼`,
timestamp: new Date()
};
case '/ls':
return await files.listFiles();
case '/create':
return await files.createFile(rest[0], rest.slice(1).join(' '));
case '/read':
return await files.readFile(rest[0]);
case '/edit':
return await files.editFile(rest[0], rest.slice(1).join(' '));
case '/delete':
return await files.deleteFile(rest[0]);
case '/chat':
return { type: 'ai', content: await callLLM(rest.join(' ')), timestamp: new Date() };
default:
return { type: 'error', content: `未知のコマンドです: ${cmd}`, timestamp: new Date() };
}
}
/* =================================================
* WebSocket サーバー
* ================================================= */
const wss = new WebSocket.Server({ port: wsPort });
const clients = new Set();
wss.on('connection', ws => {
clients.add(ws);
ws.send(JSON.stringify({
type: 'system',
content: 'MCP サーバーへようこそ!/help でコマンド一覧',
timestamp: new Date()
}));
ws.on('message', async msg => {
try {
const data = JSON.parse(msg);
const res = await handleCommand(data);
ws.send(JSON.stringify(res));
} catch (e) {
ws.send(JSON.stringify({ type: 'error', content: '解析エラー', timestamp: new Date() }));
}
});
ws.on('close', () => clients.delete(ws));
ws.on('error', () => clients.delete(ws));
});
/* =================================================
* HTTP サーバー
* ================================================= */
app.listen(port, () => {
console.log(`HTTP : http://localhost:${port}`);
console.log(`WebSocket : ws://localhost:${wsPort}`);
});
file-utils.js(ベースパス修正済み)
src/file-utils.js
const fs = require('fs').promises;
const path = require('path');
class FileUtils {
constructor(baseDir = path.resolve(__dirname, '../workspace')) {
this.baseDir = baseDir;
}
async createFile(rel, text) {
const full = path.join(this.baseDir, rel);
try {
await fs.mkdir(path.dirname(full), { recursive: true });
await fs.writeFile(full, text);
return { success: true, message: `ファイルを作成しました: ${rel}` };
} catch (e) {
return { success: false, message: `ファイル作成エラー: ${e.message}` };
}
}
async readFile(rel) {
const full = path.join(this.baseDir, rel);
try {
const content = await fs.readFile(full, 'utf8');
return { success: true, content };
} catch (e) {
return { success: false, message: `ファイル読み込みエラー: ${e.message}` };
}
}
async editFile(rel, text) {
const full = path.join(this.baseDir, rel);
try {
await fs.writeFile(full, text);
return { success: true, message: `ファイルを更新しました: ${rel}` };
} catch (e) {
return { success: false, message: `ファイル編集エラー: ${e.message}` };
}
}
async listFiles(rel = '') {
const full = path.join(this.baseDir, rel);
try {
const items = await fs.readdir(full, { withFileTypes: true });
return {
success: true,
files: items.map(i => ({ name: i.name, isDirectory: i.isDirectory(), path: path.join(rel, i.name) }))
};
} catch (e) {
return { success: false, message: `ファイル一覧取得エラー: ${e.message}` };
}
}
async deleteFile(rel) {
const full = path.join(this.baseDir, rel);
try {
await fs.unlink(full);
return { success: true, message: `ファイルを削除しました: ${rel}` };
} catch (e) {
return { success: false, message: `ファイル削除エラー: ${e.message}` };
}
}
}
module.exports = FileUtils;
main-client.js(変更なし)
src/main-client.js
const WebSocket = require('ws');
const readline = require('readline');
const ws = new WebSocket('ws://localhost:9000');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
ws.on('open', () => {
console.log('サーバーに接続しました');
console.log('メッセージを入力してください(終了するには "exit" と入力):');
rl.on('line', input => {
if (input.toLowerCase() === 'exit') {
ws.close();
rl.close();
return;
}
const msg = { type: 'message', content: input, timestamp: new Date().toISOString() };
ws.send(JSON.stringify(msg));
});
});
ws.on('message', data => {
try {
const res = JSON.parse(data);
console.log('\nサーバーからの応答:', res.content);
console.log('\nメッセージを入力してください:');
} catch (e) {
console.error('メッセージ解析エラー:', e);
}
});
ws.on('error', e => console.error('エラー:', e));
ws.on('close', () => {
console.log('接続が切断されました');
process.exit(0);
});
起動方法
# HTTP + WS サーバー
node src/main-server.js # cd は不要
# 別ターミナルでクライアント
node src/main-client.js
コマンド例
/help
/ls
/create test.txt これはテストファイルです
/read test.txt
/edit test.txt 内容を更新しました
/delete test.txt
/chat 株式会社サンプル 田中一郎 新規営業 ITサービス提案
よくあるエラーと対処法
症状 | 原因・対策 |
---|---|
Cannot find module | npm install 実行漏れ |
EADDRINUSE | ポート競合 → .env で PORT を変更 |
ファイル操作エラー | workspace/ が無い/権限不足 |
コマンド無反応 | サーバー・クライアントのポート不一致を確認 |
セキュリティと運用メモ
-
.env
を 絶対にリポジトリへコミットしない - WebSocket は本番環境で
wss://
(TLS 終端)を推奨 - 任意コマンド実行リスクがあるため 認証/認可 を実装する
- ロギングは winston などでファイル+コンソールへ二重出力すると保守が楽
まとめ
- MCP は "チャット = メソッド呼び出し" の発想でリアルタイム制御に強い
- Node.js + WebSocket で 約 100 行 の最小サーバーが作れる
- /help で自己完結する CLI 風コマンド体系が初心者学習に最適
- .env やポート管理で 環境依存を排除 するとチーム開発にも展開しやすい