@chocodrop/mcp 0.1.0-alpha.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +19 -0
  3. package/bin/chocodrop-mcp.js +13 -0
  4. package/package.json +42 -0
  5. package/runtime/.chocodrop-mcp-runtime +1 -0
  6. package/runtime/asset-bridge.js +460 -0
  7. package/runtime/mcp-server.js +107 -0
  8. package/runtime/site/examples/basic/README.md +38 -0
  9. package/runtime/site/examples/basic/index.html +1730 -0
  10. package/runtime/site/examples/bookmarklet-v2.html +141 -0
  11. package/runtime/site/examples/bookmarklet.html +126 -0
  12. package/runtime/site/examples/lofi-room/index.html +2570 -0
  13. package/runtime/site/examples/lofi-room/sandbox-lite.html +124 -0
  14. package/runtime/site/examples/music-garden/index.html +2150 -0
  15. package/runtime/site/examples/pixel-ocean/index.html +1780 -0
  16. package/runtime/site/examples/react-three-fiber/AdvancedExample.jsx +338 -0
  17. package/runtime/site/examples/react-three-fiber/App.jsx +97 -0
  18. package/runtime/site/examples/react-three-fiber/README.md +294 -0
  19. package/runtime/site/examples/react-three-fiber/package.json +25 -0
  20. package/runtime/site/examples/sdk-test/README.md +70 -0
  21. package/runtime/site/examples/space/index.html +1975 -0
  22. package/runtime/site/examples/toy-city/index.html +2288 -0
  23. package/runtime/site/examples/wabi-sabi/index.html +2515 -0
  24. package/runtime/site/getting-started.html +84 -0
  25. package/runtime/site/index.html +1370 -0
  26. package/runtime/site/public/CommandUI.js +6341 -0
  27. package/runtime/site/public/LiveCommandClient.js +432 -0
  28. package/runtime/site/public/SceneManager.js +5493 -0
  29. package/runtime/site/public/bookmarklet.js +206 -0
  30. package/runtime/site/public/bootstrap.js +50 -0
  31. package/runtime/site/public/chocodrop-demo.umd.js +24020 -0
  32. package/runtime/site/public/chocodrop-demo.umd.min.js +24020 -0
  33. package/runtime/site/public/chocodrop.esm.js +2 -0
  34. package/runtime/site/public/html-sandbox/frame.js +865 -0
  35. package/runtime/site/public/icons/icon-192.png +0 -0
  36. package/runtime/site/public/icons/icon-512.png +0 -0
  37. package/runtime/site/public/icons/icon-maskable-512.png +0 -0
  38. package/runtime/site/public/immersive-launcher.js +175 -0
  39. package/runtime/site/public/immersive.html +165 -0
  40. package/runtime/site/public/index.html +1011 -0
  41. package/runtime/site/public/index.js +9 -0
  42. package/runtime/site/public/load-chocodrop.js +25 -0
  43. package/runtime/site/public/load-three.js +26 -0
  44. package/runtime/site/public/manifest.webmanifest +59 -0
  45. package/runtime/site/public/pwa-bootstrap.js +128 -0
  46. package/runtime/site/public/sample-card.svg +15 -0
  47. package/runtime/site/public/service-worker.js +117 -0
  48. package/runtime/site/public/translation-dictionary.js +257 -0
  49. package/runtime/site/public/xr/xr-ui.css +338 -0
  50. package/runtime/site/public/xr-viewer.html +263 -0
  51. package/runtime/site/src/client/local-bridge.js +83 -0
