RR

rassi0429/resolink-mcp

Developer tools
27 stars 0 forks Quality 60 Trend 33

ResoniteLink WebSocket プロトコルを使用して Resonite VR ワールドを操作するための MCP サーバ & CLI ツール。

Overview

ResoniteLink WebSocket プロトコルを使用して Resonite VR ワールドを操作するための MCP サーバ & CLI ツール。 プロジェクトルートの .mcp.json に設定済み。Claude Code を再起動すると自動的にMCPサーバーが利用可能になります。 1. コマンドライン引数 --url (最優先) 2. 環境変数 RESONITELINK_URL 3. デフォルト値: ws://localhost:29551 環境変数を設定すれば、毎回 --url オプションを指定する必要がなくなります。 問題が発生した場合、デバッグログを有効にすると SEND/RECV メッセージを確認できます: レスポンスが返らない場合(不正な形式のデータ送信時など)、タイムアウトでエラーが発生します: タイムアウト発生時は Error: Request timeout after 10000ms: updateComponent (...) のようなエラーがスローされます。 ResoniteLink のアセットインポートAPIを使用して、テクスチャをインポートできます。

README

ResoniteLink MCP

ResoniteLink WebSocket プロトコルを使用して Resonite VR ワールドを操作するための MCP サーバ & CLI ツール。

インストール

npm install
npm run build

MCP サーバとして使用

Claude Code 設定

プロジェクトルートの .mcp.json に設定済み。Claude Code を再起動すると自動的にMCPサーバーが利用可能になります。

Claude Desktop 設定

claude_desktop_config.json に以下を追加:

{
  "mcpServers": {
    "resonitelink": {
      "command": "node",
      "args": ["C:/Users/neo/GitHub/resolink_mcp/dist/mcp-server.js"],
      "env": {
        "RESONITE_WS_URL": "ws://localhost:29551"
      }
    }
  }
}

利用可能なツール

ツール 説明
connect Resonite に接続
disconnect 接続を切断
get_slot スロット情報を取得
find_slot 名前でスロットを検索
add_slot スロットを追加
remove_slot スロットを削除
update_slot スロットを更新
add_component コンポーネントを追加
get_component コンポーネント情報を取得
update_component コンポーネントを更新
remove_component コンポーネントを削除
import_texture_file テクスチャをファイルからインポート
search_components コンポーネントを検索
get_component_info コンポーネント詳細を取得
list_categories カテゴリ一覧
search_by_category カテゴリで検索
search_by_member メンバー名で検索
get_component_source ソースコードを取得
grep_source ソースを全文検索

CLI として使用

WebSocket URL の指定方法

CLI は WebSocket URL を以下の優先順位で決定します:

  1. コマンドライン引数 --url (最優先)
  2. 環境変数 RESONITELINK_URL
  3. デフォルト値: ws://localhost:29551

環境変数で設定する例:

# Windows (PowerShell)
$env:RESONITELINK_URL = "ws://localhost:29469"

# Windows (コマンドプロンプト)
set RESONITELINK_URL=ws://localhost:29469

# Linux/Mac
export RESONITELINK_URL=ws://localhost:29469

環境変数を設定すれば、毎回 --url オプションを指定する必要がなくなります。

基本的なコマンド

# Rootスロット情報を取得
node dist/cli.js root --depth 1

# 特定のスロット情報を取得
node dist/cli.js get-slot --id  --depth 1

# コンポーネント情報も含めて取得
node dist/cli.js get-slot --id  --components

# 名前でスロットを検索
node dist/cli.js find --name MyObject

# スロットを追加
node dist/cli.js add-slot --name NewSlot --position 0,1,0

# 親スロットを指定してスロットを追加
node dist/cli.js add-slot --parent  --name MySlot --position 0,1,0 --scale 1,1,1

# コンポーネントを追加
node dist/cli.js add-component --slot  --type "[FrooxEngine]FrooxEngine.BoxMesh"

