Skip to main content
Glama
yshk-mrt

Presentation Buddy MCP Server

by yshk-mrt

SetAudioVolume

Adjust audio source volume levels in OBS Studio using volume multiplier values from 0.0 to 1.0 for precise control during streaming production.

Instructions

Sets the volume of an audio source.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • Handler logic for SetAudioVolume tool: extracts source name and volume level from input parameters, validates the volume range (0.0-1.0), and sends SetInputVolume request to OBS WebSocket with inputVolumeMul.
    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;
  • src/index.ts:364-991 (registration)
    Dynamic registration of the SetAudioVolume tool (and others) via server.tool() call within the loop over toolDefinitions.actions from obs_mcp_tool_def.json. Includes schema conversion to Zod if defined.
        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 that converts MCP tool schema (from JSON) to Zod schema for input validation in the registered tools, including SetAudioVolume.
    function zodSchemaFromMcpSchema(mcpSchema: any): ZodType | undefined {
        if (!mcpSchema || !mcpSchema.type) {
            return undefined;
        }
    
        switch (mcpSchema.type) {
            case "object":
                const shape: Record<string, ZodType> = {};
                if (mcpSchema.properties) {
                    for (const key in mcpSchema.properties) {
                        const propSchema = zodSchemaFromMcpSchema(mcpSchema.properties[key]);
                        if (propSchema) {
                            shape[key] = propSchema;
                        }
                    }
                }
                let zodObject = z.object(shape);
                // Zod doesn't have a direct equivalent for making only specific fields optional easily from schema alone
                // MCP's "required" array means fields NOT in it are optional. Zod by default makes all fields in an object required.
                // We would need to iterate 'properties' and if a key is not in 'mcpSchema.required', apply .optional()
                // For now, this simplified version assumes all defined properties are required unless explicitly made optional in a more complex mapping
                if (mcpSchema.required && Array.isArray(mcpSchema.required)) {
                    // This part is tricky with Zod's builder pattern.
                    // A more robust solution might involve constructing the object then iterating properties to add .optional()
                    // For simplicity, we'll rely on Zod's default behavior (all fields required unless .optional() is called)
                    // and users should define schemas carefully.
                }
                return zodObject;
            case "string":
                let zodString = z.string();
                if (mcpSchema.description) zodString = zodString.describe(mcpSchema.description);
                // Handle enums if present
                if (mcpSchema.enum && Array.isArray(mcpSchema.enum)) {
                     // z.enum requires at least one value, and TypeScript needs it to be const or string literal array
                     // This dynamic creation is a bit tricky. For simplicity, assuming string enums.
                     if (mcpSchema.enum.length > 0) {
                        // Cast to [string, ...string[]] to satisfy z.enum type
                        return z.enum(mcpSchema.enum as [string, ...string[]]);
                     }
                }
                return zodString;
            case "boolean":
                let zodBoolean = z.boolean();
                if (mcpSchema.description) zodBoolean = zodBoolean.describe(mcpSchema.description);
                return zodBoolean;
            case "integer":
                let zodInteger = z.number().int();
                if (mcpSchema.description) zodInteger = zodInteger.describe(mcpSchema.description);
                return zodInteger;
            case "number":
                let zodNumber = z.number();
                if (mcpSchema.description) zodNumber = zodNumber.describe(mcpSchema.description);
                return zodNumber;
            // Add other type mappings as needed (array, etc.)
            default:
                console.warn(`Unsupported MCP schema type: ${mcpSchema.type}`);
                return undefined;
        }
    }
  • Core helper function used by SetAudioVolume handler to send 'SetInputVolume' requests to OBS and handle responses/errors.
    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
        });
    }

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