RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/AI Models
AI Models/2026-04-02Advanced

Rork × MCP (Model Context Protocol) Complete Implementation Guide — Building Next-Gen Mobile Apps with AI Tool Integration

A complete guide to integrating MCP (Model Context Protocol) into your Rork app. Covers architecture design, streaming, authentication, and production operations with working code examples.

MCP3Model Context ProtocolRork515AI Integration2Claude4Tool Use2React Native209advanced6

Setup and context: How MCP Is Transforming Mobile App Development

When Anthropic introduced MCP (Model Context Protocol) in late 2024, it established a universal standard for connecting AI models to external tools and data sources. By 2026, MCP adoption has exploded on the desktop and server side — but mobile app integration remains largely uncharted territory for most developers.

Bringing MCP into Rork-powered mobile apps unlocks some genuinely powerful capabilities:

  • Dynamic tool integration: AI autonomously invokes database queries, web search, code execution, and more
  • Beyond RAG: Real-time data access that static retrieval pipelines simply can't match
  • Multi-provider flexibility: Switch between Claude, Gemini, and GPT-4o through a single interface
  • Custom tool surfaces: Expose your own APIs as MCP servers and let AI call them on demand

This guide gives you everything you need to integrate MCP into a Rork app — from architecture design and working implementation code to production security patterns. If you've already worked through Building an AI Assistant App with Rork × Claude API and Intelligent Assistant Apps with Rork × AI Function Calling, you're ready for this next level.


Understanding MCP: Architecture and Core Concepts

The Three-Layer Model

MCP defines three distinct roles:

  • MCP Host: The application that drives the AI — in our case, the Rork app itself
  • MCP Client: A library embedded in the host that manages communication with MCP servers
  • MCP Server: A service that exposes tools, resources, and prompts to AI models
[Rork App (MCP Host)]
      ↓ MCP Client
[MCP Server Pool]
  ├── Brave Search MCP Server
  ├── Supabase MCP Server
  ├── GitHub MCP Server
  └── Your Custom API Server

Transport Options

MCP supports several transport mechanisms:

  • stdio: Inter-process communication (desktop and server-side only)
  • SSE (Server-Sent Events): HTTP-based event streaming — ideal for mobile apps
  • Streamable HTTP: Introduced in 2025 as the preferred server-side option

Mobile apps communicate over the network, so SSE or Streamable HTTP is the practical choice.

How Tool Definitions Work

MCP servers expose their capabilities through a tools/list endpoint using JSON Schema. The client passes these definitions to the AI model, which then calls tools/call whenever it decides a tool is relevant.

{
  "name": "search_products",
  "description": "Search the product database for matching items",
  "inputSchema": {
    "type": "object",
    "properties": {
      "query": { "type": "string", "description": "Search query" },
      "maxResults": { "type": "number", "default": 5 }
    },
    "required": ["query"]
  }
}

Environment Setup and Prerequisites

What You'll Need

  • Rork Max (required for custom code editing and native module support)
  • Node.js 20+ (for local MCP server testing)
  • Anthropic API key (Claude claude-3-5-sonnet or claude-opus-4 recommended)

Installing Required Packages

Add these to your Rork app:

# MCP client SDK (React Native compatible)
npm install @modelcontextprotocol/sdk
 
# EventSource polyfill for React Native
npm install eventsource
 
# Anthropic SDK
npm install @anthropic-ai/sdk

Setting Up Your MCP Server (Cloudflare Workers)

For this guide, we'll deploy a custom MCP server on Cloudflare Workers. If you're connecting to existing public MCP servers (Brave Search, Supabase, etc.), you can skip this section.

npm create cloudflare@latest my-mcp-server -- --template worker
cd my-mcp-server
npm install @modelcontextprotocol/sdk

Building the MCP Server (Cloudflare Workers)

Core Server Structure

// src/index.ts (Cloudflare Workers MCP Server)
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() {
    // Tool 1: Database search
    this.server.tool(
      "search_products",
      "Search the product database",
      {
        query: z.string().describe("Search keyword"),
        category: z.string().optional().describe("Category filter"),
        limit: z.number().default(10).describe("Maximum results"),
      },
      async ({ query, category, limit }) => {
        const results = await searchProductsFromDB(query, category, limit);
        return {
          content: [{ type: "text", text: JSON.stringify(results, null, 2) }],
        };
      }
    );
 
    // Tool 2: Stock check
    this.server.tool(
      "check_stock",
      "Check the inventory count for a product",
      {
        productId: z.string().describe("Product ID"),
      },
      async ({ productId }) => {
        const stock = await getStockFromDB(productId);
        return {
          content: [
            { type: "text", text: `Product ${productId} stock: ${stock} units` },
          ],
        };
      }
    );
  }
}
 