# スロットを更新
node dist/cli.js update-slot --id  --name NewName --position 0,2,0

# スロットを削除
node dist/cli.js remove-slot --id 

# コンポーネントを取得
node dist/cli.js get-component --id 

# コンポーネントを削除
node dist/cli.js remove-component --id 

# スロット階層をツリー表示
node dist/cli.js tree --depth 3

# WebSocket URLを指定(異なるポートの場合)
node dist/cli.js root --url ws://localhost:9422 --depth 2

ライブラリとして使用

import { ResoniteLinkClient } from './src/index.js';

const client = new ResoniteLinkClient({ url: 'ws://localhost:29551' });
await client.connect();

// スロットを追加
await client.addSlot({ name: 'MyObject', position: { x: 0, y: 1, z: 0 } });

// コンポーネントを追加
await client.addComponent({
  containerSlotId: slotId,
  componentType: '[FrooxEngine]FrooxEngine.BoxMesh'
});

client.disconnect();

クライアントオプション

const client = new ResoniteLinkClient({
  url: 'ws://localhost:29551',  // WebSocket URL(必須)
  debug: true,                   // コンソールにログ出力
  logFile: 'debug.log',          // ファイルにログ出力
  requestTimeout: 30000,         // リクエストタイムアウト(ミリ秒、デフォルト: 30000)
  autoReconnect: false,          // 自動再接続
  reconnectInterval: 5000,       // 再接続間隔(ミリ秒)
});

デバッグログ

問題が発生した場合、デバッグログを有効にすると SEND/RECV メッセージを確認できます:

const client = new ResoniteLinkClient({
  url: 'ws://localhost:29551',
  debug: true,           // コンソール出力
  logFile: 'debug.log',  // ファイル出力
});

ログ例:

[2026-01-08T06:10:24.504Z] SEND: { "$type": "addSlot", "messageId": "..." }
[2026-01-08T06:10:24.506Z] RECV: { "success": true, "messageId": "...", "error": null }

リクエストタイムアウト

レスポンスが返らない場合(不正な形式のデータ送信時など)、タイムアウトでエラーが発生します:

const client = new ResoniteLinkClient({
  url: 'ws://localhost:29551',
  requestTimeout: 10000,  // 10秒でタイムアウト
});

タイムアウト発生時は Error: Request timeout after 10000ms: updateComponent (...) のようなエラーがスローされます。

テクスチャインポート

ResoniteLink のアセットインポートAPIを使用して、テクスチャをインポートできます。

ファイルからインポート

// Resoniteホストのローカルファイルからテクスチャをインポート
const result = await client.importTexture2DFile({
  filePath: 'C:/path/to/texture.png'
});

if (result.success) {
  console.log('Asset URL:', result.assetURL);
  // assetURL を StaticTexture2D などに設定可能
}

生データからインポート

// RGBAピクセルデータからテクスチャをインポート
const width = 256;
const height = 256;
const rawData = Buffer.alloc(width * height * 4); // RGBA, 4 bytes per pixel

// ピクセルデータを生成(例: グラデーション)
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    const offset = (y * width + x) * 4;
    rawData[offset] = x;         // R
    rawData[offset + 1] = y;     // G
    rawData[offset + 2] = 128;   // B
    rawData[offset + 3] = 255;   // A
  }
}

const result = await client.importTexture2DRawData({
  width,
  height,
  colorProfile: 'sRGB',  // 'sRGB' または 'Linear'
  rawData
});

if (result.success) {
  console.log('Asset URL:', result.assetURL);
}

注意事項

  • filePath は Resonite が動作しているホストのローカルパス
  • assetURL はセッション内でのみ有効(一時的なURL)
  • サポートされる形式: PNG, JPG など一般的な画像形式
  • 生データは RGBA フォーマット(1ピクセル4バイト)

