Skip to main content
Glama
yshk-mrt

Presentation Buddy MCP Server

by yshk-mrt

SaveReplayBufferAndAdd

Save replay buffer content and automatically add it as a media source to a specified scene for immediate display during streaming.

Instructions

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

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • Handler implementation for the SaveReplayBufferAndAdd tool. Saves the OBS replay buffer, locates the latest replay file, and creates a new ffmpeg_source input in the specified scene using that file as a looping media source.
    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:364-990 (registration)
    Dynamic registration of the SaveReplayBufferAndAdd tool (and others) from the obs_mcp_tool_def.json file using server.tool().
    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}`);
  • Dynamic schema generation from tool definition JSON using zodSchemaFromMcpSchema for input validation of SaveReplayBufferAndAdd tool.
    const requestSchema = action.requestDataSchema ? zodSchemaFromMcpSchema(action.requestDataSchema) : undefined;
  • Helper function sendToObs used by the SaveReplayBufferAndAdd handler to communicate with OBS WebSocket.
    async function sendToObs<T = any>(requestType: string, requestData?: any, mcpContext?: any, actionName?: string): Promise<T> {
        if (!obsClient || obsClient.readyState !== WebSocket.OPEN) {
            if (!isObsConnecting) { // Avoid multiple concurrent connection attempts from sendToObs
                console.log("OBS not connected. Attempting to connect before sending.");
                try {
                    await connectToObs(); // Attempt to connect first
                    if (!obsClient || obsClient.readyState !== WebSocket.OPEN) { // Check again after attempt
                         throw new Error("OBS WebSocket is not connected after attempt.");
                    }
                } catch (connectError) {
                     console.error("Failed to connect to OBS for sendToObs:", connectError);
                     throw new Error(`OBS Connection Error: ${(connectError as Error).message}`);
                }
            } else {
                 // If it's already connecting, we should ideally wait for obsConnectionPromise
                 // For now, throwing an error or queuing the request might be options.
                 // Let's throw, as the logic to queue and wait can get complex quickly here.
                 console.warn("OBS is currently connecting. Request will likely fail or be delayed.");
                 // Fall through to try sending, but it might fail if connection isn't ready.
            }
        }
        
        // Re-check after potential connectToObs call
        if (!obsClient || obsClient.readyState !== WebSocket.OPEN) {
            throw new Error("OBS WebSocket is not connected or ready.");
        }
    
    
        const obsRequestId = await generateRequestId();
        const payload: ObsRequest = {
            op: 6, // Request
            d: {
                requestType,
                requestId: obsRequestId,
                requestData,
            },
        };
        
        console.log("Full OBS request payload:", JSON.stringify(payload, null, 2));
    
        return new Promise((resolve, reject) => {
            if (mcpContext && actionName) { // Only store if it's an MCP-initiated request needing response mapping
                pendingObsRequests.set(obsRequestId, { resolve, reject, mcpContext, actionName });
            } else {
                // If not from MCP tool (e.g. internal calls), handle response directly or differently
                // For now, still use pendingObsRequests for simplicity
                pendingObsRequests.set(obsRequestId, { resolve, reject, mcpContext: mcpContext!, actionName: actionName || requestType });
            }
    
            // console.log("OBS TX:", JSON.stringify(payload, null, 2));
            obsClient!.send(JSON.stringify(payload), (err) => {
                if (err) {
                    console.error(`Error sending to OBS for request '${requestType}':`, err);
                    pendingObsRequests.delete(obsRequestId);
                    reject(err);
                }
            });
    
            // Timeout for OBS requests
            setTimeout(() => {
                if (pendingObsRequests.has(obsRequestId)) {
                    console.warn(`OBS request '${requestType}' (ID: ${obsRequestId}) timed out.`);
                    pendingObsRequests.get(obsRequestId)?.reject(new Error(`OBS request '${requestType}' timed out.`));
                    pendingObsRequests.delete(obsRequestId);
                }
            }, 10000); // 10 seconds timeout
        });
    }
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description carries the full burden. It mentions saving and adding to a scene but does not disclose behavioral traits such as whether this operation is destructive (e.g., overwrites existing files), requires specific permissions, has rate limits, or what happens on failure. The description is minimal and lacks critical operational details for a tool with potential side effects.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness3/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is brief but includes redundant Japanese text that adds no value. The first sentence is front-loaded with the core action, but the overall structure could be improved by removing the redundancy. It is concise but not optimally structured for clarity.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the complexity (a tool that saves and adds media with 3 nested parameters), no annotations, no output schema, and 0% schema description coverage, the description is incomplete. It lacks details on behavior, parameter meanings, error handling, and output, making it inadequate for safe and effective use by an AI agent.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters2/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The description provides no information about parameters beyond what is implied by the tool name. The input schema has 1 parameter ('params') with 3 nested properties, but schema description coverage is 0%, meaning none of these are documented in the schema. The description does not compensate by explaining 'sceneName', 'sourceName', or 'replayFolder', leaving parameters largely unexplained.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Saves the replay buffer and adds it as a media source to a scene') and identifies the resource (replay buffer, scene). It distinguishes from sibling tools like 'SaveReplayBuffer' (which likely only saves) and 'CreateSource' (which may create sources without saving replays). However, the Japanese text '用途: リプレイをすぐにシーンに表示' is redundant and adds no new information, slightly reducing clarity.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no explicit guidance on when to use this tool versus alternatives. It does not mention when to choose this over 'SaveReplayBuffer' followed by 'CreateSource', or when to use it in relation to other media or scene management tools. The Japanese text implies immediate display but does not clarify usage context or exclusions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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