0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCP自作入門

Last updated at Posted at 2025-04-30

はじめに

皆さんこんにちは。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 やポート管理で 環境依存を排除 するとチーム開発にも展開しやすい
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?