この記事について
この記事は、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 - ファイル読み書き、ディレクトリ操作
開発系
- GitHub MCP - リポジトリ操作、PR作成、Issue管理
- Playwright MCP - ブラウザ自動化、スクリーンショット取得
- Figma MCP - デザインデータ取得、コード生成
- Chrome DevTools MCP - ブラウザ開発者ツール連携
AWS系
- AWS Documentation MCP - AWSドキュメント検索
- AWS Knowledge 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ではaplayやpaplay、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で公開する
ぜひお試しあれ〜🙌