@momo-kits/mcp-expo 2.0.0-beta.1 → 2.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/remote-server.mjs +16 -1
- package/server.mjs +58 -1
- package/template/App.tsx +144 -123
package/package.json
CHANGED
package/remote-server.mjs
CHANGED
|
@@ -97,8 +97,23 @@ async function ensureDeps() {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
async function killPort(port) {
|
|
101
|
+
try {
|
|
102
|
+
const { execSync } = await import('child_process');
|
|
103
|
+
const pids = execSync(`lsof -ti:${port} 2>/dev/null`, { encoding: 'utf8' }).trim();
|
|
104
|
+
if (pids) {
|
|
105
|
+
for (const pid of pids.split('\n')) {
|
|
106
|
+
try { process.kill(parseInt(pid), 'SIGTERM'); } catch {}
|
|
107
|
+
}
|
|
108
|
+
log(`Killed existing processes on port ${port}: ${pids.replace(/\n/g, ', ')}`);
|
|
109
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
110
|
+
}
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
async function startExpoMetro() {
|
|
101
115
|
await ensureDeps();
|
|
116
|
+
await killPort(8081);
|
|
102
117
|
|
|
103
118
|
return new Promise((resolve, reject) => {
|
|
104
119
|
let settled = false;
|
|
@@ -112,7 +127,7 @@ async function startExpoMetro() {
|
|
|
112
127
|
|
|
113
128
|
log(`Starting Expo Metro in ${TEMPLATE_DIR}...`);
|
|
114
129
|
|
|
115
|
-
metroProcess = spawn('npx', ['expo', 'start', '--
|
|
130
|
+
metroProcess = spawn('npx', ['expo', 'start', '--tunnel', '--port', '8081'], {
|
|
116
131
|
cwd: TEMPLATE_DIR,
|
|
117
132
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
118
133
|
env: { ...process.env, FORCE_COLOR: '0' },
|
package/server.mjs
CHANGED
|
@@ -125,9 +125,66 @@ async function handleGetStatus() {
|
|
|
125
125
|
|
|
126
126
|
// ── MCP Server ─────────────────────────────────────────────────────────────────
|
|
127
127
|
|
|
128
|
+
const SERVER_INSTRUCTIONS = `
|
|
129
|
+
You are connected to the mcp-expo server — a tool for live-previewing React Native UI on a real device via Expo Go.
|
|
130
|
+
|
|
131
|
+
## How to use push_screen_and_run
|
|
132
|
+
|
|
133
|
+
The "code" field must be a **complete, self-contained App.tsx file**. Follow these rules:
|
|
134
|
+
|
|
135
|
+
1. **Default export required**: The file must \`export default\` a React component.
|
|
136
|
+
2. **Only use built-in React Native + Expo packages**:
|
|
137
|
+
- react, react-native (View, Text, ScrollView, TouchableOpacity, StyleSheet, Image, TextInput, FlatList, etc.)
|
|
138
|
+
- expo-status-bar, expo-linear-gradient, expo-constants
|
|
139
|
+
- Do NOT import packages that aren't pre-installed (no react-navigation, no external UI libs).
|
|
140
|
+
3. **Inline all styles**: Use StyleSheet.create() at the bottom of the file. No external style files.
|
|
141
|
+
4. **Inline all assets**: Use emoji, Unicode, or remote image URLs. No local asset imports.
|
|
142
|
+
5. **No multi-file**: Everything must be in one single file. Define all sub-components in the same file.
|
|
143
|
+
6. **TypeScript OK**: The file is App.tsx, TypeScript is supported.
|
|
144
|
+
|
|
145
|
+
## Example skeleton
|
|
146
|
+
|
|
147
|
+
\`\`\`tsx
|
|
148
|
+
import React from 'react';
|
|
149
|
+
import { View, Text, StyleSheet, ScrollView, StatusBar } from 'react-native';
|
|
150
|
+
|
|
151
|
+
export default function App() {
|
|
152
|
+
return (
|
|
153
|
+
<View style={styles.container}>
|
|
154
|
+
<StatusBar barStyle="dark-content" />
|
|
155
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
156
|
+
<Text style={styles.title}>Hello World</Text>
|
|
157
|
+
</ScrollView>
|
|
158
|
+
</View>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const styles = StyleSheet.create({
|
|
163
|
+
container: { flex: 1, backgroundColor: '#fff' },
|
|
164
|
+
content: { padding: 24, paddingTop: 60 },
|
|
165
|
+
title: { fontSize: 24, fontWeight: '700' },
|
|
166
|
+
});
|
|
167
|
+
\`\`\`
|
|
168
|
+
|
|
169
|
+
## Workflow
|
|
170
|
+
1. Generate the full App.tsx code following the rules above.
|
|
171
|
+
2. Call push_screen_and_run with the code.
|
|
172
|
+
3. The app will reload on the user's device via HMR. Share the expo URL if it's the first run.
|
|
173
|
+
4. If the user wants changes, update the code and call push_screen_and_run again (no need for reset=true unless there's a crash).
|
|
174
|
+
|
|
175
|
+
## Common mistakes to avoid
|
|
176
|
+
- Do NOT use \`import\` for local files or uninstalled packages — this will crash Metro.
|
|
177
|
+
- Do NOT forget \`export default\`.
|
|
178
|
+
- Do NOT use \`require()\` for images — use URL strings instead.
|
|
179
|
+
- Do NOT send partial code or snippets — always send the complete file.
|
|
180
|
+
`.trim();
|
|
181
|
+
|
|
128
182
|
const server = new Server(
|
|
129
183
|
{ name: 'mcp-expo', version: '2.0.0' },
|
|
130
|
-
{
|
|
184
|
+
{
|
|
185
|
+
capabilities: { tools: {} },
|
|
186
|
+
instructions: SERVER_INSTRUCTIONS,
|
|
187
|
+
}
|
|
131
188
|
);
|
|
132
189
|
|
|
133
190
|
const tools = [
|
package/template/App.tsx
CHANGED
|
@@ -1,154 +1,175 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
import React, { useState } from 'react';
|
|
2
3
|
import {
|
|
3
4
|
View,
|
|
4
5
|
Text,
|
|
5
6
|
StyleSheet,
|
|
6
|
-
TouchableOpacity,
|
|
7
7
|
ScrollView,
|
|
8
8
|
StatusBar,
|
|
9
|
+
TouchableOpacity,
|
|
10
|
+
Platform,
|
|
9
11
|
} from 'react-native';
|
|
10
12
|
|
|
13
|
+
const Typography: Record<string, { fontSize: number; fontWeight: string; lineHeight: number; textDecorationLine?: string }> = {
|
|
14
|
+
headline_default_bold: { fontSize: 28, fontWeight: '700', lineHeight: 36 },
|
|
15
|
+
header_m_bold: { fontSize: 22, fontWeight: '700', lineHeight: 28 },
|
|
16
|
+
header_default_bold: { fontSize: 20, fontWeight: '700', lineHeight: 26 },
|
|
17
|
+
header_s_semibold: { fontSize: 18, fontWeight: '600', lineHeight: 24 },
|
|
18
|
+
header_xs_semibold: { fontSize: 16, fontWeight: '600', lineHeight: 22 },
|
|
19
|
+
body_default_regular: { fontSize: 16, fontWeight: '400', lineHeight: 24 },
|
|
20
|
+
body_default_regularstrikethrought: { fontSize: 16, fontWeight: '400', lineHeight: 24, textDecorationLine: 'line-through' },
|
|
21
|
+
description_default_regular: { fontSize: 14, fontWeight: '400', lineHeight: 20 },
|
|
22
|
+
description_default_regularstrikethrought: { fontSize: 14, fontWeight: '400', lineHeight: 20, textDecorationLine: 'line-through' },
|
|
23
|
+
description_xs_regular: { fontSize: 12, fontWeight: '400', lineHeight: 18 },
|
|
24
|
+
description_xs_regularstrikethrought: { fontSize: 12, fontWeight: '400', lineHeight: 18, textDecorationLine: 'line-through' },
|
|
25
|
+
label_default_medium: { fontSize: 16, fontWeight: '500', lineHeight: 22 },
|
|
26
|
+
label_s_medium: { fontSize: 14, fontWeight: '500', lineHeight: 20 },
|
|
27
|
+
label_xs_medium: { fontSize: 12, fontWeight: '500', lineHeight: 18 },
|
|
28
|
+
action_default_bold: { fontSize: 16, fontWeight: '700', lineHeight: 22 },
|
|
29
|
+
action_s_bold: { fontSize: 14, fontWeight: '700', lineHeight: 20 },
|
|
30
|
+
action_xs_bold: { fontSize: 12, fontWeight: '700', lineHeight: 18 },
|
|
31
|
+
action_xxs_bold: { fontSize: 10, fontWeight: '700', lineHeight: 16 },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const TextColors = [
|
|
35
|
+
{ name: 'default', color: '#1A1A1A' },
|
|
36
|
+
{ name: 'secondary', color: '#7A7A7A' },
|
|
37
|
+
{ name: 'disable', color: '#B8B8B8' },
|
|
38
|
+
{ name: 'primary', color: '#D82D8B' },
|
|
39
|
+
{ name: 'success', color: '#00C853' },
|
|
40
|
+
{ name: 'warning', color: '#FF9100' },
|
|
41
|
+
{ name: 'error', color: '#FF1744' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
type Tab = 'typography' | 'color';
|
|
45
|
+
|
|
11
46
|
export default function App() {
|
|
47
|
+
const [activeTab, setActiveTab] = useState<Tab>('typography');
|
|
48
|
+
|
|
12
49
|
return (
|
|
13
50
|
<View style={styles.container}>
|
|
14
|
-
<StatusBar barStyle="dark-content"
|
|
15
|
-
<
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
>
|
|
19
|
-
<Text style={styles.logo}>🍎</Text>
|
|
20
|
-
<Text style={styles.title}>MCP Expo</Text>
|
|
21
|
-
<Text style={styles.subtitle}>
|
|
22
|
-
AI-generated code will replace this file via the{' '}
|
|
23
|
-
<Text style={styles.bold}>push_screen_and_run</Text> MCP tool.
|
|
24
|
-
</Text>
|
|
51
|
+
<StatusBar barStyle="dark-content" />
|
|
52
|
+
<View style={styles.header}>
|
|
53
|
+
<Text style={styles.title}>Text</Text>
|
|
54
|
+
<Text style={styles.subtitle}>@momo-kits/foundation</Text>
|
|
55
|
+
</View>
|
|
25
56
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<Text style={styles.cardText}>
|
|
29
|
-
This is a working Expo template. When the MCP server writes new code,
|
|
30
|
-
it overwrites this App.tsx and the app reloads via HMR.
|
|
31
|
-
</Text>
|
|
32
|
-
</View>
|
|
33
|
-
|
|
34
|
-
<View style={styles.buttonGroup}>
|
|
35
|
-
<TouchableOpacity style={styles.button} activeOpacity={0.7}>
|
|
36
|
-
<Text style={styles.buttonText}>Primary</Text>
|
|
37
|
-
</TouchableOpacity>
|
|
57
|
+
<View style={styles.tabRow}>
|
|
58
|
+
{(['typography', 'color'] as Tab[]).map(tab => (
|
|
38
59
|
<TouchableOpacity
|
|
39
|
-
|
|
40
|
-
|
|
60
|
+
key={tab}
|
|
61
|
+
style={[styles.tab, activeTab === tab && styles.tabActive]}
|
|
62
|
+
onPress={() => setActiveTab(tab)}
|
|
41
63
|
>
|
|
42
|
-
<Text style={[styles.
|
|
43
|
-
|
|
64
|
+
<Text style={[styles.tabText, activeTab === tab && styles.tabTextActive]}>
|
|
65
|
+
{tab.charAt(0).toUpperCase() + tab.slice(1)}
|
|
44
66
|
</Text>
|
|
45
67
|
</TouchableOpacity>
|
|
46
|
-
|
|
68
|
+
))}
|
|
69
|
+
</View>
|
|
47
70
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
<
|
|
51
|
-
|
|
71
|
+
<ScrollView contentContainerStyle={styles.content}>
|
|
72
|
+
{activeTab === 'typography' && (
|
|
73
|
+
<View>
|
|
74
|
+
<Text style={styles.sectionTitle}>Typography Scale</Text>
|
|
75
|
+
{Object.entries(Typography).map(([name, style]) => (
|
|
76
|
+
<View key={name} style={styles.typoCard}>
|
|
77
|
+
<Text
|
|
78
|
+
style={{
|
|
79
|
+
fontSize: style.fontSize,
|
|
80
|
+
fontWeight: style.fontWeight as any,
|
|
81
|
+
lineHeight: style.lineHeight,
|
|
82
|
+
textDecorationLine: (style.textDecorationLine as any) || 'none',
|
|
83
|
+
color: '#1A1A1A',
|
|
84
|
+
}}
|
|
85
|
+
>
|
|
86
|
+
{name}
|
|
87
|
+
</Text>
|
|
88
|
+
<Text style={styles.meta}>
|
|
89
|
+
{style.fontSize}px / {style.fontWeight} / LH {style.lineHeight}
|
|
90
|
+
{style.textDecorationLine ? ' / strikethrough' : ''}
|
|
91
|
+
</Text>
|
|
92
|
+
</View>
|
|
93
|
+
))}
|
|
94
|
+
</View>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{activeTab === 'color' && (
|
|
98
|
+
<View>
|
|
99
|
+
<Text style={styles.sectionTitle}>Text Colors</Text>
|
|
100
|
+
{TextColors.map(item => (
|
|
101
|
+
<View key={item.name} style={styles.colorCard}>
|
|
102
|
+
<View style={styles.colorRow}>
|
|
103
|
+
<View style={[styles.colorDot, { backgroundColor: item.color }]} />
|
|
104
|
+
<View style={styles.colorInfo}>
|
|
105
|
+
<Text style={[styles.colorSample, { color: item.color }]}>
|
|
106
|
+
The quick brown fox jumps over the lazy dog
|
|
107
|
+
</Text>
|
|
108
|
+
<Text style={styles.colorMeta}>
|
|
109
|
+
{item.name} — {item.color}
|
|
110
|
+
</Text>
|
|
111
|
+
</View>
|
|
112
|
+
</View>
|
|
113
|
+
</View>
|
|
114
|
+
))}
|
|
115
|
+
</View>
|
|
116
|
+
)}
|
|
52
117
|
</ScrollView>
|
|
53
118
|
</View>
|
|
54
119
|
);
|
|
55
120
|
}
|
|
56
121
|
|
|
57
122
|
const styles = StyleSheet.create({
|
|
58
|
-
container: {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
fontWeight: '700',
|
|
76
|
-
color: '#111111',
|
|
77
|
-
marginBottom: 8,
|
|
78
|
-
},
|
|
79
|
-
subtitle: {
|
|
80
|
-
fontSize: 15,
|
|
81
|
-
color: '#666666',
|
|
82
|
-
textAlign: 'center',
|
|
83
|
-
lineHeight: 22,
|
|
84
|
-
marginBottom: 32,
|
|
85
|
-
paddingHorizontal: 8,
|
|
123
|
+
container: { flex: 1, backgroundColor: '#F8F8F8' },
|
|
124
|
+
header: {
|
|
125
|
+
paddingTop: Platform.OS === 'ios' ? 60 : 48,
|
|
126
|
+
paddingHorizontal: 20,
|
|
127
|
+
paddingBottom: 12,
|
|
128
|
+
backgroundColor: '#fff',
|
|
129
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
130
|
+
borderBottomColor: '#E0E0E0',
|
|
131
|
+
},
|
|
132
|
+
title: { fontSize: 28, fontWeight: '700', color: '#1A1A1A' },
|
|
133
|
+
subtitle: { fontSize: 13, color: '#999', marginTop: 2 },
|
|
134
|
+
tabRow: {
|
|
135
|
+
flexDirection: 'row',
|
|
136
|
+
backgroundColor: '#fff',
|
|
137
|
+
paddingHorizontal: 16,
|
|
138
|
+
paddingBottom: 8,
|
|
139
|
+
gap: 8,
|
|
86
140
|
},
|
|
87
|
-
|
|
141
|
+
tab: {
|
|
142
|
+
paddingVertical: 8,
|
|
143
|
+
paddingHorizontal: 16,
|
|
144
|
+
borderRadius: 20,
|
|
145
|
+
backgroundColor: '#F0F0F0',
|
|
146
|
+
},
|
|
147
|
+
tabActive: { backgroundColor: '#D82D8B' },
|
|
148
|
+
tabText: { fontSize: 14, fontWeight: '500', color: '#666' },
|
|
149
|
+
tabTextActive: { color: '#fff' },
|
|
150
|
+
content: { padding: 16, paddingBottom: 40 },
|
|
151
|
+
sectionTitle: {
|
|
152
|
+
fontSize: 18,
|
|
88
153
|
fontWeight: '600',
|
|
89
|
-
color: '#
|
|
154
|
+
color: '#1A1A1A',
|
|
155
|
+
marginBottom: 12,
|
|
90
156
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
padding: 20,
|
|
96
|
-
marginBottom: 24,
|
|
97
|
-
shadowColor: '#000',
|
|
98
|
-
shadowOffset: { width: 0, height: 2 },
|
|
99
|
-
shadowOpacity: 0.06,
|
|
100
|
-
shadowRadius: 8,
|
|
101
|
-
elevation: 2,
|
|
102
|
-
},
|
|
103
|
-
cardTitle: {
|
|
104
|
-
fontSize: 16,
|
|
105
|
-
fontWeight: '600',
|
|
106
|
-
color: '#111111',
|
|
157
|
+
typoCard: {
|
|
158
|
+
backgroundColor: '#fff',
|
|
159
|
+
borderRadius: 12,
|
|
160
|
+
padding: 16,
|
|
107
161
|
marginBottom: 8,
|
|
108
162
|
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
lineHeight: 20,
|
|
113
|
-
},
|
|
114
|
-
buttonGroup: {
|
|
115
|
-
flexDirection: 'row',
|
|
116
|
-
gap: 12,
|
|
117
|
-
marginBottom: 32,
|
|
118
|
-
},
|
|
119
|
-
button: {
|
|
120
|
-
flex: 1,
|
|
121
|
-
backgroundColor: '#111111',
|
|
163
|
+
meta: { fontSize: 11, color: '#999', marginTop: 6 },
|
|
164
|
+
colorCard: {
|
|
165
|
+
backgroundColor: '#fff',
|
|
122
166
|
borderRadius: 12,
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
},
|
|
126
|
-
buttonOutline: {
|
|
127
|
-
backgroundColor: 'transparent',
|
|
128
|
-
borderWidth: 1.5,
|
|
129
|
-
borderColor: '#111111',
|
|
130
|
-
},
|
|
131
|
-
buttonText: {
|
|
132
|
-
fontSize: 15,
|
|
133
|
-
fontWeight: '600',
|
|
134
|
-
color: '#ffffff',
|
|
135
|
-
},
|
|
136
|
-
buttonTextOutline: {
|
|
137
|
-
color: '#111111',
|
|
138
|
-
},
|
|
139
|
-
statusRow: {
|
|
140
|
-
flexDirection: 'row',
|
|
141
|
-
alignItems: 'center',
|
|
142
|
-
gap: 8,
|
|
143
|
-
},
|
|
144
|
-
dot: {
|
|
145
|
-
width: 8,
|
|
146
|
-
height: 8,
|
|
147
|
-
borderRadius: 4,
|
|
148
|
-
backgroundColor: '#22c55e',
|
|
149
|
-
},
|
|
150
|
-
statusText: {
|
|
151
|
-
fontSize: 13,
|
|
152
|
-
color: '#888888',
|
|
167
|
+
padding: 16,
|
|
168
|
+
marginBottom: 8,
|
|
153
169
|
},
|
|
170
|
+
colorRow: { flexDirection: 'row', alignItems: 'center' },
|
|
171
|
+
colorDot: { width: 32, height: 32, borderRadius: 16, marginRight: 12 },
|
|
172
|
+
colorInfo: { flex: 1 },
|
|
173
|
+
colorSample: { fontSize: 15, fontWeight: '500', lineHeight: 22 },
|
|
174
|
+
colorMeta: { fontSize: 11, color: '#999', marginTop: 4 },
|
|
154
175
|
});
|