Skip to main content
Glama
yshk-mrt

Presentation Buddy MCP Server

by yshk-mrt

StartRecording

Begin recording your presentation or stream in OBS Studio to capture content for sharing or later use.

Instructions

Starts the OBS recording.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault

No arguments

Implementation Reference

  • The core handler logic for the 'StartRecording' MCP tool. It sends an OBS WebSocket request of type 'StartRecord' with empty parameters to start recording in OBS Studio.
    case "StartRecording":
        obsResponseData = await sendToObs("StartRecord", {}, context, action.name);
        break;
  • src/index.ts:360-991 (registration)
    Dynamic registration of MCP tools (including 'StartRecording') from tool definitions loaded from 'obs_mcp_tool_def.json'. Each tool is registered via server.tool() with schema-derived input validation and a shared handler function that dispatches based on action.name.
    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}`);
        });
  • Helper function used by the StartRecording handler to send the 'StartRecord' request to the OBS WebSocket server, including connection management, request ID generation, pending request tracking, and error handling.
    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
        });
    }
  • Helper function that establishes and maintains the WebSocket connection to OBS, required before sending commands like StartRecord.
    async function connectToObs(): Promise<void> {
        if (obsClient && obsClient.readyState === WebSocket.OPEN) {
            return Promise.resolve();
        }
        if (isObsConnecting && obsConnectionPromise) {
            return obsConnectionPromise;
        }
        isObsConnecting = true;
    
        console.log(`Attempting to connect to OBS at ${OBS_WEBSOCKET_URL}...`);
    
        obsConnectionPromise = new Promise((resolve, reject) => {
            const ws = new WebSocket(OBS_WEBSOCKET_URL);
            obsClient = ws; // Assign early for listener setup
    
            ws.onopen = () => {
                console.log("OBS WebSocket connection established. Waiting for Hello.");
                // No immediate resolve here, wait for identify sequence
            };
    
            ws.onmessage = async (event) => {
                try {
                    const message = JSON.parse(event.data.toString()) as ObsResponse;
                    // console.log("OBS RX:", JSON.stringify(message, null, 2));
    
                    if (message.op === 0) { // Hello
                        console.log("OBS Hello received.");
                        const identifyPayload: ObsRequest = {
                            op: 1,
                            d: {
                                rpcVersion: 1,
                                requestType: "Identify", // Not standard but for clarity
                                requestId: await generateRequestId(),
                            },
                        };
                        if (OBS_PASSWORD && message.d.authentication) {
                            const auth = message.d.authentication;
                            const secret = crypto.createHash('sha256').update(OBS_PASSWORD + auth.salt).digest();
                            const authResponse = crypto.createHash('sha256').update(secret.toString('base64') + auth.challenge).digest().toString('base64');
                            identifyPayload.d.authentication = authResponse;
                        } else if (message.d.authentication && !OBS_PASSWORD) {
                            console.warn("OBS requires authentication, but no password provided in config.");
                            // Potentially reject or close here if strict auth is needed.
                        }
                        ws.send(JSON.stringify(identifyPayload));
                        console.log("OBS Identify sent.");
                    } else if (message.op === 2) { // Identified
                        console.log("Successfully connected and identified with OBS.");
                        isObsConnecting = false;
                        resolve(); // Connection is fully established
                    } else if (message.op === 7) { // RequestResponse
                        const reqId = message.d.requestId;
                        if (reqId && pendingObsRequests.has(reqId)) {
                            const pending = pendingObsRequests.get(reqId)!;
                            if (message.d.requestStatus?.code === 100) { // Success code for obs-websocket v5
                                pending.resolve(message.d.responseData || {});
                            } else {
                                const errorDetail = message.d.requestStatus?.comment || `OBS Error Code: ${message.d.requestStatus?.code}`;
                                console.error(`OBS request '${message.d.requestType}' failed for MCP action '${pending.actionName}':`, errorDetail);
                                pending.reject(new Error(errorDetail));
                            }
                            pendingObsRequests.delete(reqId);
                        }
                    } else if (message.op === 5) { // Event
                        // console.log(`OBS Event: ${message.d.eventType}`, message.d.eventData);
                        // TODO: Translate OBS events to MCP events and broadcast
                        // Example:
                        // if (message.d.eventType === "CurrentProgramSceneChanged") {
                        //     server.emitEvent("SceneChanged", { to: message.d.eventData?.sceneName });
                        // }
                    } else {
                        // console.log("OBS Unhandled Op:", message.op, message.d);
                    }
                } catch (e) {
                    console.error("Error processing OBS message:", e);
                }
            };
    
            ws.onerror = (error) => {
                console.error("OBS WebSocket error:", error.message);
                if (isObsConnecting) { // Only reject original promise if this is the initial connection attempt
                    reject(error);
                }
                // Don't reset obsClient here if onclose will handle it
            };
    
            ws.onclose = (event) => {
                console.log(`OBS WebSocket connection closed. Code: ${event.code}, Reason: ${event.reason}`);
                obsClient = null; // Critical to mark as disconnected
                if (isObsConnecting) { // If it was still in the connecting phase
                     reject(new Error(`OBS connection closed during setup. Code: ${event.code}`));
                }
                isObsConnecting = false;
                obsConnectionPromise = null; // Clear the promise so next call attempts to reconnect
    
                // Clean up pending requests on disconnect
                pendingObsRequests.forEach(pending => {
                    pending.reject(new Error("OBS WebSocket disconnected"));
                });
                pendingObsRequests.clear();
    
                console.log(`Attempting to reconnect to OBS in ${reconnectInterval / 1000}s...`);
                setTimeout(() => {
                     connectToObs().catch(err => console.error("Reconnect attempt failed:", err)); // Start a new connection attempt
                }, reconnectInterval);
            };
        });
        return obsConnectionPromise;
    }

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