export default new OAuthProvider({
  apiHandlers: { "/mcp": MyMCPServer.mount("/mcp") },
});

Deploying to Cloudflare Workers

npx wrangler deploy
# → Available at https://mcp.yourdomain.com

Implementing the Rork App Side

Step 1: MCP Client Initialization

// 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("✅ Connected to 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();
    return await this.client.callTool({ name, arguments: args });
  }
 
  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: Integrating Anthropic with MCP

// 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: Fetch available tools from the MCP server
  const mcpTools = await mcpClient.listTools();
 
  // Step 2: Convert MCP tool definitions to Anthropic format
  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: Send message to Claude with tool use enabled
  let response = await anthropic.messages.create({
    model: "claude-opus-4-5",
    max_tokens: 4096,
    tools: anthropicTools,
    messages,
    stream: false,
  });
 
  // Step 4: Tool use loop — keep executing until Claude is done calling tools
  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(`🔧 Calling tool: ${toolUse.name}`, toolUse.input);
 
      // Execute the tool via 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") || "Tool execution complete";
 
      toolResults.push({
        type: "tool_result",
        tool_use_id: toolUse.id,
        content: resultText,
      });
    }
 
    // Return tool results to Claude to continue generation
    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: Stream the final text response
  const finalText = response.content
    .filter((block): block is Anthropic.TextBlock => block.type === "text")
    .map((block) => block.text)
    .join("");
 
  const chunkSize = 10;
  for (let i = 0; i < finalText.length; i += chunkSize) {
    yield finalText.slice(i, i + chunkSize);
    await new Promise((resolve) => setTimeout(resolve, 0));
  }
}

Step 3: The React Native Chat 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 history: ConversationMessage[] = messages.map((m) => ({
      role: m.role,
      content: m.content,
    }));
 
    setMessages((prev) => [...prev, userMessage, assistantMessage]);
    setInputText("");
    setIsLoading(true);
 
    try {
      for await (const chunk of streamWithMCP(userMessage.content, history)) {
        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 chat error:", error);
      setMessages((prev) =>
        prev.map((m) =>
          m.id === assistantMessage.id
            ? { ...m, content: "An error occurred. Please try again.", 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.bubble,
              item.role === "user" ? styles.userBubble : styles.aiBubble,
            ]}
          >
            <Text style={styles.bubbleText}>{item.content}</Text>
            {item.isStreaming && <ActivityIndicator size="small" color="#666" />}
          </View>
        )}
        contentContainerStyle={styles.list}
        onContentSizeChange={() =>
          flatListRef.current?.scrollToEnd({ animated: true })
        }
      />
      <View style={styles.inputRow}>
        <TextInput
          style={styles.input}
          value={inputText}
          onChangeText={setInputText}
          placeholder="Type a message..."
          multiline
          editable={!isLoading}
        />
        <TouchableOpacity
          style={[styles.sendBtn, isLoading && styles.disabled]}
          onPress={sendMessage}
          disabled={isLoading}
        >
          <Text style={styles.sendBtnText}>Send</Text>
        </TouchableOpacity>
      </View>
    </KeyboardAvoidingView>
  );
};
 
const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: "#f5f5f5" },
  list: { padding: 16, gap: 8 },
  bubble: { maxWidth: "80%", padding: 12, borderRadius: 16 },
  userBubble: { alignSelf: "flex-end", backgroundColor: "#007AFF" },
  aiBubble: { alignSelf: "flex-start", backgroundColor: "#fff", elevation: 2 },
  bubbleText: { fontSize: 15, lineHeight: 22 },
  inputRow: {
    flexDirection: "row",
    padding: 12,
    backgroundColor: "#fff",
    borderTopWidth: 1,
    borderTopColor: "#e0e0e0",
    gap: 8,
  },
  input: {
    flex: 1,
    borderWidth: 1,
    borderColor: "#e0e0e0",
    borderRadius: 20,
    paddingHorizontal: 16,
    paddingVertical: 8,
    maxHeight: 100,
    fontSize: 15,
  },
  sendBtn: {
    backgroundColor: "#007AFF",
    borderRadius: 20,
    paddingHorizontal: 16,
    justifyContent: "center",
  },
  disabled: { opacity: 0.5 },
  sendBtnText: { color: "#fff", fontWeight: "600" },
});

Authentication and Security

Never Expose API Keys in the Client

// ❌ Never do this — API keys in client code are leaked in build artifacts
const mcpClient = new MobileMCPClient({
  serverUrl: "https://mcp.yourdomain.com/mcp",
  apiKey: "sk-ant-XXXX", // Critically insecure
});
 
// ✅ Route through your authenticated backend instead
// Rork App → Your Backend (auth check) → MCP Server

JWT-Based Auth Flow