重要: ワールドシステムオブジェクト

Resonite ワールドの Root 以下には、削除してはいけないシステムオブジェクトがあります。

削除禁止オブジェクト

オブジェクト名 説明
Controllers コントローラー入力システム
Roles ユーザーロール管理
SpawnArea ユーザーのスポーン位置
Light ワールドの照明
Skybox 空・背景
User 接続中のユーザー(削除するとキックされる)
__TEMP 一時オブジェクト管理
Undo Manager アンドゥ履歴
Assets 共有アセット
Clipboard Importer クリップボードインポート機能

安全な削除方法

const SYSTEM_OBJECTS = [
  'Controllers', 'Roles', 'SpawnArea', 'Light', 'Skybox',
  '__TEMP', 'Undo Manager', 'Assets', 'Clipboard Importer'
];

// システムオブジェクトとUserはスキップ
if (SYSTEM_OBJECTS.includes(name) || name.startsWith('User ')) {
  continue;
}

コンポーネントタイプの書式

[FrooxEngine]FrooxEngine.ComponentName

よく使うコンポーネント

コンポーネント 用途
BoxMesh 直方体メッシュ
SphereMesh 球体メッシュ
CylinderMesh 円柱メッシュ
ConeMesh 円錐メッシュ
BevelBoxMesh 角丸直方体
RampMesh スロープ
FrameMesh フレーム
TorusMesh トーラス
CapsuleMesh カプセル
MeshRenderer メッシュ描画
PBS_Metallic PBRマテリアル
Light ライト

マテリアルの設定例

await client.updateComponent({
  id: materialId,
  members: {
    AlbedoColor: { $type: 'colorX', value: { r: 1, g: 0, b: 0, a: 1, profile: 'sRGB' } },
    Smoothness: { $type: 'float', value: 0.5 },
    Metallic: { $type: 'float', value: 0.2 },
  }
});

Enum型の設定(BlendMode, LightType など)

Enum 型のメンバーを更新する場合、以下の形式を使用する必要があります:

{
  $type: 'enum',      // 小文字の 'enum'
  value: 'Alpha',     // 文字列で値を指定(数値ではない)
  enumType: 'BlendMode'  // Enum の型名
}

BlendMode の値

値 説明
Opaque 不透明(デフォルト)
Cutout カットアウト(アルファテスト)
Alpha 半透明(アルファブレンド)

半透明マテリアルの例

await client.updateComponent({
  id: materialId,
  members: {
    BlendMode: { $type: 'enum', value: 'Alpha', enumType: 'BlendMode' },
    AlbedoColor: { $type: 'colorX', value: { r: 0.6, g: 0.75, b: 0.9, a: 0.3, profile: 'sRGB' } },
  }
});

LightType の値

値 説明
Directional ディレクショナルライト
Point ポイントライト
Spot スポットライト
await client.updateComponent({
  id: lightId,
  members: {
    LightType: { $type: 'enum', value: 'Point', enumType: 'LightType' },
    Intensity: { $type: 'float', value: 2.0 },
    Range: { $type: 'float', value: 10.0 },
  }
});

注意事項

  • $type は必ず小文字の 'enum'('Enum' ではない)
  • value は数値ではなく文字列で指定
  • enumType を省略すると動作しない場合がある
  • 正しくない形式を送信するとレスポンスが返らずタイムアウトする

メンバーの型一覧

$type 説明 例
float 浮動小数点 { $type: 'float', value: 0.5 }
int 整数 { $type: 'int', value: 10 }
bool 真偽値 { $type: 'bool', value: true }
float2 2Dベクトル { $type: 'float2', value: { x: 1, y: 1 } }
float3 3Dベクトル { $type: 'float3', value: { x: 1, y: 2, z: 3 } }
floatQ クォータニオン { $type: 'floatQ', value: { x: 0, y: 0, z: 0, w: 1 } }
colorX 色 { $type: 'colorX', value: { r: 1, g: 0, b: 0, a: 1, profile: 'sRGB' } }
enum 列挙型 { $type: 'enum', value: 'Alpha', enumType: 'BlendMode' }
reference 参照 { $type: 'reference', targetId: 'Reso_XXXXX' }
list リスト { $type: 'list', elements: [...] }
empty 出力メンバー { $type: 'empty', id: 'Reso_XXXXX' }

