レシピ#6-9: 実践的なMCP活用でBYOAアプリを進化させる
08章でMCPの基礎を学びました。この章では、07章で作った特化型機能にMCPを統合して、さらに強力なBYOAアプリを構築する方法を学びます。
実際のユースケースを通じて、MCPがどのように独自の価値を生み出すかを見ていきましょう。
MCPが特化型機能にもたらす価値
07章の特化型機能の限界
【育児アプリ】
✅ 記録からアルバム生成(07章で実装)
❌ でも、地域の保育園情報は手動で探す
❌ 予防接種スケジュールは別管理
【栽培アプリ】
✅ 栽培記録から販売ページ生成(07章で実装)
❌ でも、天気は自分で確認
❌ 市場価格は別途リサーチ
【家計簿アプリ】
✅ 投資分析機能(07章で実装)
❌ でも、株価は手動で入力
❌ 為替レートも手動更新
MCPによる拡張
【育児アプリ + MCP】
✅ 地域保育園情報を自動取得(Nursery MCP)
✅ 予防接種スケジュールを統合(Health MCP)
✅ 成長データを医療基準と比較
【栽培アプリ + MCP】
✅ リアルタイム気象データ(Weather MCP)
✅ 市場価格情報(Market MCP)
✅ 最適な出荷タイミングを提案
【家計簿アプリ + MCP】
✅ 株価・為替の自動更新(Finance MCP)
✅ ニュースと投資判断の統合(News MCP)
✅ ポートフォリオの最適化提案
ユースケース1: 育児アプリ × MCP
実装する機能
【基本機能】(06-07章で実装済み)
- 日々の成長記録
- アルバム自動生成
【MCP統合で追加】
✅ 地域の保育園情報検索
✅ 予防接種スケジュール管理
✅ 成長曲線との比較
✅ 地域イベント情報
カスタムMCP: 保育園情報サーバー
nursery-mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// 保育園データ型定義
interface Nursery {
id: string;
name: string;
address: string;
latitude: number;
longitude: number;
capacity: number;
ageRange: string;
phone: string;
website?: string;
openingHours: string;
features: string[];
}
// サンプルデータ(実際はDBやAPIから取得)
const nurseries: Nursery[] = [
{
id: '1',
name: 'さくら保育園',
address: '東京都渋谷区○○1-2-3',
latitude: 35.6586,
longitude: 139.7454,
capacity: 60,
ageRange: '0-5歳',
phone: '03-1234-5678',
openingHours: '7:30-18:30',
features: ['園庭あり', '給食あり', '延長保育'],
},
{
id: '2',
name: 'ひまわり保育園',
address: '東京都渋谷区△△2-3-4',
latitude: 35.6612,
longitude: 139.7435,
capacity: 40,
ageRange: '1-5歳',
phone: '03-2345-6789',
openingHours: '8:00-18:00',
features: ['少人数制', '英語教育', '食育'],
},
];
// 2点間の距離計算(Haversine公式)
function calculateDistance(
lat1: number,
lon1: number,
lat2: number,
lon2: number
): number {
const R = 6371; // 地球の半径(km)
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) *
Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
const server = new Server(
{
name: 'nursery-info',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_nurseries',
description: '現在地から指定距離内の保育園を検索します',
inputSchema: {
type: 'object',
properties: {
latitude: {
type: 'number',
description: '緯度',
},
longitude: {
type: 'number',
description: '経度',
},
radius: {
type: 'number',
description: '検索半径(km)',
default: 3,
},
ageRange: {
type: 'string',
description: '対象年齢(例: 0歳, 1-2歳)',
},
},
required: ['latitude', 'longitude'],
},
},
{
name: 'get_nursery_details',
description: '保育園の詳細情報を取得します',
inputSchema: {
type: 'object',
properties: {
nurseryId: {
type: 'string',
description: '保育園ID',
},
},
required: ['nurseryId'],
},
},
{
name: 'get_vaccination_schedule',
description: '予防接種スケジュールを取得します',
inputSchema: {
type: 'object',
properties: {
birthDate: {
type: 'string',
description: '生年月日(YYYY-MM-DD)',
},
completedVaccinations: {
type: 'array',
items: { type: 'string' },
description: '接種済みのワクチン',
},
},
required: ['birthDate'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'search_nurseries': {
const lat = args.latitude as number;
const lon = args.longitude as number;
const radius = (args.radius as number) || 3;
const nearbyNurseries = nurseries
.map((n) => ({
...n,
distance: calculateDistance(lat, lon, n.latitude, n.longitude),
}))
.filter((n) => n.distance <= radius)
.sort((a, b) => a.distance - b.distance);
const result = nearbyNurseries
.map(
(n) => `
📍 ${n.name}
住所: ${n.address}
距離: ${n.distance.toFixed(2)}km
定員: ${n.capacity}名
対象: ${n.ageRange}
特徴: ${n.features.join(', ')}
電話: ${n.phone}
`
)
.join('\n---\n');
return {
content: [
{
type: 'text',
text: result || '指定範囲内に保育園が見つかりませんでした',
},
],
};
}
case 'get_nursery_details': {
const nurseryId = args.nurseryId as string;
const nursery = nurseries.find((n) => n.id === nurseryId);
if (!nursery) {
throw new Error(`保育園が見つかりません: ${nurseryId}`);
}
const details = `
# ${nursery.name}
## 基本情報
- 住所: ${nursery.address}
- 電話: ${nursery.phone}
- 開園時間: ${nursery.openingHours}
${nursery.website ? `- Website: ${nursery.website}` : ''}
## 定員・対象年齢
- 定員: ${nursery.capacity}名
- 対象: ${nursery.ageRange}
## 特徴
${nursery.features.map((f) => `- ${f}`).join('\n')}
`.trim();
return {
content: [{ type: 'text', text: details }],
};
}
case 'get_vaccination_schedule': {
const birthDate = new Date(args.birthDate as string);
const completed = (args.completedVaccinations as string[]) || [];
// 予防接種スケジュール(日本の標準的なスケジュール)
const schedule = [
{
name: 'ヒブワクチン',
timing: '生後2ヶ月〜',
doses: 4,
interval: '4-8週間隔で3回、追加1回',
},
{
name: '小児用肺炎球菌',
timing: '生後2ヶ月〜',
doses: 4,
interval: '4週間隔で3回、追加1回',
},
{
name: 'B型肝炎',
timing: '生後2ヶ月〜',
doses: 3,
interval: '4週間隔で2回、追加1回',
},
{
name: 'ロタウイルス',
timing: '生後2ヶ月〜',
doses: 2,
interval: '4週間隔',
},
{
name: '四種混合(DPT-IPV)',
timing: '生後2ヶ月〜',
doses: 4,
interval: '3-8週間隔で3回、追加1回',
},
{
name: 'BCG',
timing: '生後5ヶ月〜8ヶ月未満',
doses: 1,
interval: '1回',
},
{
name: 'MR(麻しん風しん混合)',
timing: '1歳〜',
doses: 2,
interval: '1期、2期(年長)',
},
{
name: '水痘(水ぼうそう)',
timing: '1歳〜',
doses: 2,
interval: '3ヶ月以上あけて2回',
},
];
const now = new Date();
const ageInMonths =
(now.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30);
const upcoming = schedule
.filter((v) => !completed.includes(v.name))
.map((v) => {
let status = '今後接種';
if (v.timing.includes('2ヶ月') && ageInMonths >= 2) {
status = '接種可能';
} else if (v.timing.includes('5ヶ月') && ageInMonths >= 5) {
status = '接種可能';
} else if (v.timing.includes('1歳') && ageInMonths >= 12) {
status = '接種可能';
}
return `
${v.name}
接種時期: ${v.timing}
回数: ${v.doses}回(${v.interval})
状態: ${status}
`.trim();
})
.join('\n\n');
const result = `
# 予防接種スケジュール
現在の月齢: ${Math.floor(ageInMonths)}ヶ月
## 接種済み
${completed.length > 0 ? completed.map((v) => `- ${v}`).join('\n') : 'なし'}
## 今後の予定
${upcoming}
`.trim();
return {
content: [{ type: 'text', text: result }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Nursery Info MCP Server running');
}
main().catch(console.error);
育児アプリでのMCP活用
Claude Projectsでの使用例
【あなた】
現在地(緯度35.6586、経度139.7454)から
2km以内の保育園を探して
【Claude】
Nursery Info MCPを使って検索します...
📍 さくら保育園
住所: 東京都渋谷区○○1-2-3
距離: 0.00km
定員: 60名
対象: 0-5歳
特徴: 園庭あり, 給食あり, 延長保育
電話: 03-1234-5678
---
📍 ひまわり保育園
住所: 東京都渋谷区△△2-3-4
距離: 0.35km
定員: 40名
対象: 1-5歳
特徴: 少人数制, 英語教育, 食育
電話: 03-2345-6789
【あなた】
さくら保育園の詳細を教えて
【Claude】
[詳細情報を表示]
【あなた】
子供の生年月日は2024-06-15。
予防接種スケジュールを確認して
【Claude】
# 予防接種スケジュール
現在の月齢: 6ヶ月
## 今後の予定
ヒブワクチン
接種時期: 生後2ヶ月〜
回数: 4回(4-8週間隔で3回、追加1回)
状態: 接種可能
小児用肺炎球菌
接種時期: 生後2ヶ月〜
回数: 4回(4週間隔で3回、追加1回)
状態: 接種可能
BCG
接種時期: 生後5ヶ月〜8ヶ月未満
回数: 1回
状態: 接種可能
...
ユースケース2: 栽培アプリ × MCP
実装する機能
【基本機能】(06-07章で実装済み)
- 栽培記録
- 販売用Web生成
【MCP統合で追加】
✅ リアルタイム気象データ
✅ 栽培アドバイス(気象条件に基づく)
✅ 市場価格情報
✅ 最適な出荷タイミング提案
カスタムMCP: 農業支援サーバー
agriculture-mcp-server.py
import asyncio
from typing import Any
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
import httpx
from datetime import datetime, timedelta
app = Server("agriculture-support")
# 気象データ取得(Open-Meteo API)
async def get_weather_data(latitude: float, longitude: float):
async with httpx.AsyncClient() as client:
response = await client.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": latitude,
"longitude": longitude,
"daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,soil_moisture_0_to_10cm",
"timezone": "Asia/Tokyo",
"forecast_days": 7
}
)
return response.json()
# 栽培アドバイス生成
def generate_cultivation_advice(weather_data: dict, crop: str) -> str:
daily = weather_data["daily"]
today = daily["time"][0]
temp_max = daily["temperature_2m_max"][0]
temp_min = daily["temperature_2m_min"][0]
precip = daily["precipitation_sum"][0]
soil_moisture = daily["soil_moisture_0_to_10cm"][0]
advice = f"# {today}の栽培アドバイス\n\n"
advice += f"作物: {crop}\n\n"
# 気温に基づくアドバイス
if temp_max > 30:
advice += "⚠️ 高温注意\n"
advice += "- 朝夕の涼しい時間に水やり\n"
advice += "- 遮光ネットの使用を検討\n"
advice += "- 葉焼けに注意\n\n"
elif temp_min < 10:
advice += "⚠️ 低温注意\n"
advice += "- 霜対策が必要\n"
advice += "- 不織布やビニールトンネルで保温\n"
advice += "- 水やりは午前中に\n\n"
else:
advice += "✅ 栽培適温\n"
advice += "- 通常の管理で問題ありません\n\n"
# 降水量に基づくアドバイス
if precip > 10:
advice += "☔ 降雨予報\n"
advice += "- 水やり不要\n"
advice += "- 排水対策を確認\n"
advice += "- 病気予防(風通し確保)\n\n"
elif precip > 0:
advice += "🌦️ 小雨予報\n"
advice += "- 水やりは控えめに\n\n"
else:
advice += "☀️ 晴天\n"
advice += f"- 土壌水分: {soil_moisture:.1f}%\n"
if soil_moisture < 20:
advice += "- 水やりが必要です\n\n"
else:
advice += "- 水やりは様子を見て\n\n"
# 作業提案
advice += "## 本日の作業\n"
if precip == 0 and temp_max < 30:
advice += "- 除草作業に適しています\n"
advice += "- 追肥のタイミングです\n"
if temp_max > 25 and crop in ["トマト", "キュウリ"]:
advice += "- 収穫適期です(朝の涼しい時間に)\n"
return advice
# 市場価格情報(サンプル)
MARKET_PRICES = {
"トマト": {"price": 800, "trend": "上昇", "demand": "高"},
"キュウリ": {"price": 600, "trend": "安定", "demand": "中"},
"ナス": {"price": 700, "trend": "下降", "demand": "中"},
"ピーマン": {"price": 900, "trend": "上昇", "demand": "高"},
}
@app.list_tools()
async def list_tools() -> list[Tool]:
return [
Tool(
name="get_cultivation_advice",
description="気象データに基づいた栽培アドバイスを取得します",
inputSchema={
"type": "object",
"properties": {
"latitude": {"type": "number", "description": "緯度"},
"longitude": {"type": "number", "description": "経度"},
"crop": {"type": "string", "description": "作物名"}
},
"required": ["latitude", "longitude", "crop"]
}
),
Tool(
name="get_market_price",
description="作物の市場価格情報を取得します",
inputSchema={
"type": "object",
"properties": {
"crop": {"type": "string", "description": "作物名"}
},
"required": ["crop"]
}
),
Tool(
name="suggest_harvest_timing",
description="気象と市場価格から最適な収穫・出荷タイミングを提案します",
inputSchema={
"type": "object",
"properties": {
"latitude": {"type": "number", "description": "緯度"},
"longitude": {"type": "number", "description": "経度"},
"crop": {"type": "string", "description": "作物名"},
"estimated_harvest_date": {
"type": "string",
"description": "予想収穫日(YYYY-MM-DD)"
}
},
"required": ["latitude", "longitude", "crop", "estimated_harvest_date"]
}
)
]
@app.call_tool()
async def call_tool(name: str, arguments: Any) -> list[TextContent]:
if name == "get_cultivation_advice":
lat = arguments["latitude"]
lon = arguments["longitude"]
crop = arguments["crop"]
weather_data = await get_weather_data(lat, lon)
advice = generate_cultivation_advice(weather_data, crop)
return [TextContent(type="text", text=advice)]
elif name == "get_market_price":
crop = arguments["crop"]
price_info = MARKET_PRICES.get(crop, {
"price": 0,
"trend": "不明",
"demand": "不明"
})
result = f"""
# {crop}の市場価格情報
現在価格: ¥{price_info['price']}/kg
価格動向: {price_info['trend']}
需要: {price_info['demand']}
## 出荷アドバイス
"""
if price_info['trend'] == "上昇":
result += "- 価格上昇中です。出荷のタイミングとして良好。\n"
elif price_info['trend'] == "下降":
result += "- 価格下降中です。早めの出荷を検討してください。\n"
else:
result += "- 価格は安定しています。通常通りの出荷で問題ありません。\n"
return [TextContent(type="text", text=result.strip())]
elif name == "suggest_harvest_timing":
lat = arguments["latitude"]
lon = arguments["longitude"]
crop = arguments["crop"]
harvest_date = arguments["estimated_harvest_date"]
weather_data = await get_weather_data(lat, lon)
price_info = MARKET_PRICES.get(crop, {"price": 0, "trend": "不明"})
daily = weather_data["daily"]
result = f"# {crop}の収穫・出荷タイミング提案\n\n"
result += f"予定収穫日: {harvest_date}\n\n"
result += "## 今後7日間の天気\n"
for i in range(7):
date = daily["time"][i]
temp_max = daily["temperature_2m_max"][i]
precip = daily["precipitation_sum"][i]
result += f"{date}: 最高{temp_max}°C, 降水{precip}mm\n"
result += f"\n## 市場価格\n"
result += f"現在価格: ¥{price_info['price']}/kg\n"
result += f"動向: {price_info['trend']}\n\n"
result += "## 推奨アクション\n"
# 天気と価格から判断
rainy_days = sum(1 for p in daily["precipitation_sum"][:3] if p > 5)
if rainy_days > 1:
result += "⚠️ 今後3日間、雨が多い予報です。\n"
result += "- 収穫は雨の前に済ませましょう\n"
result += "- 品質低下を防ぐため早めの出荷を推奨\n"
else:
result += "✅ 天候は安定しています。\n"
result += "- 計画通りの収穫で問題ありません\n"
if price_info['trend'] == "上昇":
result += "\n💰 価格上昇中です。\n"
result += "- 可能であれば数日待つと良い価格で出荷できる可能性があります\n"
elif price_info['trend'] == "下降":
result += "\n📉 価格下降中です。\n"
result += "- 早めの出荷をお勧めします\n"
return [TextContent(type="text", text=result)]
else:
raise ValueError(f"Unknown tool: {name}")
async def main():
async with stdio_server() as (read_stream, write_stream):
await app.run(
read_stream,
write_stream,
app.create_initialization_options()
)
if __name__ == "__main__":
asyncio.run(main())
栽培アプリでのMCP活用
【あなた】
畑の位置(緯度35.6586、経度139.7454)で
トマトを育てているんだけど、今日の栽培アドバイスを教えて
【Claude】
Agriculture Support MCPを使って気象データを取得し、
栽培アドバイスを生成します...
# 2025-12-22の栽培アドバイス
作物: トマト
✅ 栽培適温
- 通常の管理で問題ありません
☀️ 晴天
- 土壌水分: 15.2%
- 水やりが必要です
## 本日の作業
- 除草作業に適しています
- 追肥のタイミングです
- 収穫適期です(朝の涼しい時間に)
【あなた】
トマトの市場価格も教えて
【Claude】
# トマトの市場価格情報
現在価格: ¥800/kg
価格動向: 上昇
需要: 高
## 出荷アドバイス
- 価格上昇中です。出荷のタイミングとして良好。
【あなた】
収穫予定は12月25日。最適な出荷タイミングを提案して
【Claude】
# トマトの収穫・出荷タイミング提案
予定収穫日: 2025-12-25
## 今後7日間の天気
2025-12-22: 最高16°C, 降水0mm
2025-12-23: 最高14°C, 降水0mm
2025-12-24: 最高15°C, 降水2mm
2025-12-25: 最高13°C, 降水8mm
2025-12-26: 最高12°C, 降水12mm
2025-12-27: 最高14°C, 降水0mm
2025-12-28: 最高15°C, 降水0mm
## 市場価格
現在価格: ¥800/kg
動向: 上昇
## 推奨アクション
⚠️ 今後3日間、雨が多い予報です。
- 収穫は雨の前に済ませましょう
- 品質低下を防ぐため早めの出荷を推奨
💰 価格上昇中です。
- 可能であれば数日待つと良い価格で出荷できる可能性があります
**提案: 12月24日(雨の前)に収穫し、12月27日(価格安定後)に出荷**
ユースケース3: 家計簿アプリ × MCP
実装する機能
【基本機能】(06-07章で実装済み)
- 収支記録
- 投資分析
【MCP統合で追加】
✅ 株価・為替の自動更新
✅ 経済ニュースの統合
✅ ポートフォリオ最適化提案
✅ リスク分析
カスタムMCP: 金融データサーバー
finance-mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
// 株価データ(実際はYahoo FinanceやAlpha Vantage APIを使用)
interface StockPrice {
symbol: string;
price: number;
change: number;
changePercent: number;
volume: number;
}
// サンプルデータ
const mockStockData: { [key: string]: StockPrice } = {
'AAPL': {
symbol: 'AAPL',
price: 195.50,
change: 2.30,
changePercent: 1.19,
volume: 50000000,
},
'GOOGL': {
symbol: 'GOOGL',
price: 142.80,
change: -0.50,
changePercent: -0.35,
volume: 25000000,
},
'MSFT': {
symbol: 'MSFT',
price: 378.90,
change: 5.20,
changePercent: 1.39,
volume: 35000000,
},
};
// 為替レート(実際はFX APIを使用)
const exchangeRates: { [key: string]: number } = {
'USD/JPY': 155.50,
'EUR/JPY': 168.20,
'GBP/JPY': 195.80,
};
const server = new Server(
{
name: 'finance-data',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'get_stock_price',
description: '株価情報を取得します',
inputSchema: {
type: 'object',
properties: {
symbol: {
type: 'string',
description: 'ティッカーシンボル(例: AAPL, GOOGL)',
},
},
required: ['symbol'],
},
},
{
name: 'get_exchange_rate',
description: '為替レートを取得します',
inputSchema: {
type: 'object',
properties: {
pair: {
type: 'string',
description: '通貨ペア(例: USD/JPY)',
},
},
required: ['pair'],
},
},
{
name: 'analyze_portfolio',
description: 'ポートフォリオを分析し、リスクとリターンを評価します',
inputSchema: {
type: 'object',
properties: {
holdings: {
type: 'array',
items: {
type: 'object',
properties: {
symbol: { type: 'string' },
shares: { type: 'number' },
avgCost: { type: 'number' },
},
},
description: '保有銘柄リスト',
},
},
required: ['holdings'],
},
},
{
name: 'suggest_rebalancing',
description: 'ポートフォリオのリバランス提案を生成します',
inputSchema: {
type: 'object',
properties: {
currentAllocation: {
type: 'object',
description: '現在の資産配分(%)',
},
targetAllocation: {
type: 'object',
description: '目標の資産配分(%)',
},
totalValue: {
type: 'number',
description: '総資産額',
},
},
required: ['currentAllocation', 'targetAllocation', 'totalValue'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'get_stock_price': {
const symbol = (args.symbol as string).toUpperCase();
const stock = mockStockData[symbol];
if (!stock) {
throw new Error(`株価データが見つかりません: ${symbol}`);
}
const result = `
# ${stock.symbol} 株価情報
現在値: $${stock.price.toFixed(2)}
変動: ${stock.change >= 0 ? '+' : ''}${stock.change.toFixed(2)} (${
stock.change >= 0 ? '+' : ''
}${stock.changePercent.toFixed(2)}%)
出来高: ${stock.volume.toLocaleString()}
${stock.changePercent > 0 ? '📈 上昇中' : '📉 下落中'}
`.trim();
return {
content: [{ type: 'text', text: result }],
};
}
case 'get_exchange_rate': {
const pair = args.pair as string;
const rate = exchangeRates[pair];
if (!rate) {
throw new Error(`為替レートが見つかりません: ${pair}`);
}
const result = `
# ${pair} 為替レート
現在レート: ¥${rate.toFixed(2)}
【活用例】
$100 = ¥${(100 * rate).toFixed(0)}
$1,000 = ¥${(1000 * rate).toFixed(0)}
`.trim();
return {
content: [{ type: 'text', text: result }],
};
}
case 'analyze_portfolio': {
const holdings = args.holdings as Array<{
symbol: string;
shares: number;
avgCost: number;
}>;
let totalValue = 0;
let totalCost = 0;
const analysis: string[] = [];
for (const holding of holdings) {
const stock = mockStockData[holding.symbol];
if (!stock) continue;
const currentValue = stock.price * holding.shares;
const cost = holding.avgCost * holding.shares;
const gain = currentValue - cost;
const gainPercent = (gain / cost) * 100;
totalValue += currentValue;
totalCost += cost;
analysis.push(`
${holding.symbol}
保有数: ${holding.shares}株
平均取得単価: $${holding.avgCost.toFixed(2)}
現在価格: $${stock.price.toFixed(2)}
評価額: $${currentValue.toFixed(2)}
損益: ${gain >= 0 ? '+' : ''}$${gain.toFixed(2)} (${
gain >= 0 ? '+' : ''
}${gainPercent.toFixed(2)}%)
`.trim());
}
const totalGain = totalValue - totalCost;
const totalGainPercent = (totalGain / totalCost) * 100;
const result = `
# ポートフォリオ分析
## 保有銘柄
${analysis.join('\n\n')}
## 全体サマリー
総評価額: $${totalValue.toFixed(2)}
総投資額: $${totalCost.toFixed(2)}
総損益: ${totalGain >= 0 ? '+' : ''}$${totalGain.toFixed(2)} (${
totalGain >= 0 ? '+' : ''
}${totalGainPercent.toFixed(2)}%)
${totalGain > 0 ? '✅ 利益が出ています' : '⚠️ 含み損があります'}
`.trim();
return {
content: [{ type: 'text', text: result }],
};
}
case 'suggest_rebalancing': {
const current = args.currentAllocation as { [key: string]: number };
const target = args.targetAllocation as { [key: string]: number };
const total = args.totalValue as number;
const suggestions: string[] = [];
for (const asset in target) {
const currentPercent = current[asset] || 0;
const targetPercent = target[asset];
const diff = targetPercent - currentPercent;
if (Math.abs(diff) > 5) {
// 5%以上のずれ
const amount = (total * diff) / 100;
if (diff > 0) {
suggestions.push(
`${asset}: ${amount.toFixed(0)}円 買い増し(${currentPercent.toFixed(
1
)}% → ${targetPercent.toFixed(1)}%)`
);
} else {
suggestions.push(
`${asset}: ${Math.abs(amount).toFixed(
0
)}円 売却(${currentPercent.toFixed(1)}% → ${targetPercent.toFixed(
1
)}%)`
);
}
}
}
const result = `
# リバランス提案
総資産: ¥${total.toLocaleString()}
## 現在の配分
${Object.entries(current)
.map(([k, v]) => `- ${k}: ${v.toFixed(1)}%`)
.join('\n')}
## 目標配分
${Object.entries(target)
.map(([k, v]) => `- ${k}: ${v.toFixed(1)}%`)
.join('\n')}
## 推奨アクション
${suggestions.length > 0 ? suggestions.join('\n') : '現在の配分は目標に近い状態です。'}
## 注意事項
- リバランスは手数料を考慮してください
- 一度に全て調整する必要はありません
- 市場環境を見ながら段階的に実施しましょう
`.trim();
return {
content: [{ type: 'text', text: result }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Finance Data MCP Server running');
}
main().catch(console.error);
家計簿アプリでのMCP活用
【あなた】
AAPLの現在の株価を教えて
【Claude】
Finance Data MCPで株価を取得します...
# AAPL 株価情報
現在値: $195.50
変動: +$2.30 (+1.19%)
出来高: 50,000,000
📈 上昇中
【あなた】
ポートフォリオを分析して。
AAPL 10株(平均$180)、GOOGL 5株(平均$145)持ってる
【Claude】
# ポートフォリオ分析
## 保有銘柄
AAPL
保有数: 10株
平均取得単価: $180.00
現在価格: $195.50
評価額: $1,955.00
損益: +$155.00 (+8.61%)
GOOGL
保有数: 5株
平均取得単価: $145.00
現在価格: $142.80
評価額: $714.00
損益: -$11.00 (-1.52%)
## 全体サマリー
総評価額: $2,669.00
総投資額: $2,525.00
総損益: +$144.00 (+5.70%)
✅ 利益が出ています
【あなた】
目標配分は株式60%、債券30%、現金10%。
現在は株式75%、債券20%、現金5%で、総資産300万円。
リバランス提案して
【Claude】
# リバランス提案
総資産: ¥3,000,000
## 現在の配分
- 株式: 75.0%
- 債券: 20.0%
- 現金: 5.0%
## 目標配分
- 株式: 60.0%
- 債券: 30.0%
- 現金: 10.0%
## 推奨アクション
株式: 450,000円 売却(75.0% → 60.0%)
債券: 300,000円 買い増し(20.0% → 30.0%)
現金: 150,000円 買い増し(5.0% → 10.0%)
## 注意事項
- リバランスは手数料を考慮してください
- 一度に全て調整する必要はありません
- 市場環境を見ながら段階的に実施しましょう
ユースケース4: 読書アプリ × MCP
実装する機能
【基本機能】(06-07章で実装済み)
- 読書記録
- 書評ブログ自動生成
【MCP統合で追加】
✅ 書籍情報API連携(Google Books等)
✅ 書籍レビューの収集
✅ おすすめ本の提案
✅ 読書傾向の分析
カスタムMCP: 書籍情報サーバー
book-info-mcp-server.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
const server = new Server(
{
name: 'book-info',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: 'search_books',
description: '書籍を検索します(Google Books API使用)',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: '検索クエリ(タイトル、著者、ISBN等)',
},
maxResults: {
type: 'number',
description: '最大結果数',
default: 5,
},
},
required: ['query'],
},
},
{
name: 'get_book_details',
description: '書籍の詳細情報を取得します',
inputSchema: {
type: 'object',
properties: {
bookId: {
type: 'string',
description: 'Google Books ID',
},
},
required: ['bookId'],
},
},
{
name: 'recommend_books',
description: '読書履歴に基づいておすすめの本を提案します',
inputSchema: {
type: 'object',
properties: {
readBooks: {
type: 'array',
items: { type: 'string' },
description: '既読書籍のタイトルリスト',
},
genres: {
type: 'array',
items: { type: 'string' },
description: '好きなジャンル',
},
},
required: ['readBooks'],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case 'search_books': {
const query = args.query as string;
const maxResults = (args.maxResults as number) || 5;
// Google Books API呼び出し(実際の実装)
const response = await fetch(
`https://www.googleapis.com/books/v1/volumes?q=${encodeURIComponent(
query
)}&maxResults=${maxResults}`
);
const data = await response.json();
if (!data.items || data.items.length === 0) {
return {
content: [
{ type: 'text', text: '検索結果が見つかりませんでした' },
],
};
}
const results = data.items
.map((item: any, index: number) => {
const info = item.volumeInfo;
return `
${index + 1}. ${info.title}
著者: ${info.authors?.join(', ') || '不明'}
出版社: ${info.publisher || '不明'}
出版日: ${info.publishedDate || '不明'}
${info.description ? `概要: ${info.description.substring(0, 100)}...` : ''}
ID: ${item.id}
`.trim();
})
.join('\n\n');
return {
content: [{ type: 'text', text: results }],
};
}
case 'get_book_details': {
const bookId = args.bookId as string;
const response = await fetch(
`https://www.googleapis.com/books/v1/volumes/${bookId}`
);
const data = await response.json();
const info = data.volumeInfo;
const details = `
# ${info.title}
## 基本情報
- 著者: ${info.authors?.join(', ') || '不明'}
- 出版社: ${info.publisher || '不明'}
- 出版日: ${info.publishedDate || '不明'}
- ページ数: ${info.pageCount || '不明'}
- ISBN: ${info.industryIdentifiers?.find((id: any) => id.type === 'ISBN_13')?.identifier || '不明'}
## カテゴリ
${info.categories?.join(', ') || 'なし'}
## 概要
${info.description || '概要なし'}
## リンク
${info.infoLink || 'なし'}
`.trim();
return {
content: [{ type: 'text', text: details }],
};
}
case 'recommend_books': {
const readBooks = args.readBooks as string[];
const genres = (args.genres as string[]) || [];
// 簡易的なレコメンデーション(実際はML等を使用)
const recommendations = `
# おすすめ本の提案
## あなたの読書傾向
既読書籍: ${readBooks.length}冊
${genres.length > 0 ? `好きなジャンル: ${genres.join(', ')}` : ''}
## おすすめ理由
既読書籍の傾向から、以下のジャンルがお好きなようです:
- 技術書 / ビジネス書
## おすすめ書籍
1. "Clean Code" - Robert C. Martin
理由: コード品質に関心がある方におすすめ
2. "The Pragmatic Programmer" - Andrew Hunt, David Thomas
理由: 実践的なプログラミング哲学
3. "Domain-Driven Design" - Eric Evans
理由: ソフトウェア設計の深い理解
※ これらの本を検索して詳細を確認できます
`.trim();
return {
content: [{ type: 'text', text: recommendations }],
};
}
default:
throw new Error(`Unknown tool: ${name}`);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('Book Info MCP Server running');
}
main().catch(console.error);
複数MCPの組み合わせパターン
パターン1: データ連携
【育児アプリ + 複数MCP】
Nursery MCP
↓
保育園情報取得
↓
Weather MCP
↓
今日の天気確認
↓
統合アドバイス:
「今日は雨なので、保育園までの移動は
レインコートを忘れずに。
さくら保育園(徒歩5分)が最寄りです。」
パターン2: ワークフロー自動化
【栽培アプリ + 複数MCP】
Agriculture MCP → 栽培アドバイス取得
↓
Weather MCP → 今後の天気確認
↓
Market MCP → 市場価格取得
↓
Task Manager MCP → 作業タスク自動生成
↓
「今日の作業リスト」が自動で完成
パターン3: 意思決定支援
【投資判断 + 複数MCP】
Finance MCP → 株価・為替取得
↓
News MCP → 経済ニュース取得
↓
Analysis MCP → リスク分析
↓
総合判断:
「現在の市場環境では、リバランスは
1ヶ月後が適切です。理由は...」
MCPベストプラクティス(実践編)
1. エラーハンドリング
// 外部API呼び出し時の堅牢なエラーハンドリング
async function fetchWithRetry(url: string, retries = 3): Promise<any> {
for (let i = 0; i < retries; i++) {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error(`Attempt ${i + 1} failed:`, error);
if (i === retries - 1) {
throw new Error(
`Failed after ${retries} attempts: ${(error as Error).message}`
);
}
// 指数バックオフ
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
2. キャッシング
// データキャッシュで API 呼び出しを削減
class DataCache {
private cache: Map<string, { data: any; expiry: number }> = new Map();
set(key: string, data: any, ttlMs: number = 300000) {
// デフォルト5分
this.cache.set(key, {
data,
expiry: Date.now() + ttlMs,
});
}
get(key: string): any | null {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() > item.expiry) {
this.cache.delete(key);
return null;
}
return item.data;
}
}
const cache = new DataCache();
// 使用例
async function getStockPrice(symbol: string) {
const cached = cache.get(`stock:${symbol}`);
if (cached) {
console.error('Cache hit:', symbol);
return cached;
}
const data = await fetchStockData(symbol);
cache.set(`stock:${symbol}`, data, 60000); // 1分キャッシュ
return data;
}
3. レート制限
// APIレート制限の遵守
class RateLimiter {
private requests: number[] = [];
private limit: number;
private windowMs: number;
constructor(limit: number, windowMs: number) {
this.limit = limit;
this.windowMs = windowMs;
}
async acquire(): Promise<void> {
const now = Date.now();
this.requests = this.requests.filter((time) => now - time < this.windowMs);
if (this.requests.length >= this.limit) {
const oldestRequest = this.requests[0];
const waitTime = this.windowMs - (now - oldestRequest);
console.error(`Rate limit reached, waiting ${waitTime}ms`);
await new Promise((resolve) => setTimeout(resolve, waitTime));
return this.acquire();
}
this.requests.push(now);
}
}
// 使用例: 1分間に10リクエストまで
const limiter = new RateLimiter(10, 60000);
async function apiCall() {
await limiter.acquire();
// API呼び出し
}
まとめ
BYOAアプリの進化の全体像
【06章】個人用アプリ作成
- チャットのみ
- LocalStorage
- 基本機能
↓
【07章】コラボで進化
- 特化型機能追加
- データ活用
- 独自の価値
↓
【08章】MCP基礎
- MCPの概念
- 既存MCPサーバー活用
- カスタムMCP作成
↓
【09章】MCP実践(この章)
✅ 育児アプリ × 保育園情報・予防接種MCP
✅ 栽培アプリ × 気象・市場価格MCP
✅ 家計簿アプリ × 金融データMCP
✅ 読書アプリ × 書籍情報MCP
✅ 複数MCPの組み合わせ
✅ 実践的なベストプラクティス
↓
【05章 / Chef's Specials】本格開発へ
- Cursor + Claude
- Flutter
- CI/CD
MCPが実現する価値
【データ連携】
外部システムとシームレスに統合
→ 手動入力の削減
→ リアルタイムな情報
【専門性の追加】
ドメイン固有の知識を統合
→ 保育園情報
→ 気象データ
→ 市場価格
→ 金融データ
【意思決定支援】
複数のデータソースを統合
→ 最適なタイミング提案
→ リスク分析
→ 総合的な判断
【自動化】
ワークフローを自動化
→ 作業リスト自動生成
→ アラート通知
→ レポート作成
次のステップ
今すぐできること:
-
既存MCPサーバーを試す - Filesystem MCP - GitHub MCP
-
簡単なカスタムMCPを作る - 自分のデータソース - 既存APIの統合
-
07章の特化型機能にMCPを統合 - 育児アプリ + 地域情報 - 栽培アプリ + 気象データ - 家計簿 + 金融データ
-
複数MCPを組み合わせる - ワークフロー自動化 - 意思決定支援
本格開発への準備が整ったら:
📘 05章(Chef's Specials)へ - Cursor + Claude で効率開発 - Flutter でマルチプラットフォーム - MCP を組み込んだ本格アプリ - コミュニティ駆動開発
MCPは、あなたのBYOAアプリを外の世界と繋ぎ、 専門的な価値を生み出す強力なツールです。
一歩ずつ統合を進めて、唯一無二のアプリを作りましょう!