⚡ TL;DR
Integrate a Model Context Protocol (MCP) server into your React Native workflow. Step-by-step setup with Claude Code, Expo, and automation examples.
Model Context Protocol (MCP) lets AI tools like Claude Code plug into your local development environment. In this guide, I’ll walk you through adding an MCP server to your React Native workflow so Claude Code can read your project structure, run Metro bundler commands, manage Expo configurations, and auto-generate navigation boilerplate.
After 3 months of using MCP with React Native, I can say it’s the missing piece for AI-assisted mobile development. Here’s the complete setup.
What is MCP for React Native?
MCP (Model Context Protocol) is an open standard that lets AI applications connect to data sources and tools safely. For React Native, an MCP server can:
- Read your
package.json,app.json, andmetro.config.js - Analyze your
src/andcomponents/directories - Run
npx react-native doctorand report issues - Generate navigation screens that follow your existing patterns
- Execute
expo startornpm run android/ios - Parse
src/navigation/AppNavigator.tsxto understand your routing
Prerequisites
- Node.js 18+ installed
- React Native CLI or Expo CLI
- Claude Code CLI (
npm install -g @anthropic-ai/claude-code) - Basic React Native knowledge
Step 1: Create a Node.js/TypeScript MCP Server
mkdir mcp-react-native-server
cd mcp-react-native-server
npm init -y
npm install @modelcontextprotocol/sdk fs-extraStep 2: Implement the MCP Server
Create index.js:
const { Server } = require("@modelcontextprotocol/sdk/server/index.js");
const { StdioServerTransport } = require("@modelcontextprotocol/sdk/server/stdio.js");
const {
CallToolRequestSchema,
ListToolsRequestSchema,
} = require("@modelcontextprotocol/sdk/types.js");
const fs = require("fs-extra");
const path = require("path");
const { exec } = require("child_process");
class ReactNativeMcpServer {
constructor() {
this.server = new Server(
{
name: "react-native-mcp-server",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
this.setupToolHandlers();
this.server.onerror = (error) => console.error("[MCP Error]", error);
}
setupToolHandlers() {
// Tool: Get project structure
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "getProjectStructure",
description: "Get the structure of a React Native project",
inputSchema: {
type: "object",
properties: {
projectPath: {
type: "string",
description: "Path to the React Native project",
},
},
required: ["projectPath"],
},
},
{
name: "runRnDoctor",
description: "Run react-native doctor to check environment health",
inputSchema: {
type: "object",
properties: {
projectPath: { type: "string" },
},
required: ["projectPath"],
},
},
{
name: "generateNavigationScreen",
description: "Generate a new screen with boilerplate code matching project patterns",
inputSchema: {
type: "object",
properties: {
projectPath: { type: "string" },
screenName: { type: "string" },
screenType: {
type: "string",
enum: ["stack", "tab", "drawer"],
default: "stack",
},
},
required: ["projectPath", "screenName"],
},
},
{
name: "analyzeComponents",
description: "Scan components directory and report patterns",
inputSchema: {
type: "object",
properties: {
projectPath: { type: "string" },
},
required: ["projectPath"],
},
},
],
};
});
// Handle tool calls
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
switch (name) {
case "getProjectStructure":
return await this.getProjectStructure(args.projectPath);
case "runRnDoctor":
return await this.runRnDoctor(args.projectPath);
case "generateNavigationScreen":
return await this.generateNavigationScreen(args);
case "analyzeComponents":
return await this.analyzeComponents(args.projectPath);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
}
async getProjectStructure(projectPath) {
const structure = {};
// Read package.json
try {
const packageJson = await fs.readJson(path.join(projectPath, "package.json"));
structure.packageJson = {
name: packageJson.name,
dependencies: Object.keys(packageJson.dependencies || {}),
devDependencies: Object.keys(packageJson.devDependencies || {}),
};
} catch (error) {
structure.packageJsonError = error.message;
}
// Scan src directory
const srcPath = path.join(projectPath, "src");
if (await fs.pathExists(srcPath)) {
structure.srcTree = await this.buildFileTree(srcPath, projectPath);
}
// Scan components
const componentsPath = path.join(projectPath, "src", "components");
if (await fs.pathExists(componentsPath)) {
const components = await fs.readdir(componentsPath);
structure.components = components.filter((item) =>
item.endsWith(".tsx") || item.endsWith(".jsx")
);
}
return {
content: [
{
type: "text",
text: JSON.stringify(structure, null, 2),
},
],
};
}
async buildFileTree(dir, rootDir) {
const files = await fs.readdir(dir);
const tree = {};
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = await fs.stat(fullPath);
if (stat.isDirectory()) {
tree[file] = await this.buildFileTree(fullPath, rootDir);
} else {
const relativePath = path.relative(rootDir, fullPath);
tree[file] = { size: stat.size, path: relativePath };
}
}
return tree;
}
async runRnDoctor(projectPath) {
return new Promise((resolve) => {
exec(
"npx react-native doctor",
{ cwd: projectPath, maxBuffer: 1024 * 1024 * 5 },
(error, stdout, stderr) => {
resolve({
content: [
{
type: "text",
text: `Doctor output:\n${stdout}\n\nErrors:\n${stderr}`,
},
],
isError: !!error,
});
}
);
});
}
async generateNavigationScreen(args) {
const { projectPath, screenName, screenType } = args;
const screensPath = path.join(projectPath, "src", "screens");
// Read existing screen patterns
let template = `import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const ${screenName}Screen = () => {
return (
<View style={styles.container}>
<Text style={styles.title}>${screenName}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
export default ${screenName}Screen;
`;
// Check if project uses TypeScript
const tsConfigPath = path.join(projectPath, "tsconfig.json");
const useTypeScript = await fs.pathExists(tsConfigPath);
if (useTypeScript) {
template = `import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
const ${screenName}Screen: React.FC = () => {
return (
<View style={styles.container}>
<Text style={styles.title}>${screenName}</Text>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
});
export default ${screenName}Screen;
`;
}
// Write the file
await fs.ensureDir(screensPath);
const filePath = path.join(screensPath, `${screenName}Screen.${useTypeScript ? "tsx" : "jsx"}`);
await fs.writeFile(filePath, template);
// Update navigation file if it exists
const navPath = path.join(projectPath, "src", "navigation", "AppNavigator.tsx");
if (await fs.pathExists(navPath)) {
let navContent = await fs.readFile(navPath, "utf-8");
// Add import
const importStatement = `import ${screenName}Screen from '../screens/${screenName}Screen';\n`;
if (!navContent.includes(importStatement)) {
navContent = navContent.replace(
/(import.*from.*screens.*;\n)/,
`$1${importStatement}`
);
}
// Add screen to navigator
if (screenType === "stack") {
navContent = navContent.replace(
/(<Stack\.Screen\s+name=.*)/,
`$1\n <Stack.Screen name="${screenName}" component={${screenName}Screen} />`
);
}
await fs.writeFile(navPath, navContent);
}
return {
content: [
{
type: "text",
text: `Generated ${screenName}Screen at ${filePath}`,
},
],
};
}
async analyzeComponents(projectPath) {
const componentsPath = path.join(projectPath, "src", "components");
const report = {
totalComponents: 0,
componentTypes: { functional: 0, class: 0 },
hooksUsed: {},
commonPatterns: [],
};
if (!(await fs.pathExists(componentsPath))) {
return {
content: [{ type: "text", text: "No components directory found" }],
};
}
const files = await fs.readdir(componentsPath);
const tsxFiles = files.filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx"));
for (const file of tsxFiles) {
const content = await fs.readFile(path.join(componentsPath, file), "utf-8");
report.totalComponents++;
// Detect functional vs class components
if (/const\s+\w+\s*=\s*\(/g.test(content) || /function\s+\w+\s*\(/g.test(content)) {
report.componentTypes.functional++;
} else if (/class\s+\w+\s+extends/g.test(content)) {
report.componentTypes.class++;
}
// Detect hooks
const hooks = ["useState", "useEffect", "useContext", "useCallback", "useMemo", "useRef"];
hooks.forEach((hook) => {
if (content.includes(hook)) {
report.hooksUsed[hook] = (report.hooksUsed[hook] || 0) + 1;
}
});
}
return {
content: [
{
type: "text",
text: JSON.stringify(report, null, 2),
},
],
};
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error("React Native MCP Server running on stdio");
}
}
const server = new ReactNativeMcpServer();
server.run();Step 3: Configure Claude Code
Edit ~/.claude-code/mcp_config.json:
{
"mcpServers": {
"react-native": {
"command": "node",
"args": ["/absolute/path/to/mcp-react-native-server/index.js"],
"description": "React Native MCP server for Claude Code"
}
}
}Step 4: Use with Claude Code
Start Claude Code in your React Native project:
cd /path/to/your/react-native-project
claude-codeAsk Claude:
Use the MCP server to:
1. Analyze my project structure
2. Run doctor to check my environment
3. Generate a new "Settings" screen as a stack screen
4. Report what hooks patterns my components useClaude Code will call your MCP tools and report back.
Step 5: Expo Integration
For Expo projects, add Expo-specific tools:
// Add to your tool handlers
{
name: "expoStart",
description: "Start Expo development server",
inputSchema: {
type: "object",
properties: {
projectPath: { type: "string" },
platform: {
type: "string",
enum: ["ios", "android", "web"],
default: "ios"
},
},
required: ["projectPath"],
},
}Implementation:
async expoStart(args) {
const { projectPath, platform } = args;
return new Promise((resolve) => {
exec(
`npx expo start --${platform}`,
{ cwd: projectPath },
(error, stdout, stderr) => {
resolve({
content: [{ type: "text", text: stdout }],
});
}
);
});
}Real-World Use Cases
1. Onboarding New Developers
Ask Claude: "Explain the navigation architecture and show me how screens are registered"The MCP server reads AppNavigator.tsx and src/screens/, and Claude explains the flow.
2. Pre-Commit Checks
Ask Claude: "Run doctor and fix any issues, then generate missing unit tests for new screens"3. Dependency Updates
Ask Claude: "Check for outdated packages and suggest updates, especially around React Navigation and Expo SDK"MCP reads package.json, Claude suggests upgrade paths.
Troubleshooting
MCP Server Not Starting
# Check Node version (needs 18+)
node --version
# Install dependencies
cd mcp-react-native-server
npm install
# Test directly
node index.jsClaude Code Integration Issues
Verify config is loaded:
# Check Claude Code sees the MCP server
claude-code --list-mcp-serversIf missing, check JSON syntax in ~/.claude-code/mcp_config.json.
Security Considerations
- File System Access: The server can read/write files. Only use with trusted projects.
- Command Execution:
exec()calls are dangerous. Validate inputs:if (!projectPath.startsWith("/safe/root/dir")) { throw new Error("Invalid project path"); } - Authentication: For team deployments, add API keys or OAuth.
Conclusion
Adding MCP to your React Native workflow transforms AI assistance from “chatting about code” to “understanding and modifying your project.” Whether it’s generating screens that match your navigation patterns, running health checks, or analyzing component architecture, MCP bridges the gap between AI and your mobile development environment.
Start with the basic server, then extend it with tools for your specific stack (Expo, bare React Native, React Navigation vs. React Native Navigation, state management patterns like Redux vs. Zustand). The investment in setup pays off in autonomous code generation that actually fits your project.
