7
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?

AIエージェントに声と手足を与える 〜自作MCPサーバー実践ガイド〜

Last updated at Posted at 2025-12-18

この記事について

この記事は、MCPサーバーを自作してみたい方向けの実践ガイドです。

項目 内容
対象読者 MCPの概念は知っているが、自作したことがない方
前提知識 Node.js/TypeScriptの基本、ターミナル操作
動作確認環境 macOS Sequoia 15.x、Node.js 24.x

はじめに

2024年11月、AnthropicがMCP(Model Context Protocol)を発表しました。

MCPは、AIエージェントの能力を拡張するためのオープンな規格です。簡単に言うと、AIエージェントに「手足」を与える仕組み

ファイル操作、GitHub連携、ブラウザ操作など、公式のMCPサーバーも充実してきました。でも、「欲しい機能がない」「もうちょっとカスタマイズしたい」ということもありますよね。

この記事では、実際に自作したMCPサーバー3つを紹介します。読み終わる頃には、あなたも「欲しい機能がなければ作ればいい」と思えるようになっているはずです。

  • 🔊 音声通知 MCP(VoiceVox連携)
  • 🔔 Mac通知 MCP(デスクトップ通知)
  • 💬 Slack MCP(公式が消えたので自作)

「未来感あふれる音声通知 → カオスになって断念 → 結局シンプルが一番」という試行錯誤の過程も含めてお伝えします😊

MCPとは(サラッと)

AIエージェントの能力拡張

MCPは、AIエージェントが外部ツールと連携するための標準規格です。

AIエージェント側は「MCPプロトコルに対応したサーバーと話す」だけでOK。MCPサーバー側が、実際の外部サービスとの連携を担当します。

公式MCPサーバー

Anthropicや各社が公式のMCPサーバーを提供しています。私が普段使っているものの一部を紹介します。

汎用

  • Filesystem MCP - ファイル読み書き、ディレクトリ操作

開発系

AWS系

他にも多数のMCPサーバーが公開されています。一覧は公式リポジトリをチェック!

これらを組み合わせるだけでも、かなり強力なAIエージェントが作れます。

でも、欲しい機能がないこともある

公式サーバーは汎用的に作られているので、「ちょっと違う」「この機能が欲しい」ということも。

そんな時は、自分で作っちゃえばいいんです💪

自作MCPサーバー紹介

1. 音声通知 MCP 〜未来感と現実〜

背景:AIが喋ったら未来じゃない?

AIエージェントに長時間の処理を任せている時、「終わったら教えてほしい」と思いますよね。

最初に思いついたのが音声通知でした。

「AIが声で報告してくれる」って、めちゃくちゃ未来感ありませんか?

実装:VoiceVox連携

VoiceVoxは、無料で使える高品質な音声合成エンジンです。ローカルで動作するので、APIキーも不要。

前提条件: VoiceVoxがローカルで起動している必要があります。公式サイトからダウンロード・インストールしてください。

// voice-notification-mcp/index.js(抜粋)
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "speak") {
    const { text, speakerId = 1 } = request.params.arguments;

    // 1. VoiceVoxで音声クエリを作成
    const queryRes = await fetch(
      `${VOICEVOX_HOST}/audio_query?text=${encodeURIComponent(text)}&speaker=${speakerId}`,
      { method: "POST" }
    );
    const query = await queryRes.json();

    // 2. 音声合成
    const synthRes = await fetch(
      `${VOICEVOX_HOST}/synthesis?speaker=${speakerId}`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(query),
      }
    );

    // 3. 再生(macOS専用。Linuxはaplay、Windowsはpowershellなど要変更)
    const audioBuffer = await synthRes.arrayBuffer();
    await writeFile(wavPath, Buffer.from(audioBuffer));
    await execAsync(`afplay "${wavPath}"`);
  }
});

これで、AIエージェントが speak("作業が完了しました") と呼び出すと、VoiceVoxの声で読み上げてくれます。

注意: afplayコマンドはmacOS専用です。Linuxではaplaypaplay、Windowsではpowershell経由での再生に変更が必要です。

現実:並列実行でカオスに

最初は感動しました。「おお、AIが喋ってる!未来だ!」と。

でも、問題が発生したのはAIエージェントを並列実行した時でした。

複数のエージェントが同時に処理を完了すると…

エージェントA: 「作業が完了しま──」
エージェントB: 「──ビルドが成功し──」
エージェントC: 「──テストが──」

カオス。