empty 型(ProtoFlux 出力メンバー)

ResoniteLink の更新により、ProtoFlux ノードの出力メンバーが $type: "empty" として返されるようになりました。

背景

ProtoFlux ノードには入力(SyncRef)と出力(NodeValueOutput)があります:

  • 入力: 値を受け取る(reference型で返される)
  • 出力: 値を出力する(以前は返されなかった → 今は empty 型で返される)

例: GlobalTransform ノード

{
  "componentType": "...GlobalTransform",
  "members": {
    "Instance": { "$type": "reference", "targetId": null, ... },
    "GlobalPosition": { "$type": "empty", "id": "Reso_82E" },
    "GlobalRotation": { "$type": "empty", "id": "Reso_82F" },
    "GlobalScale": { "$type": "empty", "id": "Reso_830" }
  }
}

出力メンバーの参照方法

出力メンバーの id を使って、他のノードの入力に接続できます:

// GlobalTransform の GlobalPosition 出力を取得
const globalTransformComp = slotData.data?.components?.find(c =>
  c.componentType?.includes('GlobalTransform')
);
const globalPositionId = globalTransformComp.members.GlobalPosition.id; // "Reso_82E"

// ValueSub の A 入力に GlobalPosition を接続
await client.updateComponent({
  id: subComp.id,
  members: {
    A: { $type: 'reference', targetId: globalPositionId },  // 出力IDを直接参照!
  }
});

以前との違い

項目 以前 現在
出力メンバー JSONに含まれない $type: "empty" で返される
複数出力ノードの参照 コンポーネントID全体を参照 → エラー 出力IDを個別に参照 → 成功

これにより、GlobalTransform のような複数出力を持つノードも正しく接続できるようになりました。

Materials リストの更新(2段階)

MeshRenderer の Materials リストを更新するには2段階の操作が必要です。

なぜ2段階必要か

ResoniteLink の制限により、リスト要素への参照設定は以下の動作をします:

  1. 1回目の更新: リストに新しい要素が追加されるが、targetId は null になる
  2. 2回目の更新: 要素の id を指定することで、既存要素の targetId を設定できる

つまり、要素の追加と参照の設定は別々の操作として行う必要があります。

コード例

// 1. まずリストに要素を追加(この時点では targetId は null になる)
await client.updateComponent({
  id: rendererId,
  members: {
    Materials: {
      $type: 'list',
      elements: [{ $type: 'reference', targetId: materialId }]
    }
  }
});

// 2. 追加された要素のIDを取得
const rendererData = await client.getComponent(rendererId);
const elementId = rendererData.data.members.Materials.elements[0].id;

// 3. 要素のIDを指定して、参照を設定
await client.updateComponent({
  id: rendererId,
  members: {
    Materials: {
      $type: 'list',
      elements: [{ $type: 'reference', id: elementId, targetId: materialId }]
    }
  }
});

重要なポイント

  • id フィールドを省略すると、新しい要素が追加される(既存要素は更新されない)
  • id フィールドを指定すると、その ID を持つ既存要素が更新される
  • 1回目で targetId を指定しても無視され、null になる

ProtoFlux コンポーネントの追加

ProtoFlux ノード(ジェネリック型コンポーネント)を追加するには、特定の形式が必要です。

正しい形式

[ProtoFluxBindings]FrooxEngine.FrooxEngine.ProtoFlux.CoreNodes.

ポイント