@@ -0,0 +1,432 @@
1
+ /**
2
+ * ChocoDrop Client - サーバーとの通信クライアント
3
+ */
4
+ export class ChocoDropClient {
5
+ constructor(serverUrl = null, sceneManager = null, options = {}) {
6
+ this.serverUrl = null;
7
+ this.sceneManager = sceneManager;
8
+ this.initialized = false;
9
+ this.initPromise = null;
10
+ this.enableServerHealthCheck = options.enableServerHealthCheck !== false; // デフォルトtrue
11
+
12
+ if (serverUrl) {
13
+ this.serverUrl = serverUrl;
14
+ this.initialized = true;
15
+ console.log('🍫 ChocoDropClient initialized:', serverUrl);
16
+ } else if (this.enableServerHealthCheck) {
17
+ // サーバーヘルスチェックが有効な場合のみ設定取得を試みる
18
+ this.initPromise = this.initializeWithConfig();
19
+ } else {
20
+ // サーバーヘルスチェック無効の場合はnullのまま(静的サイト用)
21
+ this.serverUrl = null;
22
+ this.initialized = true;
23
+ console.log('🍫 ChocoDropClient initialized without server (static site mode)');
24
+ }
25
+ }
26
+
27
+ /**
28
+ * サーバーから設定を取得して初期化
29
+ */
30
+ async initializeWithConfig() {
31
+ try {
32
+ // 現在のページのホストとポートを基準に設定API呼び出し
33
+ const baseUrl = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
34
+
35
+ const response = await fetch(`${baseUrl}/api/config`);
36
+ if (response.ok) {
37
+ const config = await response.json();
38
+ this.serverUrl = config.serverUrl;
39
+ console.log('🍫 ChocoDropClient initialized from config:', this.serverUrl);
40
+ } else {
41
+ // フォールバック:ポート推測
42
+ this.serverUrl = this.detectServerUrl();
43
+ console.log('🍫 ChocoDropClient fallback to detected URL:', this.serverUrl);
44
+ }
45
+ } catch (error) {
46
+ console.warn('⚠️ ChocoDrop config fetch failed, using fallback:', error);
47
+ this.serverUrl = this.detectServerUrl();
48
+ }
49
+
50
+ this.initialized = true;
51
+ }
52
+
53
+ /**
54
+ * サーバーURL自動検出(フォールバック)
55
+ */
56
+ detectServerUrl() {
57
+ const currentPort = window.location.port;
58
+ const protocol = window.location.protocol;
59
+ const hostname = window.location.hostname;
60
+
61
+ // ポートが未指定の場合(ファイルプロトコル等)は既定の 3011 を使用
62
+ if (!currentPort) {
63
+ return `${protocol}//${hostname}:3011`;
64
+ }
65
+
66
+ return `${protocol}//${hostname}:${currentPort}`;
67
+ }
68
+
69
+ /**
70
+ * 初期化完了を待機
71
+ */
72
+ async ensureInitialized() {
73
+ if (this.initialized) return;
74
+
75
+ // initPromiseがあれば待機
76
+ if (this.initPromise) {
77
+ await this.initPromise;
78
+ return;
79
+ }
80
+
81
+ // フォールバック:初期化されていない場合はエラー
82
+ throw new Error('ChocoDropClient not initialized');
83
+ }
84
+
85
+ /**
86
+ * ネットワークエラーを検出して利用者向けメッセージに変換
87
+ */
88
+ createConnectionError(context) {
89
+ const serverInfo = this.serverUrl ? `(接続先: ${this.serverUrl})` : '';
90
+ const hint = 'ChocoDrop ローカルサーバー(Express)が起動しているか確認してください(例: `npm run dev`)。';
91
+ return new Error(`${context}\nサーバーへ接続できません。${hint}${serverInfo}`);
92
+ }
93
+
94
+ isNetworkError(error) {
95
+ if (!error) return false;
96
+ const message = typeof error.message === 'string' ? error.message : '';
97
+ return (
98
+ error.name === 'TypeError' ||
99
+ message.includes('Failed to fetch') ||
100
+ message.includes('NetworkError') ||
101
+ message.includes('connect ECONNREFUSED') ||
102
+ message.includes('ERR_CONNECTION')
103
+ );
104
+ }
105
+
106
+ handleRequestError(error, context) {
107
+ if (this.isNetworkError(error)) {
108
+ const connectionError = this.createConnectionError(context);
109
+ connectionError.code = 'LOCAL_SERVER_UNREACHABLE';
110
+ connectionError.cause = error;
111
+ return connectionError;
112
+ }
113
+ if (error instanceof Error) {
114
+ return error;
115
+ }
116
+ return new Error(context);
117
+ }
118
+
119
+ /**
120
+ * 画像生成リクエスト
121
+ */
122
+ async generateImage(prompt, options = {}) {
123
+ await this.ensureInitialized();
124
+ console.log(`🎨 Requesting image generation: "${prompt}"`);
125
+
126
+ try {
127
+ const payload = {
128
+ prompt,
129
+ width: options.width || 512,
130
+ height: options.height || 512
131
+ };
132
+
133
+ if (options.service) {
134
+ payload.service = options.service;
135
+ }
136
+
137
+ const response = await fetch(`${this.serverUrl}/api/generate`, {
138
+ method: 'POST',
139
+ headers: {
140
+ 'Content-Type': 'application/json'
141
+ },
142
+ body: JSON.stringify(payload)
143
+ });
144
+
145
+ if (!response.ok) {
146
+ let errorPayload = null;
147
+ try {
148
+ errorPayload = await response.json();
149
+ } catch (parseError) {
150
+ // ignore JSON parse errors
151
+ }
152
+ const serverError = new Error(errorPayload?.error || `Server error: ${response.status}`);
153
+ if (errorPayload?.errorCategory) {
154
+ serverError.code = errorPayload.errorCategory;
155
+ }
156
+ throw serverError;
157
+ }
158
+
159
+ const result = await response.json();
160
+ console.log('✅ Image generation result:', result);
161
+
162
+ return result;
163
+
164
+ } catch (error) {
165
+ console.error('❌ Image generation request failed:', error);
166
+ throw this.handleRequestError(error, '画像生成リクエストに失敗しました。');
167
+ }
168
+ }
169
+
170
+ /**
171
+ * 動画生成リクエスト
172
+ */
173
+ async generateVideo(prompt, options = {}) {
174
+ await this.ensureInitialized();
175
+ console.log(`🎬 Requesting video generation: "${prompt}"`);
176
+
177
+ try {
178
+ const safeDefaults = {
179
+ // aspect_ratio: サーバー側で各モデル最適な比率を自動選択
180
+ resolution: '720p',
181
+ enable_safety_checker: true,
182
+ enable_prompt_expansion: true
183
+ };
184
+
185
+ const payload = {
186
+ prompt,
187
+ duration: typeof options.duration === 'number' && options.duration > 0 ? options.duration : 3,
188
+ resolution: options.resolution || safeDefaults.resolution,
189
+ enable_safety_checker: options.enable_safety_checker ?? safeDefaults.enable_safety_checker,
190
+ enable_prompt_expansion: options.enable_prompt_expansion ?? safeDefaults.enable_prompt_expansion
191
+ };
192
+
193
+ // ユーザーが明示的にアスペクト比を指定した場合のみ追加
194
+ if (options.aspect_ratio) {
195
+ payload.aspect_ratio = options.aspect_ratio;
196
+ }
197
+ // それ以外はサーバー側で各モデルに最適な比率を自動選択
198
+
199
+ if (options.model) {
200
+ payload.model = options.model;
201
+ }
202
+
203
+ if (typeof options.width === 'number' && options.width > 0) {
204
+ payload.width = options.width;
205
+ }
206
+
207
+ if (typeof options.height === 'number' && options.height > 0) {
208
+ payload.height = options.height;
209
+ }
210
+
211
+ if (typeof options.seed === 'number') {
212
+ payload.seed = options.seed;
213
+ }
214
+
215
+ if (options.negative_prompt) {
216
+ payload.negative_prompt = options.negative_prompt;
217
+ }
218
+
219
+ if (typeof options.frames_per_second === 'number' && options.frames_per_second > 0) {
220
+ payload.frames_per_second = options.frames_per_second;
221
+ }
222
+
223
+ if (typeof options.guidance_scale === 'number') {
224
+ payload.guidance_scale = options.guidance_scale;
225
+ }
226
+
227
+ const response = await fetch(`${this.serverUrl}/api/generate-video`, {
228
+ method: 'POST',
229
+ headers: {
230
+ 'Content-Type': 'application/json'
231
+ },
232
+ body: JSON.stringify(payload)
233
+ });
234
+
235
+ if (!response.ok) {
236
+ let errorPayload = null;
237
+ try {
238
+ errorPayload = await response.json();
239
+ } catch (parseError) {
240
+ // ignore
241
+ }
242
+ const serverError = new Error(errorPayload?.error || `Server error: ${response.status}`);
243
+ if (errorPayload?.errorCategory) {
244
+ serverError.code = errorPayload.errorCategory;
245
+ }
246
+ throw serverError;
247
+ }
248
+
249
+ const result = await response.json();
250
+ console.log('✅ Video generation result:', result);
251
+
252
+ return result;
253
+
254
+ } catch (error) {
255
+ console.error('❌ Video generation request failed:', error);
256
+ throw this.handleRequestError(error, '動画生成リクエストに失敗しました。');
257
+ }
258
+ }
259
+
260
+ /**
261
+ * 自然言語コマンド実行
262
+ */
263
+ async executeCommand(command) {
264
+ await this.ensureInitialized();
265
+ console.log(`🎯 Executing command: "${command}"`);
266
+
267
+ try {
268
+ const response = await fetch(`${this.serverUrl}/api/command`, {
269
+ method: 'POST',
270
+ headers: {
271
+ 'Content-Type': 'application/json'
272
+ },
273
+ body: JSON.stringify({ command })
274
+ });
275
+
276
+ if (!response.ok) {
277
+ let errorPayload = null;
278
+ try {
279
+ errorPayload = await response.json();
280
+ } catch (parseError) {
281
+ // ignore
282
+ }
283
+ const serverError = new Error(errorPayload?.error || `Server error: ${response.status}`);
284
+ if (errorPayload?.errorCategory) {
285
+ serverError.code = errorPayload.errorCategory;
286
+ }
287
+ throw serverError;
288
+ }
289
+
290
+ const result = await response.json();
291
+ console.log('✅ Command execution result:', result);
292
+
293
+ return result;
294
+
295
+ } catch (error) {
296
+ console.error('❌ Command execution failed:', error);
297
+ throw this.handleRequestError(error, 'コマンド実行に失敗しました。');
298
+ }
299
+ }
300
+
301
+ /**
302
+ * 選択されたオブジェクトを変更
303
+ */
304
+ async modifySelectedObject(selectedObject, command) {
305
+ await this.ensureInitialized();
306
+ console.log(`🔧 Modifying selected object: "${command}"`);
307
+
308
+ try {
309
+ // SceneManagerの統合コマンド処理機能を使用
310
+ if (this.sceneManager) {
311
+ console.log('🎨 Using SceneManager integrated command processing');
312
+
313
+ // SceneManagerのparseCommandでコマンドを解析(変更モードを明示)
314
+ const trimmedCommand = typeof command === 'string' ? command.trim() : '';
315
+ const commandForParsing = trimmedCommand.startsWith('[変更]')
316
+ ? trimmedCommand
317
+ : `[変更] ${trimmedCommand}`;
318
+
319
+ const parsed = this.sceneManager.parseCommand(commandForParsing);
320
+ console.log('🔍 Parsed command result:', parsed);
321
+
322
+ if (parsed && (parsed.color !== null || (parsed.effects && parsed.effects.length > 0) || parsed.movement !== null)) {
323
+ // 選択されたオブジェクトに直接適用
324
+ let modified = false;
325
+
326
+ // 色変更
327
+ if (parsed.color !== null && selectedObject.material) {
328
+ if (selectedObject.material.map) {
329
+ selectedObject.material.color.setHex(parsed.color);
330
+ selectedObject.material.needsUpdate = true;
331
+ console.log(`🎨 Texture color tint changed to: #${parsed.color.toString(16)}`);
332
+ } else {
333
+ selectedObject.material.color.setHex(parsed.color);
334
+ selectedObject.material.needsUpdate = true;
335
+ console.log(`🎨 Material color changed to: #${parsed.color.toString(16)}`);
336
+ }
337
+ modified = true;
338
+ }
339
+
340
+ // エフェクト適用
341
+ if (parsed.effects && parsed.effects.length > 0) {
342
+ const effectsApplied = this.sceneManager.applyEffects(selectedObject, parsed.effects);
343
+ if (effectsApplied) {
344
+ modified = true;
345
+ }
346
+ }
347
+
348
+ // 位置移動
349
+ if (parsed.movement !== null) {
350
+ const currentPos = selectedObject.position;
351
+ const newPos = {
352
+ x: currentPos.x + parsed.movement.x,
353
+ y: currentPos.y + parsed.movement.y,
354
+ z: currentPos.z + parsed.movement.z
355
+ };
356
+ selectedObject.position.set(newPos.x, newPos.y, newPos.z);
357
+ console.log(`📍 Object moved to: (${newPos.x.toFixed(2)}, ${newPos.y.toFixed(2)}, ${newPos.z.toFixed(2)})`);
358
+ modified = true;
359
+ }
360
+
361
+ if (modified) {
362
+ console.log('✅ Object modification applied successfully');
363
+ return {
364
+ success: true,
365
+ message: 'オブジェクトを変更しました',
366
+ isClientSideEffect: true
367
+ };
368
+ }
369
+ }
370
+ }
371
+
372
+ // SceneManagerで処理できない場合は、サーバー側で処理(画像再生成)
373
+ console.log('🔄 Falling back to server-side processing');
374
+ const modifyCommand = `${command} (対象オブジェクト: ${selectedObject?.userData?.objectId || selectedObject?.id || 'unknown'})`;
375
+
376
+ const response = await fetch(`${this.serverUrl}/api/command`, {
377
+ method: 'POST',
378
+ headers: {
379
+ 'Content-Type': 'application/json'
380
+ },
381
+ body: JSON.stringify({ command: modifyCommand })
382
+ });
383
+
384
+ if (!response.ok) {
385
+ let errorPayload = null;
386
+ try {
387
+ errorPayload = await response.json();
388
+ } catch (parseError) {
389
+ // ignore
390
+ }
391
+ const serverError = new Error(errorPayload?.error || `Server error: ${response.status}`);
392
+ if (errorPayload?.errorCategory) {
393
+ serverError.code = errorPayload.errorCategory;
394
+ }
395
+ throw serverError;
396
+ }
397
+
398
+ const result = await response.json();
399
+ console.log('✅ Object modification result:', result);
400
+
401
+ return result;
402
+
403
+ } catch (error) {
404
+ console.error('❌ Object modification failed:', error);
405
+ throw this.handleRequestError(error, 'オブジェクト変更リクエストに失敗しました。');
406
+ }
407
+ }
408
+
409
+ /**
410
+ * 利用可能なサービス一覧取得
411
+ */
412
+ async getAvailableServices() {
413
+ await this.ensureInitialized();
414
+ try {
415
+ const response = await fetch(`${this.serverUrl}/api/services`);
416
+
417
+ if (!response.ok) {
418
+ throw new Error(`Server error: ${response.status}`);
419
+ }
420
+
421
+ return await response.json();
422
+
423
+ } catch (error) {
424
+ console.error('❌ Failed to get services:', error);
425
+ return [];
426
+ }
427
+ }
428
+ }
429
+
430
+ // 後方互換のため旧名称もエクスポート
431
+ export const LiveCommandClient = ChocoDropClient;
432
+ export const ChocoDroClient = ChocoDropClient;