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
        });
    }

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