@momo-kits/mcp-expo 2.0.0-beta.1 → 2.0.0-beta.2
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/server.mjs +58 -1
- package/template/App.tsx +148 -141
package/package.json
CHANGED
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,161 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, ActivityIndicator, Alert } from 'react-native';
|
|
4
|
+
|
|
5
|
+
const Colors = {
|
|
6
|
+
primary: '#D82D8B',
|
|
7
|
+
danger: '#E53935',
|
|
8
|
+
white: '#FFFFFF',
|
|
9
|
+
black: '#1A1A1A',
|
|
10
|
+
textSecondary: '#6D6D6D',
|
|
11
|
+
textDisable: '#BDBDBD',
|
|
12
|
+
bgSurface: '#FFFFFF',
|
|
13
|
+
bgTonal: '#FCE4F0',
|
|
14
|
+
bgDisable: '#F0F0F0',
|
|
15
|
+
borderDefault: '#E0E0E0',
|
|
16
|
+
sectionBg: '#F5F5F5',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const SizeConfig: Record<string, any> = {
|
|
20
|
+
large: { height: 48, px: 16, minWidth: 128, radius: 8, fontSize: 16 },
|
|
21
|
+
medium: { height: 36, px: 12, minWidth: 80, radius: 8, fontSize: 14 },
|
|
22
|
+
small: { height: 28, px: 8, minWidth: 60, radius: 8, fontSize: 12 },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getTypeStyle(type: string) {
|
|
26
|
+
switch (type) {
|
|
27
|
+
case 'primary': return { bg: Colors.primary, text: Colors.white, border: '' };
|
|
28
|
+
case 'tonal': return { bg: Colors.bgTonal, text: Colors.primary, border: '' };
|
|
29
|
+
case 'secondary': return { bg: Colors.bgSurface, text: Colors.black, border: Colors.borderDefault };
|
|
30
|
+
case 'outline': return { bg: Colors.bgSurface, text: Colors.primary, border: Colors.primary };
|
|
31
|
+
case 'text': return { bg: 'transparent', text: Colors.primary, border: '' };
|
|
32
|
+
case 'danger': return { bg: Colors.danger, text: Colors.white, border: '' };
|
|
33
|
+
case 'disabled': return { bg: Colors.bgDisable, text: Colors.textDisable, border: '' };
|
|
34
|
+
default: return { bg: Colors.primary, text: Colors.white, border: '' };
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function KitButton(props: { title: string; type?: string; size?: string; full?: boolean; loading?: boolean; onPress?: () => void }) {
|
|
39
|
+
const { title = 'Button', type = 'primary', size = 'large', full = true, loading = false, onPress } = props;
|
|
40
|
+
const s = SizeConfig[size] || SizeConfig.large;
|
|
41
|
+
const t = getTypeStyle(type);
|
|
42
|
+
const isDisabled = type === 'disabled' || loading;
|
|
43
|
+
|
|
44
|
+
const containerStyle: any = {
|
|
45
|
+
height: s.height,
|
|
46
|
+
paddingHorizontal: s.px,
|
|
47
|
+
minWidth: s.minWidth,
|
|
48
|
+
borderRadius: s.radius,
|
|
49
|
+
backgroundColor: t.bg,
|
|
50
|
+
alignItems: 'center',
|
|
51
|
+
justifyContent: 'center',
|
|
52
|
+
flexDirection: 'row',
|
|
53
|
+
opacity: loading ? 0.75 : 1,
|
|
54
|
+
alignSelf: full ? 'stretch' : 'flex-start',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
if (t.border) {
|
|
58
|
+
containerStyle.borderWidth = 1;
|
|
59
|
+
containerStyle.borderColor = t.border;
|
|
60
|
+
}
|
|
61
|
+
|
|
12
62
|
return (
|
|
13
|
-
<
|
|
14
|
-
<
|
|
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>
|
|
25
|
-
|
|
26
|
-
<View style={styles.card}>
|
|
27
|
-
<Text style={styles.cardTitle}>Quick Test</Text>
|
|
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>
|
|
63
|
+
<TouchableOpacity activeOpacity={0.7} disabled={isDisabled} onPress={onPress} style={containerStyle}>
|
|
64
|
+
{loading && <ActivityIndicator size="small" color={t.text} style={{ marginRight: 8 }} />}
|
|
65
|
+
<Text style={{ color: t.text, fontSize: s.fontSize, fontWeight: '700' }}>{title}</Text>
|
|
66
|
+
</TouchableOpacity>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
33
69
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
70
|
+
function App() {
|
|
71
|
+
const [loadingId, setLoadingId] = useState('');
|
|
72
|
+
|
|
73
|
+
const simulateLoading = (id: string) => {
|
|
74
|
+
setLoadingId(id);
|
|
75
|
+
setTimeout(() => setLoadingId(''), 2000);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const types = ['primary', 'tonal', 'secondary', 'outline', 'text', 'danger', 'disabled'];
|
|
79
|
+
const sizes = ['large', 'medium', 'small'];
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<ScrollView style={{ flex: 1, backgroundColor: '#FFF' }} contentContainerStyle={{ padding: 20, paddingTop: 60 }}>
|
|
83
|
+
<Text style={{ fontSize: 28, fontWeight: '800', color: Colors.black, marginBottom: 4 }}>Button</Text>
|
|
84
|
+
<Text style={{ fontSize: 14, color: Colors.textSecondary, marginBottom: 24 }}>@momo-kits/foundation</Text>
|
|
47
85
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
<Text style={styles.sectionTitle}>TYPES</Text>
|
|
87
|
+
<View style={styles.section}>
|
|
88
|
+
{types.map((type) => (
|
|
89
|
+
<View key={type} style={{ marginBottom: 10 }}>
|
|
90
|
+
<KitButton title={type.charAt(0).toUpperCase() + type.slice(1)} type={type} onPress={() => Alert.alert('Pressed', type)} />
|
|
91
|
+
</View>
|
|
92
|
+
))}
|
|
93
|
+
</View>
|
|
94
|
+
|
|
95
|
+
<Text style={styles.sectionTitle}>SIZES</Text>
|
|
96
|
+
<View style={styles.section}>
|
|
97
|
+
{sizes.map((size) => (
|
|
98
|
+
<View key={size} style={{ marginBottom: 10 }}>
|
|
99
|
+
<KitButton title={size + ' (' + SizeConfig[size].height + 'px)'} size={size} onPress={() => Alert.alert('Size', size)} />
|
|
100
|
+
</View>
|
|
101
|
+
))}
|
|
102
|
+
</View>
|
|
103
|
+
|
|
104
|
+
<Text style={styles.sectionTitle}>WIDTH</Text>
|
|
105
|
+
<View style={styles.section}>
|
|
106
|
+
<View style={{ marginBottom: 10 }}>
|
|
107
|
+
<KitButton title="Full Width (default)" full={true} onPress={() => {}} />
|
|
51
108
|
</View>
|
|
52
|
-
|
|
53
|
-
|
|
109
|
+
<KitButton title="Auto Width" full={false} onPress={() => {}} />
|
|
110
|
+
</View>
|
|
111
|
+
|
|
112
|
+
<Text style={styles.sectionTitle}>LOADING STATE</Text>
|
|
113
|
+
<View style={styles.section}>
|
|
114
|
+
{['primary', 'secondary', 'danger'].map((type) => (
|
|
115
|
+
<View key={type} style={{ marginBottom: 10 }}>
|
|
116
|
+
<KitButton
|
|
117
|
+
title={loadingId === type ? 'Loading...' : 'Tap to load (' + type + ')'}
|
|
118
|
+
type={type}
|
|
119
|
+
loading={loadingId === type}
|
|
120
|
+
onPress={() => simulateLoading(type)}
|
|
121
|
+
/>
|
|
122
|
+
</View>
|
|
123
|
+
))}
|
|
124
|
+
</View>
|
|
125
|
+
|
|
126
|
+
<Text style={styles.sectionTitle}>SIZE x TYPE</Text>
|
|
127
|
+
<View style={styles.section}>
|
|
128
|
+
{sizes.map((size) => (
|
|
129
|
+
<View key={size}>
|
|
130
|
+
<Text style={{ fontSize: 12, fontWeight: '600', color: Colors.textSecondary, marginBottom: 6 }}>{size}</Text>
|
|
131
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginBottom: 12 }}>
|
|
132
|
+
{['primary', 'tonal', 'outline', 'danger'].map((type) => (
|
|
133
|
+
<KitButton key={size + type} title={type.slice(0, 3).toUpperCase()} type={type} size={size} full={false} onPress={() => {}} />
|
|
134
|
+
))}
|
|
135
|
+
</View>
|
|
136
|
+
</View>
|
|
137
|
+
))}
|
|
138
|
+
</View>
|
|
139
|
+
|
|
140
|
+
<View style={{ height: 48 }} />
|
|
141
|
+
</ScrollView>
|
|
54
142
|
);
|
|
55
143
|
}
|
|
56
144
|
|
|
145
|
+
export default App;
|
|
146
|
+
|
|
57
147
|
const styles = StyleSheet.create({
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
backgroundColor: '#f5f5f5',
|
|
61
|
-
},
|
|
62
|
-
content: {
|
|
63
|
-
flexGrow: 1,
|
|
64
|
-
paddingHorizontal: 24,
|
|
65
|
-
paddingTop: 48,
|
|
66
|
-
paddingBottom: 40,
|
|
67
|
-
alignItems: 'center',
|
|
68
|
-
},
|
|
69
|
-
logo: {
|
|
70
|
-
fontSize: 64,
|
|
71
|
-
marginBottom: 16,
|
|
72
|
-
},
|
|
73
|
-
title: {
|
|
74
|
-
fontSize: 28,
|
|
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,
|
|
86
|
-
},
|
|
87
|
-
bold: {
|
|
88
|
-
fontWeight: '600',
|
|
89
|
-
color: '#111111',
|
|
90
|
-
},
|
|
91
|
-
card: {
|
|
92
|
-
width: '100%',
|
|
93
|
-
backgroundColor: '#ffffff',
|
|
94
|
-
borderRadius: 16,
|
|
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,
|
|
148
|
+
sectionTitle: {
|
|
149
|
+
fontSize: 13,
|
|
105
150
|
fontWeight: '600',
|
|
106
|
-
color: '#
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
fontSize: 14,
|
|
111
|
-
color: '#666666',
|
|
112
|
-
lineHeight: 20,
|
|
113
|
-
},
|
|
114
|
-
buttonGroup: {
|
|
115
|
-
flexDirection: 'row',
|
|
116
|
-
gap: 12,
|
|
117
|
-
marginBottom: 32,
|
|
151
|
+
color: '#6D6D6D',
|
|
152
|
+
letterSpacing: 0.5,
|
|
153
|
+
marginTop: 24,
|
|
154
|
+
marginBottom: 12,
|
|
118
155
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
backgroundColor: '#111111',
|
|
156
|
+
section: {
|
|
157
|
+
backgroundColor: '#F5F5F5',
|
|
122
158
|
borderRadius: 12,
|
|
123
|
-
|
|
124
|
-
alignItems: 'center',
|
|
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',
|
|
159
|
+
padding: 16,
|
|
153
160
|
},
|
|
154
161
|
});
|