Skip to main content
Glama
yshk-mrt
by yshk-mrt

SaveReplayBufferAndAdd

Automatically saves the replay buffer and integrates it as a media source into a specified scene, streamlining replay display during live streaming.

Instructions

Saves the replay buffer and adds it as a media source to a scene. 用途: リプレイをすぐにシーンに表示

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • The handler logic for the SaveReplayBufferAndAdd tool. Saves the OBS replay buffer, waits for file write, finds the most recent replay file (mp4/mov containing 'replay') in the specified folder (or default), then creates and enables a new ffmpeg_source input in the given scene using that file with looping enabled. Returns success with file path and sceneItemId.
    case "SaveReplayBufferAndAdd": const replayParams = params as { sceneName: string; sourceName: string; replayFolder?: string }; console.log(`Executing SaveReplayBufferAndAdd with params:`, replayParams); try { // 1. Save the replay buffer await sendToObs("SaveReplayBuffer", {}, context, action.name); // 2. Wait a bit for the file to be written to disk await new Promise(resolve => setTimeout(resolve, 2000)); // 3. Find the latest replay file const defaultRecordingPath = process.env.OBS_REPLAY_PATH || path.join(os.homedir(), 'Movies'); const replayFolder = replayParams.replayFolder || defaultRecordingPath; console.log(`Looking for the latest replay file in: ${replayFolder}`); // Get a list of replay files in the directory let files: string[] = []; try { files = fs.readdirSync(replayFolder) .filter(file => file.toLowerCase().includes('replay') && (file.endsWith('.mp4') || file.endsWith('.mov'))) .map(file => path.join(replayFolder, file)); // Sort by modification time, newest first files.sort((a, b) => { return fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime(); }); console.log(`Found ${files.length} replay files`); } catch (fsError: any) { console.error(`Error reading replay directory ${replayFolder}:`, fsError.message); return { structuredContent: { success: false, error: `Failed to read replay directory: ${fsError.message}` } }; } if (files.length === 0) { return { structuredContent: { success: false, error: "No replay files found" } }; } // Get the most recent file const latestReplayFile = files[0]; console.log(`Latest replay file: ${latestReplayFile}`); // 4. Create a media source with this file const mediaSourceResponse = await sendToObs<{ sceneItemId: number }>("CreateInput", { sceneName: replayParams.sceneName, inputName: replayParams.sourceName, inputKind: "ffmpeg_source", inputSettings: { local_file: latestReplayFile, looping: true }, sceneItemEnabled: true }, context, action.name); console.log(`Created media source with ID: ${mediaSourceResponse.sceneItemId}`); return { structuredContent: { success: true, filePath: latestReplayFile, sceneItemId: mediaSourceResponse.sceneItemId } }; } catch (e: any) { console.error(`Error in SaveReplayBufferAndAdd for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; }
  • src/index.ts:360-991 (registration)
    Dynamic registration of MCP tools from obs_mcp_tool_def.json, including SaveReplayBufferAndAdd. Registers each action with server.tool(), using schema from JSON if available, and inline handler with switch dispatching to specific cases.
    if (toolDefinitions && toolDefinitions.actions) { toolDefinitions.actions.forEach(action => { const requestSchema = action.requestDataSchema ? zodSchemaFromMcpSchema(action.requestDataSchema) : undefined; server.tool( action.name, action.description || "", requestSchema ? { params: requestSchema } : {}, async (params: any, context: any) => { // Using any for context if ToolContext is not easily available console.log(`Tool '${action.name}' called with params:`, JSON.stringify(params, null, 2)); try { // Ensure OBS is connected before attempting to send a command if (!obsClient || obsClient.readyState !== WebSocket.OPEN) { if (!isObsConnecting) { console.log("OBS not connected. Attempting to connect before processing tool action."); await connectToObs(); // This will throw if it fails if (!obsClient || obsClient.readyState !== WebSocket.OPEN) { throw new Error("Failed to connect to OBS."); } } else { // Wait for existing connection attempt await obsConnectionPromise; if (!obsClient || obsClient.readyState !== WebSocket.OPEN) { throw new Error("Failed to connect to OBS after waiting."); } } } let obsResponseData: any; // Map MCP actions to OBS requestTypes and params switch (action.name) { case "SwitchScene": console.log("SwitchScene params:", JSON.stringify(params, null, 2)); // Extract the scene name from the parameters const sceneName = params.scene || (params.params && params.params.scene); console.log("Extracted sceneName:", sceneName); if (!sceneName) { throw new Error("No scene name provided"); } try { // NOTE: Changed from SetCurrentProgramScene to use the direct parameters expected by OBS obsResponseData = await sendToObs( "SetCurrentProgramScene", { "sceneName": sceneName }, // Make sure we use the exact field name OBS expects context, action.name ); console.log("OBS response:", JSON.stringify(obsResponseData, null, 2)); } catch (error) { console.error("OBS error:", error); throw error; } break; case "StartStream": obsResponseData = await sendToObs("StartStream", {}, context, action.name); break; case "StopStream": obsResponseData = await sendToObs("StopStream", {}, context, action.name); break; case "StartRecording": obsResponseData = await sendToObs("StartRecord", {}, context, action.name); break; case "StopRecording": obsResponseData = await sendToObs("StopRecord", {}, context, action.name); break; case "SetSourceVisibility": console.log("SetSourceVisibility params:", JSON.stringify(params, null, 2)); // Try different parameter access patterns const sourceScene = params.scene || (params.params && params.params.scene); const sourceName = params.source || (params.params && params.params.source); const visible = params.visible !== undefined ? params.visible : (params.params && params.params.visible !== undefined ? params.params.visible : null); if (!sourceScene || !sourceName || visible === null) { throw new Error("Missing required parameters: scene, source, or visible"); } const sceneItemIdResponse = await sendToObs<{ sceneItemId: number }>( "GetSceneItemId", { sceneName: sourceScene, sourceName: sourceName }, context, action.name ); obsResponseData = await sendToObs( "SetSceneItemEnabled", { sceneName: sourceScene, sceneItemId: sceneItemIdResponse.sceneItemId, sceneItemEnabled: visible }, context, action.name ); break; case "GetSceneItemList": // Extract sceneName from params const sceneNameForItems = params.sceneName || params.scene || (params.params && (params.params.sceneName || params.params.scene)); if (!sceneNameForItems) { throw new Error("Missing required parameter: sceneName"); } obsResponseData = await sendToObs( "GetSceneItemList", { sceneName: sceneNameForItems }, context, action.name ); // Transform isGroup: null to isGroup: false to match schema if (obsResponseData && Array.isArray(obsResponseData.sceneItems)) { obsResponseData.sceneItems.forEach((item: any) => { if (item.isGroup === null) { item.isGroup = false; } }); } break; case "GetSceneList": obsResponseData = await sendToObs("GetSceneList", {}, context, action.name); break; case "SetTextContent": console.log("SetTextContent params:", JSON.stringify(params, null, 2)); // Extract parameters const textSourceName = params.source || (params.params && params.params.source); const textContent = params.text || (params.params && params.params.text); if (!textSourceName || textContent === undefined) { throw new Error("Missing required parameters: source or text"); } console.log(`Setting text content for source "${textSourceName}" to: ${textContent}`); try { // Use SetInputSettings to update the text property obsResponseData = await sendToObs( "SetInputSettings", { inputName: textSourceName, inputSettings: { text: textContent } }, context, action.name ); console.log("SetTextContent response:", JSON.stringify(obsResponseData, null, 2)); } catch (error) { console.error("Error setting text content:", error); throw error; } break; case "SetAudioMute": console.log("SetAudioMute params:", JSON.stringify(params, null, 2)); // Extract parameters const audioSourceName = params.source || (params.params && params.params.source); const muteState = params.mute !== undefined ? params.mute : (params.params && params.params.mute !== undefined ? params.params.mute : null); if (!audioSourceName || muteState === null) { throw new Error("Missing required parameters: source or mute"); } console.log(`Setting mute state for source "${audioSourceName}" to: ${muteState}`); try { obsResponseData = await sendToObs( "SetInputMute", { inputName: audioSourceName, inputMuted: muteState }, context, action.name ); console.log("SetAudioMute response:", JSON.stringify(obsResponseData, null, 2)); } catch (error) { console.error("Error setting audio mute state:", error); throw error; } break; case "SetAudioVolume": console.log("SetAudioVolume params:", JSON.stringify(params, null, 2)); // Extract parameters const volumeSourceName = params.source || (params.params && params.params.source); const volumeLevel = params.volume !== undefined ? params.volume : (params.params && params.params.volume !== undefined ? params.params.volume : null); if (!volumeSourceName || volumeLevel === null) { throw new Error("Missing required parameters: source or volume"); } if (volumeLevel < 0 || volumeLevel > 1) { throw new Error("Volume must be between 0.0 and 1.0"); } console.log(`Setting volume for source "${volumeSourceName}" to: ${volumeLevel}`); try { obsResponseData = await sendToObs( "SetInputVolume", { inputName: volumeSourceName, inputVolumeMul: volumeLevel // Using multiplier (0.0 to 1.0) format }, context, action.name ); console.log("SetAudioVolume response:", JSON.stringify(obsResponseData, null, 2)); } catch (error) { console.error("Error setting audio volume:", error); throw error; } break; case "SetSourcePosition": console.log("SetSourcePosition params:", JSON.stringify(params, null, 2)); // Extract parameters const posSceneName = params.scene || (params.params && params.params.scene); const posSourceName = params.source || (params.params && params.params.source); const xPos = params.x !== undefined ? params.x : (params.params && params.params.x !== undefined ? params.params.x : null); const yPos = params.y !== undefined ? params.y : (params.params && params.params.y !== undefined ? params.params.y : null); if (!posSceneName || !posSourceName || xPos === null || yPos === null) { throw new Error("Missing required parameters: scene, source, x, or y"); } console.log(`Setting position for source "${posSourceName}" in scene "${posSceneName}" to: x=${xPos}, y=${yPos}`); try { // First get the scene item ID const posItemIdResponse = await sendToObs<{ sceneItemId: number }>( "GetSceneItemId", { sceneName: posSceneName, sourceName: posSourceName }, context, action.name ); // Then set the position obsResponseData = await sendToObs( "SetSceneItemTransform", { sceneName: posSceneName, sceneItemId: posItemIdResponse.sceneItemId, sceneItemTransform: { positionX: xPos, positionY: yPos } }, context, action.name ); console.log("SetSourcePosition response:", JSON.stringify(obsResponseData, null, 2)); } catch (error) { console.error("Error setting source position:", error); throw error; } break; case "SetSourceScale": console.log("SetSourceScale params:", JSON.stringify(params, null, 2)); // Extract parameters const scaleSceneName = params.scene || (params.params && params.params.scene); const scaleSourceName = params.source || (params.params && params.params.source); const scaleX = params.scaleX !== undefined ? params.scaleX : (params.params && params.params.scaleX !== undefined ? params.params.scaleX : null); const scaleY = params.scaleY !== undefined ? params.scaleY : (params.params && params.params.scaleY !== undefined ? params.params.scaleY : null); if (!scaleSceneName || !scaleSourceName || scaleX === null || scaleY === null) { throw new Error("Missing required parameters: scene, source, scaleX, or scaleY"); } console.log(`Setting scale for source "${scaleSourceName}" in scene "${scaleSceneName}" to: scaleX=${scaleX}, scaleY=${scaleY}`); try { // First get the scene item ID const scaleItemIdResponse = await sendToObs<{ sceneItemId: number }>( "GetSceneItemId", { sceneName: scaleSceneName, sourceName: scaleSourceName }, context, action.name ); // Then set the scale obsResponseData = await sendToObs( "SetSceneItemTransform", { sceneName: scaleSceneName, sceneItemId: scaleItemIdResponse.sceneItemId, sceneItemTransform: { scaleX: scaleX, scaleY: scaleY } }, context, action.name ); console.log("SetSourceScale response:", JSON.stringify(obsResponseData, null, 2)); } catch (error) { console.error("Error setting source scale:", error); throw error; } break; case "TakeSourceScreenshot": // Try to get source from params or params.params const mcpInputParams = params.params || params; const sourceNameForScreenshot = mcpInputParams.source; // Assuming 'source' is the key from MCP if (!sourceNameForScreenshot) { console.error("TakeSourceScreenshot Error: Missing required parameter 'source'. Params received:", JSON.stringify(params, null, 2)); throw new Error("Missing required parameter: source"); } // Ensure sourceNameForScreenshot is a string before calling replace if (typeof sourceNameForScreenshot !== 'string') { console.error("TakeSourceScreenshot Error: 'source' parameter is not a string. Value:", sourceNameForScreenshot); throw new Error("'source' parameter must be a string."); } console.log(`Executing TakeSourceScreenshot for source: ${sourceNameForScreenshot}`); const format = mcpInputParams.imageFormat || "png"; const timestamp = Date.now(); const filename = `screenshot-${sourceNameForScreenshot.replace(/[^a-z0-9]/gi, '_')}-${timestamp}.${format}`; const absoluteFilePath = path.join(SCREENSHOTS_DIR_ABS, filename); // URI that will be returned to the client const resourceUri = `file://${absoluteFilePath}`; console.log(`Attempting to save screenshot to: ${absoluteFilePath}`); try { const obsRequestData: any = { sourceName: sourceNameForScreenshot, // OBS uses sourceName for SaveSourceScreenshot imageFormat: format, imageFilePath: absoluteFilePath, }; if (mcpInputParams.width !== undefined) obsRequestData.imageWidth = mcpInputParams.width; if (mcpInputParams.height !== undefined) obsRequestData.imageHeight = mcpInputParams.height; if (mcpInputParams.compressionQuality !== undefined) obsRequestData.imageCompressionQuality = mcpInputParams.compressionQuality; // Use SaveSourceScreenshot OBS request const obsResponse = await sendToObs("SaveSourceScreenshot", obsRequestData, context, "TakeSourceScreenshot"); console.log(`SaveSourceScreenshot OBS response:`, obsResponse); console.log(`Screenshot for source ${sourceNameForScreenshot} saved to ${absoluteFilePath}. Returning URI: ${resourceUri}`); // Return the URI to the client as a resource return { content: [{ type: "resource", resource: { uri: resourceUri, text: `Screenshot for ${sourceNameForScreenshot} available at ${filename}`, // mimeType: `image/${format}` // Optional: include mimeType if known } }], filename: filename // Additional top-level info }; } catch (e: any) { console.error(`Error in TakeSourceScreenshot for source ${sourceNameForScreenshot}:`, e.message); throw new Error(`Failed to take screenshot for ${sourceNameForScreenshot}: ${e.message}`); } break; case "SetTransitionSettings": const transitionParams = params as { transitionName: string; transitionDuration: number }; console.log(`Executing SetTransitionSettings with params:`, transitionParams); try { await sendToObs("SetCurrentSceneTransition", { transitionName: transitionParams.transitionName }, context, action.name); await sendToObs("SetCurrentSceneTransitionSettings", { transitionSettings: { duration: transitionParams.transitionDuration }, overlay: false }, context, action.name); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in SetTransitionSettings for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "TriggerStudioModeTransition": console.log(`Executing TriggerStudioModeTransition`); try { await sendToObs("TriggerStudioModeTransition", {}, context, action.name); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in TriggerStudioModeTransition for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "PlayPauseMedia": const mediaParams = params as { sourceName: string; mediaAction: string }; console.log(`Executing PlayPauseMedia with params:`, mediaParams); try { await sendToObs("TriggerMediaInputAction", { inputName: mediaParams.sourceName, mediaAction: mediaParams.mediaAction.toUpperCase() }, context, action.name); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in PlayPauseMedia for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "SetMediaTime": const timeParams = params as { sourceName: string; mediaTime: number }; console.log(`Executing SetMediaTime with params:`, timeParams); try { await sendToObs("SetMediaInputCursor", { inputName: timeParams.sourceName, mediaCursor: timeParams.mediaTime }, context, action.name); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in SetMediaTime for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "SaveReplayBuffer": console.log(`Executing SaveReplayBuffer`); try { await sendToObs("SaveReplayBuffer", {}, context, action.name); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in SaveReplayBuffer for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "SaveReplayBufferAndAdd": const replayParams = params as { sceneName: string; sourceName: string; replayFolder?: string }; console.log(`Executing SaveReplayBufferAndAdd with params:`, replayParams); try { // 1. Save the replay buffer await sendToObs("SaveReplayBuffer", {}, context, action.name); // 2. Wait a bit for the file to be written to disk await new Promise(resolve => setTimeout(resolve, 2000)); // 3. Find the latest replay file const defaultRecordingPath = process.env.OBS_REPLAY_PATH || path.join(os.homedir(), 'Movies'); const replayFolder = replayParams.replayFolder || defaultRecordingPath; console.log(`Looking for the latest replay file in: ${replayFolder}`); // Get a list of replay files in the directory let files: string[] = []; try { files = fs.readdirSync(replayFolder) .filter(file => file.toLowerCase().includes('replay') && (file.endsWith('.mp4') || file.endsWith('.mov'))) .map(file => path.join(replayFolder, file)); // Sort by modification time, newest first files.sort((a, b) => { return fs.statSync(b).mtime.getTime() - fs.statSync(a).mtime.getTime(); }); console.log(`Found ${files.length} replay files`); } catch (fsError: any) { console.error(`Error reading replay directory ${replayFolder}:`, fsError.message); return { structuredContent: { success: false, error: `Failed to read replay directory: ${fsError.message}` } }; } if (files.length === 0) { return { structuredContent: { success: false, error: "No replay files found" } }; } // Get the most recent file const latestReplayFile = files[0]; console.log(`Latest replay file: ${latestReplayFile}`); // 4. Create a media source with this file const mediaSourceResponse = await sendToObs<{ sceneItemId: number }>("CreateInput", { sceneName: replayParams.sceneName, inputName: replayParams.sourceName, inputKind: "ffmpeg_source", inputSettings: { local_file: latestReplayFile, looping: true }, sceneItemEnabled: true }, context, action.name); console.log(`Created media source with ID: ${mediaSourceResponse.sceneItemId}`); return { structuredContent: { success: true, filePath: latestReplayFile, sceneItemId: mediaSourceResponse.sceneItemId } }; } catch (e: any) { console.error(`Error in SaveReplayBufferAndAdd for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "CreateSource": const sourceParams = params as { sceneName: string; sourceName: string; sourceKind: string; sourceSettings: object; setVisible?: boolean }; console.log(`Executing CreateSource with params:`, sourceParams); try { const response = await sendToObs<{ sceneItemId: number }>("CreateInput", { sceneName: sourceParams.sceneName, inputName: sourceParams.sourceName, inputKind: sourceParams.sourceKind, inputSettings: sourceParams.sourceSettings, sceneItemEnabled: typeof sourceParams.setVisible === 'boolean' ? sourceParams.setVisible : true }, context, action.name); return { structuredContent: { success: true, sceneItemId: response.sceneItemId } }; } catch (e: any) { console.error(`Error in CreateSource for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "SetShaderFilter": const shaderParams = params as { sourceName: string; filterName: string; shaderCode?: string; shaderParameters?: Record<string, any> }; console.log(`Executing SetShaderFilter with params:`, shaderParams); try { // Directly construct filterSettings without GetSourceFilterInfo const filterSettings: Record<string, any> = {}; if (shaderParams.shaderCode) { // The actual key for shader code might depend on the specific shader filter plugin. // Common ones are 'shader_text', 'glsl', or 'code'. // Assuming 'shader_text' based on common OBS shader filter plugins. filterSettings.shader_text = shaderParams.shaderCode; } if (shaderParams.shaderParameters) { // Shader parameters are often applied directly at the root of filterSettings // or under a specific key like 'defaults'. // This assumes they are direct key-value pairs. for (const [key, value] of Object.entries(shaderParams.shaderParameters)) { filterSettings[key] = value; } } // Apply the constructed filter settings await sendToObs( "SetSourceFilterSettings", { sourceName: shaderParams.sourceName, filterName: shaderParams.filterName, filterSettings: filterSettings }, context, action.name ); console.log(`Successfully attempted to update shader filter settings for ${shaderParams.filterName} on ${shaderParams.sourceName}`); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in SetShaderFilter for OBS:`, e.message); return { structuredContent: { success: false, error: e.message } }; } case "SetLutFilter": console.log(`Executing SetLutFilter with params:`, JSON.stringify(params, null, 2)); try { // Parse the input parameters // Check if params are directly in params or in a nested params object const paramsObj = params.params || params; const sourceName = paramsObj.sourceName; const filterName = paramsObj.filterName; const amount = paramsObj.amount; const path = paramsObj.path; console.log(`SetLutFilter: Parsed parameters - sourceName: "${sourceName}", filterName: "${filterName}", amount: ${amount}, path: ${path || "undefined"}`); if (!sourceName || !filterName) { throw new Error("Missing required parameters: sourceName or filterName"); } console.log(`SetLutFilter: Setting LUT filter "${filterName}" on source "${sourceName}"`); // Direct approach - skip getting current settings console.log("Using direct filter update approach"); const filterSettings: Record<string, any> = {}; if (amount !== undefined) { filterSettings.amount = amount; } if (path) { filterSettings.path = path; } console.log(`Filter settings to apply:`, JSON.stringify(filterSettings, null, 2)); const setFilterResponse = await sendToObs( "SetSourceFilterSettings", { sourceName: sourceName, filterName: filterName, filterSettings: filterSettings }, context, action.name ); console.log(`SetSourceFilterSettings response:`, JSON.stringify(setFilterResponse, null, 2)); console.log(`Successfully updated LUT filter settings directly for ${filterName} on ${sourceName}`); return { structuredContent: { success: true } }; } catch (e: any) { console.error(`Error in SetLutFilter for OBS:`, e.message); console.error(`Error details:`, e); return { structuredContent: { success: false, error: e.message } }; } default: console.error(`Unknown MCP action: ${action.name}`); throw new Error(`Action '${action.name}' is not implemented.`); } // ★★★ デバッグログ追加箇所 ★★★ console.log(`DEBUG: Formatting response for action: '${action.name}'`); console.log(`DEBUG: obsResponseData content for MCP: ${JSON.stringify(obsResponseData, null, 2)}`); // ★★★ここまで★★★ // Return the appropriate response format based on the action if (action.name === "GetSceneItemList") { // EXPERIMENT: Return as content to test Claude client behavior console.log(`DEBUG: Formatting GetSceneItemList response as 'content' for Claude client test.`); return { content: [{ type: "text", text: JSON.stringify(obsResponseData, null, 2) }] }; } else if (action.name === "GetSceneList") { // For GetSceneList, return the full response as content return { content: [ { type: "text", text: JSON.stringify(obsResponseData, null, 2) } ] }; } else { // For other actions, return a simple success message return { content: [ { type: "text", text: `Action '${action.name}' executed successfully.` } ] }; } } catch (error: any) { console.error(`Error executing tool '${action.name}':`, error); throw error; } } ); console.log(`Registered tool: ${action.name}`); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/yshk-mrt/obs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server