取り組みの背景:MCPがモバイルアプリ開発の常識を変える
2024年末にAnthropicが発表した**MCP(Model Context Protocol)**は、AIモデルと外部ツール・データソースを接続するための標準プロトコルです。2026年現在、デスクトップアプリやサーバーサイドでのMCP活用は急速に普及しましたが、モバイルアプリへの統合はまだ多くの開発者にとって未踏の領域です。
RorkのようなAIファーストなモバイルアプリ開発環境でMCPを活用できれば、以下のような強力な機能が実現します。
- 動的ツール統合: データベース操作・Web検索・コード実行などをAIが自律的に呼び出す
- コンテキスト拡張: RAGを超えた、リアルタイムデータへのアクセス
- マルチプロバイダー対応: Claude・Gemini・GPT-4oを同一インターフェースで切り替える
- カスタムツールの追加: 自社APIをMCPサーバーとして公開し、AIに呼び出させる
ここではRorkアプリにMCPを統合するための完全な実装ガイドを提供します。アーキテクチャ設計から実装コード、プロダクション運用まで、すぐに活用できる知識を体系的にまとめましました。
対象読者はRorkでのアプリ開発経験があり、Rork × Claude API で作る AI アシスタントアプリやRork × AI Function Calling で構築するインテリジェントアシスタントアプリを読んだ方、あるいはそれと同等の知識をお持ちの上級開発者を想定しています。
MCPとは何か:アーキテクチャと基本概念
プロトコルの全体像
MCPは以下の3つの役割で構成されます。
- MCPホスト(Host): AIモデルを呼び出すアプリケーション。今回はRorkアプリがこの役割を担う
- MCPクライアント(Client): ホスト内でMCPサーバーとの通信を管理するライブラリ
- MCPサーバー(Server): ツール(tools)・リソース(resources)・プロンプト(prompts)を提供するサービス
[Rorkアプリ(MCPホスト)]
↓ MCPクライアント
[MCPサーバー群]
├── Brave Search MCP Server
├── Supabase MCP Server
├── GitHub MCP Server
└── カスタムAPIサーバー
トランスポート層
MCPのトランスポートには主に2種類あります。
- stdio: プロセス間通信(デスクトップ/サーバー向け)
- SSE(Server-Sent Events): HTTP経由のイベントストリーム(モバイルアプリに最適)
- Streamable HTTP: 2025年に追加された新プロトコル(サーバー側で推奨)
モバイルアプリではネットワーク経由での通信が前提となるため、SSEまたはStreamable HTTPを使うのが基本です。
ツール定義の仕組み
MCPサーバーは tools/list エンドポイントでツール一覧をJSON Schemaで提供します。クライアントはこれをAIモデルに渡し、モデルが必要に応じて tools/call を呼び出す流れです。
{
"name": "search_web",
"description": "Webを検索して最新情報を取得する",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "検索クエリ" },
"maxResults": { "type": "number", "default": 5 }
},
"required": ["query"]
}
}前提知識・環境準備
必要な環境
- Rork Max(カスタムコード編集・ネイティブモジュール対応が必要)
- Node.js 20以上(MCPサーバーのローカルテスト用)
- Anthropic APIキー(Claude claude-3-5-sonnetまたはclaude-opus-4以上を推奨)
使用するパッケージ
Rorkアプリ側(React Native)に以下を追加します。
# MCPクライアントSDK(React Native対応フォーク)
npm install @modelcontextprotocol/sdk
# HTTPクライアント(fetch APIのストリーミング対応補完)
npm install eventsource
# Anthropic SDK
npm install @anthropic-ai/sdkMCPサーバーのセットアップ(バックエンド)
npm create cloudflare@latest my-mcp-server -- --template worker
cd my-mcp-server
npm install @modelcontextprotocol/sdkMCPサーバーの実装(Cloudflare Workers)
基本的なMCPサーバー構造
// src/index.ts(Cloudflare Workers MCPサーバー)
import { McpAgent } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
export class MyMCPServer extends McpAgent {
server = new McpServer({
name: "my-mobile-mcp-server",
version: "1.0.0",
});
async init() {
// ツール1: データベース検索
this.server.tool(
"search_products",
"商品データベースを検索する",
{
query: z.string().describe("検索キーワード"),
category: z.string().optional().describe("カテゴリ絞り込み"),
limit: z.number().default(10).describe("最大取得件数"),
},
async ({ query, category, limit }) => {
// Supabaseなどのデータベースから検索
const results = await searchProductsFromDB(query, category, limit);
return {
content: [
{
type: "text",
text: JSON.stringify(results, null, 2),
},
],
};
}
);
// ツール2: 在庫確認
this.server.tool(
"check_stock",
"商品の在庫数を確認する",
{
productId: z.string().describe("商品ID"),
},
async ({ productId }) => {
const stock = await getStockFromDB(productId);
return {
content: [
{
type: "text",
text: `商品ID ${productId} の在庫: ${stock}個`,
},
],
};
}
);
}
}
// Streamable HTTP トランスポートでエクスポート
export default new OAuthProvider({
apiHandlers: {
"/mcp": MyMCPServer.mount("/mcp"),
},
// 認証設定(後述)
});Cloudflare Workersへのデプロイ
# wrangler.toml の設定
# [[routes]]
# pattern = "mcp.yourdomain.com/*"
npx wrangler deploy
# → https://mcp.yourdomain.com にデプロイされるRorkアプリ側の実装
Step 1: MCPクライアントの初期化
// lib/mcp-client.ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
export interface MCPClientConfig {
serverUrl: string;
apiKey?: string;
timeout?: number;
}
export class MobileMCPClient {
private client: Client;
private transport: SSEClientTransport;
private isConnected: boolean = false;
constructor(private config: MCPClientConfig) {
this.transport = new SSEClientTransport(
new URL(config.serverUrl),
{
// モバイルネットワーク向けのタイムアウト設定
requestInit: {
headers: {
Authorization: `Bearer ${config.apiKey}`,
"X-App-Version": "1.0.0",
},
},
}
);
this.client = new Client(
{ name: "rork-mobile-app", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
}
async connect(): Promise<void> {
if (this.isConnected) return;
await this.client.connect(this.transport);
this.isConnected = true;
console.log("✅ MCP Server に接続しました");
}
async listTools() {
await this.connect();
const { tools } = await this.client.listTools();
return tools;
}
async callTool(name: string, args: Record<string, unknown>) {
await this.connect();
const result = await this.client.callTool({ name, arguments: args });
return result;
}
async disconnect(): Promise<void> {
if (!this.isConnected) return;
await this.client.close();
this.isConnected = false;
}
}
// シングルトンインスタンス
export const mcpClient = new MobileMCPClient({
serverUrl: process.env.EXPO_PUBLIC_MCP_SERVER_URL!,
apiKey: process.env.EXPO_PUBLIC_MCP_API_KEY,
timeout: 30000,
});Step 2: AnthropicとMCPを統合するAIサービス
// lib/ai-mcp-service.ts
import Anthropic from "@anthropic-ai/sdk";
import { mcpClient } from "./mcp-client";
const anthropic = new Anthropic({
apiKey: process.env.EXPO_PUBLIC_ANTHROPIC_API_KEY,
});
export interface ConversationMessage {
role: "user" | "assistant";
content: string;
}
export async function* streamWithMCP(
userMessage: string,
conversationHistory: ConversationMessage[]
): AsyncGenerator<string> {
// Step 1: MCPサーバーからツール一覧を取得
const mcpTools = await mcpClient.listTools();
// Step 2: MCP ツールを Anthropic ツール形式に変換
const anthropicTools: Anthropic.Tool[] = mcpTools.map((tool) => ({
name: tool.name,
description: tool.description || "",
input_schema: tool.inputSchema as Anthropic.Tool.InputSchema,
}));
const messages: Anthropic.MessageParam[] = [
...conversationHistory.map((m) => ({
role: m.role as "user" | "assistant",
content: m.content,
})),
{ role: "user", content: userMessage },
];
// Step 3: Claudeにメッセージを送信(ツール使用許可)
let response = await anthropic.messages.create({
model: "claude-opus-4-5",
max_tokens: 4096,
tools: anthropicTools,
messages,
stream: false,
});
// Step 4: ツール呼び出しループ
while (response.stop_reason === "tool_use") {
const toolUseBlocks = response.content.filter(
(block): block is Anthropic.ToolUseBlock => block.type === "tool_use"
);
// ツール結果を格納
const toolResults: Anthropic.ToolResultBlockParam[] = [];
for (const toolUse of toolUseBlocks) {
console.log(`🔧 ツール呼び出し: ${toolUse.name}`, toolUse.input);
// MCPサーバーにツール実行を依頼
const mcpResult = await mcpClient.callTool(
toolUse.name,
toolUse.input as Record<string, unknown>
);
const resultText =
mcpResult.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n") || "ツール実行完了";
toolResults.push({
type: "tool_result",
tool_use_id: toolUse.id,
content: resultText,
});
}
// ツール結果をClaudeに返して続きを生成
messages.push(
{ role: "assistant", content: response.content },
{ role: "user", content: toolResults }
);
response = await anthropic.messages.create({
model: "claude-opus-4-5",
max_tokens: 4096,
tools: anthropicTools,
messages,
stream: false,
});
}
// Step 5: 最終テキスト応答をストリーミングで返す
const finalText = response.content
.filter((block): block is Anthropic.TextBlock => block.type === "text")
.map((block) => block.text)
.join("");
// チャンクに分けてyield(UI更新用)
const chunkSize = 10;
for (let i = 0; i < finalText.length; i += chunkSize) {
yield finalText.slice(i, i + chunkSize);
// UIのレンダリングを優先するための非同期ポーズ
await new Promise((resolve) => setTimeout(resolve, 0));
}
}Step 3: React NativeのUIコンポーネント
// components/MCPChatScreen.tsx
import React, { useState, useRef, useCallback } from "react";
import {
View,
Text,
TextInput,
FlatList,
TouchableOpacity,
ActivityIndicator,
StyleSheet,
KeyboardAvoidingView,
Platform,
} from "react-native";
import { streamWithMCP, ConversationMessage } from "../lib/ai-mcp-service";
interface Message extends ConversationMessage {
id: string;
isStreaming?: boolean;
}
export const MCPChatScreen: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, setInputText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const flatListRef = useRef<FlatList>(null);
const sendMessage = useCallback(async () => {
if (!inputText.trim() || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: inputText.trim(),
};
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: "",
isStreaming: true,
};
const conversationHistory: ConversationMessage[] = messages.map((m) => ({
role: m.role,
content: m.content,
}));
setMessages((prev) => [...prev, userMessage, assistantMessage]);
setInputText("");
setIsLoading(true);
try {
const stream = streamWithMCP(userMessage.content, conversationHistory);
for await (const chunk of stream) {
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMessage.id
? { ...m, content: m.content + chunk }
: m
)
);
// 最下部にスクロール
flatListRef.current?.scrollToEnd({ animated: false });
}
// ストリーミング完了
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMessage.id ? { ...m, isStreaming: false } : m
)
);
} catch (error) {
console.error("MCP チャットエラー:", error);
setMessages((prev) =>
prev.map((m) =>
m.id === assistantMessage.id
? {
...m,
content: "エラーが発生しました。もう一度お試しください。",
isStreaming: false,
}
: m
)
);
} finally {
setIsLoading(false);
}
}, [inputText, isLoading, messages]);
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<FlatList
ref={flatListRef}
data={messages}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View
style={[
styles.messageBubble,
item.role === "user" ? styles.userBubble : styles.assistantBubble,
]}
>
<Text style={styles.messageText}>{item.content}</Text>
{item.isStreaming && (
<ActivityIndicator size="small" color="#666" />
)}
</View>
)}
contentContainerStyle={styles.messageList}
onContentSizeChange={() =>
flatListRef.current?.scrollToEnd({ animated: true })
}
/>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
value={inputText}
onChangeText={setInputText}
placeholder="メッセージを入力..."
multiline
editable={!isLoading}
onSubmitEditing={sendMessage}
/>
<TouchableOpacity
style={[styles.sendButton, isLoading && styles.disabled]}
onPress={sendMessage}
disabled={isLoading}
>
<Text style={styles.sendButtonText}>送信</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: "#f5f5f5" },
messageList: { padding: 16, gap: 8 },
messageBubble: {
maxWidth: "80%",
padding: 12,
borderRadius: 16,
gap: 4,
},
userBubble: {
alignSelf: "flex-end",
backgroundColor: "#007AFF",
},
assistantBubble: {
alignSelf: "flex-start",
backgroundColor: "#fff",
shadowColor: "#000",
shadowOpacity: 0.05,
shadowRadius: 4,
elevation: 2,
},
messageText: { fontSize: 15, lineHeight: 22 },
inputContainer: {
flexDirection: "row",
padding: 12,
backgroundColor: "#fff",
gap: 8,
borderTopWidth: 1,
borderTopColor: "#e0e0e0",
},
input: {
flex: 1,
borderWidth: 1,
borderColor: "#e0e0e0",
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 8,
maxHeight: 100,
fontSize: 15,
},
sendButton: {
backgroundColor: "#007AFF",
borderRadius: 20,
paddingHorizontal: 16,
paddingVertical: 8,
justifyContent: "center",
},
disabled: { opacity: 0.5 },
sendButtonText: { color: "#fff", fontWeight: "600" },
});認証・セキュリティの実装
APIキーの安全な管理
モバイルアプリでのAPIキー管理は特に注意が必要です。クライアントサイドにAPIキーを直接埋め込むのは避け、以下のパターンを採用します。
// ❌ 危険:APIキーを直接埋め込む
const mcpClient = new MobileMCPClient({
serverUrl: "https://mcp.yourdomain.com/mcp",
apiKey: "sk-ant-XXXX", // 絶対にNG
});
// ✅ 安全:バックエンドプロキシ経由でAPIキーを隠蔽
// Rorkアプリ → 自社バックエンド(認証済み) → MCPサーバーJWTトークンによる認証フロー
// lib/mcp-auth.ts
import * as SecureStore from "expo-secure-store";
export async function getMCPToken(): Promise<string> {
// キャッシュされたトークンを確認
const cached = await SecureStore.getItemAsync("mcp_token");
if (cached) {
const { token, expiresAt } = JSON.parse(cached);
// 5分以上残っていれば再利用
if (Date.now() < expiresAt - 300000) {
return token;
}
}
// バックエンドから新しいトークンを取得
const supabaseToken = await getSupabaseToken(); // Supabaseセッショントークン
const response = await fetch(
`${process.env.EXPO_PUBLIC_API_BASE_URL}/api/mcp-token`,
{
method: "POST",
headers: {
Authorization: `Bearer ${supabaseToken}`,
"Content-Type": "application/json",
},
}
);
const { token, expiresIn } = await response.json();
await SecureStore.setItemAsync(
"mcp_token",
JSON.stringify({
token,
expiresAt: Date.now() + expiresIn * 1000,
})
);
return token;
}レート制限とタイムアウトの設定
// lib/mcp-rate-limiter.ts
export class MCPRateLimiter {
private requestCount = 0;
private resetTime = Date.now() + 60000;
private readonly maxRequestsPerMinute: number;
constructor(maxRequestsPerMinute = 20) {
this.maxRequestsPerMinute = maxRequestsPerMinute;
}
async throttle(): Promise<void> {
if (Date.now() > this.resetTime) {
this.requestCount = 0;
this.resetTime = Date.now() + 60000;
}
if (this.requestCount >= this.maxRequestsPerMinute) {
const waitMs = this.resetTime - Date.now();
console.log(`レート制限: ${waitMs}ms 待機します`);
await new Promise((resolve) => setTimeout(resolve, waitMs));
this.requestCount = 0;
this.resetTime = Date.now() + 60000;
}
this.requestCount++;
}
}実際のユースケース:ECアプリでのMCP活用例
シナリオ:AI商品アドバイザー
MCPを使った実践的なユースケースとして、ECアプリの「AI商品アドバイザー」を考えます。ユーザーが「1万円以内でプレゼントを探している」と言うと、AIが以下を自律的に実行します。
search_productsツールで条件に合う商品を検索check_stockツールで在庫を確認get_reviewsツールで評価を取得- 結果を総合して自然な推薦文を生成
// この会話フローが自動で実現される
// ユーザー: 「彼女への誕生日プレゼント、予算1万円でおすすめは?」
//
// AI内部処理:
// 1. search_products({ query: "誕生日 プレゼント 女性", maxPrice: 10000, limit: 5 })
// 2. check_stock({ productId: "P001" }) × 5件
// 3. get_reviews({ productId: "P001", sortBy: "rating" })
//
// AI返答:
// 「1万円以内で特に評価が高いのは、◯◯(¥8,900・在庫あり・★4.8)です。
// レビューでは『見た目が可愛い』という声が多く、誕生日ギフトに最適です。」このフローはコードを一切変更せず、MCPサーバーにツールを追加するだけで機能を拡張できます。まさにMCPの真価です。
よくあるエラーと対処法
エラー1: SSEセッションが頻繁に切断される
モバイルアプリはバックグラウンド移行やネットワーク切り替え時にSSE接続が切れます。
// 自動再接続ロジックの実装
class ReconnectingMCPClient extends MobileMCPClient {
private reconnectAttempts = 0;
private readonly maxReconnects = 5;
async callToolWithRetry(
name: string,
args: Record<string, unknown>,
retries = 3
) {
for (let i = 0; i < retries; i++) {
try {
return await this.callTool(name, args);
} catch (error: unknown) {
if (
error instanceof Error &&
error.message.includes("connection") &&
i < retries - 1
) {
console.log(
`再接続試行 ${i + 1}/${retries}...`
);
this.isConnected = false;
await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
continue;
}
throw error;
}
}
}
}エラー2: Tool not found エラー
MCPサーバーのツール名とクライアントからの呼び出し名が一致しない場合に発生します。
// デバッグ:利用可能なツール一覧を出力
const tools = await mcpClient.listTools();
console.log("利用可能なツール:", tools.map((t) => t.name));
// → ["search_products", "check_stock", "get_reviews"]エラー3: Android で EventSource is not defined
React Native は標準で EventSource をサポートしていません。
// polyfill のインポート(App.tsx の最上部)
import "eventsource-polyfill";
// または
import EventSource from "react-native-event-source";
global.EventSource = EventSource;まとめ
MCP(Model Context Protocol)をRorkアプリに統合することで、AIが外部ツールを自律的に呼び出すインテリジェントなモバイルアプリを構築できます。本記事で学んだポイントをまとめます。
- MCPのアーキテクチャ: ホスト・クライアント・サーバーの3層構造と、モバイルアプリに適したSSE/Streamable HTTPトランスポート
- 実装の核心: MCPツール一覧をAnthropicのツール定義に変換し、ツール呼び出しループで自律エージェントを実現
- プロダクション品質: JWTトークン認証・レート制限・自動再接続によるモバイル特有の課題への対応
- 拡張性: MCPサーバーにツールを追加するだけで、アプリ側コードを変更せずに機能を拡張できる
MCPは2026年に最も注目すべきAIインフラの一つです。Rork × Gemini 2.0 Flash 完全ガイドで学んだGeminiとの統合と組み合わせることで、マルチAIプロバイダー対応のアプリも構築できます。ぜひこのガイドを参考に、MCPを活用したオリジナルのインテリジェントアプリを作成してみてください。
MCPについてさらに深く