Skip to main content
Glama
yshk-mrt

Presentation Buddy MCP Server

by yshk-mrt

SetSourceVisibility

Control source visibility in OBS scenes to manage streaming content. Show or hide specific sources during presentations for automated production.

Instructions

Sets the visibility of a source in a specific scene.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
paramsYes

Implementation Reference

  • Handler function for the 'SetSourceVisibility' tool. Extracts scene, source, and visibility parameters, retrieves the scene item ID using OBS 'GetSceneItemId', then sets visibility using 'SetSceneItemEnabled'.
    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;
  • src/index.ts:360-991 (registration)
    Dynamic registration of all tools including 'SetSourceVisibility' from obs_mcp_tool_def.json. Calls server.tool for each action, providing schema-derived input validation and shared handler logic with switch-case dispatching.
    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 to convert MCP JSON schema (from tool definitions) to Zod schema for input validation in server.tool calls.
    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 SetSourceVisibility handler to send requests to OBS WebSocket, handling connection, authentication, responses, and 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
        });
    }
Behavior2/5

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

No annotations are provided, so the description carries full burden. It states the tool 'Sets the visibility,' implying a mutation, but doesn't disclose behavioral traits such as whether this requires specific permissions, if changes are immediate or reversible, or potential side effects. This is inadequate for a mutation tool with zero annotation coverage.

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

Conciseness5/5

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

The description is a single, efficient sentence that front-loads the core purpose without any wasted words. It's appropriately sized for the tool's complexity, making it easy to parse quickly.

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

Completeness2/5

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

Given this is a mutation tool with no annotations and no output schema, the description is incomplete. It doesn't explain what happens on success or failure, return values, or error conditions. For a tool that modifies scene state, more context is needed to ensure safe and effective use.

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

Parameters4/5

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

The input schema has 100% description coverage for its parameters (scene, source, visible), clearly documenting each. The description adds no additional parameter semantics beyond what the schema provides, but with high schema coverage, the baseline is 3. Since there are 0 parameters in the top-level (the nested 'params' object counts as 1 parameter in context signals), this slightly elevates the score to 4, as the description doesn't need to compensate for gaps.

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

Purpose4/5

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

The description clearly states the action ('Sets') and target ('visibility of a source in a specific scene'), making the purpose immediately understandable. However, it doesn't distinguish this tool from similar sibling tools like SetSourcePosition or SetSourceScale, which also modify source properties in scenes, so it misses full differentiation.

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

Usage Guidelines2/5

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

The description provides no guidance on when to use this tool versus alternatives. For example, it doesn't mention if this is for real-time scene adjustments or if there are prerequisites like the source existing in the scene. With many sibling tools for modifying sources, this lack of context is a significant gap.

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

Install Server

Other Tools

Latest Blog Posts

MCP directory API

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

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

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