項目 正しい形式 間違った形式
アセンブリ名 [ProtoFluxBindings] [FrooxEngine]
名前空間 FrooxEngine.FrooxEngine.ProtoFlux.CoreNodes FrooxEngine.ProtoFlux.CoreNodes
ジェネリック型 , , `` , `1[System.Boolean] ``
  • 名前空間: FrooxEngine が2回繰り返される
  • 型指定: C# エイリアス(bool, int, float)を使用する(System.Boolean ではない)
  • 記法: <> 形式を使用する(.NET のバッククォート記法 `1[...] ではない)

複合型パラメータ(Slot, User など)

スロットやユーザーなどの複合型を型パラメータに使う場合は、アセンブリ名付きの完全修飾名を使用する:

[ProtoFluxBindings]...RefObjectInput
型 正しい形式 間違った形式
Slot `` ,
User `` ``
IButton `` ``

重要: プリミティブ型(int, float, bool など)はエイリアスをそのまま使い、複合型(Slot, User など)は [FrooxEngine]FrooxEngine.TypeName 形式を使う。

動作確認済みコンポーネント

ノード componentType
ValueInput\ [ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.ValueInput
ValueAdd\ [ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.Operators.ValueAdd
ValueDisplay\ [ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.ValueDisplay
WorldTimeFloat [ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.FrooxEngine.Time.WorldTimeFloat
AxisAngle_floatQ [ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.Math.Quaternions.AxisAngle_floatQ
HSV_ToColorX [ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.Color.HSV_ToColorX
ValueFieldDrive\ [ProtoFluxBindings]FrooxEngine.FrooxEngine.ProtoFlux.CoreNodes.ValueFieldDrive
ValueFieldDrive\ [ProtoFluxBindings]FrooxEngine.FrooxEngine.ProtoFlux.CoreNodes.ValueFieldDrive
ValueFieldDrive\ [ProtoFluxBindings]FrooxEngine.FrooxEngine.ProtoFlux.CoreNodes.ValueFieldDrive

コード例

// ValueFieldDrive を追加(回転ドライブ用)
await client.addComponent({
  containerSlotId: slotId,
  componentType: '[ProtoFluxBindings]FrooxEngine.FrooxEngine.ProtoFlux.CoreNodes.ValueFieldDrive'
});

// WorldTimeFloat を追加(時間取得用)
await client.addComponent({
  containerSlotId: slotId,
  componentType: '[ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.FrooxEngine.Time.WorldTimeFloat'
});

// HSV_ToColorX を追加(色変換用)
await client.addComponent({
  containerSlotId: slotId,
  componentType: '[ProtoFluxBindings]FrooxEngine.ProtoFlux.Runtimes.Execution.Nodes.Color.HSV_ToColorX'
});

ProtoFlux ノード間の接続

// ValueAdd の A, B 入力に ValueInput ノードを接続
await client.updateComponent({
  id: addCompId,
  members: {
    A: { $type: 'reference', targetId: input1CompId },
    B: { $type: 'reference', targetId: input2CompId },
  }
});

// ValueDisplay の Input に ValueAdd の出力を接続
await client.updateComponent({
  id: displayCompId,
  members: {
    Input: { $type: 'reference', targetId: addCompId },
  }
});

よく使う ProtoFlux ノード

コンポーネント 用途
ValueFieldDrive フィールドをドライブ
ReferenceFieldDrive 参照フィールドをドライブ
GlobalValue グローバル値
GlobalReference グローバル参照

ProtoFlux ノードの配置

ProtoFlux ノードを追加する際は、各ノードを別々のスロットに配置し、適切な位置に並べることで視認性が向上します。

座標系

Resonite の座標系:

  • X軸: 左右(右が正)
  • Y軸: 上下(上が正)
  • Z軸: 前後(手前が正)

ProtoFlux ノードは**左から右(X軸方向)**に配置するのが一般的です。

配置の基本パターン

左 ────────────────────────────────────────────→ 右 (X軸)

[入力ノード群]  →  [処理ノード]  →  [出力ノード]  →  [ドライブノード]
   x=-1.5            x=-1.0           x=-0.5            x=0

複数入力がある場合

Y軸で上下にずらして配置:

// 入力1(上側)
await client.addSlot({ name: 'Input1', position: { x: -1.5, y: 0.15, z: 0 } });

// 入力2(下側)
await client.addSlot({ name: 'Input2', position: { x: -1.5, y: -0.15, z: 0 } });

// 処理ノード(中央)
await client.addSlot({ name: 'Process', position: { x: -1.0, y: 0, z: 0 } });

実践例: 回転するボックス

// 親スロット
const fluxSlot = await client.addSlot({ name: 'Flux', position: { x: 0, y: 2, z: 0 } });

// 各ノードを左から右に配置
const nodes = [
  { name: 'AxisInput',      x: -0.6, y: 0.15 },  // 回転軸入力
  { name: 'TimeNode',       x: -0.6, y: -0.15 }, // 時間取得
  { name: 'AxisAngleNode',  x: -0.3, y: 0 },     // 軸角度→クォータニオン変換
  { name: 'DriveNode',      x: 0,    y: 0 },     // 回転ドライブ
];

for (const node of nodes) {
  await client.addSlot({
    name: node.name,
    parentId: fluxSlot.data.id,
    position: { x: node.x, y: node.y, z: 0 }
  });
}

配置のコツ

項目 推奨値
ノード間の水平間隔 0.3〜0.5
分岐時の垂直間隔 0.15〜0.3
親スロットからの相対座標 使用する
  • 親スロット(例: Flux)を作成し、その下に各ノードを配置
  • 位置は親スロットからの相対座標になる
  • データフローが左から右に流れるように配置
  • 分岐がある場合は Y軸で上下にずらす

制限事項

一部の ProtoFlux ノードは追加できない場合があります:

  • 複雑なジェネリック制約を持つノード
  • 特殊な初期化が必要なノード

回避策:

  • Resonite 内で手動で ProtoFlux を作成
  • 既存の ProtoFlux をテンプレートとして複製
  • PackedObject として保存したものをインポート

デコンパイル検索 (CLI)

# コンポーネント名で検索
node dist/cli.js search --query Mesh

# コンポーネント詳細を表示
node dist/cli.js info --name PBS_Metallic

# カテゴリ一覧
node dist/cli.js categories

# カテゴリで検索
node dist/cli.js category --query "Materials"

# メンバー名で検索
node dist/cli.js member --query Smoothness

# ソースコード全文検索
node dist/cli.js grep --query "SyncPlayback"

# ソースコード表示
node dist/cli.js source --name BoxMesh

サンプルスクリプト

# ProtoFlux 1+1 を作成(ValueInput → ValueAdd → ValueDisplay)
node dist/scripts/create-flux-add.js ws://localhost:58971

# 東京タワー(詳細版)を作成
node dist/scripts/create-tokyo-tower-detailed.js ws://localhost:58971

# 東京スカイツリーを作成
node dist/scripts/create-skytree.js ws://localhost:58971

# 東京タワーを削除
node dist/scripts/delete-tokyo-tower.js ws://localhost:58971

# モダンハウス(内装付き)を作成
node dist/scripts/create-house3.js

# 街を作成
node dist/scripts/create-town.js

# 雲を作成
node dist/scripts/create-clouds.js

# すべて削除して床だけにする
node dist/scripts/reset-to-floor.js

License

MIT

View this README on GitHub

Install

This server does not publish a one-line install command.

Open the repository installation guide

Configuration

{ "mcpServers": { "resonitelink": { "command": "node", "args": ["C:/Users/neo/GitHub/resolink_mcp/dist/mcp-server.js"], "env": { "RESONITE_WS_URL": "ws://localhost:29551" } } } }