音声が重なって何を言ってるか分からない。しかも、VoiceVoxの合成には少し時間がかかるので、通知が遅れることも。

結局、音声通知は「1つのエージェントだけ動かす時」限定になりました。

教訓

未来感はあったけど、実用性を考えると課題も。でも、デモや発表の場では最高にウケるので、持っておいて損はないです😊

設定例

{
  "mcpServers": {
    "voice-notification": {
      "command": "node",
      "args": ["/path/to/voice-notification-mcp/index.js"],
      "env": {
        "VOICEVOX_HOST": "http://localhost:50021",
        "VOICEVOX_SPEAKER_ID": "2"
      },
      "timeout": 60000
    }
  }
}
  • VOICEVOX_HOST: VoiceVoxのホスト(デフォルト: http://localhost:50021
  • VOICEVOX_SPEAKER_ID: 話者ID(2=四国めたん ノーマル、3=ずんだもん ノーマル など)

話者IDは以下のコマンドで確認できます。

curl -s http://localhost:50021/speakers | jq '.[] | {name: .name, styles: [.styles[] | {name: .name, id: .id}]}'
📄 voice-notification-mcp/index.js(全文)
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { exec } from "child_process";
import { promisify } from "util";
import { writeFile, unlink } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";

const execAsync = promisify(exec);

const VOICEVOX_HOST = process.env.VOICEVOX_HOST || "http://localhost:50021";
const DEFAULT_SPEAKER_ID = parseInt(process.env.VOICEVOX_SPEAKER_ID || "2", 10);

const server = new Server(
  {
    name: "voice-notification-mcp",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "speak",
      description: "Speak text using VoiceVox text-to-speech engine",
      inputSchema: {
        type: "object",
        properties: {
          text: {
            type: "string",
            description: "Text to speak",
          },
          speakerId: {
            type: "number",
            description: "VoiceVox speaker ID (default: 2 = 四国めたん ノーマル)",
          },
        },
        required: ["text"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "speak") {
    const { text, speakerId = DEFAULT_SPEAKER_ID } = request.params.arguments;
    const wavPath = join(tmpdir(), `voice-notification-${Date.now()}.wav`);

    try {
      // 1. Get audio query from VoiceVox
      const queryRes = await fetch(
        `${VOICEVOX_HOST}/audio_query?text=${encodeURIComponent(text)}&speaker=${speakerId}`,
        { method: "POST" }
      );
      if (!queryRes.ok) {
        throw new Error(`Failed to create audio query: ${queryRes.status}`);
      }
      const query = await queryRes.json();

      // 2. Synthesize audio
      const synthRes = await fetch(
        `${VOICEVOX_HOST}/synthesis?speaker=${speakerId}`,
        {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(query),
        }
      );
      if (!synthRes.ok) {
        throw new Error(`Failed to synthesize audio: ${synthRes.status}`);
      }
      const audioBuffer = await synthRes.arrayBuffer();

      // 3. Save to temp file and play
      await writeFile(wavPath, Buffer.from(audioBuffer));
      await execAsync(`afplay "${wavPath}"`);
      await unlink(wavPath);

      return {
        content: [{ type: "text", text: `Spoke: ${text}` }],
      };
    } catch (error) {
      return {
        content: [{ type: "text", text: `Failed to speak: ${error.message}` }],
        isError: true,
      };
    }
  }

  throw new Error(`Unknown tool: ${request.params.name}`);
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

2. Mac通知 MCP 〜結局シンプルが一番〜

背景:音声通知の反省を活かして

音声通知のカオスを経験して、「もっとシンプルな方法」を考えました。

答えはデスクトップ通知。macOSの通知センターに表示するだけ。

  • 並列実行しても通知が重なるだけで、カオスにならない
  • 音声合成の時間がないので、即座に通知される
  • 通知センターに履歴が残る

実装:2つのアプローチ

macOSで通知を送る方法は主に2つあります。

方法1: osascript(依存なし)

let script = `display notification "${message}" with title "${title}"`;
if (sound) {
  script += ` sound name "${sound}"`;
}
await execAsync(`osascript -e '${script}'`);
  • ✅ 外部ツール不要、macOS標準機能のみ
  • ❌ アイコンがスクリプトエディタになる

方法2: terminal-notifier(カスタム画像対応)

brew install terminal-notifier
const args = [
  "-title", `"${title}"`,
  "-message", `"${message}"`
];
if (sound) args.push("-sound", `"${sound}"`);
if (contentImage) args.push("-contentImage", `"${contentImage}"`);

await execAsync(`terminal-notifier ${args.join(" ")}`);
  • ✅ カスタム画像を指定できる
  • terminal-notifier のインストールが必要

macOS 11以降、-appIcon(通知の大きいアイコン)は無視される仕様になりました。代わりに -contentImage を使うと、通知の右側に小さく画像を表示できます。

私は -contentImage に Kiro のアイコンを指定しています。小さいけど、Kiroからの通知だと分かってかわいいので😊

設定例

{
  "mcpServers": {
    "notification": {
      "command": "node",
      "args": ["/path/to/mac-notification-mcp/index.js"],
      "timeout": 60000
    }
  }
}

環境変数は不要。terminal-notifier がインストールされていればOK。

📄 mac-notification-mcp/index.js(全文)
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

const server = new Server(
  {
    name: "mac-notification-mcp",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "send_notification",
      description: "Send notification to macOS Notification Center",
      inputSchema: {
        type: "object",
        properties: {
          title: {
            type: "string",
            description: "Notification title",
          },
          message: {
            type: "string",
            description: "Notification message",
          },
          sound: {
            type: "string",
            description: "Notification sound name",
            enum: ["Basso", "Blow", "Bottle", "Frog", "Funk", "Glass", "Hero", "Morse", "Ping", "Pop", "Purr", "Sosumi", "Submarine", "Tink"],
          },
          contentImage: {
            type: "string",
            description: "Path to content image (displayed on the right side)",
          },
        },
        required: ["title", "message"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "send_notification") {
    const { title, message, sound, contentImage } = request.params.arguments;
    
    const args = [
      "-title", `"${title.replace(/"/g, '\\"')}"`,
      "-message", `"${message.replace(/"/g, '\\"')}"`
    ];
    
    if (sound) {
      args.push("-sound", `"${sound}"`);
    }
    
    if (contentImage) {
      args.push("-contentImage", `"${contentImage}"`);
    }
    
    const cmd = `terminal-notifier ${args.join(" ")}`;
    
    try {
      await execAsync(cmd);
      return {
        content: [
          {
            type: "text",
            text: `Notification sent: ${title}`,
          },
        ],
      };
    } catch (error) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to send notification: ${error.message}`,
          },
        ],
        isError: true,
      };
    }
  }
  
  throw new Error(`Unknown tool: ${request.params.name}`);
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

main().catch((error) => {
  console.error("Server error:", error);
  process.exit(1);
});

活用例

AIエージェントの処理完了時に通知を送るよう、ルール(Steering)に書いておくと便利です。

# Steering設定例
**通知が必要なタイミング**
- 5分以上の処理完了時
- ファイル生成・更新完了時
- エラー発生でユーザー判断が必要な場合

教訓

シンプルが一番。派手さはないけど、実用性は抜群です。

3. Slack MCP 〜公式が消えたので自作〜

背景:公式Slack MCPが404に

MCPの公式リポジトリには、以前Slack MCPがありました。

…が、今アクセスすると404。消えてます。

「ないなら作るか」の精神で、自作しました。

実装:Slack Web API連携

Slack Web APIを使って、基本的な機能を実装しました。

// slack-mcp-server/src/index.ts(抜粋)
{
  name: "slack_get_channel_history",
  description: "Get recent messages from a channel",
  inputSchema: {
    type: "object",
    properties: {
      channel_id: { type: "string", description: "The ID of the channel" },
      limit: { type: "number", description: "Number of messages to retrieve" },
    },
    required: ["channel_id"],
  },
},
{
  name: "slack_post_message",
  description: "Post a new message to a Slack channel",
  inputSchema: {
    type: "object",
    properties: {
      channel_id: { type: "string", description: "The ID of the channel" },
      text: { type: "string", description: "The message text" },
    },
    required: ["channel_id", "text"],
  },
},

カスタマイズ例:ページネーション追加

使っているうちに、「検索結果が多い時にページネーションしたい」という要望が出てきました。

公式ライブラリなら「Issue立てて待つ」ですが、自作なら自分で直せる

// ページネーション対応を追加
{
  name: "slack_search_user_messages",
  inputSchema: {
    properties: {
      // ... 既存のパラメータ
      page: {
        type: "number",
        description: "Page number for pagination (default: 1)",
        default: 1,
      },
    },
  },
}

// API呼び出し時にpageパラメータを渡す
const result = await slackUser.search.messages({
  query,
  count: args.count || 20,
  page: args.page || 1,  // ← 追加
});

数分で実装完了。自作の強みですね。

設定例

{
  "mcpServers": {
    "slack-custom": {
      "command": "node",
      "args": ["/path/to/slack-mcp-server/dist/index.js"],
      "env": {
        "SLACK_BOT_TOKEN": "xoxb-xxxx-xxxx-xxxx",
        "SLACK_USER_TOKEN": "xoxp-xxxx-xxxx-xxxx",
        "SLACK_TEAM_ID": "T0XXXXXXX"
      },
      "timeout": 60000
    }
  }
}
  • SLACK_BOT_TOKEN: Bot User OAuth Token(必須)
  • SLACK_USER_TOKEN: User OAuth Token(検索機能を使う場合に必要)
  • SLACK_TEAM_ID: ワークスペースのチームID

TypeScriptで書いているので、npm run build でコンパイル後、dist/index.js を指定します。

📄 slack-mcp-server/src/index.ts(全文)
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { WebClient } from "@slack/web-api";

const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
const SLACK_USER_TOKEN = process.env.SLACK_USER_TOKEN;
const SLACK_TEAM_ID = process.env.SLACK_TEAM_ID;

if (!SLACK_BOT_TOKEN) {
  throw new Error("SLACK_BOT_TOKEN environment variable is required");
}

const slack = new WebClient(SLACK_BOT_TOKEN, { timeout: 60000 });
const slackUser = SLACK_USER_TOKEN
  ? new WebClient(SLACK_USER_TOKEN, { timeout: 60000 })
  : null;

const server = new Server(
  { name: "slack-mcp-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "slack_list_channels",
        description: "Get a list of all channels in the workspace",
        inputSchema: {
          type: "object",
          properties: {
            cursor: { type: "string", description: "Pagination cursor" },
            limit: { type: "number", description: "Max channels (default 100)", default: 100 },
          },
        },
      },
      {
        name: "slack_post_message",
        description: "Post a new message to a Slack channel",
        inputSchema: {
          type: "object",
          properties: {
            channel_id: { type: "string", description: "Channel ID" },
            text: { type: "string", description: "Message text" },
          },
          required: ["channel_id", "text"],
        },
      },
      {
        name: "slack_reply_to_thread",
        description: "Reply to a specific message thread",
        inputSchema: {
          type: "object",
          properties: {
            channel_id: { type: "string", description: "Channel ID" },
            thread_ts: { type: "string", description: "Parent message timestamp" },
            text: { type: "string", description: "Reply text" },
          },
          required: ["channel_id", "thread_ts", "text"],
        },
      },
      {
        name: "slack_add_reaction",
        description: "Add a reaction emoji to a message",
        inputSchema: {
          type: "object",
          properties: {
            channel_id: { type: "string", description: "Channel ID" },
            timestamp: { type: "string", description: "Message timestamp" },
            reaction: { type: "string", description: "Emoji name (without ::)" },
          },
          required: ["channel_id", "timestamp", "reaction"],
        },
      },
      {
        name: "slack_get_channel_history",
        description: "Get recent messages from a channel",
        inputSchema: {
          type: "object",
          properties: {
            channel_id: { type: "string", description: "Channel ID" },
            limit: { type: "number", description: "Number of messages (default 10)", default: 10 },
          },
          required: ["channel_id"],
        },
      },
      {
        name: "slack_get_thread_replies",
        description: "Get all replies in a message thread",
        inputSchema: {
          type: "object",
          properties: {
            channel_id: { type: "string", description: "Channel ID" },
            thread_ts: { type: "string", description: "Parent message timestamp" },
          },
          required: ["channel_id", "thread_ts"],
        },
      },
      {
        name: "slack_get_users",
        description: "Get a list of all users in the workspace",
        inputSchema: {
          type: "object",
          properties: {
            cursor: { type: "string", description: "Pagination cursor" },
            limit: { type: "number", description: "Max users (default 100)", default: 100 },
          },
        },
      },
      {
        name: "slack_get_user_profile",
        description: "Get detailed profile information for a user",
        inputSchema: {
          type: "object",
          properties: {
            user_id: { type: "string", description: "User ID" },
          },
          required: ["user_id"],
        },
      },
      {
        name: "slack_search_user_messages",
        description: "Search for messages from a specific user (requires User Token)",
        inputSchema: {
          type: "object",
          properties: {
            user_id: { type: "string", description: "User ID" },
            query: { type: "string", description: "Additional search query" },
            count: { type: "number", description: "Results count (default 20)", default: 20 },
            page: { type: "number", description: "Page number (default 1)", default: 1 },
          },
          required: ["user_id"],
        },
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;
    if (!args) throw new Error("Arguments are required");

    switch (name) {
      case "slack_list_channels": {
        const result = await slack.conversations.list({
          team_id: SLACK_TEAM_ID,
          cursor: args.cursor as string | undefined,
          limit: (args.limit as number) || 100,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_post_message": {
        const result = await slack.chat.postMessage({
          channel: args.channel_id as string,
          text: args.text as string,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_reply_to_thread": {
        const result = await slack.chat.postMessage({
          channel: args.channel_id as string,
          thread_ts: args.thread_ts as string,
          text: args.text as string,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_add_reaction": {
        const result = await slack.reactions.add({
          channel: args.channel_id as string,
          timestamp: args.timestamp as string,
          name: args.reaction as string,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_get_channel_history": {
        const result = await slack.conversations.history({
          channel: args.channel_id as string,
          limit: (args.limit as number) || 10,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_get_thread_replies": {
        const result = await slack.conversations.replies({
          channel: args.channel_id as string,
          ts: args.thread_ts as string,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_get_users": {
        const result = await slack.users.list({
          team_id: SLACK_TEAM_ID,
          cursor: args.cursor as string | undefined,
          limit: (args.limit as number) || 100,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_get_user_profile": {
        const result = await slack.users.profile.get({
          user: args.user_id as string,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      case "slack_search_user_messages": {
        if (!slackUser) {
          throw new Error("SLACK_USER_TOKEN is required for search");
        }
        const query = args.query
          ? `from:<@${args.user_id}> ${args.query}`
          : `from:<@${args.user_id}>`;
        const result = await slackUser.search.messages({
          query,
          count: (args.count as number) || 20,
          page: (args.page as number) || 1,
        });
        return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error: any) {
    return {
      content: [{ type: "text", text: JSON.stringify({ error: error.message }, null, 2) }],
      isError: true,
    };
  }
});

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Slack MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error:", error);
  process.exit(1);
});

ハマりポイント

1. botをチャンネルに招待し忘れる

Slack botは、招待されていないチャンネルのメッセージを取得できません。

Error: not_in_channel

このエラーが出たら、botをチャンネルに招待しましょう。

2. User TokenとBot Tokenの使い分け

  • Bot Token (xoxb-): チャンネルへの投稿、履歴取得
  • User Token (xoxp-): 検索(search.messagesはUser Tokenが必要)

検索機能を使うなら、両方のトークンが必要です。

自作MCPサーバーの作り方(簡潔に)

初期セットアップ

Node.js v18以上がインストールされている前提で進めます(@modelcontextprotocol/sdkの要件)。

mkdir my-mcp-server && cd my-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk

package.json"type": "module"を追加するのを忘れずに。

基本構造

MCPサーバーは、@modelcontextprotocol/sdkを使うと簡単に作れます。

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server(
  { name: "my-mcp-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// ツール一覧を返す
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "my_tool",
      description: "My custom tool",
      inputSchema: { /* JSON Schema */ },
    },
  ],
}));

// ツール実行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "my_tool") {
    // 処理
    return { content: [{ type: "text", text: "Done!" }] };
  }
});

// 起動
const transport = new StdioServerTransport();
await server.connect(transport);

設定ファイルへの追加

作ったMCPサーバーは、AIエージェントの設定ファイルに追加します。

{
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["/path/to/my-mcp-server/index.js"],
      "timeout": 60000
    }
  }
}

まとめ

MCPを使えば、AIエージェントの能力を自由に拡張できます。

  • 公式サーバーで基本的な機能はカバー
  • 欲しい機能がなければ自作できる
  • カスタマイズも自由自在

今回紹介した3つのMCPサーバーをまとめます。

サーバー 用途 学び
音声通知 MCP AIが喋る 未来感はあるが並列実行に弱い
Mac通知 MCP デスクトップ通知 シンプルが一番
Slack MCP Slack連携 公式がなければ作ればいい

「AIエージェントにこんな機能があったらいいな」と思ったら、ぜひ自作MCPサーバーに挑戦してみてください。

意外と簡単に作れますよ😊

次のステップ

  • 🔧 紹介したMCPサーバーを試してみる
  • 💡 自分だけのMCPサーバーを作ってみる
  • 📢 作ったMCPサーバーをGitHubで公開する

ぜひお試しあれ〜🙌

参考リンク

7
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
7
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?