@tmhs/mobile-mcp 0.7.0 → 0.9.0
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/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/tools/addMap.d.ts +3 -0
- package/dist/tools/addMap.d.ts.map +1 -0
- package/dist/tools/addMap.js +223 -0
- package/dist/tools/addMap.js.map +1 -0
- package/dist/tools/generateForm.d.ts +3 -0
- package/dist/tools/generateForm.d.ts.map +1 -0
- package/dist/tools/generateForm.js +337 -0
- package/dist/tools/generateForm.js.map +1 -0
- package/dist/tools/generateTestFile.d.ts +3 -0
- package/dist/tools/generateTestFile.d.ts.map +1 -0
- package/dist/tools/generateTestFile.js +184 -0
- package/dist/tools/generateTestFile.js.map +1 -0
- package/dist/tools/runTests.d.ts +3 -0
- package/dist/tools/runTests.d.ts.map +1 -0
- package/dist/tools/runTests.js +165 -0
- package/dist/tools/runTests.js.map +1 -0
- package/dist/tools/setupCI.d.ts +3 -0
- package/dist/tools/setupCI.d.ts.map +1 -0
- package/dist/tools/setupCI.js +202 -0
- package/dist/tools/setupCI.js.map +1 -0
- package/dist/tools/setupI18n.d.ts +3 -0
- package/dist/tools/setupI18n.d.ts.map +1 -0
- package/dist/tools/setupI18n.js +155 -0
- package/dist/tools/setupI18n.js.map +1 -0
- package/dist/tools/setupRealtime.d.ts +3 -0
- package/dist/tools/setupRealtime.d.ts.map +1 -0
- package/dist/tools/setupRealtime.js +311 -0
- package/dist/tools/setupRealtime.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { textResponse, errorResponse } from "../types.js";
|
|
5
|
+
const inputSchema = {
|
|
6
|
+
project_path: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Absolute path to the project root. Defaults to cwd."),
|
|
10
|
+
framework: z
|
|
11
|
+
.enum(["expo", "flutter"])
|
|
12
|
+
.optional()
|
|
13
|
+
.default("expo")
|
|
14
|
+
.describe("Project framework (default: expo)."),
|
|
15
|
+
default_locale: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.default("en")
|
|
19
|
+
.describe("Default locale code (default: en)."),
|
|
20
|
+
additional_locales: z
|
|
21
|
+
.array(z.string())
|
|
22
|
+
.optional()
|
|
23
|
+
.default([])
|
|
24
|
+
.describe("Additional locale codes to create placeholder files for (e.g. ['es', 'fr', 'de'])."),
|
|
25
|
+
};
|
|
26
|
+
function generateExpoI18nConfig(defaultLocale) {
|
|
27
|
+
return `import i18n from "i18next";
|
|
28
|
+
import { initReactI18next } from "react-i18next";
|
|
29
|
+
import { getLocales } from "expo-localization";
|
|
30
|
+
import ${defaultLocale} from "./${defaultLocale}.json";
|
|
31
|
+
|
|
32
|
+
const deviceLocale = getLocales()[0]?.languageCode ?? "${defaultLocale}";
|
|
33
|
+
|
|
34
|
+
i18n.use(initReactI18next).init({
|
|
35
|
+
resources: {
|
|
36
|
+
${defaultLocale}: { translation: ${defaultLocale} },
|
|
37
|
+
},
|
|
38
|
+
lng: deviceLocale,
|
|
39
|
+
fallbackLng: "${defaultLocale}",
|
|
40
|
+
interpolation: {
|
|
41
|
+
escapeValue: false,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export default i18n;
|
|
46
|
+
`;
|
|
47
|
+
}
|
|
48
|
+
function generateLocaleJson(locale) {
|
|
49
|
+
return JSON.stringify({
|
|
50
|
+
common: {
|
|
51
|
+
ok: locale === "en" ? "OK" : `[${locale}] OK`,
|
|
52
|
+
cancel: locale === "en" ? "Cancel" : `[${locale}] Cancel`,
|
|
53
|
+
save: locale === "en" ? "Save" : `[${locale}] Save`,
|
|
54
|
+
delete: locale === "en" ? "Delete" : `[${locale}] Delete`,
|
|
55
|
+
loading: locale === "en" ? "Loading..." : `[${locale}] Loading...`,
|
|
56
|
+
},
|
|
57
|
+
errors: {
|
|
58
|
+
generic: locale === "en" ? "Something went wrong" : `[${locale}] Something went wrong`,
|
|
59
|
+
network: locale === "en" ? "Check your internet connection" : `[${locale}] Check your internet connection`,
|
|
60
|
+
},
|
|
61
|
+
}, null, 2);
|
|
62
|
+
}
|
|
63
|
+
function generateFlutterL10nYaml(defaultLocale) {
|
|
64
|
+
return `arb-dir: lib/l10n
|
|
65
|
+
template-arb-file: app_${defaultLocale}.arb
|
|
66
|
+
output-localization-file: app_localizations.dart
|
|
67
|
+
output-class: AppLocalizations
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
function generateArbFile(locale) {
|
|
71
|
+
const data = {
|
|
72
|
+
"@@locale": locale,
|
|
73
|
+
ok: locale === "en" ? "OK" : `[${locale}] OK`,
|
|
74
|
+
"@ok": JSON.stringify({ description: "Generic OK button label" }),
|
|
75
|
+
cancel: locale === "en" ? "Cancel" : `[${locale}] Cancel`,
|
|
76
|
+
"@cancel": JSON.stringify({ description: "Generic Cancel button label" }),
|
|
77
|
+
save: locale === "en" ? "Save" : `[${locale}] Save`,
|
|
78
|
+
"@save": JSON.stringify({ description: "Generic Save button label" }),
|
|
79
|
+
errorGeneric: locale === "en" ? "Something went wrong" : `[${locale}] Something went wrong`,
|
|
80
|
+
"@errorGeneric": JSON.stringify({ description: "Generic error message" }),
|
|
81
|
+
};
|
|
82
|
+
return JSON.stringify(data, null, 2);
|
|
83
|
+
}
|
|
84
|
+
export function register(server) {
|
|
85
|
+
server.tool("mobile_setupI18n", "Initialize internationalization (i18n) config with locale files and translation structure. Supports i18next for Expo and flutter_localizations for Flutter.", inputSchema, async (args) => {
|
|
86
|
+
try {
|
|
87
|
+
const root = args.project_path || process.cwd();
|
|
88
|
+
const locales = [args.default_locale, ...args.additional_locales];
|
|
89
|
+
const filesCreated = [];
|
|
90
|
+
if (args.framework === "flutter") {
|
|
91
|
+
const l10nDir = join(root, "lib", "l10n");
|
|
92
|
+
mkdirSync(l10nDir, { recursive: true });
|
|
93
|
+
const l10nYamlPath = join(root, "l10n.yaml");
|
|
94
|
+
if (!existsSync(l10nYamlPath)) {
|
|
95
|
+
writeFileSync(l10nYamlPath, generateFlutterL10nYaml(args.default_locale), "utf-8");
|
|
96
|
+
filesCreated.push(l10nYamlPath);
|
|
97
|
+
}
|
|
98
|
+
for (const locale of locales) {
|
|
99
|
+
const arbPath = join(l10nDir, `app_${locale}.arb`);
|
|
100
|
+
if (!existsSync(arbPath)) {
|
|
101
|
+
writeFileSync(arbPath, generateArbFile(locale), "utf-8");
|
|
102
|
+
filesCreated.push(arbPath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return textResponse(JSON.stringify({
|
|
106
|
+
success: true,
|
|
107
|
+
framework: "flutter",
|
|
108
|
+
files_created: filesCreated,
|
|
109
|
+
default_locale: args.default_locale,
|
|
110
|
+
locales,
|
|
111
|
+
next_steps: [
|
|
112
|
+
"Add flutter_localizations to pubspec.yaml dependencies",
|
|
113
|
+
"Add generate: true to pubspec.yaml",
|
|
114
|
+
"Import and add AppLocalizations.delegate to MaterialApp localizationsDelegates",
|
|
115
|
+
"Add AppLocalizations.supportedLocales to MaterialApp supportedLocales",
|
|
116
|
+
"Run flutter gen-l10n to generate the dart files",
|
|
117
|
+
"Use AppLocalizations.of(context)!.ok in your widgets",
|
|
118
|
+
],
|
|
119
|
+
}, null, 2));
|
|
120
|
+
}
|
|
121
|
+
const i18nDir = join(root, "i18n");
|
|
122
|
+
mkdirSync(i18nDir, { recursive: true });
|
|
123
|
+
for (const locale of locales) {
|
|
124
|
+
const localePath = join(i18nDir, `${locale}.json`);
|
|
125
|
+
if (!existsSync(localePath)) {
|
|
126
|
+
writeFileSync(localePath, generateLocaleJson(locale) + "\n", "utf-8");
|
|
127
|
+
filesCreated.push(localePath);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const configPath = join(i18nDir, "index.ts");
|
|
131
|
+
if (!existsSync(configPath)) {
|
|
132
|
+
writeFileSync(configPath, generateExpoI18nConfig(args.default_locale), "utf-8");
|
|
133
|
+
filesCreated.push(configPath);
|
|
134
|
+
}
|
|
135
|
+
return textResponse(JSON.stringify({
|
|
136
|
+
success: true,
|
|
137
|
+
framework: "expo",
|
|
138
|
+
files_created: filesCreated,
|
|
139
|
+
default_locale: args.default_locale,
|
|
140
|
+
locales,
|
|
141
|
+
next_steps: [
|
|
142
|
+
"Install dependencies: npx expo install i18next react-i18next expo-localization",
|
|
143
|
+
'Import i18n config in your app entry: import "./i18n"',
|
|
144
|
+
'Use translations: const { t } = useTranslation(); t("common.ok")',
|
|
145
|
+
"Add more locale files to i18n/ for each supported language",
|
|
146
|
+
"Consider i18next-parser for extracting strings from code",
|
|
147
|
+
],
|
|
148
|
+
}, null, 2));
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
return errorResponse(err);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=setupI18n.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupI18n.js","sourceRoot":"","sources":["../../src/tools/setupI18n.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupRealtime.d.ts","sourceRoot":"","sources":["../../src/tools/setupRealtime.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAiQzE,wBAAgB,QAAQ,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAyFhD"}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { textResponse, errorResponse } from "../types.js";
|
|
5
|
+
const inputSchema = {
|
|
6
|
+
project_path: z
|
|
7
|
+
.string()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe("Absolute path to the project root. Defaults to cwd."),
|
|
10
|
+
provider: z
|
|
11
|
+
.enum(["websocket", "supabase", "socketio"])
|
|
12
|
+
.optional()
|
|
13
|
+
.default("websocket")
|
|
14
|
+
.describe("Real-time provider (default: websocket)."),
|
|
15
|
+
output_directory: z
|
|
16
|
+
.string()
|
|
17
|
+
.optional()
|
|
18
|
+
.default("lib")
|
|
19
|
+
.describe("Output directory relative to project root (default: lib)."),
|
|
20
|
+
};
|
|
21
|
+
function generateWebSocketClient() {
|
|
22
|
+
return `type MessageHandler = (data: unknown) => void;
|
|
23
|
+
|
|
24
|
+
interface WebSocketClientOptions {
|
|
25
|
+
url: string;
|
|
26
|
+
reconnect?: boolean;
|
|
27
|
+
maxRetries?: number;
|
|
28
|
+
onOpen?: () => void;
|
|
29
|
+
onClose?: () => void;
|
|
30
|
+
onError?: (error: Event) => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class RealtimeClient {
|
|
34
|
+
private ws: WebSocket | null = null;
|
|
35
|
+
private handlers = new Map<string, Set<MessageHandler>>();
|
|
36
|
+
private retryCount = 0;
|
|
37
|
+
private url: string;
|
|
38
|
+
private options: WebSocketClientOptions;
|
|
39
|
+
|
|
40
|
+
constructor(options: WebSocketClientOptions) {
|
|
41
|
+
this.url = options.url;
|
|
42
|
+
this.options = {
|
|
43
|
+
reconnect: true,
|
|
44
|
+
maxRetries: 5,
|
|
45
|
+
...options,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
connect(): void {
|
|
50
|
+
this.ws = new WebSocket(this.url);
|
|
51
|
+
|
|
52
|
+
this.ws.onopen = () => {
|
|
53
|
+
this.retryCount = 0;
|
|
54
|
+
this.options.onOpen?.();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.ws.onmessage = (event) => {
|
|
58
|
+
try {
|
|
59
|
+
const message = JSON.parse(event.data as string) as {
|
|
60
|
+
type: string;
|
|
61
|
+
payload: unknown;
|
|
62
|
+
};
|
|
63
|
+
const listeners = this.handlers.get(message.type);
|
|
64
|
+
if (listeners) {
|
|
65
|
+
for (const handler of listeners) {
|
|
66
|
+
handler(message.payload);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Non-JSON message, ignore
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
this.ws.onclose = () => {
|
|
75
|
+
this.options.onClose?.();
|
|
76
|
+
if (this.options.reconnect && this.retryCount < (this.options.maxRetries ?? 5)) {
|
|
77
|
+
const delay = Math.min(1000 * 2 ** this.retryCount, 30000);
|
|
78
|
+
this.retryCount++;
|
|
79
|
+
setTimeout(() => this.connect(), delay);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
this.ws.onerror = (error) => {
|
|
84
|
+
this.options.onError?.(error);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
on(event: string, handler: MessageHandler): () => void {
|
|
89
|
+
if (!this.handlers.has(event)) {
|
|
90
|
+
this.handlers.set(event, new Set());
|
|
91
|
+
}
|
|
92
|
+
this.handlers.get(event)!.add(handler);
|
|
93
|
+
|
|
94
|
+
return () => {
|
|
95
|
+
this.handlers.get(event)?.delete(handler);
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
send(type: string, payload: unknown): void {
|
|
100
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
101
|
+
this.ws.send(JSON.stringify({ type, payload }));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
disconnect(): void {
|
|
106
|
+
this.options.reconnect = false;
|
|
107
|
+
this.ws?.close();
|
|
108
|
+
this.ws = null;
|
|
109
|
+
this.handlers.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
function generateSupabaseRealtimeClient() {
|
|
115
|
+
return `import { createClient, type RealtimeChannel } from "@supabase/supabase-js";
|
|
116
|
+
|
|
117
|
+
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL!;
|
|
118
|
+
const supabaseAnonKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY!;
|
|
119
|
+
|
|
120
|
+
const supabase = createClient(supabaseUrl, supabaseAnonKey);
|
|
121
|
+
|
|
122
|
+
type ChangeHandler<T> = (payload: {
|
|
123
|
+
eventType: "INSERT" | "UPDATE" | "DELETE";
|
|
124
|
+
new: T;
|
|
125
|
+
old: T;
|
|
126
|
+
}) => void;
|
|
127
|
+
|
|
128
|
+
type PresenceHandler = (state: Record<string, unknown[]>) => void;
|
|
129
|
+
|
|
130
|
+
export class RealtimeClient {
|
|
131
|
+
private channels = new Map<string, RealtimeChannel>();
|
|
132
|
+
|
|
133
|
+
subscribeToTable<T extends Record<string, unknown>>(
|
|
134
|
+
table: string,
|
|
135
|
+
onChange: ChangeHandler<T>,
|
|
136
|
+
filter?: string,
|
|
137
|
+
): () => void {
|
|
138
|
+
const channel = supabase
|
|
139
|
+
.channel(\`table-\${table}\`)
|
|
140
|
+
.on(
|
|
141
|
+
"postgres_changes" as any,
|
|
142
|
+
{
|
|
143
|
+
event: "*",
|
|
144
|
+
schema: "public",
|
|
145
|
+
table,
|
|
146
|
+
...(filter ? { filter } : {}),
|
|
147
|
+
},
|
|
148
|
+
(payload: any) => {
|
|
149
|
+
onChange({
|
|
150
|
+
eventType: payload.eventType,
|
|
151
|
+
new: payload.new as T,
|
|
152
|
+
old: payload.old as T,
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
)
|
|
156
|
+
.subscribe();
|
|
157
|
+
|
|
158
|
+
this.channels.set(table, channel);
|
|
159
|
+
|
|
160
|
+
return () => {
|
|
161
|
+
supabase.removeChannel(channel);
|
|
162
|
+
this.channels.delete(table);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
joinPresence(
|
|
167
|
+
roomId: string,
|
|
168
|
+
userState: Record<string, unknown>,
|
|
169
|
+
onSync: PresenceHandler,
|
|
170
|
+
): () => void {
|
|
171
|
+
const channel = supabase
|
|
172
|
+
.channel(\`presence-\${roomId}\`)
|
|
173
|
+
.on("presence", { event: "sync" }, () => {
|
|
174
|
+
const state = channel.presenceState();
|
|
175
|
+
onSync(state);
|
|
176
|
+
})
|
|
177
|
+
.subscribe(async (status) => {
|
|
178
|
+
if (status === "SUBSCRIBED") {
|
|
179
|
+
await channel.track(userState);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
this.channels.set(\`presence-\${roomId}\`, channel);
|
|
184
|
+
|
|
185
|
+
return () => {
|
|
186
|
+
supabase.removeChannel(channel);
|
|
187
|
+
this.channels.delete(\`presence-\${roomId}\`);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
disconnectAll(): void {
|
|
192
|
+
for (const channel of this.channels.values()) {
|
|
193
|
+
supabase.removeChannel(channel);
|
|
194
|
+
}
|
|
195
|
+
this.channels.clear();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
`;
|
|
199
|
+
}
|
|
200
|
+
function generateSocketIOClient() {
|
|
201
|
+
return `import { io, type Socket } from "socket.io-client";
|
|
202
|
+
|
|
203
|
+
type EventHandler = (...args: unknown[]) => void;
|
|
204
|
+
|
|
205
|
+
interface SocketClientOptions {
|
|
206
|
+
url: string;
|
|
207
|
+
auth?: Record<string, string>;
|
|
208
|
+
reconnect?: boolean;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export class RealtimeClient {
|
|
212
|
+
private socket: Socket | null = null;
|
|
213
|
+
private options: SocketClientOptions;
|
|
214
|
+
|
|
215
|
+
constructor(options: SocketClientOptions) {
|
|
216
|
+
this.options = options;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
connect(): void {
|
|
220
|
+
this.socket = io(this.options.url, {
|
|
221
|
+
auth: this.options.auth,
|
|
222
|
+
reconnection: this.options.reconnect ?? true,
|
|
223
|
+
reconnectionAttempts: 5,
|
|
224
|
+
reconnectionDelay: 1000,
|
|
225
|
+
reconnectionDelayMax: 30000,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
on(event: string, handler: EventHandler): () => void {
|
|
230
|
+
this.socket?.on(event, handler);
|
|
231
|
+
return () => {
|
|
232
|
+
this.socket?.off(event, handler);
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
emit(event: string, ...args: unknown[]): void {
|
|
237
|
+
this.socket?.emit(event, ...args);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
joinRoom(room: string): void {
|
|
241
|
+
this.socket?.emit("join", room);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
leaveRoom(room: string): void {
|
|
245
|
+
this.socket?.emit("leave", room);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
disconnect(): void {
|
|
249
|
+
this.socket?.disconnect();
|
|
250
|
+
this.socket = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
export function register(server) {
|
|
256
|
+
server.tool("mobile_setupRealtime", "Add a real-time client module with connection management, event subscriptions, reconnection, and cleanup. Supports raw WebSocket, Supabase Realtime, and Socket.IO.", inputSchema, async (args) => {
|
|
257
|
+
try {
|
|
258
|
+
const root = args.project_path || process.cwd();
|
|
259
|
+
const outDir = join(root, args.output_directory);
|
|
260
|
+
mkdirSync(outDir, { recursive: true });
|
|
261
|
+
const fileName = "realtime-client.ts";
|
|
262
|
+
const filePath = join(outDir, fileName);
|
|
263
|
+
if (existsSync(filePath)) {
|
|
264
|
+
return errorResponse(new Error(`File already exists: ${filePath}`));
|
|
265
|
+
}
|
|
266
|
+
let content;
|
|
267
|
+
let dependencies;
|
|
268
|
+
switch (args.provider) {
|
|
269
|
+
case "supabase":
|
|
270
|
+
content = generateSupabaseRealtimeClient();
|
|
271
|
+
dependencies = ["@supabase/supabase-js"];
|
|
272
|
+
break;
|
|
273
|
+
case "socketio":
|
|
274
|
+
content = generateSocketIOClient();
|
|
275
|
+
dependencies = ["socket.io-client"];
|
|
276
|
+
break;
|
|
277
|
+
default:
|
|
278
|
+
content = generateWebSocketClient();
|
|
279
|
+
dependencies = [];
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
writeFileSync(filePath, content, "utf-8");
|
|
283
|
+
const nextSteps = [];
|
|
284
|
+
if (dependencies.length > 0) {
|
|
285
|
+
nextSteps.push(`Install dependencies: npx expo install ${dependencies.join(" ")}`);
|
|
286
|
+
}
|
|
287
|
+
switch (args.provider) {
|
|
288
|
+
case "supabase":
|
|
289
|
+
nextSteps.push("Set EXPO_PUBLIC_SUPABASE_URL and EXPO_PUBLIC_SUPABASE_ANON_KEY in .env", "Enable Realtime on your table in the Supabase dashboard", "Use subscribeToTable() to listen for INSERT/UPDATE/DELETE events", "Use joinPresence() for user presence and typing indicators");
|
|
290
|
+
break;
|
|
291
|
+
case "socketio":
|
|
292
|
+
nextSteps.push("Create a new RealtimeClient({ url: 'wss://your-server' })", "Call connect() to establish the connection", "Use on() to subscribe and emit() to send events", "Call disconnect() in your cleanup/unmount");
|
|
293
|
+
break;
|
|
294
|
+
default:
|
|
295
|
+
nextSteps.push("Create a new RealtimeClient({ url: 'wss://your-server/ws' })", "Call connect() to establish the WebSocket connection", "Use on() to subscribe to typed events and send() to publish", "Call disconnect() in your cleanup/unmount", "Reconnection with exponential backoff is built in");
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
return textResponse(JSON.stringify({
|
|
299
|
+
success: true,
|
|
300
|
+
provider: args.provider,
|
|
301
|
+
file_created: filePath,
|
|
302
|
+
dependencies_needed: dependencies.length > 0 ? dependencies : undefined,
|
|
303
|
+
next_steps: nextSteps,
|
|
304
|
+
}, null, 2));
|
|
305
|
+
}
|
|
306
|
+
catch (err) {
|
|
307
|
+
return errorResponse(err);
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=setupRealtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"setupRealtime.js","sourceRoot":"","sources":["../../src/tools/setupRealtime.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmhs/mobile-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for mobile app development -
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "MCP server for mobile app development - 26 tools for environment checks, project scaffolding, device deployment, screen/component generation, dependency installation, permissions, AI integration, build health, push notifications, deep links, dev environment reset, store builds, metadata validation, App Store submission, Play Store submission, screenshot capture, bundle analysis, OTA update configuration, test execution, CI/CD setup, test file generation, i18n setup, map integration, form generation, and real-time client setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|