// 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);
    // Reuse if more than 5 minutes remain
    if (Date.now() < expiresAt - 300000) return token;
  }
 
  const supabaseToken = await getSupabaseToken();
  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;
}

Rate Limiting

// lib/mcp-rate-limiter.ts
export class MCPRateLimiter {
  private requestCount = 0;
  private resetTime = Date.now() + 60000;
 
  constructor(private readonly maxPerMinute = 20) {}
 
  async throttle(): Promise<void> {
    if (Date.now() > this.resetTime) {
      this.requestCount = 0;
      this.resetTime = Date.now() + 60000;
    }
    if (this.requestCount >= this.maxPerMinute) {
      const wait = this.resetTime - Date.now();
      await new Promise((r) => setTimeout(r, wait));
      this.requestCount = 0;
      this.resetTime = Date.now() + 60000;
    }
    this.requestCount++;
  }
}

Real-World Use Case: AI Shopping Advisor in an E-Commerce App

Here's how MCP plays out in a concrete scenario. A user opens your shopping app and types: "I'm looking for a birthday gift for my girlfriend, budget around $80."

Without any additional code changes, the AI autonomously:

  1. Calls search_products({ query: "birthday gift women", maxPrice: 80, limit: 5 })
  2. Calls check_stock({ productId: "P001" }) for each result
  3. Calls get_reviews({ productId: "P001", sortBy: "rating" })
  4. Synthesizes everything into a natural-language recommendation
// The entire flow above happens automatically through the tool-use loop.
// No hardcoded logic — just tool definitions on the MCP server.
//
// AI response example:
// "Based on top-rated options under $80, I'd recommend the Rose Gold Jewelry Set
// ($72, in stock, ★4.9). Reviewers consistently call it 'elegant and gift-ready,'
// making it a great birthday pick."

The real power: adding a new tool to the MCP server instantly makes it available to the AI — no app update required.


Troubleshooting Common Issues

Issue 1: SSE Connection Drops Frequently

Mobile apps lose SSE connections when backgrounded or when switching networks. Add automatic reconnection logic:

class ReconnectingMCPClient extends MobileMCPClient {
  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) {
        const isConnectionError =
          error instanceof Error && error.message.includes("connection");
        if (isConnectionError && i < retries - 1) {
          console.log(`Reconnect attempt ${i + 1}/${retries}...`);
          this.isConnected = false;
          await new Promise((r) => setTimeout(r, 1000 * (i + 1)));
          continue;
        }
        throw error;
      }
    }
  }
}

Issue 2: Tool not found Error

Verify that tool names match exactly between server and client:

const tools = await mcpClient.listTools();
console.log("Available tools:", tools.map((t) => t.name));
// → ["search_products", "check_stock", "get_reviews"]

Issue 3: EventSource is not defined on Android

React Native doesn't include EventSource by default. Add the polyfill at the very top of App.tsx:

import "eventsource-polyfill";
// or
import EventSource from "react-native-event-source";
global.EventSource = EventSource;

Summary

Integrating MCP into your Rork app transforms it from a standard AI chatbot into an autonomous agent that can access live data, call external APIs, and reason across multiple tool results — all without you writing a new code path for every capability.

Key takeaways from this guide:

  • Architecture: Three-layer Host/Client/Server model with SSE transport for mobile
  • Core loop: Convert MCP tool definitions to Anthropic format, run the tool-use loop, stream the final response
  • Production hardening: JWT auth, rate limiting, and auto-reconnect for mobile network realities
  • Scalability: Add tools to your MCP server; the app picks them up automatically

For a strong foundation in AI-powered mobile features, revisit Rork × Gemini 2.0 Flash Complete Guide alongside this article — combining multiple AI providers through MCP is where things get truly interesting.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

AI Models2026-06-19
Before You Pay $200/mo for Rork Max, Map How Far Expo Reaches in Three Tiers
Wanting widgets or Live Activities makes Rork Max tempting, but most of those features are reachable from the Expo setup that standard Rork generates. Here is how I sort each Apple-native feature into three tiers—reachable in Expo, reachable with a custom module, or where Max is the pragmatic answer—and verify which tier my app is in before paying.
AI Models2026-06-17
Making Credits Add Up in a Rork AI Image App — Field Notes on Atomic Ledgers and Moderation
Credit billing in a Rork AI image and video app breaks in production because of the order between generation and deduction. Here are the field notes — atomic ledger consumption, idempotency, refunds on failure, and moderation — with Supabase code you can ship.
AI Models2026-06-14
Calling Apple Foundation Models from a Rork (Expo) App: Bridging On-Device AI Through a Native Module
Rork generates Expo (React Native) apps, but Apple Foundation Models ships as a Swift framework you can't touch from JavaScript. Here's how to write an Expo Modules API bridge, gate it by availability, and fall back to the cloud on unsupported devices.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →